[
  {
    "path": ".dockerignore",
    "content": "# Exclude .vscode directory\r\n.vscode/\r\n\r\n# Exclude .git directory\r\n.git/\r\n.github/\r\n\r\n*.md\r\n.gitignore\r\n.dockerignore"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [ellite]\ncustom: ['https://www.paypal.com/paypalme/miguelr']\n"
  },
  {
    "path": ".github/workflows/build-release.yaml",
    "content": "name: Build & Release\non:\n  push:\n    branches:\n      - \"*\"\n  pull_request:\n    branches:\n      - main\npermissions:\n  contents: write\n  pull-requests: write\n  packages: write\nenv:\n  # login to docker hub with provided secrets\n  REGISTRY: docker.io\n  REGISTRY_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}\n  REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}\n  IMAGE_NAME: ${{ vars.DOCKERHUB_TAG }}\n  # For release-please, see available types at https://github.com/google-github-actions/release-please-action/tree/v4/?tab=readme-ov-file#release-types-supported\n  PROJECT_TYPE: simple\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - id: rp\n        if: github.event_name != 'pull_request' && github.ref_name == 'main'\n        uses: google-github-actions/release-please-action@v4\n        with:\n          release-type: ${{ env.PROJECT_TYPE }}\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.REGISTRY_USERNAME }}\n          password: ${{ env.REGISTRY_PASSWORD }}\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Prepare tags for Docker meta\n        id: tags\n        env:\n          # When release please is skipped, these values will be empty\n          is_release: ${{ steps.rp.outputs.release_created }}\n          version: v${{ steps.rp.outputs.major }}.${{ steps.rp.outputs.minor }}.${{ steps.rp.outputs.patch }}\n        run: |\n          tags=\"\"\n          if [[ \"$is_release\" = 'true' ]]; then\n            tags=\"type=semver,pattern={{version}},value=$version\n            type=ref,event=branch,value=main\"\n          else\n            tags=\"type=ref,event=branch\n            type=ref,event=pr\"\n          fi\n          {\n            echo 'tags<<EOF'\n            echo \"$tags\"\n            echo EOF\n          } >> \"$GITHUB_OUTPUT\"\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n            ghcr.io/ellite/wallos\n          tags: ${{ steps.tags.outputs.tags }}\n      # necessary for multi-platform images\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n      # necessary for multi-platform images\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64,linux/arm/v7\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n      - name: Send notification to Discord\n        if: github.event_name != 'pull_request'\n        uses: Ilshidur/action-discord@master\n        with:\n          args: \"A new release has been created: ${{ steps.meta.outputs.tags }}\"\n        env:\n          DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/db/*\r\n!/db/wallos.empty.db\r\n/images/uploads/logos/*\r\n!/images/uploads/logos/wallos.png\r\n.DS_Store\r\n.idea/\r\n.vscode/\r\n"
  },
  {
    "path": ".tmp/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [4.7.2](https://github.com/ellite/Wallos/compare/v4.7.1...v4.7.2) (2026-03-19)\n\n\n### Bug Fixes\n\n* password reset tokens now expire after 60 minutes ([90bb618](https://github.com/ellite/Wallos/commit/90bb6186ee4091590b6efdef824c85f2494ff2bb))\n* vulnerability would allow to bypass 2fa ([#1021](https://github.com/ellite/Wallos/issues/1021)) ([90bb618](https://github.com/ellite/Wallos/commit/90bb6186ee4091590b6efdef824c85f2494ff2bb))\n\n## [4.7.1](https://github.com/ellite/Wallos/compare/v4.7.0...v4.7.1) (2026-03-19)\n\n\n### Bug Fixes\n\n* remove extra line on languages.php causing headers already sent ([#1019](https://github.com/ellite/Wallos/issues/1019)) ([f5c9a34](https://github.com/ellite/Wallos/commit/f5c9a3498ed2df8ae6b225fc63ce01a8ed5ce348))\n\n## [4.7.0](https://github.com/ellite/Wallos/compare/v4.6.2...v4.7.0) (2026-03-19)\n\n\n### Features\n\n* add romanian translations ([#1017](https://github.com/ellite/Wallos/issues/1017)) ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* mask ai api key on the settings page ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n\n\n### Bug Fixes\n\n* ai recommendation numbering when deleting a recommendation ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* calendar ocurrences to respect subscriptions start date ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* logo search ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* retain first and last name when switching language during registration ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* set login cookie to httponly ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* ssrf vulnerability on several endpoints ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* unicode character on the css file ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n* xss vulnerability on payment method rename endpoint ([e87387f](https://github.com/ellite/Wallos/commit/e87387f0ebb540cd33e6dfda7181db9db650ecef))\n\n## [4.6.2](https://github.com/ellite/Wallos/compare/v4.6.1...v4.6.2) (2026-03-05)\n\n\n### Bug Fixes\n\n* ssrf vulnerability on all test notifications endpoint ([e8a5135](https://github.com/ellite/Wallos/commit/e8a513591dbbf885966e2ef55c38622785b9060d))\n* vulnerability allowed to delete avatars from other users ([e8a5135](https://github.com/ellite/Wallos/commit/e8a513591dbbf885966e2ef55c38622785b9060d))\n* xss vulnerability on password reset page ([e8a5135](https://github.com/ellite/Wallos/commit/e8a513591dbbf885966e2ef55c38622785b9060d))\n\n## [4.6.1](https://github.com/ellite/Wallos/compare/v4.6.0...v4.6.1) (2026-02-10)\n\n\n### Bug Fixes\n\n* vulnerabily on add subscription endpoint ([#991](https://github.com/ellite/Wallos/issues/991)) ([76a53df](https://github.com/ellite/Wallos/commit/76a53df9cb4658123b8f0b7cf1826f1ba7d1c960))\n\n## [4.6.0](https://github.com/ellite/Wallos/compare/v4.5.0...v4.6.0) (2025-12-20)\n\n\n### Features\n\n* add catalan translation ([#970](https://github.com/ellite/Wallos/issues/970)) ([f5746e7](https://github.com/ellite/Wallos/commit/f5746e76a5dd6bbda7d52b1a2229c02bb9fad94b))\n* add robots.txt to disallow indexing. ([f5746e7](https://github.com/ellite/Wallos/commit/f5746e76a5dd6bbda7d52b1a2229c02bb9fad94b))\n* add serverchan notifications. ([f5746e7](https://github.com/ellite/Wallos/commit/f5746e76a5dd6bbda7d52b1a2229c02bb9fad94b))\n* notifications for subscription can be triggered up to 180 days before payment date. ([f5746e7](https://github.com/ellite/Wallos/commit/f5746e76a5dd6bbda7d52b1a2229c02bb9fad94b))\n\n\n### Bug Fixes\n\n* use RFC 5545 compliant date format in iCal exports ([#965](https://github.com/ellite/Wallos/issues/965)) ([b6b0abe](https://github.com/ellite/Wallos/commit/b6b0abed0d916c3ae5a31257f4c0b1a34436ad91))\n* use RFC 5545 compliant date format in iCal exports. ([f5746e7](https://github.com/ellite/Wallos/commit/f5746e76a5dd6bbda7d52b1a2229c02bb9fad94b))\n* use stable UID for iCal events to prevent duplicates. ([f5746e7](https://github.com/ellite/Wallos/commit/f5746e76a5dd6bbda7d52b1a2229c02bb9fad94b))\n\n## [4.5.0](https://github.com/ellite/Wallos/compare/v4.4.1...v4.5.0) (2025-10-18)\n\n\n### Features\n\n* enforce CSRF protection and POST-only policy across endpoints ([#940](https://github.com/ellite/Wallos/issues/940)) ([3247ce2](https://github.com/ellite/Wallos/commit/3247ce2c8768d8e5910f74e5b8eba657b5b05cc1))\n\n## [4.4.1](https://github.com/ellite/Wallos/compare/v4.4.0...v4.4.1) (2025-10-12)\n\n\n### Bug Fixes\n\n* get_subscriptions api endpoint was not returning subscriptions ([#937](https://github.com/ellite/Wallos/issues/937)) ([d6329a7](https://github.com/ellite/Wallos/commit/d6329a7af5a48f74b5f1d44a51cdc8c09dc2508b))\n\n## [4.4.0](https://github.com/ellite/Wallos/compare/v4.3.0...v4.4.0) (2025-10-12)\n\n\n### Features\n\n* add mattermost notifications ([#923](https://github.com/ellite/Wallos/issues/923)) ([#934](https://github.com/ellite/Wallos/issues/934)) ([5629a31](https://github.com/ellite/Wallos/commit/5629a319bc5eb6cb80abfca06725aed9d2d9df88))\n* add openrouter ai endpoint ([#922](https://github.com/ellite/Wallos/issues/922)) ([5629a31](https://github.com/ellite/Wallos/commit/5629a319bc5eb6cb80abfca06725aed9d2d9df88))\n* enhance get_subscriptions API with admin access ([#928](https://github.com/ellite/Wallos/issues/928)) ([5629a31](https://github.com/ellite/Wallos/commit/5629a319bc5eb6cb80abfca06725aed9d2d9df88))\n\n\n### Bug Fixes\n\n* add autocomplete attribute to inputes ([#926](https://github.com/ellite/Wallos/issues/926)) ([5629a31](https://github.com/ellite/Wallos/commit/5629a319bc5eb6cb80abfca06725aed9d2d9df88))\n\n## [4.3.0](https://github.com/ellite/Wallos/compare/v4.2.0...v4.3.0) (2025-09-15)\n\n\n### Features\n\n* add health endpoint and healthcheck to container ([#919](https://github.com/ellite/Wallos/issues/919)) ([852cb48](https://github.com/ellite/Wallos/commit/852cb485a65a58c91577b369fb9ea293d370bda8))\n\n## [4.2.0](https://github.com/ellite/Wallos/compare/v4.1.1...v4.2.0) (2025-09-14)\n\n\n### Features\n\n* add pushplus notification service  ([#911](https://github.com/ellite/Wallos/issues/911)) ([27ac805](https://github.com/ellite/Wallos/commit/27ac805141c0d170a40c2a7796a589a5ef29544f))\n* make container shutdown instant & graceful ([27ac805](https://github.com/ellite/Wallos/commit/27ac805141c0d170a40c2a7796a589a5ef29544f))\n* make container shutdown instant & graceful  ([#916](https://github.com/ellite/Wallos/issues/916)) ([27ac805](https://github.com/ellite/Wallos/commit/27ac805141c0d170a40c2a7796a589a5ef29544f))\n* option to delete ai recommendations ([27ac805](https://github.com/ellite/Wallos/commit/27ac805141c0d170a40c2a7796a589a5ef29544f))\n\n\n### Bug Fixes\n\n* parsing ai recommendations from gemini ([#909](https://github.com/ellite/Wallos/issues/909)) ([27ac805](https://github.com/ellite/Wallos/commit/27ac805141c0d170a40c2a7796a589a5ef29544f))\n\n## [4.1.1](https://github.com/ellite/Wallos/compare/v4.1.0...v4.1.1) (2025-08-13)\n\n\n### Bug Fixes\n\n* missing apikey validation error on get_monthly_cost api endpoint ([3ecc160](https://github.com/ellite/Wallos/commit/3ecc160ccb73f22367bea427315519876de74a65))\n* redirect from dashboard to subscriptions for new users ([3ecc160](https://github.com/ellite/Wallos/commit/3ecc160ccb73f22367bea427315519876de74a65))\n* wrong check for disabling password login ([3ecc160](https://github.com/ellite/Wallos/commit/3ecc160ccb73f22367bea427315519876de74a65))\n\n## [4.1.0](https://github.com/ellite/Wallos/compare/v4.0.0...v4.1.0) (2025-08-11)\n\n\n### Features\n\n* add at a glance dashboard ([ba6dddf](https://github.com/ellite/Wallos/commit/ba6dddf52601fdbeb18897731beacc48d16043c3))\n* add get_oidc_settings endpoint to the api ([ba6dddf](https://github.com/ellite/Wallos/commit/ba6dddf52601fdbeb18897731beacc48d16043c3))\n* ai recommendations with chatgpt, gemini or ollama ([ba6dddf](https://github.com/ellite/Wallos/commit/ba6dddf52601fdbeb18897731beacc48d16043c3))\n* allow to disable password login when oidc is enabled ([ba6dddf](https://github.com/ellite/Wallos/commit/ba6dddf52601fdbeb18897731beacc48d16043c3))\n* display ai recommendations on the dashboard ([ba6dddf](https://github.com/ellite/Wallos/commit/ba6dddf52601fdbeb18897731beacc48d16043c3))\n* refactor css colors ([ba6dddf](https://github.com/ellite/Wallos/commit/ba6dddf52601fdbeb18897731beacc48d16043c3))\n\n\n### Bug Fixes\n\n* accept both api_key and apiKey as parameter on the api ([ba6dddf](https://github.com/ellite/Wallos/commit/ba6dddf52601fdbeb18897731beacc48d16043c3))\n\n## [4.0.0](https://github.com/ellite/Wallos/compare/v3.3.1...v4.0.0) (2025-07-21)\n\n\n### ⚠ BREAKING CHANGES\n\n* add oauth / oidc support ([#875](https://github.com/ellite/Wallos/issues/875))\n\n### Features\n\n* add oauth / oidc support ([#875](https://github.com/ellite/Wallos/issues/875)) ([805e688](https://github.com/ellite/Wallos/commit/805e688ec0fac1dbb362e847ed8a4e3e301ee113))\n* add oauth/oidc support ([#873](https://github.com/ellite/Wallos/issues/873)) ([c0d53e4](https://github.com/ellite/Wallos/commit/c0d53e4423996595e5c82404af92e077c00eae47))\n\n## [3.3.1](https://github.com/ellite/Wallos/compare/v3.3.0...v3.3.1) (2025-07-19)\n\n\n### Bug Fixes\n\n* code of new taiwan dollar ([596cbc4](https://github.com/ellite/Wallos/commit/596cbc42464100dc8c6db5d07c090dab4b767268))\n* decoding of header from database on the webhook notifications ([596cbc4](https://github.com/ellite/Wallos/commit/596cbc42464100dc8c6db5d07c090dab4b767268))\n* unicode issue on telegram notifications ([#871](https://github.com/ellite/Wallos/issues/871)) ([596cbc4](https://github.com/ellite/Wallos/commit/596cbc42464100dc8c6db5d07c090dab4b767268))\n\n## [3.3.0](https://github.com/ellite/Wallos/compare/v3.2.0...v3.3.0) (2025-06-09)\n\n\n### Features\n\n* set todays date on start subscription field for new subscriptions by default ([#848](https://github.com/ellite/Wallos/issues/848)) ([d3fd938](https://github.com/ellite/Wallos/commit/d3fd9387d34f430adb84ef553193b4ad3080c009))\n\n\n### Bug Fixes\n\n* visual issue with date fields on ios ([#846](https://github.com/ellite/Wallos/issues/846)) ([e2df8f7](https://github.com/ellite/Wallos/commit/e2df8f7e24678f9d62f36f68c94de838fc741913))\n\n## [3.2.0](https://github.com/ellite/Wallos/compare/v3.1.1...v3.2.0) (2025-06-08)\n\n\n### Features\n\n* add button to auto fill the next payment date ([48db4e3](https://github.com/ellite/Wallos/commit/48db4e300df6128b7cc0b4e0c86271bfb3159545))\n* add first and last names to the user profile ([48db4e3](https://github.com/ellite/Wallos/commit/48db4e300df6128b7cc0b4e0c86271bfb3159545))\n* add indonesian language ([#842](https://github.com/ellite/Wallos/issues/842)) ([48db4e3](https://github.com/ellite/Wallos/commit/48db4e300df6128b7cc0b4e0c86271bfb3159545))\n* add new currency ([48db4e3](https://github.com/ellite/Wallos/commit/48db4e300df6128b7cc0b4e0c86271bfb3159545))\n* Add new currency ([#829](https://github.com/ellite/Wallos/issues/829)) ([288ad45](https://github.com/ellite/Wallos/commit/288ad456564c307018541a09df447898e1d62d26))\n* enable IPv6 environments by configuring a dual-stack listen in nginx ([48db4e3](https://github.com/ellite/Wallos/commit/48db4e300df6128b7cc0b4e0c86271bfb3159545))\n\n\n### Bug Fixes\n\n* vulnerability on test webhook endpoint ([48db4e3](https://github.com/ellite/Wallos/commit/48db4e300df6128b7cc0b4e0c86271bfb3159545))\n\n## [3.1.1](https://github.com/ellite/Wallos/compare/v3.1.0...v3.1.1) (2025-05-15)\n\n\n### Bug Fixes\n\n* issue listing prices when uah  was added to the list of currencies ([#823](https://github.com/ellite/Wallos/issues/823)) ([bd20b56](https://github.com/ellite/Wallos/commit/bd20b5697659fc6117113205a3995d7e5f9026c9))\n\n## [3.1.0](https://github.com/ellite/Wallos/compare/v3.0.2...v3.1.0) (2025-05-08)\n\n\n### Features\n\n* add danish translation ([0cfefc7](https://github.com/ellite/Wallos/commit/0cfefc7f07056d59ad911f926cc56ff3e6c8e261))\n\n\n### Bug Fixes\n\n* disable totp with backup code ([0cfefc7](https://github.com/ellite/Wallos/commit/0cfefc7f07056d59ad911f926cc56ff3e6c8e261))\n* gotify settings test ([0cfefc7](https://github.com/ellite/Wallos/commit/0cfefc7f07056d59ad911f926cc56ff3e6c8e261))\n* vulnerability adding logos from url ([0cfefc7](https://github.com/ellite/Wallos/commit/0cfefc7f07056d59ad911f926cc56ff3e6c8e261))\n\n## [3.0.2](https://github.com/ellite/Wallos/compare/v3.0.1...v3.0.2) (2025-05-03)\n\n\n### Bug Fixes\n\n* delete avatar would not work if wallos is on a subfolder ([69c7d52](https://github.com/ellite/Wallos/commit/69c7d52cf8d708bcb046343faa663209c8d36779))\n* some strings not using translations on the calendar page ([69c7d52](https://github.com/ellite/Wallos/commit/69c7d52cf8d708bcb046343faa663209c8d36779))\n* vulnerability on delete avatar ([69c7d52](https://github.com/ellite/Wallos/commit/69c7d52cf8d708bcb046343faa663209c8d36779))\n\n## [3.0.1](https://github.com/ellite/Wallos/compare/v3.0.0...v3.0.1) (2025-04-30)\n\n\n### Bug Fixes\n\n* allow to clear the budget field ([f6b8fb9](https://github.com/ellite/Wallos/commit/f6b8fb9162c5fb4fefa1fbd9cde65c201e96be6c))\n* don't show budget alert when budget is 0 ([f6b8fb9](https://github.com/ellite/Wallos/commit/f6b8fb9162c5fb4fefa1fbd9cde65c201e96be6c))\n\n## [3.0.0](https://github.com/ellite/Wallos/compare/v2.52.2...v3.0.0) (2025-04-27)\n\n\n### ⚠ BREAKING CHANGES\n\n* simplified webhook notifications without iterator (might break your current webhook settings)\n\n### Features\n\n* simplified webhook notifications without iterator (might break your current webhook settings) ([e0f2048](https://github.com/ellite/Wallos/commit/e0f204803e635400c404529d87e5057c579c8531))\n* use mobile style toggles instead of checkboxes ([e0f2048](https://github.com/ellite/Wallos/commit/e0f204803e635400c404529d87e5057c579c8531))\n* webhooks can now be used for cancelation notifications ([e0f2048](https://github.com/ellite/Wallos/commit/e0f204803e635400c404529d87e5057c579c8531))\n\n\n### Bug Fixes\n\n* barely readable placeholder text on textarea on dark the ([e0f2048](https://github.com/ellite/Wallos/commit/e0f204803e635400c404529d87e5057c579c8531))\n\n## [2.52.2](https://github.com/ellite/Wallos/compare/v2.52.1...v2.52.2) (2025-04-26)\n\n\n### Bug Fixes\n\n* incorrect headers on the api ([#802](https://github.com/ellite/Wallos/issues/802)) ([af68c11](https://github.com/ellite/Wallos/commit/af68c11abf5d5a64fd7136e1d2e37323d170c77e))\n\n## [2.52.1](https://github.com/ellite/Wallos/compare/v2.52.0...v2.52.1) (2025-04-26)\n\n\n### Bug Fixes\n\n* error on statistics page when budget = 0 ([#800](https://github.com/ellite/Wallos/issues/800)) ([b7712dc](https://github.com/ellite/Wallos/commit/b7712dc80d6642a6a33a28adc641f9a4b3263ae6))\n\n## [2.52.0](https://github.com/ellite/Wallos/compare/v2.51.1...v2.52.0) (2025-04-19)\n\n\n### Features\n\n* new graph cost vs budget on statistics ([#793](https://github.com/ellite/Wallos/issues/793)) ([6d67319](https://github.com/ellite/Wallos/commit/6d673195ba39f1a52e9ea16bad21221768690e7a))\n\n## [2.51.1](https://github.com/ellite/Wallos/compare/v2.51.0...v2.51.1) (2025-04-19)\n\n\n### Bug Fixes\n\n* timezone for cronjobs now comes from TZ env var first ([#791](https://github.com/ellite/Wallos/issues/791)) ([66a1a45](https://github.com/ellite/Wallos/commit/66a1a45f2dc1df99f8292cbb531d569f706eca6d))\n\n## [2.51.0](https://github.com/ellite/Wallos/compare/v2.50.1...v2.51.0) (2025-04-18)\n\n\n### Features\n\n* add over budget warnings on the calendar ([88eae10](https://github.com/ellite/Wallos/commit/88eae1002f0cc29a847e95b7698ab713779ec4f4))\n\n\n### Bug Fixes\n\n* force correct timezone on the cronjobs ([88eae10](https://github.com/ellite/Wallos/commit/88eae1002f0cc29a847e95b7698ab713779ec4f4))\n\n## [2.50.1](https://github.com/ellite/Wallos/compare/v2.50.0...v2.50.1) (2025-04-16)\n\n\n### Bug Fixes\n\n* localization on date on browsers not in english ([c7b3fb4](https://github.com/ellite/Wallos/commit/c7b3fb445182e19bc464ac987977bac266628757))\n\n## [2.50.0](https://github.com/ellite/Wallos/compare/v2.49.1...v2.50.0) (2025-04-16)\n\n\n### Features\n\n* shorten date displayed on the list of subscriptions ([68f1d47](https://github.com/ellite/Wallos/commit/68f1d4757737de50622bb4b2aeb8f291dec62972))\n* use user defined language for the date on the list of subscriptions ([68f1d47](https://github.com/ellite/Wallos/commit/68f1d4757737de50622bb4b2aeb8f291dec62972))\n\n\n### Bug Fixes\n\n* limit name display, when sub has no logo to two lines ([68f1d47](https://github.com/ellite/Wallos/commit/68f1d4757737de50622bb4b2aeb8f291dec62972))\n* use translations on the mobile menu ([68f1d47](https://github.com/ellite/Wallos/commit/68f1d4757737de50622bb4b2aeb8f291dec62972))\n\n## [2.49.1](https://github.com/ellite/Wallos/compare/v2.49.0...v2.49.1) (2025-04-13)\n\n\n### Bug Fixes\n\n* version number ([eade2d9](https://github.com/ellite/Wallos/commit/eade2d9919e5d30e7be279f53e278fb746095762))\n\n## [2.49.0](https://github.com/ellite/Wallos/compare/v2.48.1...v2.49.0) (2025-04-13)\n\n\n### Features\n\n* show name on mobile view when subscription has no logo ([9eb2907](https://github.com/ellite/Wallos/commit/9eb2907145297e3b7aac54dd5b51451d961f549a))\n* show timezone on sendnotification cronjob on admin page ([9eb2907](https://github.com/ellite/Wallos/commit/9eb2907145297e3b7aac54dd5b51451d961f549a))\n* use currencyConverter for notifications as well ([9eb2907](https://github.com/ellite/Wallos/commit/9eb2907145297e3b7aac54dd5b51451d961f549a))\n* use symbol from db when currencyFormatter does not support the currency ([9eb2907](https://github.com/ellite/Wallos/commit/9eb2907145297e3b7aac54dd5b51451d961f549a))\n\n\n### Bug Fixes\n\n* date comparison check on sendnotifications cronjob ([9eb2907](https://github.com/ellite/Wallos/commit/9eb2907145297e3b7aac54dd5b51451d961f549a))\n* emails with encryption set to none not working without ssl ([9eb2907](https://github.com/ellite/Wallos/commit/9eb2907145297e3b7aac54dd5b51451d961f549a))\n* error when not setting custom headers for ntfy ([9eb2907](https://github.com/ellite/Wallos/commit/9eb2907145297e3b7aac54dd5b51451d961f549a))\n\n## [2.48.1](https://github.com/ellite/Wallos/compare/v2.48.0...v2.48.1) (2025-03-27)\n\n\n### Bug Fixes\n\n* notifications would also be sent x days after subscription was due in some cases ([ba912a3](https://github.com/ellite/Wallos/commit/ba912a37d1a0d95401a38dabe8f98f29a6aa49db))\n\n## [2.48.0](https://github.com/ellite/Wallos/compare/v2.47.1...v2.48.0) (2025-03-20)\n\n\n### Features\n\n* add update notification and release notes to the about page ([3e0e88d](https://github.com/ellite/Wallos/commit/3e0e88d6a2adc46c17773b09dd8684618c979711))\n* increase privacy by not sending referrer to external urls ([3e0e88d](https://github.com/ellite/Wallos/commit/3e0e88d6a2adc46c17773b09dd8684618c979711))\n* small layout change on the about page ([3e0e88d](https://github.com/ellite/Wallos/commit/3e0e88d6a2adc46c17773b09dd8684618c979711))\n\n## [2.47.1](https://github.com/ellite/Wallos/compare/v2.47.0...v2.47.1) (2025-03-19)\n\n\n### Bug Fixes\n\n* small layout inconsistencies on the dashboard ([19d3067](https://github.com/ellite/Wallos/commit/19d30672b2635b6e79eaa6eb5c49100d7a27a63a))\n\n## [2.47.0](https://github.com/ellite/Wallos/compare/v2.46.1...v2.47.0) (2025-03-19)\n\n\n### Features\n\n* add filter by renew type ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n* add sort by renew type ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n* add ukranian translation ([#756](https://github.com/ellite/Wallos/issues/756)) ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n* remove \"Wallos\" text from calendar export ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n\n\n### Bug Fixes\n\n* ical trigger to spec RFC5545 ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n* special chars on calendar exports ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n* special chars on notifications ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n* state filter not cleared by clear button ([1bec973](https://github.com/ellite/Wallos/commit/1bec973803e0b3c00d2765bbf80447439127574d))\n\n## [2.46.1](https://github.com/ellite/Wallos/compare/v2.46.0...v2.46.1) (2025-03-06)\n\n\n### Bug Fixes\n\n* calculation of monthly cost progress graph ([#747](https://github.com/ellite/Wallos/issues/747)) ([77486ec](https://github.com/ellite/Wallos/commit/77486ec92c44b71f69e85b1eafb7f3a98c4a44c1))\n\n## [2.46.0](https://github.com/ellite/Wallos/compare/v2.45.2...v2.46.0) (2025-02-22)\n\n\n### Features\n\n* sorting by category or payment method respects order from the settings page ([51b2272](https://github.com/ellite/Wallos/commit/51b22727bf5656a4a263519b5b56adfe6a2d12be))\n\n\n### Bug Fixes\n\n* access to tmp folder by www-data ([51b2272](https://github.com/ellite/Wallos/commit/51b22727bf5656a4a263519b5b56adfe6a2d12be))\n\n## [2.45.2](https://github.com/ellite/Wallos/compare/v2.45.1...v2.45.2) (2025-02-05)\n\n\n### Bug Fixes\n\n* bug setting main currency for the first registered user ([c43b08a](https://github.com/ellite/Wallos/commit/c43b08aa4c45c907f82eb6afe37fd46aa5103654))\n* deprecation message ([c43b08a](https://github.com/ellite/Wallos/commit/c43b08aa4c45c907f82eb6afe37fd46aa5103654))\n* subscription progress above 100% for disabled subscriptions ([c43b08a](https://github.com/ellite/Wallos/commit/c43b08aa4c45c907f82eb6afe37fd46aa5103654))\n* typo on czech translation ([c43b08a](https://github.com/ellite/Wallos/commit/c43b08aa4c45c907f82eb6afe37fd46aa5103654))\n* use first currency on the list of currencies if user has not selected a main currency ([c43b08a](https://github.com/ellite/Wallos/commit/c43b08aa4c45c907f82eb6afe37fd46aa5103654))\n* use gd if imagick is not available ([c43b08a](https://github.com/ellite/Wallos/commit/c43b08aa4c45c907f82eb6afe37fd46aa5103654))\n\n## [2.45.1](https://github.com/ellite/Wallos/compare/v2.45.0...v2.45.1) (2025-01-28)\n\n\n### Bug Fixes\n\n* improve czech translation ([e2dc269](https://github.com/ellite/Wallos/commit/e2dc2696310159900c1f8fbe0a090e66b29b778d))\n* improve japanese translation ([#713](https://github.com/ellite/Wallos/issues/713)) ([e2dc269](https://github.com/ellite/Wallos/commit/e2dc2696310159900c1f8fbe0a090e66b29b778d))\n* improve traditional chinese translation ([e2dc269](https://github.com/ellite/Wallos/commit/e2dc2696310159900c1f8fbe0a090e66b29b778d))\n* setting pgid and puid for the container ([e2dc269](https://github.com/ellite/Wallos/commit/e2dc2696310159900c1f8fbe0a090e66b29b778d))\n\n## [2.45.0](https://github.com/ellite/Wallos/compare/v2.44.1...v2.45.0) (2025-01-19)\n\n\n### Features\n\n* add czech translations ([#701](https://github.com/ellite/Wallos/issues/701)) ([426fdfa](https://github.com/ellite/Wallos/commit/426fdfa5c79d32c7d5a0722a0590d39547cfd1fa))\n\n## [2.44.1](https://github.com/ellite/Wallos/compare/v2.44.0...v2.44.1) (2025-01-19)\n\n\n### Bug Fixes\n\n* error setting date of last exchange rates update ([#699](https://github.com/ellite/Wallos/issues/699)) ([d2f68c4](https://github.com/ellite/Wallos/commit/d2f68c457e9b1328caf983ddc6e2827430855aa6))\n\n## [2.44.0](https://github.com/ellite/Wallos/compare/v2.43.1...v2.44.0) (2025-01-12)\n\n\n### Features\n\n* allow notifications on due date ([87f148d](https://github.com/ellite/Wallos/commit/87f148d1745bec19f5713b8a367a3615871e6e33))\n\n\n### Bug Fixes\n\n* don't expose disabled notifications to ical feed ([87f148d](https://github.com/ellite/Wallos/commit/87f148d1745bec19f5713b8a367a3615871e6e33))\n* email notification test always sending to admins email ([87f148d](https://github.com/ellite/Wallos/commit/87f148d1745bec19f5713b8a367a3615871e6e33))\n\n## [2.43.1](https://github.com/ellite/Wallos/compare/v2.43.0...v2.43.1) (2025-01-12)\n\n\n### Bug Fixes\n\n* edit / delete subscription menu not accessible ([#689](https://github.com/ellite/Wallos/issues/689)) ([b668d37](https://github.com/ellite/Wallos/commit/b668d37d38f799ee0dda5a69a4824d03dd21e1bc))\n\n## [2.43.0](https://github.com/ellite/Wallos/compare/v2.42.2...v2.43.0) (2025-01-11)\n\n\n### Features\n\n* new api endpoint that returns the version ([ff13fcb](https://github.com/ellite/Wallos/commit/ff13fcb6547ec4a9c972a2c0f0b6f42d69620f8b))\n* option to show progress of subscription cycle ([ff13fcb](https://github.com/ellite/Wallos/commit/ff13fcb6547ec4a9c972a2c0f0b6f42d69620f8b))\n\n\n### Bug Fixes\n\n* currency symbol for monthly budget ([ff13fcb](https://github.com/ellite/Wallos/commit/ff13fcb6547ec4a9c972a2c0f0b6f42d69620f8b))\n\n## [2.42.2](https://github.com/ellite/Wallos/compare/v2.42.1...v2.42.2) (2024-12-21)\n\n\n### Bug Fixes\n\n* version number ([#668](https://github.com/ellite/Wallos/issues/668)) ([683a366](https://github.com/ellite/Wallos/commit/683a3662ff998066f5d8de3be88e4d40d766442a))\n\n## [2.42.1](https://github.com/ellite/Wallos/compare/v2.42.0...v2.42.1) (2024-12-21)\n\n\n### Bug Fixes\n\n* remove debug echo on stats page ([#666](https://github.com/ellite/Wallos/issues/666)) ([d9a2488](https://github.com/ellite/Wallos/commit/d9a24885ffbbdb3c08d9015804eea8cb0fea6cea))\n\n## [2.42.0](https://github.com/ellite/Wallos/compare/v2.41.0...v2.42.0) (2024-12-21)\n\n\n### Features\n\n* add total monthly cost trend graph to the statistics page ([e7185f9](https://github.com/ellite/Wallos/commit/e7185f92578b3103d097b12b8c4313635f263d9f))\n* allow email notifications without authentication ([e7185f9](https://github.com/ellite/Wallos/commit/e7185f92578b3103d097b12b8c4313635f263d9f))\n\n\n### Bug Fixes\n\n* don't update next payment date for disabled subscriptions ([e7185f9](https://github.com/ellite/Wallos/commit/e7185f92578b3103d097b12b8c4313635f263d9f))\n* xss security vulnerability with the avatar selection ([e7185f9](https://github.com/ellite/Wallos/commit/e7185f92578b3103d097b12b8c4313635f263d9f))\n\n## [2.41.0](https://github.com/ellite/Wallos/compare/v2.40.0...v2.41.0) (2024-12-11)\n\n\n### Features\n\n* add payment cycle to csv/json export ([5e6bc90](https://github.com/ellite/Wallos/commit/5e6bc903bcd95580ed58f744977d92c6330b3d9f))\n* run db migration after importing db ([5e6bc90](https://github.com/ellite/Wallos/commit/5e6bc903bcd95580ed58f744977d92c6330b3d9f))\n* run db migration after restoring database ([5e6bc90](https://github.com/ellite/Wallos/commit/5e6bc903bcd95580ed58f744977d92c6330b3d9f))\n* store weekly the total yearly cost of subscriptions ([5e6bc90](https://github.com/ellite/Wallos/commit/5e6bc903bcd95580ed58f744977d92c6330b3d9f))\n\n\n### Bug Fixes\n\n* double encoding in statistics labels ([5e6bc90](https://github.com/ellite/Wallos/commit/5e6bc903bcd95580ed58f744977d92c6330b3d9f))\n\n## [2.40.0](https://github.com/ellite/Wallos/compare/v2.39.1...v2.40.0) (2024-12-10)\n\n\n### Features\n\n* add dutch translation ([#655](https://github.com/ellite/Wallos/issues/655)) ([b5a9880](https://github.com/ellite/Wallos/commit/b5a98806d1f453180ce15724fa198d248177e488))\n\n## [2.39.1](https://github.com/ellite/Wallos/compare/v2.39.0...v2.39.1) (2024-12-06)\n\n\n### Bug Fixes\n\n* svg error on calendar page ([#650](https://github.com/ellite/Wallos/issues/650)) ([8ba79c0](https://github.com/ellite/Wallos/commit/8ba79c0725815c6de8458c74961bbdf23a7d3e9d))\n\n## [2.39.0](https://github.com/ellite/Wallos/compare/v2.38.3...v2.39.0) (2024-12-06)\n\n\n### Features\n\n* add icalendar subscription ([f5ddbff](https://github.com/ellite/Wallos/commit/f5ddbff0c1e0be676604390101c56c04c778f56a))\n\n## [2.38.3](https://github.com/ellite/Wallos/compare/v2.38.2...v2.38.3) (2024-12-06)\n\n\n### Bug Fixes\n\n* vulnerability on the restore database endpoints ([3b2de8b](https://github.com/ellite/Wallos/commit/3b2de8b7c22090afdf7115c25fd8b497a5626ea3))\n\n## [2.38.2](https://github.com/ellite/Wallos/compare/v2.38.1...v2.38.2) (2024-11-19)\n\n\n### Bug Fixes\n\n* logo search positioned below other elements ([#637](https://github.com/ellite/Wallos/issues/637)) ([72f7e57](https://github.com/ellite/Wallos/commit/72f7e5791423c45f910a791b20aafba301d0172f))\n\n## [2.38.1](https://github.com/ellite/Wallos/compare/v2.38.0...v2.38.1) (2024-11-17)\n\n\n### Bug Fixes\n\n* bug introduced on 2.38.0 on the subscriptions dashboard ([#634](https://github.com/ellite/Wallos/issues/634)) ([f63c543](https://github.com/ellite/Wallos/commit/f63c543cdd7512b216004db3b279884dbda87ce4))\n\n## [2.38.0](https://github.com/ellite/Wallos/compare/v2.37.1...v2.38.0) (2024-11-17)\n\n\n### Features\n\n* add option for manual/automatic renewals ([6e44a26](https://github.com/ellite/Wallos/commit/6e44a26703486d0ba30ee6ae8d3c46bfc3c6630a))\n* add some leeway for totp codes ([6e44a26](https://github.com/ellite/Wallos/commit/6e44a26703486d0ba30ee6ae8d3c46bfc3c6630a))\n* add start date to subscriptions ([6e44a26](https://github.com/ellite/Wallos/commit/6e44a26703486d0ba30ee6ae8d3c46bfc3c6630a))\n\n\n### Bug Fixes\n\n* layout issue with subscriptions list during search ([6e44a26](https://github.com/ellite/Wallos/commit/6e44a26703486d0ba30ee6ae8d3c46bfc3c6630a))\n\n## [2.37.1](https://github.com/ellite/Wallos/compare/v2.37.0...v2.37.1) (2024-11-15)\n\n\n### Bug Fixes\n\n* version mismatch ([#627](https://github.com/ellite/Wallos/issues/627)) ([c4a9b16](https://github.com/ellite/Wallos/commit/c4a9b1627fbc7278398bf2d8bf7cae2934d349ca))\n\n## [2.37.0](https://github.com/ellite/Wallos/compare/v2.36.2...v2.37.0) (2024-11-15)\n\n\n### Features\n\n* add monthly statistics to the calendar page ([f085f8a](https://github.com/ellite/Wallos/commit/f085f8adece3af2548858f665db16d4843d3e622))\n\n\n### Bug Fixes\n\n* notifications being sent on the wrong day ([f085f8a](https://github.com/ellite/Wallos/commit/f085f8adece3af2548858f665db16d4843d3e622))\n\n## [2.36.2](https://github.com/ellite/Wallos/compare/v2.36.1...v2.36.2) (2024-11-03)\n\n\n### Bug Fixes\n\n* only show swipe hint on mobile screens ([#612](https://github.com/ellite/Wallos/issues/612)) ([bd5e351](https://github.com/ellite/Wallos/commit/bd5e3511829a798ab47ca5e9c9d080aae45ae1a0))\n\n## [2.36.1](https://github.com/ellite/Wallos/compare/v2.36.0...v2.36.1) (2024-11-03)\n\n\n### Bug Fixes\n\n* version number ([#610](https://github.com/ellite/Wallos/issues/610)) ([4bd40f1](https://github.com/ellite/Wallos/commit/4bd40f1c561e979322375b95aeccccd18c4780fd))\n\n## [2.36.0](https://github.com/ellite/Wallos/compare/v2.35.0...v2.36.0) (2024-11-03)\n\n\n### Features\n\n* add hint for mobile swipe action ([#608](https://github.com/ellite/Wallos/issues/608)) ([49666f8](https://github.com/ellite/Wallos/commit/49666f867cdbaa4d4c0c1551d0b4b3023830606a))\n\n## [2.35.0](https://github.com/ellite/Wallos/compare/v2.34.0...v2.35.0) (2024-11-01)\n\n\n### Features\n\n* new menu icons ([28444ab](https://github.com/ellite/Wallos/commit/28444abef1cee338e41e57cbf6f13666b917bbde))\n* swipe subscription for actions on the experimental mobile navigation ([28444ab](https://github.com/ellite/Wallos/commit/28444abef1cee338e41e57cbf6f13666b917bbde))\n\n## [2.34.0](https://github.com/ellite/Wallos/compare/v2.33.1...v2.34.0) (2024-10-31)\n\n\n### Features\n\n* link version update banner to github release ([f007adf](https://github.com/ellite/Wallos/commit/f007adf9658eb1fd095c2716e4146130535f6cb7))\n* only show filters that are actually used ([f007adf](https://github.com/ellite/Wallos/commit/f007adf9658eb1fd095c2716e4146130535f6cb7))\n\n\n### Bug Fixes\n\n* filters for categories and payment method respect order from settings ([f007adf](https://github.com/ellite/Wallos/commit/f007adf9658eb1fd095c2716e4146130535f6cb7))\n\n## [2.33.1](https://github.com/ellite/Wallos/compare/v2.33.0...v2.33.1) (2024-10-30)\n\n\n### Bug Fixes\n\n* improve localization ([6480f87](https://github.com/ellite/Wallos/commit/6480f8744094d5ce0f05d7d155925540ac73b156))\n* layout issue on the settings page ([#598](https://github.com/ellite/Wallos/issues/598)) ([6480f87](https://github.com/ellite/Wallos/commit/6480f8744094d5ce0f05d7d155925540ac73b156))\n\n## [2.33.0](https://github.com/ellite/Wallos/compare/v2.32.0...v2.33.0) (2024-10-29)\n\n\n### Features\n\n* replacement for disabled subscriptions, to more accurately calculate savings ([5c92528](https://github.com/ellite/Wallos/commit/5c9252880837a7886c903ddc7ae92c8fed29b452))\n\n## [2.32.0](https://github.com/ellite/Wallos/compare/v2.31.1...v2.32.0) (2024-10-27)\n\n\n### Features\n\n* settings to allow to ignore certificates for some notification methods ([2a0e665](https://github.com/ellite/Wallos/commit/2a0e665e77eca804fa70dafc1a3a0010eb9da270))\n\n## [2.31.1](https://github.com/ellite/Wallos/compare/v2.31.0...v2.31.1) (2024-10-25)\n\n\n### Bug Fixes\n\n* add missing {{days_until}} variable to string version of the webhook ([ebc7b83](https://github.com/ellite/Wallos/commit/ebc7b83e9a0a32aecf3b1aa933408bf9b6baea3a))\n* display actual error message when email test fails ([ebc7b83](https://github.com/ellite/Wallos/commit/ebc7b83e9a0a32aecf3b1aa933408bf9b6baea3a))\n\n## [2.31.0](https://github.com/ellite/Wallos/compare/v2.30.1...v2.31.0) (2024-10-22)\n\n\n### Features\n\n* handle webhook payload as string if it is not a json object ([#583](https://github.com/ellite/Wallos/issues/583)) ([ee834d6](https://github.com/ellite/Wallos/commit/ee834d6198fa3315facd23a734655adf391bb736))\n\n## [2.30.1](https://github.com/ellite/Wallos/compare/v2.30.0...v2.30.1) (2024-10-14)\n\n\n### Bug Fixes\n\n* verify correct path before creating logos folder ([782ebcd](https://github.com/ellite/Wallos/commit/782ebcd64fc947ea82eabaac6bc26a32676271a1))\n\n## [2.30.0](https://github.com/ellite/Wallos/compare/v2.29.2...v2.30.0) (2024-10-13)\n\n\n### Features\n\n* add vietnamese translation ([#573](https://github.com/ellite/Wallos/issues/573)) ([45ff10f](https://github.com/ellite/Wallos/commit/45ff10f953f4af681252ed4d77c32b375f9c396c))\n\n## [2.29.2](https://github.com/ellite/Wallos/compare/v2.29.1...v2.29.2) (2024-10-11)\n\n\n### Bug Fixes\n\n* xss issue on the dashboard ([#568](https://github.com/ellite/Wallos/issues/568)) ([e642129](https://github.com/ellite/Wallos/commit/e6421296aa708b02c468b10e3c9d0f28012c1282))\n\n## [2.29.1](https://github.com/ellite/Wallos/compare/v2.29.0...v2.29.1) (2024-10-11)\n\n\n### Bug Fixes\n\n* mysql injection vulnerability ([3d6a8c3](https://github.com/ellite/Wallos/commit/3d6a8c340843230eff97b459e85efbea55aac01f))\n* new profile page not being cached by service worker ([3d6a8c3](https://github.com/ellite/Wallos/commit/3d6a8c340843230eff97b459e85efbea55aac01f))\n\n## [2.29.0](https://github.com/ellite/Wallos/compare/v2.28.0...v2.29.0) (2024-10-09)\n\n\n### Features\n\n* add url and notes as variables for the notifications webhook ([790defb](https://github.com/ellite/Wallos/commit/790defb2b1d1cd3d8c93738155edb19f96d0aa2a))\n\n\n### Bug Fixes\n\n* bug when looping multiple subscriptions on the notifications webhook ([790defb](https://github.com/ellite/Wallos/commit/790defb2b1d1cd3d8c93738155edb19f96d0aa2a))\n\n## [2.28.0](https://github.com/ellite/Wallos/compare/v2.27.3...v2.28.0) (2024-10-07)\n\n\n### Features\n\n* get admin setting api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get categories endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get currencies endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get fixer api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get household api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get notifications api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get payment methods api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get settings api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get subscriptions api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n* get user api endpoint ([07d456a](https://github.com/ellite/Wallos/commit/07d456a9c3d9cc3eb9ae80edb666caa103cababe))\n\n## [2.27.3](https://github.com/ellite/Wallos/compare/v2.27.2...v2.27.3) (2024-10-05)\n\n\n### Bug Fixes\n\n* missing folders on baremetal installation ([#554](https://github.com/ellite/Wallos/issues/554)) ([03f34d1](https://github.com/ellite/Wallos/commit/03f34d1aee3f74c3bf9c53c04c1494106be4bb47))\n* missing fonts ([03f34d1](https://github.com/ellite/Wallos/commit/03f34d1aee3f74c3bf9c53c04c1494106be4bb47))\n\n## [2.27.2](https://github.com/ellite/Wallos/compare/v2.27.1...v2.27.2) (2024-10-04)\n\n\n### Bug Fixes\n\n* bump version ([#546](https://github.com/ellite/Wallos/issues/546)) ([c5460bd](https://github.com/ellite/Wallos/commit/c5460bd79bdd056e788774ac52cfd4262eada5e7))\n\n## [2.27.1](https://github.com/ellite/Wallos/compare/v2.27.0...v2.27.1) (2024-10-04)\n\n\n### Bug Fixes\n\n* add missing assets to the service worker ([#542](https://github.com/ellite/Wallos/issues/542)) ([0251da2](https://github.com/ellite/Wallos/commit/0251da23f4254420a471fcd4c4951d0d0b1bb4df))\n\n## [2.27.0](https://github.com/ellite/Wallos/compare/v2.26.0...v2.27.0) (2024-10-04)\n\n\n### Features\n\n* api endpoint to calculate monthly cost ([a173d27](https://github.com/ellite/Wallos/commit/a173d2765fd2a1a641f32fbea198775b1bdc0b00))\n* fisrt api endpoint ([a173d27](https://github.com/ellite/Wallos/commit/a173d2765fd2a1a641f32fbea198775b1bdc0b00))\n* redesigned experimental mobile navigation menu ([a173d27](https://github.com/ellite/Wallos/commit/a173d2765fd2a1a641f32fbea198775b1bdc0b00))\n* split settings page into settings and profile page ([a173d27](https://github.com/ellite/Wallos/commit/a173d2765fd2a1a641f32fbea198775b1bdc0b00))\n* user has api key available on profile page ([a173d27](https://github.com/ellite/Wallos/commit/a173d2765fd2a1a641f32fbea198775b1bdc0b00))\n\n\n### Bug Fixes\n\n* small fixes and typos ([a173d27](https://github.com/ellite/Wallos/commit/a173d2765fd2a1a641f32fbea198775b1bdc0b00))\n\n## [2.26.0](https://github.com/ellite/Wallos/compare/v2.25.0...v2.26.0) (2024-09-29)\n\n\n### Features\n\n* add mobile menu navigation to experimental settings ([1dbba18](https://github.com/ellite/Wallos/commit/1dbba18446ac53568492af9d2aee3f90db7168ca))\n* use browsers locale to set dates on the dashboard ([1dbba18](https://github.com/ellite/Wallos/commit/1dbba18446ac53568492af9d2aee3f90db7168ca))\n\n## [2.25.0](https://github.com/ellite/Wallos/compare/v2.24.1...v2.25.0) (2024-09-28)\n\n\n### Features\n\n* add 2fa support ([#525](https://github.com/ellite/Wallos/issues/525)) ([2f16ab3](https://github.com/ellite/Wallos/commit/2f16ab3fdf89b8ba6b1010510d8b169aad425f38))\n\n## [2.24.1](https://github.com/ellite/Wallos/compare/v2.24.0...v2.24.1) (2024-09-23)\n\n\n### Bug Fixes\n\n* small layout issue on the settings page ([0623ceb](https://github.com/ellite/Wallos/commit/0623cebe67182b493770615c518977907e11d359))\n\n## [2.24.0](https://github.com/ellite/Wallos/compare/v2.23.2...v2.24.0) (2024-09-18)\n\n\n### Features\n\n* add button to clean up search field ([da3ee78](https://github.com/ellite/Wallos/commit/da3ee782f13c1eaa98a85de5dbe33714d173a323))\n\n\n### Bug Fixes\n\n* cases where theme and sort cookies could be missing ([da3ee78](https://github.com/ellite/Wallos/commit/da3ee782f13c1eaa98a85de5dbe33714d173a323))\n* position of dropdown on rtl layout ([da3ee78](https://github.com/ellite/Wallos/commit/da3ee782f13c1eaa98a85de5dbe33714d173a323))\n\n## [2.23.2](https://github.com/ellite/Wallos/compare/v2.23.1...v2.23.2) (2024-09-04)\n\n\n### Bug Fixes\n\n* sort order after edit subscription in case the cookie is missing ([87809fe](https://github.com/ellite/Wallos/commit/87809fea71b92c7518173fedd189d7e76ce11bfb))\n\n## [2.23.1](https://github.com/ellite/Wallos/compare/v2.23.0...v2.23.1) (2024-09-01)\n\n\n### Bug Fixes\n\n* warning on top of dashboard page ([#512](https://github.com/ellite/Wallos/issues/512)) ([9056722](https://github.com/ellite/Wallos/commit/905672243b75e6b3d367d439bdbbb37d1b5ae0fa))\n\n## [2.23.0](https://github.com/ellite/Wallos/compare/v2.22.1...v2.23.0) (2024-09-01)\n\n\n### Features\n\n* add multi email recipients ([fed0192](https://github.com/ellite/Wallos/commit/fed0192394e77409dae04d4ab3cdda0ba0c578a4))\n* add option for also showing the original price on the dashboard ([fed0192](https://github.com/ellite/Wallos/commit/fed0192394e77409dae04d4ab3cdda0ba0c578a4))\n* open edit form after cloning subscription ([fed0192](https://github.com/ellite/Wallos/commit/fed0192394e77409dae04d4ab3cdda0ba0c578a4))\n* select multiple filters on the dashboard ([fed0192](https://github.com/ellite/Wallos/commit/fed0192394e77409dae04d4ab3cdda0ba0c578a4))\n\n\n### Bug Fixes\n\n* export.php csv header typo ([#499](https://github.com/ellite/Wallos/issues/499)) ([6e96c5d](https://github.com/ellite/Wallos/commit/6e96c5d4b0c7264ab37a85e9a8b8062f96f69c5c))\n* typo on export subscriptions to csv ([fed0192](https://github.com/ellite/Wallos/commit/fed0192394e77409dae04d4ab3cdda0ba0c578a4))\n\n## [2.22.1](https://github.com/ellite/Wallos/compare/v2.22.0...v2.22.1) (2024-08-11)\n\n\n### Bug Fixes\n\n* inline items in subscription form out of place ([#489](https://github.com/ellite/Wallos/issues/489)) ([3f33ba0](https://github.com/ellite/Wallos/commit/3f33ba0310af0c903db9bef1dd6668146219142c))\n\n## [2.22.0](https://github.com/ellite/Wallos/compare/v2.21.3...v2.22.0) (2024-08-09)\n\n\n### Features\n\n* admin can manually trigger cronjobs ([1946ac9](https://github.com/ellite/Wallos/commit/1946ac9855696892b9a0790d46623614aa9aab2c))\n\n\n### Bug Fixes\n\n* only allow the system and admin to run the cronjobs ([1946ac9](https://github.com/ellite/Wallos/commit/1946ac9855696892b9a0790d46623614aa9aab2c))\n* reduce size of the log files of the cronjobs ([1946ac9](https://github.com/ellite/Wallos/commit/1946ac9855696892b9a0790d46623614aa9aab2c))\n\n## [2.21.3](https://github.com/ellite/Wallos/compare/v2.21.2...v2.21.3) (2024-08-08)\n\n\n### Bug Fixes\n\n* broken avatar upload when using the french language ([cf0d5d3](https://github.com/ellite/Wallos/commit/cf0d5d3df30909a0de7ef84aae2601d805617f90))\n* more deprecation warnings on image uploads ([cf0d5d3](https://github.com/ellite/Wallos/commit/cf0d5d3df30909a0de7ef84aae2601d805617f90))\n\n## [2.21.2](https://github.com/ellite/Wallos/compare/v2.21.1...v2.21.2) (2024-08-07)\n\n\n### Bug Fixes\n\n* add samesite directive to cookies ([8b0325c](https://github.com/ellite/Wallos/commit/8b0325c7d3c672754de220efd52b9ba9de8a9868))\n* service worker precaching logout.php causes user to be logged out ([8b0325c](https://github.com/ellite/Wallos/commit/8b0325c7d3c672754de220efd52b9ba9de8a9868))\n* sort by price ([8b0325c](https://github.com/ellite/Wallos/commit/8b0325c7d3c672754de220efd52b9ba9de8a9868))\n\n## [2.21.1](https://github.com/ellite/Wallos/compare/v2.21.0...v2.21.1) (2024-08-06)\n\n\n### Bug Fixes\n\n* deprecation message for null value ([#479](https://github.com/ellite/Wallos/issues/479)) ([0274b1d](https://github.com/ellite/Wallos/commit/0274b1d5257f8f1c4156e2a342df6acf177ad726))\n\n## [2.21.0](https://github.com/ellite/Wallos/compare/v2.20.1...v2.21.0) (2024-08-06)\n\n\n### Features\n\n* add option to list disabled subscriptions at the bottom ([3281f0c](https://github.com/ellite/Wallos/commit/3281f0ce35fbea237e21221d3a9026ed96ad84e5))\n* notification for wallos version updates ([3281f0c](https://github.com/ellite/Wallos/commit/3281f0ce35fbea237e21221d3a9026ed96ad84e5))\n\n## [2.20.1](https://github.com/ellite/Wallos/compare/v2.20.0...v2.20.1) (2024-07-29)\n\n\n### Bug Fixes\n\n* allow usernames with capital letters ([f241ba2](https://github.com/ellite/Wallos/commit/f241ba23018ee910ab859b2ce860b4c0678d6402))\n* use 2 decimal places for price on the calendar ([f241ba2](https://github.com/ellite/Wallos/commit/f241ba23018ee910ab859b2ce860b4c0678d6402))\n* use 2 decimal places for price when exporting ical in the calendar ([f241ba2](https://github.com/ellite/Wallos/commit/f241ba23018ee910ab859b2ce860b4c0678d6402))\n\n## [2.20.0](https://github.com/ellite/Wallos/compare/v2.19.3...v2.20.0) (2024-07-19)\n\n\n### Features\n\n* export subscriptions as csv ([8f1e155](https://github.com/ellite/Wallos/commit/8f1e1554787c6e3ffaf7e73369a66794c0636713))\n* export subscriptions as json ([8f1e155](https://github.com/ellite/Wallos/commit/8f1e1554787c6e3ffaf7e73369a66794c0636713))\n* user can delete their own account ([8f1e155](https://github.com/ellite/Wallos/commit/8f1e1554787c6e3ffaf7e73369a66794c0636713))\n\n## [2.19.3](https://github.com/ellite/Wallos/compare/v2.19.2...v2.19.3) (2024-07-15)\n\n\n### Bug Fixes\n\n* delete button on subscription form ([#460](https://github.com/ellite/Wallos/issues/460)) ([8cb4355](https://github.com/ellite/Wallos/commit/8cb43553fd2d3328fe9b1f7c5986e040071844c0))\n\n## [2.19.2](https://github.com/ellite/Wallos/compare/v2.19.1...v2.19.2) (2024-07-15)\n\n\n### Bug Fixes\n\n* test ntfy without custom headers ([#456](https://github.com/ellite/Wallos/issues/456)) ([8fcfc92](https://github.com/ellite/Wallos/commit/8fcfc9264726ec1ded81ca2c51daa65ae9f4e7d8))\n\n## [2.19.1](https://github.com/ellite/Wallos/compare/v2.19.0...v2.19.1) (2024-07-14)\n\n\n### Bug Fixes\n\n* unset sortOrder var ([a1fab4d](https://github.com/ellite/Wallos/commit/a1fab4dd1067f80054a2c52710edb859dba47127))\n\n## [2.19.0](https://github.com/ellite/Wallos/compare/v2.18.0...v2.19.0) (2024-07-14)\n\n\n### Features\n\n* add alphanumeric sort order for subscriptions ([#449](https://github.com/ellite/Wallos/issues/449)) ([775e6ee](https://github.com/ellite/Wallos/commit/775e6ee39457edef420d5c36fb310a75fd47bff6))\n\n## [2.18.0](https://github.com/ellite/Wallos/compare/v2.17.0...v2.18.0) (2024-07-14)\n\n\n### Features\n\n* disable display options checkbox when fixer key is not set ([5f10525](https://github.com/ellite/Wallos/commit/5f1052584b5ece93ebdcb5bce32210e2643a9f26))\n* display error message on the statistics page when the fixer key is needed but is missing ([5f10525](https://github.com/ellite/Wallos/commit/5f1052584b5ece93ebdcb5bce32210e2643a9f26))\n\n## [2.17.0](https://github.com/ellite/Wallos/compare/v2.16.1...v2.17.0) (2024-07-11)\n\n\n### Features\n\n* add filter and sort dashboard by subscription state ([afff992](https://github.com/ellite/Wallos/commit/afff992878287fdc51229297c455d1f69216c36e))\n\n\n### Bug Fixes\n\n* use the same font for inputs ([a539058](https://github.com/ellite/Wallos/commit/a5390580259105f14154b0d7ce1eb13631c471b1))\n\n## [2.16.1](https://github.com/ellite/Wallos/compare/v2.16.0...v2.16.1) (2024-07-10)\n\n\n### Bug Fixes\n\n* error when logos folder is empty ([#439](https://github.com/ellite/Wallos/issues/439)) ([e2e5061](https://github.com/ellite/Wallos/commit/e2e5061d1506652384ceed018aa4330b8548b792))\n\n## [2.16.0](https://github.com/ellite/Wallos/compare/v2.15.0...v2.16.0) (2024-07-10)\n\n\n### Features\n\n* add calendar to pwa shortcuts ([21ebf29](https://github.com/ellite/Wallos/commit/21ebf29f11405ab24b1b0ffd16eb667de4dfc189))\n* change apple touch icon ([21ebf29](https://github.com/ellite/Wallos/commit/21ebf29f11405ab24b1b0ffd16eb667de4dfc189))\n\n## [2.15.0](https://github.com/ellite/Wallos/compare/v2.14.2...v2.15.0) (2024-07-09)\n\n\n### Features\n\n* add maintenance tasks to admin page ([9f7f47b](https://github.com/ellite/Wallos/commit/9f7f47b5d1be2697c2c612bfddb6119c63a3d517))\n* add support to upload svg logos ([9f7f47b](https://github.com/ellite/Wallos/commit/9f7f47b5d1be2697c2c612bfddb6119c63a3d517))\n\n## [2.14.2](https://github.com/ellite/Wallos/compare/v2.14.1...v2.14.2) (2024-07-08)\n\n\n### Bug Fixes\n\n* broken subscription update query ([#431](https://github.com/ellite/Wallos/issues/431)) ([b00a985](https://github.com/ellite/Wallos/commit/b00a9855453663aeb2f1f4b7f0db3aca3994b12b))\n\n## [2.14.1](https://github.com/ellite/Wallos/compare/v2.14.0...v2.14.1) (2024-07-05)\n\n\n### Bug Fixes\n\n* dashboard scrolling to top when opening a subscription ([#427](https://github.com/ellite/Wallos/issues/427)) ([cb03af8](https://github.com/ellite/Wallos/commit/cb03af8e46fb5ec5138ed7ef729f4b56a23d2b37))\n\n## [2.14.0](https://github.com/ellite/Wallos/compare/v2.13.0...v2.14.0) (2024-07-05)\n\n\n### Features\n\n* add cancelation reminders ([#425](https://github.com/ellite/Wallos/issues/425)) ([c393146](https://github.com/ellite/Wallos/commit/c393146d9e3d494943de32ecd86983335358cf88))\n\n## [2.13.0](https://github.com/ellite/Wallos/compare/v2.12.0...v2.13.0) (2024-07-04)\n\n\n### Features\n\n* uniformize layout and styles (+ checkboxes and radios) ([#423](https://github.com/ellite/Wallos/issues/423)) ([c166c7e](https://github.com/ellite/Wallos/commit/c166c7e84c06ceba5ab21341c8d56bd1aaf042ec))\n\n## [2.12.0](https://github.com/ellite/Wallos/compare/v2.11.2...v2.12.0) (2024-07-03)\n\n\n### Features\n\n* ability to add custom css styles ([50bd104](https://github.com/ellite/Wallos/commit/50bd104b5b990605f457b540bec95eff5034473d))\n* cache logos for offline use ([50bd104](https://github.com/ellite/Wallos/commit/50bd104b5b990605f457b540bec95eff5034473d))\n* more uniform and aligned styles on the settings page ([50bd104](https://github.com/ellite/Wallos/commit/50bd104b5b990605f457b540bec95eff5034473d))\n* rework styles of theme section on settings page ([50bd104](https://github.com/ellite/Wallos/commit/50bd104b5b990605f457b540bec95eff5034473d))\n\n\n### Bug Fixes\n\n* don't allow saving main and accent colors if they're the same ([50bd104](https://github.com/ellite/Wallos/commit/50bd104b5b990605f457b540bec95eff5034473d))\n\n## [2.11.2](https://github.com/ellite/Wallos/compare/v2.11.1...v2.11.2) (2024-07-02)\n\n\n### Bug Fixes\n\n* menus checkmark position ([#419](https://github.com/ellite/Wallos/issues/419)) ([4da5d47](https://github.com/ellite/Wallos/commit/4da5d47e3ce8b8564921c07e7b785a367d378d6b))\n\n## [2.11.1](https://github.com/ellite/Wallos/compare/v2.11.0...v2.11.1) (2024-06-30)\n\n\n### Bug Fixes\n\n* syntax error on svg logo ([#417](https://github.com/ellite/Wallos/issues/417)) ([b82f750](https://github.com/ellite/Wallos/commit/b82f750c8e844012a8a12e33f01719f42199e7ce))\n\n## [2.11.0](https://github.com/ellite/Wallos/compare/v2.10.0...v2.11.0) (2024-06-30)\n\n\n### Features\n\n* theming engine custom colors now affect icons as well ([83e2066](https://github.com/ellite/Wallos/commit/83e2066e7bee99a152cc3c22f5b1dd9c9866c9fd))\n\n## [2.10.0](https://github.com/ellite/Wallos/compare/v2.9.0...v2.10.0) (2024-06-27)\n\n\n### Features\n\n* add purple theme ([4d74c04](https://github.com/ellite/Wallos/commit/4d74c04f0e5bab5e1ece7a4a666f14d4a221fba6))\n\n\n### Bug Fixes\n\n* file name on ics export for subscriptions with non-ascii characters ([4d74c04](https://github.com/ellite/Wallos/commit/4d74c04f0e5bab5e1ece7a4a666f14d4a221fba6))\n\n## [2.9.0](https://github.com/ellite/Wallos/compare/v2.8.0...v2.9.0) (2024-06-26)\n\n\n### Features\n\n* create users from the admin page ([#409](https://github.com/ellite/Wallos/issues/409)) ([6d2ffa6](https://github.com/ellite/Wallos/commit/6d2ffa6312b05f308117f2686681e2fcfaf734ec))\n\n## [2.8.0](https://github.com/ellite/Wallos/compare/v2.7.0...v2.8.0) (2024-06-26)\n\n\n### Features\n\n* also show previous payments on the calendar for the current month ([c2e85d6](https://github.com/ellite/Wallos/commit/c2e85d6e109d9d07cc2fdbcb09b51564d1f73341))\n* support automatic dark mode ([c2e85d6](https://github.com/ellite/Wallos/commit/c2e85d6e109d9d07cc2fdbcb09b51564d1f73341))\n\n\n### Bug Fixes\n\n* not every payment cycle was shown on the calendar ([c2e85d6](https://github.com/ellite/Wallos/commit/c2e85d6e109d9d07cc2fdbcb09b51564d1f73341))\n\n## [2.7.0](https://github.com/ellite/Wallos/compare/v2.6.1...v2.7.0) (2024-06-25)\n\n\n### Features\n\n* export subscription as ics from the calendar view ([#404](https://github.com/ellite/Wallos/issues/404)) ([f1360f7](https://github.com/ellite/Wallos/commit/f1360f7d468ef5ae7e974ec1f9bb77831ea322bb))\n\n## [2.6.1](https://github.com/ellite/Wallos/compare/v2.6.0...v2.6.1) (2024-06-25)\n\n\n### Bug Fixes\n\n* load php calendar extension ([#402](https://github.com/ellite/Wallos/issues/402)) ([c02ac77](https://github.com/ellite/Wallos/commit/c02ac770d7ac9fad1baec526b5d7dd71deaba59b))\n\n## [2.6.0](https://github.com/ellite/Wallos/compare/v2.5.2...v2.6.0) (2024-06-25)\n\n\n### Features\n\n* add calendar view ([#399](https://github.com/ellite/Wallos/issues/399)) ([369f1a2](https://github.com/ellite/Wallos/commit/369f1a2bdcd9bdf3996b3dc8de8921f8954a069d))\n\n## [2.5.2](https://github.com/ellite/Wallos/compare/v2.5.1...v2.5.2) (2024-06-24)\n\n\n### Bug Fixes\n\n* add ability to run container as an arbitrary user ([#396](https://github.com/ellite/Wallos/issues/396)) ([86fe2f3](https://github.com/ellite/Wallos/commit/86fe2f3ebb9c38ac34eaccd144a9550b7b314138))\n\n## [2.5.1](https://github.com/ellite/Wallos/compare/v2.5.0...v2.5.1) (2024-06-21)\n\n\n### Bug Fixes\n\n* ntfy notifications ([#394](https://github.com/ellite/Wallos/issues/394)) ([17722c3](https://github.com/ellite/Wallos/commit/17722c31e31eec035d8896566e9eb5596951d022))\n\n## [2.5.0](https://github.com/ellite/Wallos/compare/v2.4.2...v2.5.0) (2024-06-21)\n\n\n### Features\n\n* add option to clone subscription ([8304ed7](https://github.com/ellite/Wallos/commit/8304ed7b54f50ed7fa5ab520ff4d8d54f3ef34df))\n* edit and delete options now available directly on the subscription list ([8304ed7](https://github.com/ellite/Wallos/commit/8304ed7b54f50ed7fa5ab520ff4d8d54f3ef34df))\n\n\n### Bug Fixes\n\n* typo on webhook payload ([8304ed7](https://github.com/ellite/Wallos/commit/8304ed7b54f50ed7fa5ab520ff4d8d54f3ef34df))\n\n## [2.4.2](https://github.com/ellite/Wallos/compare/v2.4.1...v2.4.2) (2024-06-10)\n\n\n### Bug Fixes\n\n* update exchange cron only working for one user ([#384](https://github.com/ellite/Wallos/issues/384)) ([815eea7](https://github.com/ellite/Wallos/commit/815eea7e7be37e068e6173c229eb285ed8b7c30d))\n\n## [2.4.1](https://github.com/ellite/Wallos/compare/v2.4.0...v2.4.1) (2024-06-09)\n\n\n### Bug Fixes\n\n* cronjob exchange update would not work with apilayer ([#381](https://github.com/ellite/Wallos/issues/381)) ([b0b4b7a](https://github.com/ellite/Wallos/commit/b0b4b7a65cd479e7532e72e826d3c01aead403c3))\n\n## [2.4.0](https://github.com/ellite/Wallos/compare/v2.3.0...v2.4.0) (2024-06-07)\n\n\n### Features\n\n* add hability to disable login ([#378](https://github.com/ellite/Wallos/issues/378)) ([092be22](https://github.com/ellite/Wallos/commit/092be22183359f714fc9638d9013b742da828ed6))\n\n## [2.3.0](https://github.com/ellite/Wallos/compare/v2.2.0...v2.3.0) (2024-06-05)\n\n\n### Features\n\n* add ntfy as notification method ([#377](https://github.com/ellite/Wallos/issues/377)) ([65edf09](https://github.com/ellite/Wallos/commit/65edf0963b73deff0f0f7f04427e69ce335bd776))\n\n\n### Bug Fixes\n\n* custom headers for webhook notifications ([#375](https://github.com/ellite/Wallos/issues/375)) ([7217088](https://github.com/ellite/Wallos/commit/7217088bb0732735a65322bce136d7d556b1acf3))\n\n## [2.2.0](https://github.com/ellite/Wallos/compare/v2.1.0...v2.2.0) (2024-06-04)\n\n\n### Features\n\n* change filename of backup file ([fa99a73](https://github.com/ellite/Wallos/commit/fa99a735cd23918bab95baaf13b7a3142946d4b2))\n* frequency is now up to 366 ([fa99a73](https://github.com/ellite/Wallos/commit/fa99a735cd23918bab95baaf13b7a3142946d4b2))\n\n\n### Bug Fixes\n\n* add webp support to gd on the container ([fa99a73](https://github.com/ellite/Wallos/commit/fa99a735cd23918bab95baaf13b7a3142946d4b2))\n* translate: \"no category\" ([fa99a73](https://github.com/ellite/Wallos/commit/fa99a735cd23918bab95baaf13b7a3142946d4b2))\n* trim fixer api key ([fa99a73](https://github.com/ellite/Wallos/commit/fa99a735cd23918bab95baaf13b7a3142946d4b2))\n* update slovanian translations ([fa99a73](https://github.com/ellite/Wallos/commit/fa99a735cd23918bab95baaf13b7a3142946d4b2))\n\n## [2.1.0](https://github.com/ellite/Wallos/compare/v2.0.0...v2.1.0) (2024-05-27)\n\n\n### Features\n\n* add slovenian translation ([03ceb8a](https://github.com/ellite/Wallos/commit/03ceb8a6e64c8cd4deb4019668fbf98acb57c5fe))\n\n\n### Bug Fixes\n\n* currency conversion failing on the statistics page ([03ceb8a](https://github.com/ellite/Wallos/commit/03ceb8a6e64c8cd4deb4019668fbf98acb57c5fe))\n\n## [2.0.0](https://github.com/ellite/Wallos/compare/v1.29.1...v2.0.0) (2024-05-26)\n\n\n### ⚠ BREAKING CHANGES\n\n* allow registration of multiple users ([#340](https://github.com/ellite/Wallos/issues/340))\n\n### Features\n\n* add reset password functionality ([e1006e5](https://github.com/ellite/Wallos/commit/e1006e582388a7fab204f25c100347607b863e4e))\n* administration area ([e1006e5](https://github.com/ellite/Wallos/commit/e1006e582388a7fab204f25c100347607b863e4e))\n* allow registration of multiple users ([#340](https://github.com/ellite/Wallos/issues/340)) ([e1006e5](https://github.com/ellite/Wallos/commit/e1006e582388a7fab204f25c100347607b863e4e))\n\n## [1.29.1](https://github.com/ellite/Wallos/compare/v1.29.0...v1.29.1) (2024-05-20)\n\n\n### Bug Fixes\n\n* calling htmlspecialchars_decode on null objects ([#338](https://github.com/ellite/Wallos/issues/338)) ([5050a28](https://github.com/ellite/Wallos/commit/5050a28f0e64e8c1eefb4f7cca8f6f6e473177e3))\n\n## [1.29.0](https://github.com/ellite/Wallos/compare/v1.28.0...v1.29.0) (2024-05-20)\n\n\n### Features\n\n* subscriptions have personalized notification times ([#334](https://github.com/ellite/Wallos/issues/334)) ([c7146df](https://github.com/ellite/Wallos/commit/c7146dfd08c2a60d4ff6f7ac1f7cf5830fe28d9c))\n\n## [1.28.0](https://github.com/ellite/Wallos/compare/v1.27.2...v1.28.0) (2024-05-17)\n\n\n### Features\n\n* add monthly budget field and statistics ([#329](https://github.com/ellite/Wallos/issues/329)) ([b622434](https://github.com/ellite/Wallos/commit/b622434ca0791d5c8026d641e1b32f8a2f0f42b8))\n\n## [1.27.2](https://github.com/ellite/Wallos/compare/v1.27.1...v1.27.2) (2024-05-17)\n\n\n### Bug Fixes\n\n* duplicated messages on discord notifications ([d44b40b](https://github.com/ellite/Wallos/commit/d44b40b0ce80e91821fe7441c85e0d8794680618))\n* possible division by 0 on statistics page ([d44b40b](https://github.com/ellite/Wallos/commit/d44b40b0ce80e91821fe7441c85e0d8794680618))\n\n## [1.27.1](https://github.com/ellite/Wallos/compare/v1.27.0...v1.27.1) (2024-05-13)\n\n\n### Bug Fixes\n\n* import of translations for cronjobs was missing ([#321](https://github.com/ellite/Wallos/issues/321)) ([a524419](https://github.com/ellite/Wallos/commit/a524419e0a468147a2094dba81689dd643a0108b))\n\n## [1.27.0](https://github.com/ellite/Wallos/compare/v1.26.2...v1.27.0) (2024-05-11)\n\n\n### Features\n\n* add korean translation ([#314](https://github.com/ellite/Wallos/issues/314)) ([bc40320](https://github.com/ellite/Wallos/commit/bc403206905b39c3aa88f3eb51e59b41e2a5e24e))\n\n## [1.26.2](https://github.com/ellite/Wallos/compare/v1.26.1...v1.26.2) (2024-05-09)\n\n\n### Bug Fixes\n\n* russian translations ([#309](https://github.com/ellite/Wallos/issues/309)) ([8f890fc](https://github.com/ellite/Wallos/commit/8f890fc5d3a62a91feec50564179b3241ed538bf))\n\n## [1.26.1](https://github.com/ellite/Wallos/compare/v1.26.0...v1.26.1) (2024-05-09)\n\n\n### Bug Fixes\n\n* background removal experimental setting ([#307](https://github.com/ellite/Wallos/issues/307)) ([bb5ee2e](https://github.com/ellite/Wallos/commit/bb5ee2e64c11b1415da3aa50119dfaa3783be37f))\n\n## [1.26.0](https://github.com/ellite/Wallos/compare/v1.25.1...v1.26.0) (2024-05-08)\n\n\n### Features\n\n* add russian translation ([#305](https://github.com/ellite/Wallos/issues/305)) ([ae04d50](https://github.com/ellite/Wallos/commit/ae04d50329c1fb0117e186f89fef38b495cbbe9c))\n\n## [1.25.1](https://github.com/ellite/Wallos/compare/v1.25.0...v1.25.1) (2024-05-07)\n\n\n### Bug Fixes\n\n* broken discord form ([#302](https://github.com/ellite/Wallos/issues/302)) ([b435d6a](https://github.com/ellite/Wallos/commit/b435d6a5cf6f80404c487b519334b2854aab9713))\n\n## [1.25.0](https://github.com/ellite/Wallos/compare/v1.24.0...v1.25.0) (2024-05-06)\n\n\n### Features\n\n* add discord and pushover as notification agents ([#300](https://github.com/ellite/Wallos/issues/300)) ([8994829](https://github.com/ellite/Wallos/commit/899482982e7e200f5a7081ed6285475e5cb2a37d))\n\n\n### Bug Fixes\n\n* most error messages of the notifications endpoints would not reach the frontend ([8994829](https://github.com/ellite/Wallos/commit/899482982e7e200f5a7081ed6285475e5cb2a37d))\n\n## [1.24.0](https://github.com/ellite/Wallos/compare/v1.23.0...v1.24.0) (2024-05-05)\n\n\n### Features\n\n* add new notification methods (telegram, webhooks, gotify) ([#295](https://github.com/ellite/Wallos/issues/295)) ([a408031](https://github.com/ellite/Wallos/commit/a408031ef8711bf87e9f8db35f52c498f250b235))\n\n## [1.23.0](https://github.com/ellite/Wallos/compare/v1.22.0...v1.23.0) (2024-04-26)\n\n\n### Features\n\n* backup and restore ([#288](https://github.com/ellite/Wallos/issues/288)) ([7b509d2](https://github.com/ellite/Wallos/commit/7b509d2b3d769e14a9cb4fd183395dcecc9d993b))\n\n## [1.22.0](https://github.com/ellite/Wallos/compare/v1.21.1...v1.22.0) (2024-04-20)\n\n\n### Features\n\n* option to hide disabled subscriptions ([#286](https://github.com/ellite/Wallos/issues/286)) ([b80ab4b](https://github.com/ellite/Wallos/commit/b80ab4bdc662c3e80a2fd42b8b286b69beac441c))\n\n## [1.21.1](https://github.com/ellite/Wallos/compare/v1.21.0...v1.21.1) (2024-04-19)\n\n\n### Bug Fixes\n\n* small layout issues ([769f8a0](https://github.com/ellite/Wallos/commit/769f8a0587941bffd0d7463b7e7ffeb38a70e301))\n\n## [1.21.0](https://github.com/ellite/Wallos/compare/v1.20.2...v1.21.0) (2024-04-19)\n\n\n### Features\n\n* add italian translation ([70e4234](https://github.com/ellite/Wallos/commit/70e42349caee5d6647b6b704643fe2b5e26dff4e))\n* add themes and custom color options ([70e4234](https://github.com/ellite/Wallos/commit/70e42349caee5d6647b6b704643fe2b5e26dff4e))\n\n## [1.20.2](https://github.com/ellite/Wallos/compare/v1.20.1...v1.20.2) (2024-04-11)\n\n\n### Bug Fixes\n\n* encoding for url and notes ([#273](https://github.com/ellite/Wallos/issues/273)) ([ad86eb5](https://github.com/ellite/Wallos/commit/ad86eb5b9c6e60004de2795170032d62b33ddcfb))\n\n## [1.20.1](https://github.com/ellite/Wallos/compare/v1.20.0...v1.20.1) (2024-04-09)\n\n\n### Bug Fixes\n\n* special chars in subscriptions ([#271](https://github.com/ellite/Wallos/issues/271)) ([2683a7c](https://github.com/ellite/Wallos/commit/2683a7c4ba3c3575347d48f2c97b92b2ff0cc9f9))\n\n## [1.20.0](https://github.com/ellite/Wallos/compare/v1.19.0...v1.20.0) (2024-04-07)\n\n\n### Features\n\n* add serbian translation ([#268](https://github.com/ellite/Wallos/issues/268)) ([55089c0](https://github.com/ellite/Wallos/commit/55089c0715ca315feb6a8795b07d9c36167494de))\n\n## [1.19.0](https://github.com/ellite/Wallos/compare/v1.18.3...v1.19.0) (2024-04-03)\n\n\n### Features\n\n* add polish translation ([#263](https://github.com/ellite/Wallos/issues/263)) ([c752761](https://github.com/ellite/Wallos/commit/c7527610fafa49b18076971befa246b2730b79c4))\n\n## [1.18.3](https://github.com/ellite/Wallos/compare/v1.18.2...v1.18.3) (2024-03-30)\n\n\n### Bug Fixes\n\n* on initial registration page, logo can be cut off ([#258](https://github.com/ellite/Wallos/issues/258)) ([dde8695](https://github.com/ellite/Wallos/commit/dde8695fb555f483ef8bc8f24db2a610301bab16))\n\n## [1.18.2](https://github.com/ellite/Wallos/compare/v1.18.1...v1.18.2) (2024-03-28)\n\n\n### Bug Fixes\n\n* small icon size for payment icons ([#253](https://github.com/ellite/Wallos/issues/253)) ([8998e23](https://github.com/ellite/Wallos/commit/8998e23d370165ca158600550dbf0eb8c07d4bac))\n\n## [1.18.1](https://github.com/ellite/Wallos/compare/v1.18.0...v1.18.1) (2024-03-25)\n\n\n### Bug Fixes\n\n* disabled inputs on dark theme ([#250](https://github.com/ellite/Wallos/issues/250)) ([11f0e7c](https://github.com/ellite/Wallos/commit/11f0e7ce63f37adb922e530a54f3e5cc9f640eee))\n\n## [1.18.0](https://github.com/ellite/Wallos/compare/v1.17.3...v1.18.0) (2024-03-24)\n\n\n### Features\n\n* add custom avatar functionality ([#248](https://github.com/ellite/Wallos/issues/248)) ([1dbebd3](https://github.com/ellite/Wallos/commit/1dbebd3918ef6f27961f4e70b6ad007133f8ff93))\n\n## [1.17.3](https://github.com/ellite/Wallos/compare/v1.17.2...v1.17.3) (2024-03-20)\n\n\n### Bug Fixes\n\n* next payment date not updating for disabled subscriptions ([#243](https://github.com/ellite/Wallos/issues/243)) ([75a5672](https://github.com/ellite/Wallos/commit/75a5672de32a59cc53c3c76a08793e6a33cce828))\n\n## [1.17.2](https://github.com/ellite/Wallos/compare/v1.17.1...v1.17.2) (2024-03-18)\n\n\n### Bug Fixes\n\n* pwa not loading static files when offline ([#241](https://github.com/ellite/Wallos/issues/241)) ([4e3376d](https://github.com/ellite/Wallos/commit/4e3376df93ea7c2b3e184b2670ebe77fe9b15d6a))\n\n## [1.17.1](https://github.com/ellite/Wallos/compare/v1.17.0...v1.17.1) (2024-03-18)\n\n\n### Bug Fixes\n\n* cronjobs running twice ([#239](https://github.com/ellite/Wallos/issues/239)) ([00cbf8d](https://github.com/ellite/Wallos/commit/00cbf8d9e3feac87292630f8db4571a99b542db4))\n\n## [1.17.0](https://github.com/ellite/Wallos/compare/v1.16.3...v1.17.0) (2024-03-17)\n\n\n### Features\n\n* allow selecting tls or ssl for email notifications ([#237](https://github.com/ellite/Wallos/issues/237)) ([2462435](https://github.com/ellite/Wallos/commit/246243574328ead6d95d45b81b055761b01040a7))\n\n## [1.16.3](https://github.com/ellite/Wallos/compare/v1.16.2...v1.16.3) (2024-03-17)\n\n\n### Bug Fixes\n\n* allow redirects on logo search ([ae73db7](https://github.com/ellite/Wallos/commit/ae73db77907786993f52f7273145dafa660c4d36))\n* rename category after adding and sort order of categories ([ae73db7](https://github.com/ellite/Wallos/commit/ae73db77907786993f52f7273145dafa660c4d36))\n\n## [1.16.2](https://github.com/ellite/Wallos/compare/v1.16.1...v1.16.2) (2024-03-13)\n\n\n### Bug Fixes\n\n* wrong folder for payment method logos ([#227](https://github.com/ellite/Wallos/issues/227)) ([f6c1ff2](https://github.com/ellite/Wallos/commit/f6c1ff2a6be6545c6c179722235db3cd724127fd))\n\n## [1.16.1](https://github.com/ellite/Wallos/compare/v1.16.0...v1.16.1) (2024-03-12)\n\n\n### Bug Fixes\n\n* confusing wording for billing cycle ([94ad0cb](https://github.com/ellite/Wallos/commit/94ad0cb553d7f05b15e9ab27fbf4c26955fc3ff1))\n\n## [1.16.0](https://github.com/ellite/Wallos/compare/v1.15.3...v1.16.0) (2024-03-10)\n\n\n### Features\n\n* allow sorting payment methods ([#217](https://github.com/ellite/Wallos/issues/217)) ([aef2d13](https://github.com/ellite/Wallos/commit/aef2d134c22f7dc95821ff711f7bca56228bfed6))\n* don't allow to change currency code if in use ([aef2d13](https://github.com/ellite/Wallos/commit/aef2d134c22f7dc95821ff711f7bca56228bfed6))\n\n## [1.15.3](https://github.com/ellite/Wallos/compare/v1.15.2...v1.15.3) (2024-03-10)\n\n\n### Bug Fixes\n\n* sql injection vulnerability when using filters ([#214](https://github.com/ellite/Wallos/issues/214)) ([cbdc188](https://github.com/ellite/Wallos/commit/cbdc188e5e7a2c357f5b0bcaeaf2e886cd2555e3))\n\n## [1.15.2](https://github.com/ellite/Wallos/compare/v1.15.1...v1.15.2) (2024-03-09)\n\n\n### Bug Fixes\n\n* undefined var on the statistics page ([#211](https://github.com/ellite/Wallos/issues/211)) ([8b7a7b9](https://github.com/ellite/Wallos/commit/8b7a7b94e3ae9177be6d067d8fee0a05aa428f4a))\n\n## [1.15.1](https://github.com/ellite/Wallos/compare/v1.15.0...v1.15.1) (2024-03-09)\n\n\n### Bug Fixes\n\n* undefined var if sort cookie is not set ([#207](https://github.com/ellite/Wallos/issues/207)) ([288c106](https://github.com/ellite/Wallos/commit/288c10624592aa04cc76cb8ae066331d65964650))\n\n## [1.15.0](https://github.com/ellite/Wallos/compare/v1.14.1...v1.15.0) (2024-03-09)\n\n\n### Features\n\n* filters on the subscriptions page ([a396285](https://github.com/ellite/Wallos/commit/a396285b76cd87e598495f311a81dc68a7f66d36))\n* search subscriptions by name ([a396285](https://github.com/ellite/Wallos/commit/a396285b76cd87e598495f311a81dc68a7f66d36))\n\n## [1.14.1](https://github.com/ellite/Wallos/compare/v1.14.0...v1.14.1) (2024-03-08)\n\n\n### Bug Fixes\n\n* wrong message when deleting payment methods ([#202](https://github.com/ellite/Wallos/issues/202)) ([93a3d18](https://github.com/ellite/Wallos/commit/93a3d189794985c1d8cfd5558c482f66e79405a8))\n\n## [1.14.0](https://github.com/ellite/Wallos/compare/v1.13.0...v1.14.0) (2024-03-08)\n\n\n### Features\n\n* add brazilian portuguese to available languages ([#198](https://github.com/ellite/Wallos/issues/198)) ([3ea9d98](https://github.com/ellite/Wallos/commit/3ea9d98da79e9b13ab9d93a56b89062ac19c31d7))\n\n## [1.13.0](https://github.com/ellite/Wallos/compare/v1.12.1...v1.13.0) (2024-03-07)\n\n\n### Features\n\n* show name of most expensive subscription on statistics ([#194](https://github.com/ellite/Wallos/issues/194)) ([ede08b1](https://github.com/ellite/Wallos/commit/ede08b1f6ae2d52ac0f8e1aaa77edc1924f529ce))\n\n## [1.12.1](https://github.com/ellite/Wallos/compare/v1.12.0...v1.12.1) (2024-03-06)\n\n\n### Bug Fixes\n\n* broken chinese language file ([#192](https://github.com/ellite/Wallos/issues/192)) ([94c1a91](https://github.com/ellite/Wallos/commit/94c1a91387ca05fad3a50e5f318d8439c7608cbe))\n\n## [1.12.0](https://github.com/ellite/Wallos/compare/v1.11.3...v1.12.0) (2024-03-05)\n\n\n### Features\n\n* add filters to statistics page ([83234ab](https://github.com/ellite/Wallos/commit/83234ab8cd184f4693a148dc55bddef300c49e71))\n* allow deletion of the default payment methods ([83234ab](https://github.com/ellite/Wallos/commit/83234ab8cd184f4693a148dc55bddef300c49e71))\n* allow renaming / translation of payment methods ([83234ab](https://github.com/ellite/Wallos/commit/83234ab8cd184f4693a148dc55bddef300c49e71))\n* allow sorting of categories in settings ([83234ab](https://github.com/ellite/Wallos/commit/83234ab8cd184f4693a148dc55bddef300c49e71))\n\n## [1.11.3](https://github.com/ellite/Wallos/compare/v1.11.2...v1.11.3) (2024-03-02)\n\n\n### Bug Fixes\n\n* redirects with the service worker ([#183](https://github.com/ellite/Wallos/issues/183)) ([940bbbe](https://github.com/ellite/Wallos/commit/940bbbea9071a7c2687a3340bb8e9d6f4f884cc1))\n\n## [1.11.2](https://github.com/ellite/Wallos/compare/v1.11.1...v1.11.2) (2024-03-02)\n\n\n### Bug Fixes\n\n* file upload bypass vulnerability ([#181](https://github.com/ellite/Wallos/issues/181)) ([0f7853f](https://github.com/ellite/Wallos/commit/0f7853f961ba2f68f8dcd358acaad6c6eb7980e6))\n\n## [1.11.1](https://github.com/ellite/Wallos/compare/v1.11.0...v1.11.1) (2024-03-01)\n\n\n### Bug Fixes\n\n* security issue with image upload ([#175](https://github.com/ellite/Wallos/issues/175)) ([7b5e166](https://github.com/ellite/Wallos/commit/7b5e166e289f32b1b3451614b16e1f4c0b9d6f2a))\n\n## [1.11.0](https://github.com/ellite/Wallos/compare/v1.10.0...v1.11.0) (2024-03-01)\n\n\n### Features\n\n* added custom payment methods ([#173](https://github.com/ellite/Wallos/issues/173)) ([e739622](https://github.com/ellite/Wallos/commit/e73962260678caf0843b6302f7fbb7d49469a1a9))\n\n## [1.10.0](https://github.com/ellite/Wallos/compare/v1.9.1...v1.10.0) (2024-02-29)\n\n\n### Features\n\n* use brave search for the logos if google fails ([#169](https://github.com/ellite/Wallos/issues/169)) ([fff783e](https://github.com/ellite/Wallos/commit/fff783e4e87f04199817c7cb3b4bd28760d2b5f3))\n\n## [1.9.1](https://github.com/ellite/Wallos/compare/v1.9.0...v1.9.1) (2024-02-28)\n\n\n### Bug Fixes\n\n* move display settings to the bottom ([ec25d4b](https://github.com/ellite/Wallos/commit/ec25d4bc5a35f68ff15d456ae6a1d3e98d124f5f))\n* reorder subscription form ([ec25d4b](https://github.com/ellite/Wallos/commit/ec25d4bc5a35f68ff15d456ae6a1d3e98d124f5f))\n* show email field on adding household member ([ec25d4b](https://github.com/ellite/Wallos/commit/ec25d4bc5a35f68ff15d456ae6a1d3e98d124f5f))\n\n## [1.9.0](https://github.com/ellite/Wallos/compare/v1.8.3...v1.9.0) (2024-02-27)\n\n\n### Features\n\n* enable progressive web app ([a2a315e](https://github.com/ellite/Wallos/commit/a2a315e34dca2562bc11793cc5841c2082e811a9))\n\n\n### Bug Fixes\n\n* update packages to fix vulnerabilities ([a2a315e](https://github.com/ellite/Wallos/commit/a2a315e34dca2562bc11793cc5841c2082e811a9))\n\n## [1.8.3](https://github.com/ellite/Wallos/compare/v1.8.2...v1.8.3) (2024-02-26)\n\n\n### Bug Fixes\n\n* remove service worker ([#157](https://github.com/ellite/Wallos/issues/157)) ([5ccadce](https://github.com/ellite/Wallos/commit/5ccadce2f139e5873889badc51a67bfaef8a9304))\n\n## [1.8.2](https://github.com/ellite/Wallos/compare/v1.8.1...v1.8.2) (2024-02-26)\n\n\n### Bug Fixes\n\n* service worker redirect not set to follow ([3640b54](https://github.com/ellite/Wallos/commit/3640b547ee3ca28e7b872b9e2dbbcd1d31c54953))\n\n## [1.8.1](https://github.com/ellite/Wallos/compare/v1.8.0...v1.8.1) (2024-02-26)\n\n\n### Bug Fixes\n\n* service worker has redirections ([4aca7bc](https://github.com/ellite/Wallos/commit/4aca7bcb3cdbb77958db8783c4f088df131db645))\n\n## [1.8.0](https://github.com/ellite/Wallos/compare/v1.7.0...v1.8.0) (2024-02-26)\n\n\n### Features\n\n* convert wallos into a progressive web app ([#151](https://github.com/ellite/Wallos/issues/151)) ([19e2058](https://github.com/ellite/Wallos/commit/19e205897617ee894d8802f7e73fef46be386c30))\n\n\n### Bug Fixes\n\n* improve traditional chinese translations ([19e2058](https://github.com/ellite/Wallos/commit/19e205897617ee894d8802f7e73fef46be386c30))\n\n## [1.7.0](https://github.com/ellite/Wallos/compare/v1.6.0...v1.7.0) (2024-02-25)\n\n\n### Features\n\n* add email for notifications to household members ([26363dd](https://github.com/ellite/Wallos/commit/26363dd5f364b5494c526a9769626b03bba45273))\n\n## [1.6.0](https://github.com/ellite/Wallos/compare/v1.5.0...v1.6.0) (2024-02-24)\n\n\n### Features\n\n* add stats about inactive subscriptions ([#146](https://github.com/ellite/Wallos/issues/146)) ([ccac17a](https://github.com/ellite/Wallos/commit/ccac17a6f222cb1ee022fd30b7a1d34306dd0de2))\n* sort disabled subscription at the bottom ([ccac17a](https://github.com/ellite/Wallos/commit/ccac17a6f222cb1ee022fd30b7a1d34306dd0de2))\n\n## [1.5.0](https://github.com/ellite/Wallos/compare/v1.4.1...v1.5.0) (2024-02-23)\n\n\n### Features\n\n* allow to disable subscriptions ([#144](https://github.com/ellite/Wallos/issues/144)) ([50056d9](https://github.com/ellite/Wallos/commit/50056d9f03a46c166650474b3877b55a24873bb9))\n\n## [1.4.1](https://github.com/ellite/Wallos/compare/v1.4.0...v1.4.1) (2024-02-22)\n\n\n### Bug Fixes\n\n* bug on saving fixer api key ([#142](https://github.com/ellite/Wallos/issues/142)) ([866eb28](https://github.com/ellite/Wallos/commit/866eb28e88495e851336b5e224274a823ff4173d))\n\n## [1.4.0](https://github.com/ellite/Wallos/compare/v1.3.1...v1.4.0) (2024-02-21)\n\n\n### Features\n\n* persist display and experimental settings on the db ([f0a6f1a](https://github.com/ellite/Wallos/commit/f0a6f1a2f18b329c9f784a9f1953cd0e7616e1c6))\n* small styles changed ([f0a6f1a](https://github.com/ellite/Wallos/commit/f0a6f1a2f18b329c9f784a9f1953cd0e7616e1c6))\n\n## [1.3.1](https://github.com/ellite/Wallos/compare/v1.3.0...v1.3.1) (2024-02-20)\n\n\n### Bug Fixes\n\n* missing authentication check ([#133](https://github.com/ellite/Wallos/issues/133)) ([b887d3a](https://github.com/ellite/Wallos/commit/b887d3a0503585dadde4b1b59b023c981b0f7f66))\n\n## [1.3.0](https://github.com/ellite/Wallos/compare/v1.2.0...v1.3.0) (2024-02-19)\n\n\n### Features\n\n* add apilayer as provider for fixer api ([0f19dd6](https://github.com/ellite/Wallos/commit/0f19dd688fe3a2156e7d26d1bf1e1f8b30ce79ad))\n* add apilayer as provider for fixer api ([#127](https://github.com/ellite/Wallos/issues/127)) ([0f19dd6](https://github.com/ellite/Wallos/commit/0f19dd688fe3a2156e7d26d1bf1e1f8b30ce79ad))\n* update exchange rate when saving api key ([0f19dd6](https://github.com/ellite/Wallos/commit/0f19dd688fe3a2156e7d26d1bf1e1f8b30ce79ad))\n\n## [1.2.0](https://github.com/ellite/Wallos/compare/v1.1.0...v1.2.0) (2024-02-19)\n\n\n### Features\n\n* enable deployment in subdirectory ([e2af9af](https://github.com/ellite/Wallos/commit/e2af9afc32bfc248f594336c50d44ad6f36f197e))\n\n## [1.1.0](https://github.com/ellite/Wallos/compare/v1.0.1...v1.1.0) (2024-02-18)\n\n\n### Features\n\n* new statistics per payment method ([#124](https://github.com/ellite/Wallos/issues/124)) ([6200fa5](https://github.com/ellite/Wallos/commit/6200fa5e87d3f60853c3d8b95f5d676e39b378f4))\n\n## [1.0.1](https://github.com/ellite/Wallos/compare/v1.0.0...v1.0.1) (2024-02-18)\n\n\n### Bug Fixes\n\n* show translated no category when sorting by category ([#122](https://github.com/ellite/Wallos/issues/122)) ([330c061](https://github.com/ellite/Wallos/commit/330c061b74ad1580173f3d3bc7b14048492e22d2))\n\n## 1.0.0 (2024-02-15)\n\n\n### Features\n\n* add workflow for building and publishing docker images ([970c96a](https://github.com/ellite/Wallos/commit/970c96a8c904809544c944071986be2a684daf50))\n* specify image stability type when triggering build ([5b22cfd](https://github.com/ellite/Wallos/commit/5b22cfd87a94a865f53b282964961862bbea1861))\n\n\n### Bug Fixes\n\n* Currency not preselected on registration ([fc56cf6](https://github.com/ellite/Wallos/commit/fc56cf69ef22a07978022265b2e8344dc293eb14))\n* Language sort order ([884a8e5](https://github.com/ellite/Wallos/commit/884a8e569339ddbcb89af4634c0c845b053affbb))\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to wallos\n\nWe welcome contributions from the community and look forward to working with you to improve this project!\n\n## How to Contribute\n\n1.  **Fork the repository:** Start by forking the wallos repository to your own GitHub account.\n2.  **Clone your fork:** Clone the forked repository to your local machine (replace <YOUR_USERNAME> with your actual github username):\n\n    ```bash\n    git clone https://github.com/<YOUR_USERNAME>/wallos.git\n    cd wallos\n    ```\n\n3.  **Create a branch:** Create a new branch for your changes:\n\n    ```bash\n    git checkout -b feature/your-feature-name\n    ```\n\n    or\n\n    ```bash\n    git checkout -b fix/your-bug-fix-name\n    ```\n\n4.  **Make your changes:** Implement your feature or bug fix.\n5.  **Test your changes:** Ensure that your changes work as expected.\n6.  **Commit your changes:** Commit your changes with a clear and concise message:\n\n    ```bash\n    git add .\n    git commit -m \"Add your feature or fix\"\n    ```\n\n7.  **Push your changes:** Push your branch to your forked repository:\n\n    ```bash\n    git push origin feature/your-feature-name\n    ```\n\n8.  **Create a Pull Request:** Go to the wallos repository on GitHub (https://github.com/ellite/wallos) and create a pull request from your branch to the `main` branch.\n\n## Pull Request Guidelines\n\n* **One feature/fix per pull request:** Please keep pull requests focused on a single feature or bug fix.\n* **Clear and descriptive title and description:** Provide a clear title and description of your changes.\n* **Include relevant tests:** If possible, include tests for your changes.\n* **Follow the project's coding style:** Adhere to the project's coding style and conventions.\n* **Keep your pull request up to date:** If changes are requested, please update your pull request accordingly.\n\n## Issues\n\n* **Bug Reports:** If you find a bug, please open an issue with a clear description of the problem and steps to reproduce it.\n* **Feature Requests:** If you have a feature request, please open an issue with a clear description of the feature and its benefits.\n* **Priority:** Bug fixes will take priority over feature requests.\n\n## Translations\n\nIf you want to contribute with a translation of wallos:\n\n1.  **Add your language code:**\n    * Open `includes/i18n/languages.php`.\n    * Add your language code in the format: `\"<language_code>\" => [\"name\" => \"<Language Name>\", \"dir\" => \"<ltr or rtl>\"],`.\n    * Please use the original language name and not the English translation.\n    * Example: `\"pt\" => [\"name\" => \"Português\", \"dir\" => \"ltr\"],`.\n\n2.  **Create language files:**\n    * Copy `includes/i18n/en.php` and rename it to your language code (e.g., `pt.php`).\n    * Translate all the values in the new language file.\n    * Copy `scripts/i18n/en.js` and rename it to your language code (e.g., `pt.js`).\n    * Translate all the values in the new javascript language file.\n    * **Note:** Incomplete translations will not be accepted.\n\n3.  **Create a Pull Request:** Follow the Pull Request Guidelines above.\n\n## Contributors\n\n<a href=\"https://github.com/ellite/wallos/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=ellite/wallos\" />\n</a>\n\n\nThank you for your contributions!\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Use the php:8.3-fpm-alpine base image\nFROM php:8.3-fpm-alpine\n\n# Set working directory to /var/www/html\nWORKDIR /var/www/html\n\n# Update packages and install dependencies\nRUN apk upgrade --no-cache && \\\n    apk add --no-cache dumb-init shadow sqlite-dev libpng libpng-dev libjpeg-turbo libjpeg-turbo-dev freetype freetype-dev curl autoconf libgomp icu-dev icu-data-full nginx dcron tzdata imagemagick imagemagick-dev libzip-dev sqlite libwebp-dev && \\\n    docker-php-ext-install pdo pdo_sqlite calendar && \\\n    docker-php-ext-enable pdo pdo_sqlite && \\\n    docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp && \\\n    docker-php-ext-install -j$(nproc) gd intl zip && \\\n    apk add --no-cache --virtual .build-deps $PHPIZE_DEPS && \\\n    pecl install imagick && \\\n    docker-php-ext-enable imagick && \\\n    apk del .build-deps\n\n# Copy your PHP application files into the container\nCOPY . .\n\n# Copy Nginx configuration\nCOPY nginx.conf /etc/nginx/nginx.conf\nCOPY nginx.default.conf /etc/nginx/http.d/default.conf\n\n# Remove nginx conf files from webroot\nRUN rm -rf /var/www/html/nginx.conf && \\\n    rm -rf /var/www/html/nginx.default.conf\n\n# Copy the custom crontab file\nCOPY cronjobs /etc/cron.d/cronjobs\n\n# Convert the line endings, allow read access to the cron file, and create cron log folder\nRUN dos2unix /etc/cron.d/cronjobs && \\\n    chmod 0644 /etc/cron.d/cronjobs && \\\n    /usr/bin/crontab /etc/cron.d/cronjobs && \\\n    mkdir /var/log/cron && \\\n    chown -R www-data:www-data /var/www/html && \\\n    chmod +x /var/www/html/startup.sh && \\\n    echo 'pm.max_children = 15' >> /usr/local/etc/php-fpm.d/zz-docker.conf && \\\n    echo 'pm.max_requests = 500' >> /usr/local/etc/php-fpm.d/zz-docker.conf\n\n# Expose port 80 for Nginx\nEXPOSE 80\n\nENTRYPOINT [\"dumb-init\", \"--\"]\n\n# Requires docker engine 25+ for the --start-interval flag\nHEALTHCHECK --interval=2m --timeout=2s --start-period=20s --start-interval=5s --retries=3 \\\n    CMD [\"curl\", \"-fsS\", \"http://127.0.0.1/health.php\"]\n\n# Start both PHP-FPM, Nginx\nCMD [\"/var/www/html/startup.sh\"]\n"
  },
  {
    "path": "LICENSE.md",
    "content": "GNU GENERAL PUBLIC LICENSE\r\n                       Version 3, 29 June 2007\r\n\r\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\r\n Everyone is permitted to copy and distribute verbatim copies\r\n of this license document, but changing it is not allowed.\r\n\r\n                            Preamble\r\n\r\n  The GNU General Public License is a free, copyleft license for\r\nsoftware and other kinds of works.\r\n\r\n  The licenses for most software and other practical works are designed\r\nto take away your freedom to share and change the works.  By contrast,\r\nthe GNU General Public License is intended to guarantee your freedom to\r\nshare and change all versions of a program--to make sure it remains free\r\nsoftware for all its users.  We, the Free Software Foundation, use the\r\nGNU General Public License for most of our software; it applies also to\r\nany other work released this way by its authors.  You can apply it to\r\nyour programs, too.\r\n\r\n  When we speak of free software, we are referring to freedom, not\r\nprice.  Our General Public Licenses are designed to make sure that you\r\nhave the freedom to distribute copies of free software (and charge for\r\nthem if you wish), that you receive source code or can get it if you\r\nwant it, that you can change the software or use pieces of it in new\r\nfree programs, and that you know you can do these things.\r\n\r\n  To protect your rights, we need to prevent others from denying you\r\nthese rights or asking you to surrender the rights.  Therefore, you have\r\ncertain responsibilities if you distribute copies of the software, or if\r\nyou modify it: responsibilities to respect the freedom of others.\r\n\r\n  For example, if you distribute copies of such a program, whether\r\ngratis or for a fee, you must pass on to the recipients the same\r\nfreedoms that you received.  You must make sure that they, too, receive\r\nor can get the source code.  And you must show them these terms so they\r\nknow their rights.\r\n\r\n  Developers that use the GNU GPL protect your rights with two steps:\r\n(1) assert copyright on the software, and (2) offer you this License\r\ngiving you legal permission to copy, distribute and/or modify it.\r\n\r\n  For the developers' and authors' protection, the GPL clearly explains\r\nthat there is no warranty for this free software.  For both users' and\r\nauthors' sake, the GPL requires that modified versions be marked as\r\nchanged, so that their problems will not be attributed erroneously to\r\nauthors of previous versions.\r\n\r\n  Some devices are designed to deny users access to install or run\r\nmodified versions of the software inside them, although the manufacturer\r\ncan do so.  This is fundamentally incompatible with the aim of\r\nprotecting users' freedom to change the software.  The systematic\r\npattern of such abuse occurs in the area of products for individuals to\r\nuse, which is precisely where it is most unacceptable.  Therefore, we\r\nhave designed this version of the GPL to prohibit the practice for those\r\nproducts.  If such problems arise substantially in other domains, we\r\nstand ready to extend this provision to those domains in future versions\r\nof the GPL, as needed to protect the freedom of users.\r\n\r\n  Finally, every program is threatened constantly by software patents.\r\nStates should not allow patents to restrict development and use of\r\nsoftware on general-purpose computers, but in those that do, we wish to\r\navoid the special danger that patents applied to a free program could\r\nmake it effectively proprietary.  To prevent this, the GPL assures that\r\npatents cannot be used to render the program non-free.\r\n\r\n  The precise terms and conditions for copying, distribution and\r\nmodification follow.\r\n\r\n                       TERMS AND CONDITIONS\r\n\r\n  0. Definitions.\r\n\r\n  \"This License\" refers to version 3 of the GNU General Public License.\r\n\r\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\r\nworks, such as semiconductor masks.\r\n\r\n  \"The Program\" refers to any copyrightable work licensed under this\r\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\r\n\"recipients\" may be individuals or organizations.\r\n\r\n  To \"modify\" a work means to copy from or adapt all or part of the work\r\nin a fashion requiring copyright permission, other than the making of an\r\nexact copy.  The resulting work is called a \"modified version\" of the\r\nearlier work or a work \"based on\" the earlier work.\r\n\r\n  A \"covered work\" means either the unmodified Program or a work based\r\non the Program.\r\n\r\n  To \"propagate\" a work means to do anything with it that, without\r\npermission, would make you directly or secondarily liable for\r\ninfringement under applicable copyright law, except executing it on a\r\ncomputer or modifying a private copy.  Propagation includes copying,\r\ndistribution (with or without modification), making available to the\r\npublic, and in some countries other activities as well.\r\n\r\n  To \"convey\" a work means any kind of propagation that enables other\r\nparties to make or receive copies.  Mere interaction with a user through\r\na computer network, with no transfer of a copy, is not conveying.\r\n\r\n  An interactive user interface displays \"Appropriate Legal Notices\"\r\nto the extent that it includes a convenient and prominently visible\r\nfeature that (1) displays an appropriate copyright notice, and (2)\r\ntells the user that there is no warranty for the work (except to the\r\nextent that warranties are provided), that licensees may convey the\r\nwork under this License, and how to view a copy of this License.  If\r\nthe interface presents a list of user commands or options, such as a\r\nmenu, a prominent item in the list meets this criterion.\r\n\r\n  1. Source Code.\r\n\r\n  The \"source code\" for a work means the preferred form of the work\r\nfor making modifications to it.  \"Object code\" means any non-source\r\nform of a work.\r\n\r\n  A \"Standard Interface\" means an interface that either is an official\r\nstandard defined by a recognized standards body, or, in the case of\r\ninterfaces specified for a particular programming language, one that\r\nis widely used among developers working in that language.\r\n\r\n  The \"System Libraries\" of an executable work include anything, other\r\nthan the work as a whole, that (a) is included in the normal form of\r\npackaging a Major Component, but which is not part of that Major\r\nComponent, and (b) serves only to enable use of the work with that\r\nMajor Component, or to implement a Standard Interface for which an\r\nimplementation is available to the public in source code form.  A\r\n\"Major Component\", in this context, means a major essential component\r\n(kernel, window system, and so on) of the specific operating system\r\n(if any) on which the executable work runs, or a compiler used to\r\nproduce the work, or an object code interpreter used to run it.\r\n\r\n  The \"Corresponding Source\" for a work in object code form means all\r\nthe source code needed to generate, install, and (for an executable\r\nwork) run the object code and to modify the work, including scripts to\r\ncontrol those activities.  However, it does not include the work's\r\nSystem Libraries, or general-purpose tools or generally available free\r\nprograms which are used unmodified in performing those activities but\r\nwhich are not part of the work.  For example, Corresponding Source\r\nincludes interface definition files associated with source files for\r\nthe work, and the source code for shared libraries and dynamically\r\nlinked subprograms that the work is specifically designed to require,\r\nsuch as by intimate data communication or control flow between those\r\nsubprograms and other parts of the work.\r\n\r\n  The Corresponding Source need not include anything that users\r\ncan regenerate automatically from other parts of the Corresponding\r\nSource.\r\n\r\n  The Corresponding Source for a work in source code form is that\r\nsame work.\r\n\r\n  2. Basic Permissions.\r\n\r\n  All rights granted under this License are granted for the term of\r\ncopyright on the Program, and are irrevocable provided the stated\r\nconditions are met.  This License explicitly affirms your unlimited\r\npermission to run the unmodified Program.  The output from running a\r\ncovered work is covered by this License only if the output, given its\r\ncontent, constitutes a covered work.  This License acknowledges your\r\nrights of fair use or other equivalent, as provided by copyright law.\r\n\r\n  You may make, run and propagate covered works that you do not\r\nconvey, without conditions so long as your license otherwise remains\r\nin force.  You may convey covered works to others for the sole purpose\r\nof having them make modifications exclusively for you, or provide you\r\nwith facilities for running those works, provided that you comply with\r\nthe terms of this License in conveying all material for which you do\r\nnot control copyright.  Those thus making or running the covered works\r\nfor you must do so exclusively on your behalf, under your direction\r\nand control, on terms that prohibit them from making any copies of\r\nyour copyrighted material outside their relationship with you.\r\n\r\n  Conveying under any other circumstances is permitted solely under\r\nthe conditions stated below.  Sublicensing is not allowed; section 10\r\nmakes it unnecessary.\r\n\r\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\r\n\r\n  No covered work shall be deemed part of an effective technological\r\nmeasure under any applicable law fulfilling obligations under article\r\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\r\nsimilar laws prohibiting or restricting circumvention of such\r\nmeasures.\r\n\r\n  When you convey a covered work, you waive any legal power to forbid\r\ncircumvention of technological measures to the extent such circumvention\r\nis effected by exercising rights under this License with respect to\r\nthe covered work, and you disclaim any intention to limit operation or\r\nmodification of the work as a means of enforcing, against the work's\r\nusers, your or third parties' legal rights to forbid circumvention of\r\ntechnological measures.\r\n\r\n  4. Conveying Verbatim Copies.\r\n\r\n  You may convey verbatim copies of the Program's source code as you\r\nreceive it, in any medium, provided that you conspicuously and\r\nappropriately publish on each copy an appropriate copyright notice;\r\nkeep intact all notices stating that this License and any\r\nnon-permissive terms added in accord with section 7 apply to the code;\r\nkeep intact all notices of the absence of any warranty; and give all\r\nrecipients a copy of this License along with the Program.\r\n\r\n  You may charge any price or no price for each copy that you convey,\r\nand you may offer support or warranty protection for a fee.\r\n\r\n  5. Conveying Modified Source Versions.\r\n\r\n  You may convey a work based on the Program, or the modifications to\r\nproduce it from the Program, in the form of source code under the\r\nterms of section 4, provided that you also meet all of these conditions:\r\n\r\n    a) The work must carry prominent notices stating that you modified\r\n    it, and giving a relevant date.\r\n\r\n    b) The work must carry prominent notices stating that it is\r\n    released under this License and any conditions added under section\r\n    7.  This requirement modifies the requirement in section 4 to\r\n    \"keep intact all notices\".\r\n\r\n    c) You must license the entire work, as a whole, under this\r\n    License to anyone who comes into possession of a copy.  This\r\n    License will therefore apply, along with any applicable section 7\r\n    additional terms, to the whole of the work, and all its parts,\r\n    regardless of how they are packaged.  This License gives no\r\n    permission to license the work in any other way, but it does not\r\n    invalidate such permission if you have separately received it.\r\n\r\n    d) If the work has interactive user interfaces, each must display\r\n    Appropriate Legal Notices; however, if the Program has interactive\r\n    interfaces that do not display Appropriate Legal Notices, your\r\n    work need not make them do so.\r\n\r\n  A compilation of a covered work with other separate and independent\r\nworks, which are not by their nature extensions of the covered work,\r\nand which are not combined with it such as to form a larger program,\r\nin or on a volume of a storage or distribution medium, is called an\r\n\"aggregate\" if the compilation and its resulting copyright are not\r\nused to limit the access or legal rights of the compilation's users\r\nbeyond what the individual works permit.  Inclusion of a covered work\r\nin an aggregate does not cause this License to apply to the other\r\nparts of the aggregate.\r\n\r\n  6. Conveying Non-Source Forms.\r\n\r\n  You may convey a covered work in object code form under the terms\r\nof sections 4 and 5, provided that you also convey the\r\nmachine-readable Corresponding Source under the terms of this License,\r\nin one of these ways:\r\n\r\n    a) Convey the object code in, or embodied in, a physical product\r\n    (including a physical distribution medium), accompanied by the\r\n    Corresponding Source fixed on a durable physical medium\r\n    customarily used for software interchange.\r\n\r\n    b) Convey the object code in, or embodied in, a physical product\r\n    (including a physical distribution medium), accompanied by a\r\n    written offer, valid for at least three years and valid for as\r\n    long as you offer spare parts or customer support for that product\r\n    model, to give anyone who possesses the object code either (1) a\r\n    copy of the Corresponding Source for all the software in the\r\n    product that is covered by this License, on a durable physical\r\n    medium customarily used for software interchange, for a price no\r\n    more than your reasonable cost of physically performing this\r\n    conveying of source, or (2) access to copy the\r\n    Corresponding Source from a network server at no charge.\r\n\r\n    c) Convey individual copies of the object code with a copy of the\r\n    written offer to provide the Corresponding Source.  This\r\n    alternative is allowed only occasionally and noncommercially, and\r\n    only if you received the object code with such an offer, in accord\r\n    with subsection 6b.\r\n\r\n    d) Convey the object code by offering access from a designated\r\n    place (gratis or for a charge), and offer equivalent access to the\r\n    Corresponding Source in the same way through the same place at no\r\n    further charge.  You need not require recipients to copy the\r\n    Corresponding Source along with the object code.  If the place to\r\n    copy the object code is a network server, the Corresponding Source\r\n    may be on a different server (operated by you or a third party)\r\n    that supports equivalent copying facilities, provided you maintain\r\n    clear directions next to the object code saying where to find the\r\n    Corresponding Source.  Regardless of what server hosts the\r\n    Corresponding Source, you remain obligated to ensure that it is\r\n    available for as long as needed to satisfy these requirements.\r\n\r\n    e) Convey the object code using peer-to-peer transmission, provided\r\n    you inform other peers where the object code and Corresponding\r\n    Source of the work are being offered to the general public at no\r\n    charge under subsection 6d.\r\n\r\n  A separable portion of the object code, whose source code is excluded\r\nfrom the Corresponding Source as a System Library, need not be\r\nincluded in conveying the object code work.\r\n\r\n  A \"User Product\" is either (1) a \"consumer product\", which means any\r\ntangible personal property which is normally used for personal, family,\r\nor household purposes, or (2) anything designed or sold for incorporation\r\ninto a dwelling.  In determining whether a product is a consumer product,\r\ndoubtful cases shall be resolved in favor of coverage.  For a particular\r\nproduct received by a particular user, \"normally used\" refers to a\r\ntypical or common use of that class of product, regardless of the status\r\nof the particular user or of the way in which the particular user\r\nactually uses, or expects or is expected to use, the product.  A product\r\nis a consumer product regardless of whether the product has substantial\r\ncommercial, industrial or non-consumer uses, unless such uses represent\r\nthe only significant mode of use of the product.\r\n\r\n  \"Installation Information\" for a User Product means any methods,\r\nprocedures, authorization keys, or other information required to install\r\nand execute modified versions of a covered work in that User Product from\r\na modified version of its Corresponding Source.  The information must\r\nsuffice to ensure that the continued functioning of the modified object\r\ncode is in no case prevented or interfered with solely because\r\nmodification has been made.\r\n\r\n  If you convey an object code work under this section in, or with, or\r\nspecifically for use in, a User Product, and the conveying occurs as\r\npart of a transaction in which the right of possession and use of the\r\nUser Product is transferred to the recipient in perpetuity or for a\r\nfixed term (regardless of how the transaction is characterized), the\r\nCorresponding Source conveyed under this section must be accompanied\r\nby the Installation Information.  But this requirement does not apply\r\nif neither you nor any third party retains the ability to install\r\nmodified object code on the User Product (for example, the work has\r\nbeen installed in ROM).\r\n\r\n  The requirement to provide Installation Information does not include a\r\nrequirement to continue to provide support service, warranty, or updates\r\nfor a work that has been modified or installed by the recipient, or for\r\nthe User Product in which it has been modified or installed.  Access to a\r\nnetwork may be denied when the modification itself materially and\r\nadversely affects the operation of the network or violates the rules and\r\nprotocols for communication across the network.\r\n\r\n  Corresponding Source conveyed, and Installation Information provided,\r\nin accord with this section must be in a format that is publicly\r\ndocumented (and with an implementation available to the public in\r\nsource code form), and must require no special password or key for\r\nunpacking, reading or copying.\r\n\r\n  7. Additional Terms.\r\n\r\n  \"Additional permissions\" are terms that supplement the terms of this\r\nLicense by making exceptions from one or more of its conditions.\r\nAdditional permissions that are applicable to the entire Program shall\r\nbe treated as though they were included in this License, to the extent\r\nthat they are valid under applicable law.  If additional permissions\r\napply only to part of the Program, that part may be used separately\r\nunder those permissions, but the entire Program remains governed by\r\nthis License without regard to the additional permissions.\r\n\r\n  When you convey a copy of a covered work, you may at your option\r\nremove any additional permissions from that copy, or from any part of\r\nit.  (Additional permissions may be written to require their own\r\nremoval in certain cases when you modify the work.)  You may place\r\nadditional permissions on material, added by you to a covered work,\r\nfor which you have or can give appropriate copyright permission.\r\n\r\n  Notwithstanding any other provision of this License, for material you\r\nadd to a covered work, you may (if authorized by the copyright holders of\r\nthat material) supplement the terms of this License with terms:\r\n\r\n    a) Disclaiming warranty or limiting liability differently from the\r\n    terms of sections 15 and 16 of this License; or\r\n\r\n    b) Requiring preservation of specified reasonable legal notices or\r\n    author attributions in that material or in the Appropriate Legal\r\n    Notices displayed by works containing it; or\r\n\r\n    c) Prohibiting misrepresentation of the origin of that material, or\r\n    requiring that modified versions of such material be marked in\r\n    reasonable ways as different from the original version; or\r\n\r\n    d) Limiting the use for publicity purposes of names of licensors or\r\n    authors of the material; or\r\n\r\n    e) Declining to grant rights under trademark law for use of some\r\n    trade names, trademarks, or service marks; or\r\n\r\n    f) Requiring indemnification of licensors and authors of that\r\n    material by anyone who conveys the material (or modified versions of\r\n    it) with contractual assumptions of liability to the recipient, for\r\n    any liability that these contractual assumptions directly impose on\r\n    those licensors and authors.\r\n\r\n  All other non-permissive additional terms are considered \"further\r\nrestrictions\" within the meaning of section 10.  If the Program as you\r\nreceived it, or any part of it, contains a notice stating that it is\r\ngoverned by this License along with a term that is a further\r\nrestriction, you may remove that term.  If a license document contains\r\na further restriction but permits relicensing or conveying under this\r\nLicense, you may add to a covered work material governed by the terms\r\nof that license document, provided that the further restriction does\r\nnot survive such relicensing or conveying.\r\n\r\n  If you add terms to a covered work in accord with this section, you\r\nmust place, in the relevant source files, a statement of the\r\nadditional terms that apply to those files, or a notice indicating\r\nwhere to find the applicable terms.\r\n\r\n  Additional terms, permissive or non-permissive, may be stated in the\r\nform of a separately written license, or stated as exceptions;\r\nthe above requirements apply either way.\r\n\r\n  8. Termination.\r\n\r\n  You may not propagate or modify a covered work except as expressly\r\nprovided under this License.  Any attempt otherwise to propagate or\r\nmodify it is void, and will automatically terminate your rights under\r\nthis License (including any patent licenses granted under the third\r\nparagraph of section 11).\r\n\r\n  However, if you cease all violation of this License, then your\r\nlicense from a particular copyright holder is reinstated (a)\r\nprovisionally, unless and until the copyright holder explicitly and\r\nfinally terminates your license, and (b) permanently, if the copyright\r\nholder fails to notify you of the violation by some reasonable means\r\nprior to 60 days after the cessation.\r\n\r\n  Moreover, your license from a particular copyright holder is\r\nreinstated permanently if the copyright holder notifies you of the\r\nviolation by some reasonable means, this is the first time you have\r\nreceived notice of violation of this License (for any work) from that\r\ncopyright holder, and you cure the violation prior to 30 days after\r\nyour receipt of the notice.\r\n\r\n  Termination of your rights under this section does not terminate the\r\nlicenses of parties who have received copies or rights from you under\r\nthis License.  If your rights have been terminated and not permanently\r\nreinstated, you do not qualify to receive new licenses for the same\r\nmaterial under section 10.\r\n\r\n  9. Acceptance Not Required for Having Copies.\r\n\r\n  You are not required to accept this License in order to receive or\r\nrun a copy of the Program.  Ancillary propagation of a covered work\r\noccurring solely as a consequence of using peer-to-peer transmission\r\nto receive a copy likewise does not require acceptance.  However,\r\nnothing other than this License grants you permission to propagate or\r\nmodify any covered work.  These actions infringe copyright if you do\r\nnot accept this License.  Therefore, by modifying or propagating a\r\ncovered work, you indicate your acceptance of this License to do so.\r\n\r\n  10. Automatic Licensing of Downstream Recipients.\r\n\r\n  Each time you convey a covered work, the recipient automatically\r\nreceives a license from the original licensors, to run, modify and\r\npropagate that work, subject to this License.  You are not responsible\r\nfor enforcing compliance by third parties with this License.\r\n\r\n  An \"entity transaction\" is a transaction transferring control of an\r\norganization, or substantially all assets of one, or subdividing an\r\norganization, or merging organizations.  If propagation of a covered\r\nwork results from an entity transaction, each party to that\r\ntransaction who receives a copy of the work also receives whatever\r\nlicenses to the work the party's predecessor in interest had or could\r\ngive under the previous paragraph, plus a right to possession of the\r\nCorresponding Source of the work from the predecessor in interest, if\r\nthe predecessor has it or can get it with reasonable efforts.\r\n\r\n  You may not impose any further restrictions on the exercise of the\r\nrights granted or affirmed under this License.  For example, you may\r\nnot impose a license fee, royalty, or other charge for exercise of\r\nrights granted under this License, and you may not initiate litigation\r\n(including a cross-claim or counterclaim in a lawsuit) alleging that\r\nany patent claim is infringed by making, using, selling, offering for\r\nsale, or importing the Program or any portion of it.\r\n\r\n  11. Patents.\r\n\r\n  A \"contributor\" is a copyright holder who authorizes use under this\r\nLicense of the Program or a work on which the Program is based.  The\r\nwork thus licensed is called the contributor's \"contributor version\".\r\n\r\n  A contributor's \"essential patent claims\" are all patent claims\r\nowned or controlled by the contributor, whether already acquired or\r\nhereafter acquired, that would be infringed by some manner, permitted\r\nby this License, of making, using, or selling its contributor version,\r\nbut do not include claims that would be infringed only as a\r\nconsequence of further modification of the contributor version.  For\r\npurposes of this definition, \"control\" includes the right to grant\r\npatent sublicenses in a manner consistent with the requirements of\r\nthis License.\r\n\r\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\r\npatent license under the contributor's essential patent claims, to\r\nmake, use, sell, offer for sale, import and otherwise run, modify and\r\npropagate the contents of its contributor version.\r\n\r\n  In the following three paragraphs, a \"patent license\" is any express\r\nagreement or commitment, however denominated, not to enforce a patent\r\n(such as an express permission to practice a patent or covenant not to\r\nsue for patent infringement).  To \"grant\" such a patent license to a\r\nparty means to make such an agreement or commitment not to enforce a\r\npatent against the party.\r\n\r\n  If you convey a covered work, knowingly relying on a patent license,\r\nand the Corresponding Source of the work is not available for anyone\r\nto copy, free of charge and under the terms of this License, through a\r\npublicly available network server or other readily accessible means,\r\nthen you must either (1) cause the Corresponding Source to be so\r\navailable, or (2) arrange to deprive yourself of the benefit of the\r\npatent license for this particular work, or (3) arrange, in a manner\r\nconsistent with the requirements of this License, to extend the patent\r\nlicense to downstream recipients.  \"Knowingly relying\" means you have\r\nactual knowledge that, but for the patent license, your conveying the\r\ncovered work in a country, or your recipient's use of the covered work\r\nin a country, would infringe one or more identifiable patents in that\r\ncountry that you have reason to believe are valid.\r\n\r\n  If, pursuant to or in connection with a single transaction or\r\narrangement, you convey, or propagate by procuring conveyance of, a\r\ncovered work, and grant a patent license to some of the parties\r\nreceiving the covered work authorizing them to use, propagate, modify\r\nor convey a specific copy of the covered work, then the patent license\r\nyou grant is automatically extended to all recipients of the covered\r\nwork and works based on it.\r\n\r\n  A patent license is \"discriminatory\" if it does not include within\r\nthe scope of its coverage, prohibits the exercise of, or is\r\nconditioned on the non-exercise of one or more of the rights that are\r\nspecifically granted under this License.  You may not convey a covered\r\nwork if you are a party to an arrangement with a third party that is\r\nin the business of distributing software, under which you make payment\r\nto the third party based on the extent of your activity of conveying\r\nthe work, and under which the third party grants, to any of the\r\nparties who would receive the covered work from you, a discriminatory\r\npatent license (a) in connection with copies of the covered work\r\nconveyed by you (or copies made from those copies), or (b) primarily\r\nfor and in connection with specific products or compilations that\r\ncontain the covered work, unless you entered into that arrangement,\r\nor that patent license was granted, prior to 28 March 2007.\r\n\r\n  Nothing in this License shall be construed as excluding or limiting\r\nany implied license or other defenses to infringement that may\r\notherwise be available to you under applicable patent law.\r\n\r\n  12. No Surrender of Others' Freedom.\r\n\r\n  If conditions are imposed on you (whether by court order, agreement or\r\notherwise) that contradict the conditions of this License, they do not\r\nexcuse you from the conditions of this License.  If you cannot convey a\r\ncovered work so as to satisfy simultaneously your obligations under this\r\nLicense and any other pertinent obligations, then as a consequence you may\r\nnot convey it at all.  For example, if you agree to terms that obligate you\r\nto collect a royalty for further conveying from those to whom you convey\r\nthe Program, the only way you could satisfy both those terms and this\r\nLicense would be to refrain entirely from conveying the Program.\r\n\r\n  13. Use with the GNU Affero General Public License.\r\n\r\n  Notwithstanding any other provision of this License, you have\r\npermission to link or combine any covered work with a work licensed\r\nunder version 3 of the GNU Affero General Public License into a single\r\ncombined work, and to convey the resulting work.  The terms of this\r\nLicense will continue to apply to the part which is the covered work,\r\nbut the special requirements of the GNU Affero General Public License,\r\nsection 13, concerning interaction through a network will apply to the\r\ncombination as such.\r\n\r\n  14. Revised Versions of this License.\r\n\r\n  The Free Software Foundation may publish revised and/or new versions of\r\nthe GNU General Public License from time to time.  Such new versions will\r\nbe similar in spirit to the present version, but may differ in detail to\r\naddress new problems or concerns.\r\n\r\n  Each version is given a distinguishing version number.  If the\r\nProgram specifies that a certain numbered version of the GNU General\r\nPublic License \"or any later version\" applies to it, you have the\r\noption of following the terms and conditions either of that numbered\r\nversion or of any later version published by the Free Software\r\nFoundation.  If the Program does not specify a version number of the\r\nGNU General Public License, you may choose any version ever published\r\nby the Free Software Foundation.\r\n\r\n  If the Program specifies that a proxy can decide which future\r\nversions of the GNU General Public License can be used, that proxy's\r\npublic statement of acceptance of a version permanently authorizes you\r\nto choose that version for the Program.\r\n\r\n  Later license versions may give you additional or different\r\npermissions.  However, no additional obligations are imposed on any\r\nauthor or copyright holder as a result of your choosing to follow a\r\nlater version.\r\n\r\n  15. Disclaimer of Warranty.\r\n\r\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\r\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\r\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\r\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\r\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\r\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\r\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\r\n\r\n  16. Limitation of Liability.\r\n\r\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\r\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\r\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\r\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\r\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\r\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\r\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\r\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\r\nSUCH DAMAGES.\r\n\r\n  17. Interpretation of Sections 15 and 16.\r\n\r\n  If the disclaimer of warranty and limitation of liability provided\r\nabove cannot be given local legal effect according to their terms,\r\nreviewing courts shall apply local law that most closely approximates\r\nan absolute waiver of all civil liability in connection with the\r\nProgram, unless a warranty or assumption of liability accompanies a\r\ncopy of the Program in return for a fee.\r\n\r\n                     END OF TERMS AND CONDITIONS\r\n\r\n            How to Apply These Terms to Your New Programs\r\n\r\n  If you develop a new program, and you want it to be of the greatest\r\npossible use to the public, the best way to achieve this is to make it\r\nfree software which everyone can redistribute and change under these terms.\r\n\r\n  To do so, attach the following notices to the program.  It is safest\r\nto attach them to the start of each source file to most effectively\r\nstate the exclusion of warranty; and each file should have at least\r\nthe \"copyright\" line and a pointer to where the full notice is found.\r\n\r\n    <one line to give the program's name and a brief idea of what it does.>\r\n    Copyright (C) <year>  <name of author>\r\n\r\n    This program is free software: you can redistribute it and/or modify\r\n    it under the terms of the GNU General Public License as published by\r\n    the Free Software Foundation, either version 3 of the License, or\r\n    (at your option) any later version.\r\n\r\n    This program is distributed in the hope that it will be useful,\r\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n    GNU General Public License for more details.\r\n\r\n    You should have received a copy of the GNU General Public License\r\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\r\n\r\nAlso add information on how to contact you by electronic and paper mail.\r\n\r\n  If the program does terminal interaction, make it output a short\r\nnotice like this when it starts in an interactive mode:\r\n\r\n    <program>  Copyright (C) <year>  <name of author>\r\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\r\n    This is free software, and you are welcome to redistribute it\r\n    under certain conditions; type `show c' for details.\r\n\r\nThe hypothetical commands `show w' and `show c' should show the appropriate\r\nparts of the General Public License.  Of course, your program's commands\r\nmight be different; for a GUI interface, you would use an \"about box\".\r\n\r\n  You should also get your employer (if you work as a programmer) or school,\r\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\r\nFor more information on this, and how to apply and follow the GNU GPL, see\r\n<https://www.gnu.org/licenses/>.\r\n\r\n  The GNU General Public License does not permit incorporating your program\r\ninto proprietary programs.  If your program is a subroutine library, you\r\nmay consider it more useful to permit linking proprietary applications with\r\nthe library.  If this is what you want to do, use the GNU Lesser General\r\nPublic License instead of this License.  But first, please read\r\n<https://www.gnu.org/licenses/why-not-lgpl.html>."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\r\n  <picture>\r\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./images/siteicons/walloswhite.png\">\r\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"./images/siteicons/wallos.png\">\r\n    <img alt=\"Wallos\" src=\"./images/siteicons/wallos.png\">\r\n  </picture>\r\n\r\n  <p>Wallos: Open-Source Personal Subscription Tracker</p>\r\n\r\n  [![Stars](https://img.shields.io/github/stars/ellite/Wallos?style=flat-square)](https://github.com/ellite/Wallos)\r\n  [![Docker](https://img.shields.io/docker/pulls/bellamy/wallos?style=flat-square)](https://hub.docker.com/r/bellamy/wallos)\r\n  [![GitHub contributors](https://img.shields.io/github/contributors/ellite/Wallos?style=flat-square)](https://github.com/ellite/Wallos/graphs/contributors)\r\n  [![GitHub Sponsors](https://img.shields.io/github/sponsors/ellite?style=flat-square)](https://github.com/sponsors/ellite)\r\n  [![Discord](https://img.shields.io/discord/1237073478910214235?logo=discord&style=flat-square)](https://discord.gg/anex9GUrPW)\r\n</div>\r\n\r\n\r\n## Table of Contents\r\n\r\n- [Introduction](#introduction)\r\n- [Features](#features)\r\n- [Demo](#demo)\r\n- [Getting Started](#getting-started)\r\n  - [Prerequisites](#prerequisites)\r\n    - [Baremetal](#baremetal)\r\n    - [Docker](#docker)\r\n  - [Installation](#installation)\r\n    - [Baremetal](#baremetal-1)\r\n      - [Updating](#updating)\r\n    - [Docker](#docker-1)\r\n    - [Docker-Compose](#docker-compose)\r\n- [Usage](#usage)\r\n- [Screenshots](#screenshots)\r\n- [OIDC](#oidc)\r\n- [API Documentation](#api-documentation)\r\n- [Contributing](#contributing)\r\n  - [Contributors](#contributors)\r\n  - [Translations](#translations)\r\n- [License](#license)\r\n- [Links](#links)\r\n\r\n## Introduction\r\n\r\nWallos is a powerful, open-source, and self-hostable web application designed to empower you in managing your finances with ease. Say goodbye to complicated spreadsheets and expensive financial software – Wallos simplifies the process of tracking expenses and helps you gain better control over your financial life.\r\n\r\n## Features\r\n\r\n- Subscription Management: Keep track of your recurring subscriptions and payments, ensuring you never miss a due date.\r\n- Category Management: Organize your expenses into customizable categories, enabling you to gain insights into your spending habits.\r\n- Multi-Currency support: Wallos supports multiple currencies, allowing you to manage your finances in the currency of your choice.\r\n- Currency Conversion: Integrates with the Fixer API so you can get exchange rates and see all your subscriptions on your main currency.\r\n- Data Privacy: As a self-hosted application, Wallos ensures that your financial data remains private and secure on your own server.\r\n- Customization: Tailor Wallos to your needs with customizable categories, currencies, themes and other display options.\r\n- Sorting Options: Allowing you to view your subscriptions from different perspectives.\r\n- Logo Search: Wallos can search the web for the logo of your subscriptions if you don't have them available for upload.\r\n- Mobile view: Wallos on the go.\r\n- Statistics: Another perspective into your spendings.\r\n- Notifications:  Wallos supports multiple notification methods (email, discord, pushover, telegram, gotify and webhooks). Get notified about your upcoming payments.\r\n- Multi Language support.\r\n- OIDC with OAuth\r\n- AI Recommendations with ChatGPT, Gemini or Local Ollama\r\n\r\n## Demo\r\n\r\nIf you want to try Wallos, a demo is available at [https://demo.wallosapp.com](https://demo.wallosapp.com).  \r\nThe database is reset every 2 hours.  \r\nTo access the demo use the following credentials:\r\n\r\n```python\r\nUsername: demo  \r\nPassword: demo\r\n```\r\n\r\n## Getting Started\r\n\r\nSee instructions to run Wallos below.\r\n\r\n### Prerequisites\r\n\r\n#### Baremetal\r\n\r\n- NGINX or APACHE websever running\r\n- PHP 8.3 with the following modules enabled:\r\n    - curl\r\n    - dom\r\n    - gd\r\n    - imagick\r\n    - intl\r\n    - openssl\r\n    - sqlite3\r\n    - zip\r\n    - mbstring\r\n    - fpm\r\n\r\n#### Docker\r\n\r\n- Docker\r\n\r\n### Installation\r\n\r\n#### Baremetal\r\n\r\n1. Download or clone this repo and move the files into your web root - usually `/var/www/html`\r\n2. Rename `/db/wallos.empty.db` to `/db/wallos.db`\r\n3. Run `http://domain.example/endpoints/db/migrate.php` on your browser\r\n4. Add the following scripts to your cronjobs with `crontab -e`\r\n\r\n```bash\r\n0 1 * * * php /var/www/html/endpoints/cronjobs/updatenextpayment.php >> /var/log/cron/updatenextpayment.log 2>&1\r\n0 2 * * * php /var/www/html/endpoints/cronjobs/updateexchange.php >> /var/log/cron/updateexchange.log 2>&1\r\n0 8 * * * php /var/www/html/endpoints/cronjobs/sendcancellationnotifications.php >> /var/log/cron/sendcancellationnotifications.log 2>&1\r\n0 9 * * * php /var/www/html/endpoints/cronjobs/sendnotifications.php >> /var/log/cron/sendnotifications.log 2>&1\r\n*/2 * * * * php /var/www/html/endpoints/cronjobs/sendverificationemails.php >> /var/log/cron/sendverificationemail.log 2>&1\r\n*/2 * * * * php /var/www/html/endpoints/cronjobs/sendresetpasswordemails.php >> /var/log/cron/sendresetpasswordemails.log 2>&1\r\n0 */6 * * * php /var/www/html/endpoints/cronjobs/checkforupdates.php >> /var/log/cron/checkforupdates.log 2>&1\r\n30 1 * * 1 php /var/www/html/endpoints/cronjobs/storetotalyearlycost.php >> /var/log/cron/storetotalyearlycost.log 2>&1\r\n```\r\n\r\n5. If your web root is not `/var/www/html/` adjust the cronjobs above accordingly.\r\n\r\n#### Updating\r\n\r\n1. Re-download the repo and move the files into the correct folder or do `git pull` (if you used git clone before)\r\n2. Check the [Prerequisites](#baremetal) and install / enable the missing ones, if any.\r\n3. Run `http://domain.example/endpoints/db/migrate.php`\r\n\r\n#### Docker\r\n\r\n```bash\r\ndocker run -d --name wallos -v /path/to/config/wallos/db:/var/www/html/db \\\r\n-v /path/to/config/wallos/logos:/var/www/html/images/uploads/logos \\\r\n-e TZ=Europe/Berlin -p 8282:80 --restart unless-stopped \\\r\nbellamy/wallos:latest\r\n```\r\n\r\nDisable healthcheck (optional, e.g., for Docker <25 or faster startup reporting):\r\n\r\n```bash\r\ndocker run -d --name wallos -v /path/to/config/wallos/db:/var/www/html/db \\\r\n-v /path/to/config/wallos/logos:/var/www/html/images/uploads/logos \\\r\n-e TZ=Europe/Berlin -p 8282:80 --restart unless-stopped \\\r\n--health-cmd=NONE \\\r\nbellamy/wallos:latest\r\n```\r\n\r\n### Docker Compose\r\n\r\n```\r\nservices:\r\n  wallos:\r\n    container_name: wallos\r\n    image: bellamy/wallos:latest\r\n    ports:\r\n      - \"8282:80/tcp\"\r\n    environment:\r\n      TZ: 'America/Toronto'\r\n    # Volumes store your data between container upgrades\r\n    volumes:\r\n      - './db:/var/www/html/db'\r\n      - './logos:/var/www/html/images/uploads/logos'\r\n    restart: unless-stopped\r\n```\r\n\r\nDisable healthcheck (optional, e.g., for Docker <25 or faster startup reporting):\r\n\r\n```\r\nservices:\r\n  wallos:\r\n    container_name: wallos\r\n    image: bellamy/wallos:latest\r\n    ports:\r\n      - \"8282:80/tcp\"\r\n    environment:\r\n      TZ: 'America/Toronto'\r\n    volumes:\r\n      - './db:/var/www/html/db'\r\n      - './logos:/var/www/html/images/uploads/logos'\r\n    restart: unless-stopped\r\n    healthcheck:\r\n      test: [\"NONE\"]\r\n```\r\n\r\n## Usage\r\n\r\nJust open the browser and open `ip:port` of the machine running wallos.  \r\nOn the first time you run wallos a user account must be created.  \r\nGo to settings and personalise your Avatar and add members of your household. While there add / remove any categories and currencies.  \r\nGet a free API Key from [Fixer](https://fixer.io/#pricing_plan) and add it in the settings.  \r\nIf you want to trigger an Update of the exchange rates, change your main currency after adding the API Key, and then change it back to your preferred one.  \r\n\r\n## Screenshots\r\n\r\n![Screenshot](screenshots/wallos-subscriptions-light.png)\r\n\r\n![Screenshot](screenshots/wallos-subscriptions-dark.png)\r\n\r\n![Screenshot](screenshots/wallos-stats.png)\r\n\r\n![Screenshot](screenshots/wallos-calendar.png)\r\n\r\n![Screenshot](screenshots/wallos-form.png)\r\n\r\n![Screenshot](screenshots/wallos-subscriptions-mobile-light.png) ![Screenshot](screenshots/wallos-subscriptions-mobile-dark.png)\r\n\r\n![Screenshot](screenshots/wallos-dashboard-mobile-light.png) ![Screenshot](screenshots/wallos-dashboard-mobile-dark.png)\r\n\r\n## OIDC\r\n\r\nOIDC can be enabled on the Admin page and can be used with providers that support OAuth.\r\n\r\n## API Documentation\r\n\r\nWallos provides a comprehensive API that allows you to interact with the application programmatically. The API documentation is available at [https://api.wallosapp.com/](https://api.wallosapp.com/).\r\n\r\n## Contributing\r\n\r\nFeel free to open Pull requests with bug fixes and features. I'll do my best to keep an eye on those.  \r\nFeel free to open issues with bug reports or feature requests. Bug fixes will take priority.  \r\nI welcome contributions from the community and look forward to working with you to improve this project.\r\n\r\n### Contributors\r\n\r\n<a href=\"https://github.com/ellite/wallos/graphs/contributors\">\r\n  <img src=\"https://contrib.rocks/image?repo=ellite/wallos\" />\r\n</a>\r\n\r\n### Translations\r\n\r\nIf you want to contribute with a translation of wallos:\r\n- Add your language code to `includes/i18n/languages.php` in the format `\"en\" => [\"name\" => \"English\", \"dir\" => \"ltr\"],`. Please use the original language name and not the english translation.\r\n- Create a copy of the file `includes/i18n/en.php` and rename it to the language code you used above. Example: pt.php for \"pt\" => [\"name\" => \"Português\", \"dir\" => \"ltr\"],.\r\n- Translate all the values on the language file to the new language. (Incomplete translations will not be accepted).\r\n- Create a copy of the file `scripts/i18n/en.js` and rename it to the language code you used above. Example: pt.js for \"pt\" => [\"name\" => \"Português\", \"dir\" => \"ltr\"],.\r\n- Translate all the values on the language file to the new language. (Incomplete translations will not be accepted).\r\n\r\n## License\r\n\r\nThis project is licensed under the [GNU General Public License, Version 3](LICENSE.md) - see the [LICENSE.md](LICENSE.md) file for details.\r\n\r\n### Why GPLv3?\r\n\r\nI chose the GNU General Public License version 3 (GPLv3) for this project because it ensures that the software remains open source and freely available to the community. GPLv3 mandates that any derivative works or modifications must also be released under the same license, promoting the principles of software freedom.\r\n\r\nI strongly believe in the importance of open source software and the collaborative nature of development, and I invite contributors to help improve this project.\r\n\r\n## Links\r\n\r\n- The author: [henrique.pt](https://henrique.pt)\r\n- Wallos Landingpage: [wallosapp.com](https://wallosapp.com)\r\n- Join the conversation: [Discord Server](https://discord.gg/anex9GUrPW)\r\n\r\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\r\n\r\n## Reporting a Vulnerability\r\n\r\nIf you discover any security vulnerabilities in this project, please report them to the developer by emailing [wallos@henrique.pt](mailto:wallos@henrique.pt). I appreciate your help in keeping the project secure.\r\n\r\n## Supported Versions\r\n\r\nThis project is currently supported with security updates for the following versions:\r\n\r\n| Version | Supported          |\r\n| ------- | ------------------ |\r\n| latest  | :white_check_mark: |\r\n| main    | :white_check_mark: |\r\n| 1.x.x   | :x:                |\r\n\r\n## Security Measures\r\n\r\nI take security seriously and am working on ways to implement security measures to protect the project.\r\n\r\nWhat is being done currenty:\r\n- Periodically scan the docker image for vulnerabilities with trivy.\r\n\r\n## Reporting a Security Concern\r\n\r\nIf you have any security concerns or questions regarding the security of this project, please contact the developer at [wallos@henrique.pt](mailto:wallos@henrique.pt).\r\n\r\n## Responsible Disclosure\r\n\r\nI kindly request that you follow responsible disclosure practices and give me reasonable time to address any reported vulnerabilities before making them public.\r\n\r\n"
  },
  {
    "path": "about.php",
    "content": "<?php\r\nrequire_once 'includes/header.php';\r\n\r\n$wallosIsUpToDate = true;\r\nif (!is_null($settings['latest_version'])) {\r\n    $latestVersion = $settings['latest_version'];\r\n    if (version_compare($version, $latestVersion) == -1) {\r\n        $wallosIsUpToDate = false;\r\n    }\r\n}\r\n?>\r\n\r\n<section class=\"contain\">\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('about', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"credits-list\">\r\n            <div>\r\n                <h3>\r\n                    Wallos <?= $version ?> <?= $demoMode ? \"Demo\" : \"\" ?>\r\n                </h3>\r\n                <span>\r\n                    <?= translate('release_notes', $i18n) ?>\r\n                    <a href=\"https://github.com/ellite/Wallos/releases/tag/<?= $version ?>\" target=\"_blank\"\r\n                        title=\"<?= translate('external_url', $i18n) ?>\" rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n            <?php if (!$wallosIsUpToDate): ?>\r\n                <div class=\"update-available\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-info-circle\"></i>\r\n                        <?= translate('update_available', $i18n) ?> <?= $latestVersion ?>\r\n                    </h3>\r\n                    <span>\r\n                        <?= translate('release_notes', $i18n) ?>\r\n                        <a href=\"https://github.com/ellite/Wallos/releases/tag/<?= $latestVersion ?>\" target=\"_blank\"\r\n                            title=\"<?= translate('external_url', $i18n) ?>\" rel=\"noreferrer\">\r\n                            <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                        </a>\r\n                    </span>\r\n                </div>\r\n            <?php endif; ?>\r\n            <div>\r\n                <h3><?= translate('license', $i18n) ?></h3>\r\n                <span>\r\n                    GPLv3\r\n                    <a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\" target=\"_blank\"\r\n                        title=\"<?= translate('external_url', $i18n) ?>\" rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n            <div>\r\n                <h3><?= translate('issues_and_requests', $i18n) ?></h3>\r\n                <span>\r\n                    GitHub\r\n                    <a href=\"https://github.com/ellite/Wallos/issues\" target=\"_blank\"\r\n                        title=\"<?= translate('external_url', $i18n) ?>\" rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n            <div>\r\n                <h3><?= translate('the_author', $i18n) ?></h3>\r\n                <span>\r\n                    https://henrique.pt\r\n                    <a href=\"https://henrique.pt/\" target=\"_blank\" title=\"<?= translate('external_url', $i18n) ?>\"\r\n                        rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n\r\n        </div>\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate(\"credits\", $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"credits-list\">\r\n            <div>\r\n                <h3><?= translate('icons', $i18n) ?></h3>\r\n                <span>\r\n                    https://www.streamlinehq.com/freebies/plump-flat-free\r\n                    <a href=\"https://www.streamlinehq.com/freebies/plump-flat-free\" target=\"_blank\"\r\n                        title=\"<?= translate('external_url', $i18n) ?>\" rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n            <div>\r\n                <h3><?= translate('payment_icons', $i18n) ?></h3>\r\n                <span>\r\n                    https://www.figma.com/file/5IMW8JfoXfB5GRlPNdTyeg/Credit-Cards-and-Payment-Methods-Icons-(Community)\r\n                    <a href=\"https://www.figma.com/file/5IMW8JfoXfB5GRlPNdTyeg/Credit-Cards-and-Payment-Methods-Icons-(Community)\"\r\n                        target=\"_blank\" title=\"<?= translate('external_url', $i18n) ?>\" rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n            <div>\r\n                <h3>Chart.js</h3>\r\n                <span>\r\n                    https://www.chartjs.org/\r\n                    <a href=\"https://www.chartjs.org/\" target=\"_blank\" title=\"<?= translate('external_url', $i18n) ?>\"\r\n                        rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n            <div>\r\n                <h3>QRCode.js</h3>\r\n                <span>\r\n                    https://github.com/davidshimjs/qrcodejs\r\n                    <a href=\"https://github.com/davidshimjs/qrcodejs\" target=\"_blank\"\r\n                        title=\"<?= translate('external_url', $i18n) ?>\" rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n            <div>\r\n                <h3>Icons by icons8</h3>\r\n                <span>\r\n                    https://icons8.com/\r\n                    <a href=\"https://icons8.com/\" target=\"_blank\" title=\"<?= translate('external_url', $i18n) ?>\"\r\n                        rel=\"noreferrer\">\r\n                        <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                    </a>\r\n                </span>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n</section>\r\n\r\n<?php\r\nrequire_once 'includes/footer.php';\r\n?>"
  },
  {
    "path": "admin.php",
    "content": "<?php\r\nrequire_once 'includes/header.php';\r\n\r\nif ($isAdmin != 1) {\r\n    header('Location: index.php');\r\n    exit;\r\n}\r\n\r\n// get admin settings from admin table\r\n$stmt = $db->prepare('SELECT * FROM admin');\r\n$result = $stmt->execute();\r\n$settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n// get OIDC settings\r\n$stmt = $db->prepare('SELECT * FROM oauth_settings WHERE id = 1');\r\n$result = $stmt->execute();\r\n$oidcSettings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($oidcSettings === false) {\r\n    // Table is empty or no row with id=1, set defaults\r\n    $oidcSettings = [\r\n        'name' => '',\r\n        'client_id' => '',\r\n        'client_secret' => '',\r\n        'authorization_url' => '',\r\n        'token_url' => '',\r\n        'user_info_url' => '',\r\n        'redirect_url' => '',\r\n        'logout_url' => '',\r\n        'user_identifier_field' => 'sub',\r\n        'scopes' => 'openid email profile',\r\n        'auth_style' => 'auto',\r\n        'auto_create_user' => 0,\r\n        'password_login_disabled' => 0\r\n    ];\r\n}\r\n\r\n// get user accounts\r\n$stmt = $db->prepare('SELECT id, username, email FROM user ORDER BY id ASC');\r\n$result = $stmt->execute();\r\n\r\n$users = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $users[] = $row;\r\n}\r\n$userCount = is_array($users) ? count($users) : 0;\r\n\r\n$loginDisabledAllowed = $userCount == 1 && $settings['registrations_open'] == 0;\r\n?>\r\n\r\n<section class=\"contain settings\">\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('registrations', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"admin-form\">\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"registrations\" <?= $settings['registrations_open'] ? 'checked' : '' ?> />\r\n                <label for=\"registrations\"><?= translate('enable_user_registrations', $i18n) ?></label>\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <label for=\"maxUsers\"><?= translate('maximum_number_users', $i18n) ?></label>\r\n                <input type=\"number\" id=\"maxUsers\" autocomplete=\"off\" value=\"<?= $settings['max_users'] ?>\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    <?= translate('max_users_info', $i18n) ?>\r\n                </p>\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    By enabling user registrations, the setting to disable login will be unavailable.\r\n                </p>\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"requireEmail\" <?= $settings['require_email_verification'] ? 'checked' : '' ?>\r\n                    <?= empty($settings['smtp_address']) ? 'disabled' : '' ?> />\r\n                <label for=\"requireEmail\">\r\n                    <?= translate('require_email_verification', $i18n) ?>\r\n                </label>\r\n            </div>\r\n            <?php\r\n            if (empty($settings['smtp_address'])) {\r\n                ?>\r\n                <div class=\"settings-notes\">\r\n                    <p>\r\n                        <i class=\"fa-solid fa-circle-info\"></i>\r\n                        <?= translate('configure_smtp_settings_to_enable', $i18n) ?>\r\n                    </p>\r\n                </div>\r\n                <?php\r\n            }\r\n            ?>\r\n            <div class=\"form-group\">\r\n                <label for=\"serverUrl\"><?= translate('server_url', $i18n) ?></label>\r\n                <input type=\"text\" id=\"serverUrl\" autocomplete=\"off\" value=\"<?= $settings['server_url'] ?>\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    <?= translate('server_url_info', $i18n) ?>\r\n                </p>\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    <?= translate('server_url_password_reset', $i18n) ?>\r\n                </p>\r\n            </div>\r\n            <hr>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"disableLogin\" <?= $settings['login_disabled'] ? 'checked' : '' ?>\r\n                    <?= $loginDisabledAllowed ? '' : 'disabled' ?> />\r\n                <label for=\"disableLogin\"><?= translate('disable_login', $i18n) ?></label>\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa fa-exclamation-triangle\" aria-hidden=\"true\"></i>\r\n                    <?= translate('disable_login_info', $i18n) ?>\r\n                </p>\r\n                <p>\r\n                    <i class=\"fa fa-exclamation-triangle\" aria-hidden=\"true\"></i>\r\n                    <?= translate('disable_login_info2', $i18n) ?>\r\n                </p>\r\n            </div>\r\n            <div class=\"buttons\">\r\n                <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                    id=\"saveAccountRegistrations\" onClick=\"saveAccountRegistrationsButton()\" />\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    if ($userCount >= 0) {\r\n        ?>\r\n\r\n        <section class=\"account-section\">\r\n            <header>\r\n                <h2><?= translate('user_management', $i18n) ?></h2>\r\n            </header>\r\n            <div class=\"user-list\">\r\n                <?php\r\n                foreach ($users as $user) {\r\n                    $userIcon = $user['id'] == 1 ? 'fa-user-tie' : 'fa-id-badge';\r\n                    ?>\r\n                    <div class=\"form-group-inline\" data-userid=\"<?= $user['id'] ?>\">\r\n                        <div class=\"user-list-row\">\r\n                            <div title=\"<?= translate('username', $i18n) ?>\">\r\n                                <div class=\"user-list-icon\">\r\n                                    <i class=\"fa-solid <?= $userIcon ?>\"></i>\r\n                                </div>\r\n                                <?= $user['username'] ?>\r\n                            </div>\r\n                            <div title=\"<?= translate('email', $i18n) ?>\">\r\n                                <div class=\"user-list-icon\">\r\n                                    <i class=\"fa-solid fa-envelope\"></i>\r\n                                </div>\r\n                                <a href=\"mailto:<?= $user['email'] ?>\"><?= $user['email'] ?></a>\r\n                            </div>\r\n                        </div>\r\n                        <div>\r\n                            <?php\r\n                            if ($user['id'] != 1) {\r\n                                ?>\r\n                                <button class=\"image-button medium\" onClick=\"removeUser(<?= $user['id'] ?>)\"\r\n                                    title=\"<?= translate('delete_user', $i18n) ?>\">\r\n                                    <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                                </button>\r\n                                <?php\r\n                            } else {\r\n                                ?>\r\n                                <button class=\"image-button medium disabled\" disabled\r\n                                    title=\"<?= translate('delete_user', $i18n) ?>\">\r\n                                    <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                                </button>\r\n                                <?php\r\n                            }\r\n                            ?>\r\n\r\n                        </div>\r\n                    </div>\r\n                    <?php\r\n                }\r\n                ?>\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    <?= translate('delete_user_info', $i18n) ?>\r\n                </p>\r\n            </div>\r\n            <h2><?= translate('create_user', $i18n) ?></h2>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"newUsername\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('username', $i18n) ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"email\" id=\"newEmail\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('email', $i18n) ?>\" />\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"password\" id=\"newPassword\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('password', $i18n) ?>\" />\r\n                <input type=\"submit\" class=\"thin\" value=\"<?= translate('add', $i18n) ?>\" id=\"addUserButton\"\r\n                    onClick=\"addUserButton()\" />\r\n            </div>\r\n        </section>\r\n\r\n        <?php\r\n    }\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('oidc_settings', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"admin-form\">\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"oidcEnabled\" <?= $settings['oidc_oauth_enabled'] ? 'checked' : '' ?>\r\n                    onchange=\"toggleOidcEnabled()\" />\r\n                <label for=\"oidcEnabled\"><?= translate('oidc_oauth_enabled', $i18n) ?></label>\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcName\" placeholder=\"Provider Name\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['name'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcClientId\" placeholder=\"Client ID\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['client_id'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcClientSecret\" placeholder=\"Client Secret\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['client_secret'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcAuthUrl\" placeholder=\"Auth URL\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['authorization_url'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcTokenUrl\" placeholder=\"Token URL\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['token_url'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcUserInfoUrl\" placeholder=\"User Info URL\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['user_info_url'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcRedirectUrl\" placeholder=\"Redirect URL\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['redirect_url'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcLogoutUrl\" placeholder=\"Logout URL\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['logout_url'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcUserIdentifierField\" placeholder=\"User Identifier Field\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['user_identifier_field'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" id=\"oidcScopes\" placeholder=\"Scopes\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['scopes'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <input type=\"hidden\" id=\"oidcAuthStyle\" placeholder=\"Auth Style\" autocomplete=\"off\"\r\n                    value=\"<?= $oidcSettings['auth_style'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"oidcAutoCreateUser\" <?= $oidcSettings['auto_create_user'] ? 'checked' : '' ?> />\r\n                <label for=\"oidcAutoCreateUser\"><?= translate('create_user_automatically', $i18n) ?></label>\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"oidcPasswordLoginDisabled\"\r\n                    <?= $oidcSettings['password_login_disabled'] ? 'checked' : '' ?> />\r\n                <label for=\"oidcPasswordLoginDisabled\"><?= translate('disable_password_login', $i18n) ?></label>\r\n            </div>\r\n            <div class=\"buttons\">\r\n                <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                    id=\"saveOidcSettingsButton\" onClick=\"saveOidcSettingsButton()\" />\r\n            </div>\r\n        </div>\r\n\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('smtp_settings', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"admin-form\">\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"text\" name=\"smtpaddress\" id=\"smtpaddress\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('smtp_address', $i18n) ?>\" value=\"<?= $settings['smtp_address'] ?>\" />\r\n                <input type=\"text\" name=\"smtpport\" id=\"smtpport\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('port', $i18n) ?>\" class=\"one-third\" value=\"<?= $settings['smtp_port'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <div>\r\n                    <input type=\"radio\" name=\"encryption\" id=\"encryptionnone\" value=\"none\"\r\n                        <?= empty($settings['encryption']) || $settings['encryption'] == \"none\" ? \"checked\" : \"\" ?> />\r\n                    <label for=\"encryptionnone\"><?= translate('none', $i18n) ?></label>\r\n                </div>\r\n                <div>\r\n                    <input type=\"radio\" name=\"encryption\" id=\"encryptiontls\" value=\"tls\"\r\n                        <?= $settings['encryption'] == \"tls\" ? \"checked\" : \"\" ?> />\r\n                    <label for=\"encryptiontls\"><?= translate('tls', $i18n) ?></label>\r\n                </div>\r\n                <div>\r\n                    <input type=\"radio\" name=\"encryption\" id=\"encryptionssl\" value=\"ssl\"\r\n                        <?= $settings['encryption'] == \"ssl\" ? \"checked\" : \"\" ?> />\r\n                    <label for=\"encryptionssl\"><?= translate('ssl', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"text\" name=\"smtpusername\" id=\"smtpusername\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('smtp_username', $i18n) ?>\" value=\"<?= $settings['smtp_username'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"password\" name=\"smtppassword\" id=\"smtppassword\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('smtp_password', $i18n) ?>\" value=\"<?= $settings['smtp_password'] ?>\" />\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"text\" name=\"fromemail\" id=\"fromemail\" autocomplete=\"off\"\r\n                    placeholder=\"<?= translate('from_email', $i18n) ?>\" value=\"<?= $settings['from_email'] ?>\" />\r\n            </div>\r\n            <div class=\"buttons\">\r\n                <input type=\"button\" class=\"secondary-button thin mobile-grow\" value=\"<?= translate('test', $i18n) ?>\"\r\n                    id=\"testSmtpSettingsButton\" onClick=\"testSmtpSettingsButton()\" />\r\n                <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                    id=\"saveSmtpSettingsButton\" onClick=\"saveSmtpSettingsButton()\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i> <?= translate('smtp_info', $i18n) ?>\r\n                </p>\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    <?= translate('smtp_usage_info', $i18n) ?>\r\n                </p>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n    <header>\r\n        <h2><?= translate('security_settings', $i18n) ?></h2> </header>\r\n    <div class=\"admin-form\">\r\n        <div class=\"form-group-inline\">\r\n            <input type=\"text\" name=\"local_webhook_notifications_allowlist\" id=\"local_webhook_notifications_allowlist\" autocomplete=\"off\"\r\n                placeholder=\"e.g., 192.168.1.5:8123, homeassistant.local\" value=\"<?= htmlspecialchars($settings['local_webhook_notifications_allowlist'] ?? '', ENT_QUOTES, 'UTF-8') ?>\" />\r\n        </div>\r\n        \r\n        <div class=\"buttons\">\r\n            <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                id=\"saveSecuritySettingsButton\" onClick=\"saveSecuritySettingsButton()\" />\r\n        </div>\r\n        \r\n        <div class=\"settings-notes\">\r\n            <p>\r\n                <i class=\"fa-solid fa-circle-info\"></i> \r\n                <?= translate('ssrf_protection_info', $i18n) ?>\r\n            </p>\r\n            <p>\r\n                <i class=\"fa-solid fa-circle-info\"></i>\r\n                <?= translate('local_webhook_info', $i18n) ?>\r\n            </p>\r\n        </div>\r\n    </div>\r\n</section>\r\n\r\n    <?php\r\n    // Get latest version from admin table\r\n    if (!is_null($settings['latest_version'])) {\r\n        $latestVersion = $settings['latest_version'];\r\n        $hasUpdate = version_compare($version, $latestVersion) == -1;\r\n    } else {\r\n        $hasUpdate = false;\r\n    }\r\n\r\n    // find unused upload logos\r\n\r\n    // Get all logos in the subscriptions table\r\n    $query = 'SELECT logo FROM subscriptions';\r\n    $stmt = $db->prepare($query);\r\n    $result = $stmt->execute();\r\n\r\n    $logosOnDisk = [];\r\n    $logosOnDB = [];\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $logosOnDB[] = $row['logo'];\r\n    }\r\n\r\n    // Get all logos in the payment_methods table\r\n    $query = 'SELECT icon FROM payment_methods';\r\n    $stmt = $db->prepare($query);\r\n    $result = $stmt->execute();\r\n\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        if (!strstr($row['icon'], \"images/uploads/icons/\")) {\r\n            $logosOnDB[] = $row['icon'];\r\n        }\r\n    }\r\n\r\n    $logosOnDB = array_unique($logosOnDB);\r\n\r\n    // Get all logos in the uploads folder\r\n    $uploadDir = 'images/uploads/logos/';\r\n    $uploadFiles = scandir($uploadDir);\r\n\r\n    foreach ($uploadFiles as $file) {\r\n        if ($file != '.' && $file != '..' && $file != 'avatars') {\r\n            $logosOnDisk[] = ['logo' => $file];\r\n        }\r\n    }\r\n\r\n    // Find unused logos\r\n    $unusedLogos = [];\r\n    foreach ($logosOnDisk as $disk) {\r\n        $found = false;\r\n        foreach ($logosOnDB as $dbLogo) {\r\n            if ($disk['logo'] == $dbLogo) {\r\n                $found = true;\r\n                break;\r\n            }\r\n        }\r\n        if (!$found) {\r\n            $unusedLogos[] = $disk;\r\n        }\r\n    }\r\n\r\n    $logosToDelete = count($unusedLogos);\r\n\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2>\r\n                <?= translate('maintenance_tasks', $i18n) ?>\r\n            </h2>\r\n        </header>\r\n        <div class=\"maintenance-tasks\">\r\n            <h3><?= translate('update', $i18n) ?></h3>\r\n            <div class=\"form-group\">\r\n                <?php\r\n                if ($hasUpdate) {\r\n                    ?>\r\n                    <div class=\"updates-list\">\r\n                        <p><?= translate('new_version_available', $i18n) ?>.</p>\r\n                        <p>\r\n                            <?= translate('current_version', $i18n) ?>:\r\n                            <span>\r\n                                <?= $version ?>\r\n                                <a href=\"https://github.com/ellite/Wallos/releases/tag/<?= $version ?>\" target=\"_blank\">\r\n                                    <i class=\"fa-solid fa-external-link\"></i>\r\n                                </a>\r\n                            </span>\r\n                        </p>\r\n                        <p>\r\n                            <?= translate('latest_version', $i18n) ?>:\r\n                            <span>\r\n                                <?= $latestVersion ?>\r\n                                <a href=\"https://github.com/ellite/Wallos/releases/tag/<?= $latestVersion ?>\"\r\n                                    target=\"_blank\">\r\n                                    <i class=\"fa-solid fa-external-link\"></i>\r\n                                </a>\r\n                            </span>\r\n                        </p>\r\n                    </div>\r\n                    <?php\r\n                } else {\r\n                    ?>\r\n                    <?= translate('on_current_version', $i18n) ?>\r\n                    <?php\r\n                }\r\n                ?>\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"updateNotification\" <?= $settings['update_notification'] ? 'checked' : '' ?>\r\n                    onchange=\"toggleUpdateNotification()\" />\r\n                <label for=\"updateNotification\"><?= translate('show_update_notification', $i18n) ?></label>\r\n            </div>\r\n            <h3><?= translate('orphaned_logos', $i18n) ?></h3>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"button\" class=\"button thin mobile-grow\" value=\"<?= translate('delete', $i18n) ?>\"\r\n                    id=\"deleteUnusedLogos\" onClick=\"deleteUnusedLogos()\" <?= $logosToDelete == 0 ? 'disabled' : '' ?> />\r\n                <span class=\"number-of-logos bold\"><?= $logosToDelete ?></span>\r\n                <?= translate('orphaned_logos', $i18n) ?>\r\n            </div>\r\n            <h3><?= translate('cronjobs', $i18n) ?></h3>\r\n            <div>\r\n                <div class=\"inline-row\">\r\n                    <input type=\"button\" value=\"Check for Updates\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('checkforupdates')\">\r\n                    <input type=\"button\" value=\"Send Notifications\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('sendnotifications')\">\r\n                    <input type=\"button\" value=\"Send Cancellation Notifications\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('sendcancellationnotifications')\">\r\n                    <input type=\"button\" value=\"Send Password Reset Emails\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('sendresetpasswordemails')\">\r\n                    <input type=\"button\" value=\"Send Verification Emails\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('sendverificationemails')\">\r\n                    <input type=\"button\" value=\"Update Exchange Rates\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('updateexchange')\">\r\n                    <input type=\"button\" value=\"Update Next Payments\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('updatenextpayment')\">\r\n                    <input type=\"button\" value=\"Store Total Yearly Cost\" class=\"button tiny mobile-grow\"\r\n                        onclick=\"executeCronJob('storetotalyearlycost')\">\r\n                </div>\r\n                <div class=\"inline-row\">\r\n                    <textarea id=\"cronjobResult\" class=\"thin\" readonly></textarea>\r\n                </div>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('backup_and_restore', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"form-group-inline\">\r\n            <input type=\"button\" class=\"button thin mobile-grow\" value=\"<?= translate('backup', $i18n) ?>\" id=\"backupDB\"\r\n                onClick=\"backupDB()\" />\r\n            <input type=\"button\" class=\"secondary-button thin mobile-grow\" value=\"<?= translate('restore', $i18n) ?>\"\r\n                id=\"restoreDB\" onClick=\"openRestoreDBFileSelect()\" />\r\n            <input type=\"file\" name=\"restoreDBFile\" id=\"restoreDBFile\" style=\"display: none;\" onChange=\"restoreDB()\"\r\n                accept=\".zip\">\r\n        </div>\r\n        <div class=\"settings-notes\">\r\n            <p>\r\n                <i class=\"fa-solid fa-circle-info\"></i>\r\n                <?= translate('restore_info', $i18n) ?>\r\n            </p>\r\n        </div>\r\n    </section>\r\n\r\n</section>\r\n<script src=\"scripts/admin.js?<?= $version ?>\"></script>\r\n\r\n<?php\r\nrequire_once 'includes/footer.php';\r\n?>"
  },
  {
    "path": "api/admin/get_admin_settings.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- admin_settings: an object containing the admin settings.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"admin_settings\",\r\n  \"admin_settings\": {\r\n    \"registrations_open\": 1,\r\n    \"max_users\": 100,\r\n    \"require_email_verification\": 1,\r\n    \"server_url\": \"http://example.com\",\r\n    \"smtp_address\": \"smtp.example.com\",\r\n    \"smtp_port\": 587,\r\n    \"smtp_username\": \"admin@example.com\",\r\n    \"smtp_password\": \"********\",\r\n    \"from_email\": \"no-reply@example.com\",\r\n    \"encryption\": \"tls\",\r\n    \"login_disabled\": 0,\r\n    \"latest_version\": \"v1.0.0\",\r\n    \"update_notification\": 1\r\n  },\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n\r\n    if ($userId !== 1) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid user\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $sql = \"SELECT * FROM 'admin'\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $admin_settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($admin_settings) {\r\n        unset($admin_settings['id']);\r\n        // if the smtp_password is set, hide it\r\n        if (isset($admin_settings['smtp_password'])) {\r\n            $admin_settings['smtp_password'] = \"********\";\r\n        }\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"admin_settings\",\r\n        \"admin_settings\" => $admin_settings,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/admin/get_oidc_settings.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- oidc_settings: an object containing the OIDC settings.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"oidc_settings\",\r\n  \"oidc_settings\": {\r\n    \"name\": \"Authentik\",\r\n    \"client_id\": \"CJMLcyyS94cUMXkitNZuokayArnn23TXxpeUv48E\",\r\n    \"client_secret\": \"SzfQBIibfN0gEAgCORrKnGnrYe9yqASWAYUuu1byelVosCHlnoqAdWlMDppblyuByb38Zw78AAlgMmdK6SWpGjOU4IiqaoltkAEh52trcqCB8briP1TqqXZdar4xfhVw\",\r\n    \"authorization_url\": \"https://auth.bellamylab.com/application/o/authorize/\",\r\n    \"token_url\": \"https://auth.bellamylab.com/application/o/token/\",\r\n    \"user_info_url\": \"https://auth.bellamylab.com/application/o/userinfo/\",\r\n    \"redirect_url\": \"http://localhost:80/wallos\",\r\n    \"logout_url\": \"https://auth.bellamylab.com/application/o/wallos/end-session/\",\r\n    \"user_identifier_field\": \"sub\",\r\n    \"scopes\": \"openid email profile\",\r\n    \"auth_style\": \"auto\",\r\n    \"created_at\": \"2025-07-20 20:31:50\",\r\n    \"updated_at\": \"2025-07-20 20:31:50\",\r\n    \"auto_create_user\": 0,\r\n    \"password_login_disabled\": 0\r\n  },\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n\r\n    if ($userId !== 1) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid user\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $sql = \"SELECT * FROM 'oauth_settings' WHERE id = 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $result = $stmt->execute();\r\n    $oidc_settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($oidc_settings) {\r\n        unset($oidc_settings['id']);\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"oidc_settings\",\r\n        \"oidc_settings\" => $oidc_settings,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/admin/set_disable_password_login.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts POST requests only.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n- disable: '1' to disable password login, '0' to enable it.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- message: detailed information or error message (string).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"Updated\",\r\n  \"message\": \"Password login has been disabled.\"\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER['REQUEST_METHOD'] !== 'POST') {\r\n    echo json_encode([\r\n        'success' => false,\r\n        'title' => 'Invalid request method',\r\n        'message' => 'Only POST requests are allowed.'\r\n    ]);\r\n    exit;\r\n}\r\n\r\n$apiKey = $_POST['api_key'] ?? null;\r\n\r\n// Authenticate user first\r\nif (!$apiKey) {\r\n    echo json_encode([\r\n        'success' => false,\r\n        'title' => 'Missing API key',\r\n        'message' => 'API key is required.'\r\n    ]);\r\n    exit;\r\n}\r\n\r\n$sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindValue(':apiKey', $apiKey);\r\n$result = $stmt->execute();\r\n$user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif (!$user || $user['id'] !== 1) {\r\n    echo json_encode([\r\n        'success' => false,\r\n        'title' => 'Unauthorized',\r\n        'message' => 'Invalid API key or insufficient privileges.'\r\n    ]);\r\n    exit;\r\n}\r\n\r\n// Now check 'disable' parameter only after authentication\r\n$disable = $_POST['disable'] ?? null;\r\nif (!isset($disable)) {\r\n    echo json_encode([\r\n        'success' => false,\r\n        'title' => 'Missing parameter',\r\n        'message' => 'Parameter \"disable\" is required.'\r\n    ]);\r\n    exit;\r\n}\r\n\r\nif (!in_array($disable, ['0', '1'], true)) {\r\n    echo json_encode([\r\n        'success' => false,\r\n        'title' => 'Invalid parameter',\r\n        'message' => 'Parameter \"disable\" must be \"0\" or \"1\".'\r\n    ]);\r\n    exit;\r\n}\r\n\r\n// Update the password_login_disabled setting\r\n$updateSql = \"UPDATE oauth_settings SET password_login_disabled = :disable WHERE id = 1\";\r\n$updateStmt = $db->prepare($updateSql);\r\n$updateStmt->bindValue(':disable', intval($disable), SQLITE3_INTEGER);\r\n$updateResult = $updateStmt->execute();\r\n\r\nif ($updateResult) {\r\n    echo json_encode([\r\n        'success' => true,\r\n        'title' => 'Updated',\r\n        'message' => \"Password login has been \" . ($disable === '1' ? \"disabled\" : \"enabled\") . \".\"\r\n    ]);\r\n} else {\r\n    echo json_encode([\r\n        'success' => false,\r\n        'title' => 'Database error',\r\n        'message' => 'Failed to update the setting.'\r\n    ]);\r\n}\r\n\r\n$db->close();\r\n"
  },
  {
    "path": "api/categories/get_categories.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- categories: an array of categories.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"categories\",\r\n  \"categories\": [\r\n    {\r\n      \"id\": 1,\r\n      \"name\": \"General\",\r\n      \"order\": 1,\r\n      \"in_use\": true\r\n    },\r\n    {\r\n      \"id\": 2,\r\n      \"name\": \"Entertainment\",\r\n      \"order\": 2,\r\n      \"in_use\": true\r\n    },\r\n    {\r\n      \"id\": 3,\r\n      \"name\": \"Music\",\r\n      \"order\": 3,\r\n      \"in_use\": true\r\n    }\r\n  ],\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n\r\n    $sql = \"SELECT * FROM categories WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $categories = [];\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $categories[] = $row;\r\n    }\r\n\r\n    foreach ($categories as $key => $value) {\r\n        unset($categories[$key]['user_id']);\r\n        // Check if it's in use in any subscription\r\n        $categoryId = $categories[$key]['id'];\r\n        $sql = \"SELECT COUNT(*) as count FROM subscriptions WHERE user_id = :userId AND category_id = :categoryId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindValue(':categoryId', $categoryId);\r\n        $stmt->bindValue(':userId', $userId);\r\n        $result = $stmt->execute();\r\n        $count = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($count['count'] > 0) {\r\n            $categories[$key]['in_use'] = true;\r\n        } else {\r\n            $categories[$key]['in_use'] = false;\r\n        }\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"categories\",\r\n        \"categories\" => $categories,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/currencies/get_currencies.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- main_currency: the main currency of the user (integer).\r\n- currencies: an array of currencies.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"currencies\",\r\n  \"main_currency\": 3,\r\n  \"currencies\": [\r\n    {\r\n      \"id\": 1,\r\n      \"name\": \"US Dollar\",\r\n      \"symbol\": \"$\",\r\n      \"code\": \"USD\",\r\n      \"rate\": \"1.1000\",\r\n      \"in_use\": true\r\n    },\r\n    {\r\n      \"id\": 2,\r\n      \"name\": \"Japanese Yen\",\r\n      \"symbol\": \"¥\",\r\n      \"code\": \"JPY\",\r\n      \"rate\": \"150.0000\",\r\n      \"in_use\": true\r\n    },\r\n    {\r\n      \"id\": 3,\r\n      \"name\": \"Euro\",\r\n      \"symbol\": \"€\",\r\n      \"code\": \"EUR\",\r\n      \"rate\": \"1.0000\",\r\n      \"in_use\": true\r\n    }\r\n  ],\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n\r\n    $sql = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $currencies = [];\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $currencies[] = $row;\r\n    }\r\n\r\n    foreach ($currencies as $key => $value) {\r\n        unset($currencies[$key]['user_id']);\r\n        // Check if it's in use in any subscription\r\n        $currencyId = $currencies[$key]['id'];\r\n        $sql = \"SELECT COUNT(*) as count FROM subscriptions WHERE user_id = :userId AND currency_id = :currencyId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindValue(':currencyId', $currencyId);\r\n        $stmt->bindValue(':userId', $userId);\r\n        $result = $stmt->execute();\r\n        $count = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($count['count'] > 0) {\r\n            $currencies[$key]['in_use'] = true;\r\n        } else {\r\n            $currencies[$key]['in_use'] = false;\r\n        }\r\n    }\r\n\r\n    $mainCurrency = $user['main_currency'];\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"currencies\",\r\n        \"main_currency\" => $mainCurrency,\r\n        \"currencies\" => $currencies,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/fixer/get_fixer.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- fixer: an object containing the Fixer settings.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"fixer\",\r\n  \"fixer\": {\r\n    \"api_key\": \"********\",\r\n    \"provider\": 0,\r\n    \"provider_name\": \"Fixer.io\"\r\n  },\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n    $providers = [\r\n        0 => \"Fixer.io\",\r\n        1 => \"APILayer.com\"\r\n    ]; \r\n\r\n    $query = \"SELECT * FROM fixer WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $fixer = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    $notes = [];\r\n\r\n    if ($fixer) {\r\n        unset($fixer['user_id']);\r\n        $fixer['provider_name'] = $providers[$fixer['provider']];\r\n        if ($fixer['api_key']) {\r\n            $fixer['api_key'] = \"********\";\r\n        }\r\n    } else {\r\n        $fixer = [];\r\n        $notes[] = \"No fixer settings found\";\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"fixer\",\r\n        \"fixer\" => $fixer,\r\n        \"notes\" => $notes\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/household/get_household.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- household: an array of household members.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"household\",\r\n  \"household\": [\r\n    {\r\n      \"id\": 1,\r\n      \"name\": \"John Doe\",\r\n      \"email\": \"john@example.com\",\r\n      \"in_use\": true\r\n    },\r\n    {\r\n      \"id\": 2,\r\n      \"name\": \"Jane Doe\",\r\n      \"email\": \"jane@example.com\",\r\n      \"in_use\": true\r\n    }\r\n  ],\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n     // Get user from API key\r\n     $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n     $stmt = $db->prepare($sql);\r\n     $stmt->bindValue(':apiKey', $apiKey);\r\n     $result = $stmt->execute();\r\n     $user = $result->fetchArray(SQLITE3_ASSOC);\r\n \r\n     // If the user is not found, return an error\r\n     if (!$user) {\r\n         $response = [\r\n             \"success\" => false,\r\n             \"title\" => \"Invalid API key\"\r\n         ];\r\n         echo json_encode($response);\r\n         exit;\r\n     }\r\n \r\n     $userId = $user['id'];\r\n\r\n    $sql = \"SELECT * FROM household WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $household = [];\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $household[] = $row;\r\n    }\r\n\r\n    foreach ($household as $key => $value) {\r\n        unset($household[$key]['user_id']);\r\n        // Check if is used in any subscriptions\r\n        $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId AND payer_user_id = :householdId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindValue(':userId', $userId);\r\n        $stmt->bindValue(':householdId', $household[$key]['id']);\r\n        $result = $stmt->execute();\r\n        $subscription = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($subscription) {\r\n            $household[$key]['in_use'] = true;\r\n        } else {\r\n            $household[$key]['in_use'] = false;\r\n        }\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"household\",\r\n        \"household\" => $household,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/notifications/get_notification_settings.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- notification_settings: an object containing the notification settings, for the enabled methods.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"notification_settings\",\r\n  \"notification_settings\": {\r\n    \"email_notifications\": {\r\n      \"enabled\": 1,\r\n      \"smtp_address\": \"smtp.example.com\",\r\n      \"smtp_port\": 587,\r\n      \"smtp_username\": \"user@example.com\",\r\n      \"smtp_password\": \"********\",\r\n      \"from_email\": \"no-reply@example.com\",\r\n      \"encryption\": \"tls\",\r\n      \"other_emails\": \"other@example.com\"\r\n    },\r\n    \"ntfy_notifications\": {\r\n      \"enabled\": 0,\r\n      \"host\": \"http://notify.example.com\",\r\n      \"topic\": \"example_topic\",\r\n      \"headers\": \"********\"\r\n    }\r\n  },\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n   $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n\r\n    $query = \"SELECT * FROM notification_settings WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $notification_settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($notification_settings) {\r\n        unset($notification_settings['user_id']);\r\n    } else {\r\n        $notification_settings = [];\r\n    }\r\n\r\n    $query = \"SELECT * FROM email_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $email_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($email_notifications) {\r\n        unset($email_notifications['user_id']);\r\n        if (isset($email_notifications['smtp_password'])) {\r\n            $email_notifications['smtp_password'] = \"********\";\r\n        }\r\n        $notification_settings['email_notifications'] = $email_notifications;\r\n    }\r\n\r\n    $query = \"SELECT * FROM discord_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $discord_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($discord_notifications) {\r\n        unset($discord_notifications['user_id']);\r\n        $notification_settings['discord_notifications'] = $discord_notifications;\r\n    }\r\n\r\n    $query = \"SELECT * FROM gotify_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $gotify_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($gotify_notifications) {\r\n        unset($gotify_notifications['user_id']);\r\n        if (isset($gotify_notifications['token'])) {\r\n            $gotify_notifications['token'] = \"********\";\r\n        }\r\n        $notification_settings['gotify_notifications'] = $gotify_notifications;\r\n    }\r\n\r\n    $query = \"SELECT * FROM ntfy_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $ntfy_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($ntfy_notifications) {\r\n        unset($ntfy_notifications['user_id']);\r\n        if (isset($ntfy_notifications['headers'])) {\r\n            $ntfy_notifications['headers'] = \"********\";\r\n        }\r\n        $notification_settings['ntfy_notifications'] = $ntfy_notifications;\r\n    }\r\n\r\n    $query = \"SELECT * FROM pushover_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $pushover_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($pushover_notifications) {\r\n        unset($pushover_notifications['user_id']);\r\n        if (isset($pushover_notifications['token'])) {\r\n            $pushover_notifications['token'] = \"********\";\r\n        }\r\n        $notification_settings['pushover_notifications'] = $pushover_notifications;\r\n    }\r\n\r\n    $query = \"SELECT * FROM telegram_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $telegram_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($telegram_notifications) {\r\n        unset($telegram_notifications['user_id']);\r\n        if (isset($telegram_notifications['bot_token'])) {\r\n            $telegram_notifications['bot_token'] = \"********\";\r\n        }\r\n        $notification_settings['telegram_notifications'] = $telegram_notifications;\r\n    }\r\n\r\n    $query = \"SELECT * FROM webhook_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $webhook_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($webhook_notifications) {\r\n        unset($webhook_notifications['user_id']);\r\n        if (isset($webhook_notifications['headers'])) {\r\n            $webhook_notifications['headers'] = \"********\";\r\n        }\r\n        $notification_settings['webhook_notifications'] = $webhook_notifications;\r\n    }\r\n\r\n    // Serverchan notifications\r\n    $query = \"SELECT * FROM serverchan_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $serverchan_notifications = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($serverchan_notifications) {\r\n        unset($serverchan_notifications['user_id']);\r\n        if (isset($serverchan_notifications['sendkey'])) {\r\n            $serverchan_notifications['sendkey'] = \"********\";\r\n        }\r\n        $notification_settings['serverchan_notifications'] = $serverchan_notifications;\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"notification_settings\",\r\n        \"notification_settings\" => $notification_settings,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/payment_methods/get_payment_methods.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- payment_methods: an array of payment methods.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"payment_methods\",\r\n  \"payment_methods\": [\r\n    {\r\n      \"id\": 1,\r\n      \"name\": \"PayPal\",\r\n      \"icon\": \"images/uploads/icons/paypal.png\",\r\n      \"enabled\": 1,\r\n      \"order\": 1,\r\n      \"in_use\": true\r\n    },\r\n    {\r\n      \"id\": 2,\r\n      \"name\": \"Credit Card\",\r\n      \"icon\": \"images/uploads/icons/creditcard.png\",\r\n      \"enabled\": 1,\r\n      \"order\": 2,\r\n      \"in_use\": true\r\n    },\r\n    {\r\n      \"id\": 3,\r\n      \"name\": \"Bank Transfer\",\r\n      \"icon\": \"images/uploads/icons/banktransfer.png\",\r\n      \"enabled\": 1,\r\n      \"order\": 3,\r\n      \"in_use\": false\r\n    },\r\n    {\r\n      \"id\": 4,\r\n      \"name\": \"Direct Debit\",\r\n      \"icon\": \"images/uploads/icons/directdebit.png\",\r\n      \"enabled\": 1,\r\n      \"order\": 4,\r\n      \"in_use\": false\r\n    },\r\n    {\r\n      \"id\": 5,\r\n      \"name\": \"Cash\",\r\n      \"icon\": \"images/uploads/icons/cash.png\",\r\n      \"enabled\": 1,\r\n      \"order\": 5,\r\n      \"in_use\": false\r\n    },\r\n    {\r\n      \"id\": 6,\r\n      \"name\": \"Google Pay\",\r\n      \"icon\": \"images/uploads/icons/googlepay.png\",\r\n      \"enabled\": 1,\r\n      \"order\": 6,\r\n      \"in_use\": true\r\n    }\r\n  ],\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n\r\n    $sql = \"SELECT * FROM payment_methods WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $payment_methods = [];\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $payment_methods[] = $row;\r\n    }\r\n\r\n    foreach ($payment_methods as $key => $value) {\r\n        unset($payment_methods[$key]['user_id']);\r\n        // Check if is used in any subscriptions\r\n        $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId AND payment_method_id = :paymentMethodId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindValue(':userId', $userId);\r\n        $stmt->bindValue(':paymentMethodId', $payment_methods[$key]['id']);\r\n        $result = $stmt->execute();\r\n        $subscription = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($subscription) {\r\n            $payment_methods[$key]['in_use'] = true;\r\n        } else {\r\n            $payment_methods[$key]['in_use'] = false;\r\n        }\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"payment_methods\",\r\n        \"payment_methods\" => $payment_methods,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/settings/get_settings.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- settings: an object containing the user settings.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"settings\",\r\n  \"settings\": {\r\n    \"dark_theme\": 0,\r\n    \"monthly_price\": 1,\r\n    \"convert_currency\": 1,\r\n    \"remove_background\": 1,\r\n    \"color_theme\": \"red\",\r\n    \"hide_disabled\": 0,\r\n    \"disabled_to_bottom\": 1,\r\n    \"show_original_price\": 0,\r\n    \"mobile_nav\": 1,\r\n    \"custom_css\": {\r\n      \"css\": \"\"\r\n    }\r\n  },\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n\r\n    $sql = \"SELECT * FROM settings WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($settings) {\r\n        unset($settings['user_id']);\r\n    }\r\n\r\n    $sql = \"SELECT * FROM custom_colors WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $custom_colors = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($custom_colors) {\r\n        unset($custom_colors['user_id']);\r\n        $settings['custom_colors'] = $custom_colors;\r\n    }\r\n    \r\n\r\n    $sql = \"SELECT * FROM custom_css_style WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $custom_css = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($custom_css) {\r\n        unset($custom_css['user_id']);\r\n        $settings['custom_css'] = $custom_css;\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"settings\",\r\n        \"settings\" => $settings,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/status/version.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- version: a string containing the version matching the github package version.\r\n- version_number: a string containing the version number.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"version\",\r\n  \"version\": \"v2.42.1\",\r\n  \"version_number\": \"2.42.1\",\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/version.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $version_number = substr($version, 1);\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"version\",\r\n        \"version\" => $version,\r\n        \"version_number\" => $version_number,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "api/subscriptions/get_ical_feed.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- convert_currency: whether to convert to the main currency (boolean) default false.\r\n- api_key: the API key of the user.\r\n\r\nIt returns a downloadable VCAL file with the active subscriptions\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    function getPriceConverted($price, $currency, $database)\r\n    {\r\n        $query = \"SELECT rate FROM currencies WHERE id = :currency\";\r\n        $stmt = $database->prepare($query);\r\n        $stmt->bindParam(':currency', $currency, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        $exchangeRate = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($exchangeRate === false) {\r\n            return $price;\r\n        } else {\r\n            $fromRate = $exchangeRate['rate'];\r\n            return $price / $fromRate;\r\n        }\r\n    }\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $userId = $user['id'];\r\n    $userCurrencyId = $user['main_currency'];\r\n\r\n    // Get last exchange update date for user\r\n    $sql = \"SELECT * FROM last_exchange_update WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $lastExchangeUpdate = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    $canConvertCurrency = empty($lastExchangeUpdate['date']) ? false : true;\r\n\r\n    // Get currencies for user\r\n    $sql = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $currencies = [];\r\n    while ($currency = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $currencies[$currency['id']] = $currency;\r\n    }\r\n\r\n    // Get categories for user\r\n    $sql = \"SELECT * FROM categories WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $categories = [];\r\n    while ($category = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $categories[$category['id']] = $category['name'];\r\n    }\r\n\r\n    // Get members for user\r\n    $sql = \"SELECT * FROM household WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $members = [];\r\n    while ($member = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $members[$member['id']] = $member['name'];\r\n    }\r\n\r\n    // Get payment methods for user\r\n    $sql = \"SELECT * FROM payment_methods WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $paymentMethods = [];\r\n    while ($paymentMethod = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $paymentMethods[$paymentMethod['id']] = $paymentMethod['name'];\r\n    }\r\n\r\n    $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId AND inactive = 0 ORDER BY next_payment ASC\";\r\n\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $subscriptions = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $subscriptions[] = $row;\r\n        }\r\n    }\r\n\r\n    $subscriptionsToReturn = array();\r\n\r\n    // Get notification settings\r\n    $notificationQuery = \"SELECT days FROM notification_settings WHERE user_id = :userId\";\r\n    $notificationQueryStmt = $db->prepare($notificationQuery);\r\n    $notificationQueryStmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $notificationResult = $notificationQueryStmt->execute();\r\n    $globalNotificationDays = 1; // Default value\r\n    if ($row = $notificationResult->fetchArray(SQLITE3_ASSOC)) {\r\n        $globalNotificationDays = $row['days'];\r\n    }\r\n\r\n    foreach ($subscriptions as $subscription) {\r\n        $subscriptionToReturn = $subscription;\r\n\r\n        if (isset($_REQUEST['convert_currency']) && $_REQUEST['convert_currency'] === 'true' && $canConvertCurrency && $subscription['currency_id'] != $userCurrencyId) {\r\n            $subscriptionToReturn['price'] = getPriceConverted($subscription['price'], $subscription['currency_id'], $db);\r\n        } else {\r\n            $subscriptionToReturn['price'] = $subscription['price'];\r\n        }\r\n\r\n        $subscriptionToReturn['category_name'] = $categories[$subscription['category_id']];\r\n        $subscriptionToReturn['payer_user_name'] = $members[$subscription['payer_user_id']];\r\n        $subscriptionToReturn['payment_method_name'] = $paymentMethods[$subscription['payment_method_id']];\r\n\r\n        $subscriptionsToReturn[] = $subscriptionToReturn;\r\n    }\r\n\r\n    $stmt->bindValue(':inactive', false, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    header('Content-Type: text/calendar; charset=utf-8');\r\n    header('Content-Disposition: attachment; filename=\"subscriptions.ics\"');\r\n\r\n    if ($result === false) {\r\n        die(\"BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:NAME:\\nEND:VCALENDAR\");\r\n    }\r\n\r\n    $icsContent = \"BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Wallos//iCalendar//EN\\nNAME:Wallos\\nX-WR-CALNAME:Wallos\\n\";\r\n\r\n    while ($subscription = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $subscription['payer_user'] = $members[$subscription['payer_user_id']];\r\n        $subscription['category'] = $categories[$subscription['category_id']];\r\n        $subscription['payment_method'] = $paymentMethods[$subscription['payment_method_id']];\r\n        $subscription['currency'] = $currencies[$subscription['currency_id']]['symbol'];\r\n        $subscription['trigger'] = ($subscription['notify_days_before'] == -1) ? $globalNotificationDays : ($subscription['notify_days_before'] ?: 1);\r\n        $subscription['price'] = number_format($subscription['price'], 2);\r\n\r\n        $uid = 'wallos-subscription-' . $subscription['id'] . '@wallos';\r\n        $summary = html_entity_decode($subscription['name'], ENT_QUOTES, 'UTF-8');\r\n        $description = \"Price: {$subscription['currency']}{$subscription['price']}\\\\nCategory: {$subscription['category']}\\\\nPayment Method: {$subscription['payment_method']}\\\\nPayer: {$subscription['payer_user']}\\\\nNotes: {$subscription['notes']}\";\r\n        $dtstamp = gmdate('Ymd\\THis\\Z');\r\n        $dtstart = (new DateTime($subscription['next_payment']))->format('Ymd');\r\n        $dtend = (new DateTime($subscription['next_payment']))->format('Ymd');\r\n        $location = isset($subscription['url']) ? $subscription['url'] : '';\r\n        $alarm_trigger = '-P' . $subscription['trigger'] . 'D';\r\n\r\n        $icsContent .= <<<ICS\r\n        BEGIN:VEVENT\r\n        UID:$uid\r\n        DTSTAMP:$dtstamp\r\n        SUMMARY:$summary\r\n        DESCRIPTION:$description\r\n        DTSTART;VALUE=DATE:$dtstart\r\n        DTEND;VALUE=DATE:$dtend\r\n        LOCATION:$location\r\n        STATUS:CONFIRMED\r\n        TRANSP:OPAQUE\r\n        BEGIN:VALARM\r\n        ACTION:DISPLAY\r\n        DESCRIPTION:Reminder\r\n        TRIGGER:$alarm_trigger\r\n        END:VALARM\r\n        END:VEVENT\r\n\r\n        ICS;\r\n    }\r\n\r\n    $icsContent .= \"END:VCALENDAR\\n\";\r\n    echo $icsContent;\r\n    $db->close();\r\n    exit;\r\n        \r\n\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "api/subscriptions/get_monthly_cost.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- month: the month for which the cost is to be calculated (integer).\r\n- year: the year for which the cost is to be calculated (integer).\r\n- api_key: the API key of the user (string).\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: a string with \"${month} ${year}\" (e.g., \"March 2025\").\r\n- monthly_cost: a float with the total cost for the given month.\r\n- localized_monthly_cost: a string with the total cost formatted according to the user's locale and currency.\r\n- currency_code: a string with the currency code of the user's main currency.\r\n- currency_symbol: a string with the currency symbol of the user's main currency.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"March 2025\",\r\n  \"monthly_cost\": \"120.24\",\r\n  \"localized_monthly_cost\": \"€120.24\",\r\n  \"currency_code\": \"EUR\",\r\n  \"currency_symbol\": \"€\",\r\n  \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey || !isset($_REQUEST['month']) || !isset($_REQUEST['year'])) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    $month = $_REQUEST['month'];\r\n    $year = $_REQUEST['year'];\r\n\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n    // If the user is not found or the API key is invalid, return an error\r\n    if (!$user) {\r\n        echo json_encode([\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\",\r\n            \"notes\" => [\"User not found or API key invalid.\"]\r\n        ]);\r\n        exit;\r\n    }\r\n\r\n    $sql = \"SELECT * FROM last_exchange_update\";\r\n    $result = $db->query($sql);\r\n    $lastExchangeUpdate = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    $userId = $user['id'];\r\n    $userCurrencyId = $user['main_currency'];\r\n    $needsCurrencyConversion = false;\r\n    $canConvertCurrency = empty($lastExchangeUpdate['date']) ? false : true;\r\n\r\n    $sql = \"SELECT * FROM currencies WHERE id = :currencyId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':currencyId', $userCurrencyId);\r\n    $result = $stmt->execute();\r\n    $currency = $result->fetchArray(SQLITE3_ASSOC);\r\n    $currency_code = $currency['code'];\r\n    $currency_symbol = $currency['symbol'];\r\n\r\n\r\n    $title = date('F Y', strtotime($year . '-' . $month . '-01'));\r\n    $monthlyCost = 0;\r\n    $notes = [];\r\n\r\n    $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId AND inactive = 0\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $subscriptions = [];\r\n    while ($subscription = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $subscriptions[] = $subscription;\r\n        if ($subscription['currency_id'] !== $userCurrencyId) {\r\n            $needsCurrencyConversion = true;\r\n        }\r\n    }\r\n\r\n    if ($needsCurrencyConversion) {\r\n        if (!$canConvertCurrency) {\r\n            $notes[] = \"You are using multiple currencies, but the exchange rates have not been updated yet. Please check your Fixer API Key.\";\r\n        } else {\r\n            $sql = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n            $stmt = $db->prepare($sql);\r\n            $stmt->bindValue(':userId', $userId);\r\n            $result = $stmt->execute();\r\n            $currencies = [];\r\n            while ($currency = $result->fetchArray(SQLITE3_ASSOC)) {\r\n                $currencies[$currency['id']] = $currency['rate'];\r\n            }\r\n        }\r\n    }\r\n\r\n    // Calculate the monthly cost based on the next_payment_date, payment cycle, and payment frequency\r\n    foreach ($subscriptions as $subscription) {\r\n        $nextPaymentDate = strtotime($subscription['next_payment']);\r\n        $cycle = $subscription['cycle']; // Integer from 1 to 4\r\n        $frequency = $subscription['frequency'];\r\n\r\n        // Determine the strtotime increment string based on cycle\r\n        switch ($cycle) {\r\n            case 1: // Days\r\n                $incrementString = \"+{$frequency} days\";\r\n                break;\r\n            case 2: // Weeks\r\n                $incrementString = \"+{$frequency} weeks\";\r\n                break;\r\n            case 3: // Months\r\n                $incrementString = \"+{$frequency} months\";\r\n                break;\r\n            case 4: // Years\r\n                $incrementString = \"+{$frequency} years\";\r\n                break;\r\n            default:\r\n                $incrementString = \"+{$frequency} months\"; // Default case, if needed\r\n        }\r\n\r\n        // Calculate the start of the month\r\n        $startOfMonth = strtotime($year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01');\r\n\r\n        // Find the first payment date of the month by moving backwards\r\n        $startDate = $nextPaymentDate;\r\n        while ($startDate > $startOfMonth) {\r\n            $startDate = strtotime(\"-\" . $incrementString, $startDate);\r\n        }\r\n\r\n        // Calculate the monthly cost\r\n        for ($date = $startDate; $date <= strtotime(\"+1 month\", $startOfMonth); $date = strtotime($incrementString, $date)) {\r\n            if (date('Y-m', $date) == $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT)) {\r\n                $price = $subscription['price'];\r\n                if ($userCurrencyId !== $subscription['currency_id']) {\r\n                    $price *= $currencies[$userCurrencyId] / $currencies[$subscription['currency_id']];\r\n                }\r\n                $monthlyCost += $price;\r\n            }\r\n        }\r\n    }\r\n\r\n    $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);\r\n    $localizedMonthlyCost = $formatter->formatCurrency($monthlyCost, $currency_code);\r\n\r\n    echo json_encode([\r\n        'success' => true,\r\n        'title' => $title,\r\n        'monthly_cost' => number_format($monthlyCost, 2),\r\n        'localized_monthly_cost' => $localizedMonthlyCost,\r\n        'currency_code' => $currency_code,\r\n        'currency_symbol' => $currency_symbol,\r\n        'notes' => $notes\r\n    ], JSON_UNESCAPED_UNICODE);\r\n\r\n}\r\n?>"
  },
  {
    "path": "api/subscriptions/get_subscriptions.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- member: comma-separated IDs of the members to filter (integer) default null.\r\n- category: the ID of the category to filter (integer) default null.\r\n- payment_method: the ID of the payment method to filter (integer) default null.\r\n- state: the state of the subscription to filter (boolean) default null [0 - active, 1 - inactive].\r\n- disabled_to_bottom: whether to sort the inactive subscriptions to the bottom (boolean) default false.\r\n- sort: the sorting method (string) default next_payment ['name', 'id', 'next_payment', 'price', 'payer_user_id', 'category_id', 'payment_method_id', 'inactive', 'alphanumeric'].\r\n- convert_currency: whether to convert to the main currency (boolean) default false.\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- subscriptions: an array of subscriptions.\r\n- notes: warning messages or additional information (array).\r\n\r\nExample response:\r\n{\r\n    \"success\": true,\r\n    \"title\": \"subscriptions\",\r\n    \"subscriptions\": [\r\n        {\r\n            \"id\": 1,\r\n            \"name\": \"Example Subscription\",\r\n            \"logo\": \"example.png\",\r\n            \"price\": 10.00,\r\n            \"currency_id\": 1,\r\n            \"start_date\": \"2024-09-01\",\r\n            \"next_payment\": \"2024-09-01\",\r\n            \"cycle\": 1,\r\n            \"frequency\": 1,\r\n            \"auto_renew\": 1,\r\n            \"notes\": \"Example note\",\r\n            \"payment_method_id\": 1,\r\n            \"payer_user_id\": 1,\r\n            \"category_id\": 1,\r\n            \"notify\": 1,\r\n            \"url\": \"https://example.com\",\r\n            \"inactive\": 0,\r\n            \"notify_days_before\": 1,\r\n            \"user_id\": 1,\r\n            \"cancelation_date\": null,\r\n            \"cancellation_date\": \"\",\r\n            \"category_name\": \"General\",\r\n            \"payer_user_name\": \"John Doe\",\r\n            \"payment_method_name\": \"PayPal\"\r\n        },\r\n        {\r\n            \"id\": 2,\r\n            \"name\": \"Another Subscription\",\r\n            \"logo\": \"another.png\",\r\n            \"price\": 15.00,\r\n            \"currency_id\": 2,\r\n            \"start_date\": \"2024-09-02\",\r\n            \"next_payment\": \"2024-09-02\",\r\n            \"cycle\": 1,\r\n            \"frequency\": 1,\r\n            \"auto_renew\": 0,\r\n            \"notes\": \"\",\r\n            \"payment_method_id\": 2,\r\n            \"payer_user_id\": 2,\r\n            \"category_id\": 2,\r\n            \"notify\": 0,\r\n            \"url\": \"\",\r\n            \"inactive\": 1,\r\n            \"notify_days_before\": null,\r\n            \"user_id\": 2,\r\n            \"cancelation_date\": null,\r\n            \"cancellation_date\": \"\",\r\n            \"category_name\": \"Entertainment\",\r\n            \"payer_user_name\": \"Jane Doe\",\r\n            \"payment_method_name\": \"Credit Card\",\r\n            \"replacement_subscription_id\": 1\r\n        }\r\n    ],\r\n    \"users\": [\r\n        {\r\n            \"id\": 1,\r\n            \"name\": \"admin\",\r\n            \"email\": \"admin@example.com\"\r\n        },\r\n        {\r\n            \"id\": 2,\r\n            \"name\": \"user\",\r\n            \"email\": \"user@example.com\"\r\n        }\r\n    ],\r\n    \"notes\": []\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    function getPriceConverted($price, $currency, $database)\r\n    {\r\n        $query = \"SELECT rate FROM currencies WHERE id = :currency\";\r\n        $stmt = $database->prepare($query);\r\n        $stmt->bindParam(':currency', $currency, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $exchangeRate = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($exchangeRate === false) {\r\n            return $price;\r\n        } else {\r\n            $fromRate = $exchangeRate['rate'];\r\n            return $price / $fromRate;\r\n        }\r\n    }\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n    $userId = $user['id'];\r\n    $userCurrencyId = $user['main_currency'];\r\n\r\n    $allUserSubscription = isset($_REQUEST['all-user-subscription']) ? $_REQUEST['all-user-subscription'] : null;\r\n    if ($allUserSubscription == 1 && $userId != 1) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Denied. Not admin user\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    // Get last exchange update date for user\r\n    $sql = \"SELECT * FROM last_exchange_update WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $lastExchangeUpdate = $result->fetchArray(SQLITE3_ASSOC);\r\n    $canConvertCurrency = empty($lastExchangeUpdate['date']) ? false : true;\r\n\r\n    // Get currencies for user\r\n    $sql = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $currencies = [];\r\n    while ($currency = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $currencies[$currency['id']] = $currency;\r\n    }\r\n\r\n    // Get categories for user\r\n    $sql = \"SELECT * FROM categories WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $categories = [];\r\n    while ($category = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $categories[$category['id']] = $category['name'];\r\n    }\r\n\r\n    // Get members for user\r\n    $sql = \"SELECT * FROM household WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $members = [];\r\n    while ($member = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $members[$member['id']] = $member['name'];\r\n    }\r\n\r\n    // Get payment methods for user\r\n    $sql = \"SELECT * FROM payment_methods WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId);\r\n    $result = $stmt->execute();\r\n    $paymentMethods = [];\r\n    while ($paymentMethod = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $paymentMethods[$paymentMethod['id']] = $paymentMethod['name'];\r\n    }\r\n\r\n    $sort = \"next_payment\";\r\n    if (isset($_REQUEST['sort'])) {\r\n        $sort = $_REQUEST['sort'];\r\n    }\r\n    $sortOrder = $sort;\r\n    $allowedSortCriteria = ['name', 'id', 'next_payment', 'price', 'payer_user_id', 'category_id', 'payment_method_id', 'inactive', 'alphanumeric'];\r\n    $order = ($sort == \"price\" || $sort == \"id\") ? \"DESC\" : \"ASC\";\r\n    if ($sort == \"alphanumeric\") {\r\n        $sort = \"name\";\r\n    }\r\n    if (!in_array($sort, $allowedSortCriteria)) {\r\n        $sort = \"next_payment\";\r\n    }\r\n\r\n    // Construction of the main SQL Query\r\n    $params = [];\r\n    if ($allUserSubscription == 1 && $userId == 1) {\r\n        $sql = \"SELECT * FROM subscriptions\";\r\n    } else {\r\n        $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId\";\r\n        $params[':userId'] = $userId;\r\n    }\r\n\r\n    if (isset($_REQUEST['member'])) {\r\n        $memberIds = explode(',', $_REQUEST['member']);\r\n        $placeholders = array_map(function ($key) {\r\n            return \":member{$key}\";\r\n        }, array_keys($memberIds));\r\n        $sql .= \" AND payer_user_id IN (\" . implode(',', $placeholders) . \")\";\r\n        foreach ($memberIds as $key => $memberId) {\r\n            $params[\":member{$key}\"] = $memberId;\r\n        }\r\n    }\r\n    if (isset($_REQUEST['category'])) {\r\n        $categoryIds = explode(',', $_REQUEST['category']);\r\n        $placeholders = array_map(function ($key) {\r\n            return \":category{$key}\";\r\n        }, array_keys($categoryIds));\r\n        $sql .= \" AND category_id IN (\" . implode(',', $placeholders) . \")\";\r\n        foreach ($categoryIds as $key => $categoryId) {\r\n            $params[\":category{$key}\"] = $categoryId;\r\n        }\r\n    }\r\n    if (isset($_REQUEST['payment'])) {\r\n        $paymentIds = explode(',', $_REQUEST['payment']);\r\n        $placeholders = array_map(function ($key) {\r\n            return \":payment{$key}\";\r\n        }, array_keys($paymentIds));\r\n        $sql .= \" AND payment_method_id IN (\" . implode(',', $placeholders) . \")\";\r\n        foreach ($paymentIds as $key => $paymentId) {\r\n            $params[\":payment{$key}\"] = $paymentId;\r\n        }\r\n    }\r\n    if (isset($_REQUEST['state']) && $_REQUEST['state'] != \"\") {\r\n        $sql .= \" AND inactive = :inactive\";\r\n        $params[':inactive'] = $_REQUEST['state'];\r\n    }\r\n\r\n    $orderByClauses = [];\r\n    if (isset($_REQUEST['disabled_to_bottom']) && $_REQUEST['disabled_to_bottom'] === 'true') {\r\n        if (in_array($sort, [\"payer_user_id\", \"category_id\", \"payment_method_id\"])) {\r\n            $orderByClauses[] = \"$sort $order\";\r\n            $orderByClauses[] = \"inactive ASC\";\r\n        } else {\r\n            $orderByClauses[] = \"inactive ASC\";\r\n            $orderByClauses[] = \"$sort $order\";\r\n        }\r\n    } else {\r\n        $orderByClauses[] = \"$sort $order\";\r\n        if ($sort != \"inactive\") {\r\n            $orderByClauses[] = \"inactive ASC\";\r\n        }\r\n    }\r\n    if ($sort != \"next_payment\") {\r\n        $orderByClauses[] = \"next_payment ASC\";\r\n    }\r\n    $sql .= \" ORDER BY \" . implode(\", \", $orderByClauses);\r\n\r\n    $stmt = $db->prepare($sql);\r\n    if (!empty($params)) {\r\n        foreach ($params as $key => $value) {\r\n            $stmt->bindValue($key, $value, SQLITE3_INTEGER);\r\n        }\r\n    }\r\n    $result = $stmt->execute();\r\n    if ($result) {\r\n        $subscriptions = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $subscriptions[] = $row;\r\n        }\r\n    }\r\n    $subscriptionsToReturn = array();\r\n    foreach ($subscriptions as $subscription) {\r\n        $subscriptionToReturn = $subscription;\r\n        if (isset($_REQUEST['convert_currency']) && $_REQUEST['convert_currency'] === 'true' && $canConvertCurrency && $subscription['currency_id'] != $userCurrencyId) {\r\n            $subscriptionToReturn['price'] = getPriceConverted($subscription['price'], $subscription['currency_id'], $db);\r\n        } else {\r\n            $subscriptionToReturn['price'] = $subscription['price'];\r\n        }\r\n        $subscriptionToReturn['category_name'] = isset($categories[$subscription['category_id']]) ? $categories[$subscription['category_id']] : 'No category';\r\n        $subscriptionToReturn['payer_user_name'] = isset($members[$subscription['payer_user_id']]) ? $members[$subscription['payer_user_id']] : 'Unknown member';\r\n        $subscriptionToReturn['payment_method_name'] = isset($paymentMethods[$subscription['payment_method_id']]) ? $paymentMethods[$subscription['payment_method_id']] : 'Unknown payment method';\r\n        $subscriptionsToReturn[] = $subscriptionToReturn;\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"subscriptions\",\r\n        \"subscriptions\" => $subscriptionsToReturn,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    if ($allUserSubscription == 1 && $userId == 1) {\r\n        $sql = \"PRAGMA table_info(user)\";\r\n        $stmt = $db->prepare($sql);\r\n        $result = $stmt->execute();\r\n        $userColumns = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $userColumns[] = $row['name'];\r\n        }\r\n        $userNameCol = in_array('username', $userColumns) ? 'username' : null;\r\n        $userEmailCol = in_array('email', $userColumns) ? 'email' : null;\r\n        if ($userNameCol && $userEmailCol) {\r\n            $sql = \"SELECT id, $userNameCol as name, $userEmailCol as email FROM user\";\r\n        } elseif ($userNameCol) {\r\n            $sql = \"SELECT id, $userNameCol as name FROM user\";\r\n        } elseif ($userEmailCol) {\r\n            $sql = \"SELECT id, $userEmailCol as email FROM user\";\r\n        } else {\r\n            $sql = \"SELECT id FROM user\";\r\n        }\r\n        $stmt = $db->prepare($sql);\r\n        $result = $stmt->execute();\r\n        $users = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $users[] = $row;\r\n        }\r\n        $response['users'] = $users;\r\n    }\r\n\r\n    echo json_encode($response);\r\n    $db->close();\r\n    exit;\r\n\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}"
  },
  {
    "path": "api/users/get_user.php",
    "content": "<?php\r\n/*\r\nThis API Endpoint accepts both POST and GET requests.\r\nIt receives the following parameters:\r\n- api_key: the API key of the user.\r\n\r\nIt returns a JSON object with the following properties:\r\n- success: whether the request was successful (boolean).\r\n- title: the title of the response (string).\r\n- notes: warning messages or additional information (array).\r\n- user: an object containing the user details.\r\n\r\nExample response:\r\n{\r\n  \"success\": true,\r\n  \"title\": \"user\",\r\n  \"user\": {\r\n    \"id\": 1,\r\n    \"username\": \"johndoe\",\r\n    \"email\": \"john.doe@example.com\",\r\n    \"password\": \"********\",\r\n    \"main_currency\": 1,\r\n    \"avatar\": \"images/uploads/logos/avatars/default-avatar.jpg\",\r\n    \"language\": \"en\",\r\n    \"budget\": 100,\r\n    \"totp_enabled\": 0,\r\n    \"api_key\": \"********\"\r\n  },\r\n  \"notes\": \"\"\r\n}\r\n*/\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nheader('Content-Type: application/json; charset=UTF-8');\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\" || $_SERVER[\"REQUEST_METHOD\"] === \"GET\") {\r\n    // if the parameters are not set, return an error\r\n\r\n    $apiKey = $_REQUEST['api_key'] ?? $_REQUEST['apiKey'] ?? null;\r\n\r\n    if (!$apiKey) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Missing parameters\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n\r\n    // Get user from API key\r\n    $sql = \"SELECT * FROM user WHERE api_key = :apiKey\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':apiKey', $apiKey);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    // If the user is not found, return an error\r\n    if (!$user) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"title\" => \"Invalid API key\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    // remove password and api_key from array\r\n    $user['password'] = \"********\";\r\n    $user['api_key'] = \"********\";\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"title\" => \"user\",\r\n        \"user\" => $user,\r\n        \"notes\" => []\r\n    ];\r\n\r\n    echo json_encode($response);\r\n\r\n    $db->close();\r\n\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"title\" => \"Invalid request method\"\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n?>"
  },
  {
    "path": "calendar.php",
    "content": "<?php\r\nrequire_once 'includes/header.php';\r\n\r\nfunction getPriceConverted($price, $currency, $database, $userId)\r\n{\r\n  $query = \"SELECT rate FROM currencies WHERE id = :currency AND user_id = :userId\";\r\n  $stmt = $database->prepare($query);\r\n  $stmt->bindParam(':currency', $currency, SQLITE3_INTEGER);\r\n  $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n  $result = $stmt->execute();\r\n\r\n  $exchangeRate = $result->fetchArray(SQLITE3_ASSOC);\r\n  if ($exchangeRate === false) {\r\n    return $price;\r\n  } else {\r\n    $fromRate = $exchangeRate['rate'];\r\n    return $price / $fromRate;\r\n  }\r\n}\r\n\r\n// Get budget from user table\r\n$query = \"SELECT budget FROM user WHERE id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n$budget = $row['budget'] ?? 0;\r\n\r\n$currentMonth = date('m');\r\n$currentYear = date('Y');\r\n$sameAsCurrent = false;\r\n\r\nif ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['month']) && isset($_GET['year'])) {\r\n  // Don't allow viewing past months\r\n  $selectedMonth = str_pad($_GET['month'], 2, '0', STR_PAD_LEFT);\r\n  $selectedYear = $_GET['year'];\r\n\r\n  $selectedTimestamp = strtotime($selectedYear . '-' . $selectedMonth . '-01');\r\n  $currentTimestamp = strtotime($currentYear . '-' . $currentMonth . '-01');\r\n\r\n  if ($selectedTimestamp < $currentTimestamp) {\r\n    $calendarMonth = $currentMonth;\r\n    $calendarYear = $currentYear;\r\n  } else {\r\n    $calendarMonth = $selectedMonth;\r\n    $calendarYear = $selectedYear;\r\n  }\r\n\r\n  if ($calendarMonth == $currentMonth && $calendarYear == $currentYear) {\r\n    $sameAsCurrent = true;\r\n  }\r\n} else {\r\n  $calendarMonth = $currentMonth;\r\n  $calendarYear = $currentYear;\r\n  $sameAsCurrent = true;\r\n}\r\n\r\n$currenciesInUse = [];\r\n$numberOfSubscriptionsToPayThisMonth = 0;\r\n$totalCostThisMonth = 0;\r\n$amountDueThisMonth = 0;\r\n\r\n$query = \"SELECT * FROM subscriptions WHERE user_id = :user_id AND inactive = 0\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$subscriptions = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n  $subscriptions[] = $row;\r\n  $currenciesInUse[] = $row['currency_id'];\r\n}\r\n\r\n$currenciesInUse = array_unique($currenciesInUse);\r\n$usesMultipleCurrencies = count($currenciesInUse) > 1;\r\n\r\n$showCantConverErrorMessage = false;\r\nif ($usesMultipleCurrencies) {\r\n  $query = \"SELECT api_key FROM fixer WHERE user_id = :userId\";\r\n  $stmt = $db->prepare($query);\r\n  $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n  $result = $stmt->execute();\r\n  if ($result->fetchArray(SQLITE3_ASSOC) === false) {\r\n    $showCantConverErrorMessage = true;\r\n  }\r\n}\r\n\r\n// Get code of main currency to display on statistics\r\n$query = \"SELECT c.code\r\n          FROM currencies c\r\n          INNER JOIN user u ON c.id = u.main_currency\r\n          WHERE u.id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n$code = $row['code'];\r\n\r\n$yearsToLoad = $calendarYear - $currentYear + 1;\r\n?>\r\n\r\n<section class=\"contain\">\r\n  <?php\r\n  if ($showCantConverErrorMessage) {\r\n    ?>\r\n    <div class=\"error-box\">\r\n      <div class=\"error-message\">\r\n        <i class=\"fa-solid fa-exclamation-circle\"></i>\r\n        <?= translate('cant_convert_currency', $i18n) ?>\r\n      </div>\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n  <div class=\"split-header\">\r\n    <h2>\r\n    <?= translate('calendar', $i18n) ?>\r\n      <button class=\"button export-ical\" onClick=\"showExportPopup()\" title=\"<?= translate('export_icalendar', $i18n) ?>\">\r\n        <?php require_once 'images/siteicons/svg/export_ical.php'; ?>\r\n      </button>\r\n    </h2>\r\n    <div id=\"subscriptions_calendar\" class=\"subscription-modal\">\r\n        <div class=\"modal-header\">\r\n            <h3><?= translate('export_icalendar', $i18n) ?></h3>\r\n            <span class=\"fa-solid fa-xmark close-modal\" onclick=\"closePopup()\"></span>\r\n        </div>\r\n        <div class=\"form-group-inline\">\r\n            <input id=\"iCalendarUrl\" type=\"text\" value=\"\" readonly>\r\n            <input type=\"hidden\" id=\"apiKey\" value=\"<?= $userData['api_key'] ?>\">\r\n            <button onclick=\"copyToClipboard()\" class=\"button tiny\"> <?= translate('copy_to_clipboard', $i18n) ?> </button>\r\n        </div>\r\n    </div>\r\n\r\n    <div class=\"calendar-nav\">\r\n      <?php\r\n      if (!$sameAsCurrent) {\r\n        ?>\r\n        <button class=\"button secondary-button tiny\" onClick=\"currentMoth()\" title=\"<?= translate('reset', $i18n) ?>\"><i\r\n            class=\"fa-solid fa-calendar-day\"></i></button>\r\n        <button class=\"button tiny\" id=\"prev\" onclick=\"prevMonth(<?= $calendarMonth ?>, <?= $calendarYear ?>)\"><i\r\n            class=\"fa-solid fa-chevron-left\"></i></button>\r\n        <?php\r\n      }\r\n      ?>\r\n      <span id=\"month\" class=\"month\"><?= translate('month-' . $calendarMonth, $i18n) ?> <?= $calendarYear ?></span>\r\n      <button class=\"button tiny\" id=\"next\" onclick=\"nextMonth(<?= $calendarMonth ?>, <?= $calendarYear ?>)\"><i\r\n          class=\"fa-solid fa-chevron-right\"></i></button>\r\n    </div>\r\n  </div>\r\n  <div>\r\n    <?php\r\n    $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $calendarMonth, $calendarYear);\r\n    $firstDay = mktime(0, 0, 0, $calendarMonth, 1, $calendarYear);\r\n    $firstDayOfWeek = date('N', $firstDay) - 1; // Adjusted to make Monday (1) the first day\r\n    $dayOfWeek = 0;\r\n    $day = 1;\r\n    $days = 1;\r\n    $week = 1;\r\n    $today = date('Y-m-d');\r\n    $today = explode('-', $today);\r\n    $todayYear = $today[0];\r\n    $todayMonth = $today[1];\r\n    $todayDay = $today[2];\r\n    $today = $todayYear . '-' . $todayMonth . '-' . $todayDay;\r\n    $today = strtotime($today);\r\n    ?>\r\n\r\n    <div class=\"calendar\">\r\n      <div class=\"calendar-header\">\r\n        <div class=\"calendar-cell\"><?= translate('mon', $i18n) ?></div>\r\n        <div class=\"calendar-cell\"><?= translate('tue', $i18n) ?></div>\r\n        <div class=\"calendar-cell\"><?= translate('wed', $i18n) ?></div>\r\n        <div class=\"calendar-cell\"><?= translate('thu', $i18n) ?></div>\r\n        <div class=\"calendar-cell\"><?= translate('fri', $i18n) ?></div>\r\n        <div class=\"calendar-cell\"><?= translate('sat', $i18n) ?></div>\r\n        <div class=\"calendar-cell\"><?= translate('sun', $i18n) ?></div>\r\n      </div>\r\n      <div class=\"calendar-body\">\r\n        <div class=\"week calendar-row\">\r\n          <?php\r\n          for ($i = 0; $i < $firstDayOfWeek; $i++) { // Fill empty cells if month doesn't start on Monday\r\n            ?>\r\n            <div class=\"calendar-cell empty\">\r\n              <div class=\"calendar-cell-header\">\r\n                <span class=\"day\">&nbsp;</span>\r\n              </div>\r\n              <div class=\"calendar-cell-content\"></div>\r\n            </div>\r\n            <?php\r\n          }\r\n          for ($i = $firstDayOfWeek; $i < 7; $i++) {\r\n            if ($day <= $daysInMonth) {\r\n              $dayClass = ($day == $todayDay && $calendarMonth == $todayMonth && $calendarYear == $todayYear) ? \"today\" : \"\";\r\n              ?>\r\n              <div class=\"calendar-cell <?= $dayClass ?>\">\r\n                <div class=\"calendar-cell-header\">\r\n                  <span class=\"day\"><?= $day ?></span>\r\n                </div>\r\n                <div class=\"calendar-cell-content\">\r\n                  <?php\r\n                  foreach ($subscriptions as $subscription) {\r\n                    $nextPaymentDate = strtotime($subscription['next_payment']);\r\n                    $cycle = $subscription['cycle']; // Integer from 1 to 4\r\n                    $frequency = $subscription['frequency'];\r\n\r\n                    $endDate = strtotime(\"+\" . $yearsToLoad . \" years\", $nextPaymentDate);\r\n\r\n                    // Determine the strtotime increment string based on cycle\r\n                    switch ($cycle) {\r\n                      case 1: // Days\r\n                        $incrementString = \"+{$frequency} days\";\r\n                        break;\r\n                      case 2: // Weeks\r\n                        $incrementString = \"+{$frequency} weeks\";\r\n                        break;\r\n                      case 3: // Months\r\n                        $incrementString = \"+{$frequency} months\";\r\n                        break;\r\n                      case 4: // Years\r\n                        $incrementString = \"+{$frequency} years\";\r\n                        break;\r\n                      default:\r\n                        $incrementString = \"+{$frequency} months\"; // Default case, if needed\r\n                    }\r\n\r\n                    // Calculate the start of the month\r\n                    $startOfMonth = strtotime($calendarYear . '-' . str_pad($calendarMonth, 2, '0', STR_PAD_LEFT) . '-01');\r\n\r\n                    // Find the first payment date of the month by moving backwards\r\n                    $startDate = $nextPaymentDate;\r\n                    while ($startDate > $startOfMonth) {\r\n                      $startDate = strtotime(\"-\" . $incrementString, $startDate);\r\n                    }\r\n\r\n                    for ($date = $startDate; $date <= $endDate; $date = strtotime($incrementString, $date)) {\r\n                      if (date('Y-m', $date) == $calendarYear . '-' . str_pad($calendarMonth, 2, '0', STR_PAD_LEFT)) {\r\n                        if (date('d', $date) == $day) {\r\n                          $totalCostThisMonth += getPriceConverted($subscription['price'], $subscription['currency_id'], $db, $userId);\r\n                          $numberOfSubscriptionsToPayThisMonth++;\r\n                          if ($date > $today) {\r\n                            $amountDueThisMonth += getPriceConverted($subscription['price'], $subscription['currency_id'], $db, $userId);\r\n                          }\r\n                          ?>\r\n                          <div class=\"calendar-subscription-title\" onClick=\"openSubscriptionModal(<?= $subscription['id'] ?>)\">\r\n                            <?= htmlspecialchars($subscription['name']) ?>\r\n                          </div>\r\n                          <?php\r\n                        }\r\n                      }\r\n                    }\r\n                  }\r\n                  ?>\r\n                </div>\r\n              </div>\r\n              <?php\r\n              $day++;\r\n            }\r\n          }\r\n          while ($day <= $daysInMonth) {\r\n            if ($dayOfWeek % 7 == 0) {\r\n              ?>\r\n            </div>\r\n            <div class=\"week calendar-row\">\r\n              <?php\r\n            }\r\n            $dayClass = ($day == $todayDay && $calendarMonth == $todayMonth && $calendarYear == $todayYear) ? \"today\" : \"\";\r\n            ?>\r\n            <div class=\"calendar-cell <?= $dayClass ?>\">\r\n              <div class=\"calendar-cell-header\">\r\n                <span class=\"day\"><?= $day ?></span>\r\n              </div>\r\n              <div class=\"calendar-cell-content\">\r\n                <?php\r\n                foreach ($subscriptions as $subscription) {\r\n                  $nextPaymentDate = strtotime($subscription['next_payment']);\r\n                  $cycle = $subscription['cycle']; // Integer from 1 to 4\r\n                  $frequency = $subscription['frequency'];\r\n\r\n                  $endDate = strtotime(\"+\" . $yearsToLoad . \" years\", $nextPaymentDate);\r\n\r\n                  // Determine the strtotime increment string based on cycle\r\n                  switch ($cycle) {\r\n                    case 1: // Days\r\n                      $incrementString = \"+{$frequency} days\";\r\n                      break;\r\n                    case 2: // Weeks\r\n                      $incrementString = \"+{$frequency} weeks\";\r\n                      break;\r\n                    case 3: // Months\r\n                      $incrementString = \"+{$frequency} months\";\r\n                      break;\r\n                    case 4: // Years\r\n                      $incrementString = \"+{$frequency} years\";\r\n                      break;\r\n                    default:\r\n                      $incrementString = \"+{$frequency} months\"; // Default case, if needed\r\n                  }\r\n\r\n                  // Calculate the start of the month\r\n                  $startOfMonth = strtotime($calendarYear . '-' . str_pad($calendarMonth, 2, '0', STR_PAD_LEFT) . '-01');\r\n\r\n                  // Find the first payment date of the month by moving backwards\r\n                  $startDate = $nextPaymentDate;\r\n                  while ($startDate > $startOfMonth) {\r\n                    $startDate = strtotime(\"-\" . $incrementString, $startDate);\r\n                  }\r\n\r\n                  for ($date = $startDate; $date <= $endDate; $date = strtotime($incrementString, $date)) {\r\n                    if (date('Y-m', $date) == $calendarYear . '-' . str_pad($calendarMonth, 2, '0', STR_PAD_LEFT)) {\r\n                      if (date('d', $date) == $day) {\r\n                        $totalCostThisMonth += getPriceConverted($subscription['price'], $subscription['currency_id'], $db, $userId);\r\n                        $numberOfSubscriptionsToPayThisMonth++;\r\n                        if ($date > $today) {\r\n                          $amountDueThisMonth += getPriceConverted($subscription['price'], $subscription['currency_id'], $db, $userId);\r\n                        }\r\n                        ?>\r\n                        <div class=\"calendar-subscription-title\" onClick=\"openSubscriptionModal(<?= $subscription['id'] ?>)\">\r\n                          <?= $subscription['name'] ?>\r\n                        </div>\r\n                        <?php\r\n                      }\r\n                    }\r\n                  }\r\n                }\r\n                ?>\r\n              </div>\r\n            </div>\r\n            <?php\r\n            $day++;\r\n            $dayOfWeek++;\r\n          }\r\n          while ($dayOfWeek % 7 != 0) { // Fill the rest of the week with empty cells\r\n            ?>\r\n            <div class=\"calendar-cell empty\">\r\n              <div class=\"calendar-cell-header\">\r\n                <span class=\"day\">&nbsp;</span>\r\n              </div>\r\n              <div class=\"calendar-cell-content\"></div>\r\n            </div>\r\n            <?php\r\n            $dayOfWeek++;\r\n          }\r\n          ?>\r\n        </div>\r\n      </div>\r\n    </div>\r\n\r\n    <?php\r\n      if ($budget > 0 && $totalCostThisMonth > $budget) {\r\n        $overBudgetAmount = $totalCostThisMonth - $budget;\r\n        $overBudgetAmount = CurrencyFormatter::format($overBudgetAmount, $code);\r\n        ?>\r\n          <div class=\"over-budget\">\r\n            <i class=\"fa-solid fa-exclamation-triangle\"></i>\r\n            <?= translate('over_budget_warning', $i18n) ?>  (<?= $overBudgetAmount ?>)\r\n          </div>\r\n        <?php\r\n      }\r\n    ?>    \r\n\r\n    <div class=\"calendar-monthly-stats\">\r\n      <div class=\"calendar-monthly-stats-header\">\r\n        <h3><?= translate(\"stats\", $i18n) ?></h3>\r\n      </div>\r\n      <div class=\"statistics\">\r\n        <div class=\"statistic\">\r\n          <span>\r\n            <?= $numberOfSubscriptionsToPayThisMonth ?></span>\r\n          <div class=\"title\"><?= translate(\"active_subscriptions\", $i18n) ?></div>\r\n        </div>\r\n        <div class=\"statistic\">\r\n          <span><?= CurrencyFormatter::format($totalCostThisMonth, $code) ?></span>\r\n          <div class=\"title\"><?= translate(\"total_cost\", $i18n) ?></div>\r\n        </div>\r\n        <div class=\"statistic\">\r\n          <span><?= CurrencyFormatter::format($amountDueThisMonth, $code) ?></span>\r\n          <div class=\"title\"><?= translate(\"amount_due\", $i18n) ?></div>\r\n        </div>\r\n      </div>\r\n    </div>\r\n\r\n</section>\r\n\r\n<div id=\"subscriptionModal\" class=\"subscription-modal\">\r\n  <div class=\"modal-content\">\r\n    <div id=\"subscriptionModalContent\"></div>\r\n  </div>\r\n</div>\r\n\r\n<script src=\"scripts/calendar.js?<?= $version ?>\"></script>\r\n<?php\r\nrequire_once 'includes/footer.php';\r\n?>"
  },
  {
    "path": "cronjobs",
    "content": "# Run the scripts every day\r\n0 1 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/updatenextpayment.php >> /var/log/cron/updatenextpayment.log 2>&1\r\n0 2 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/updateexchange.php >> /var/log/cron/updateexchange.log 2>&1\r\n0 8 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendcancellationnotifications.php >> /var/log/cron/sendcancellationnotifications.log 2>&1\r\n0 9 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendnotifications.php >> /var/log/cron/sendnotifications.log 2>&1\r\n*/2 * * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendverificationemails.php >> /var/log/cron/sendverificationemails.log 2>&1\r\n*/2 * * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendresetpasswordemails.php >> /var/log/cron/sendresetpasswordemails.log 2>&1\r\n0 */6 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/checkforupdates.php >> /var/log/cron/checkforupdates.log 2>&1\r\n30 1 * * 1 /usr/local/bin/php /var/www/html/endpoints/cronjobs/storetotalyearlycost.php >> /var/log/cron/storetotalyearlycost.log 2>&1\r\n0 3 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/cleanupresettokens.php >> /var/log/cron/cleanupresettokens.log 2>&1"
  },
  {
    "path": "docker-compose.yaml",
    "content": "services:\n  wallos:\n    container_name: wallos\n    image: bellamy/wallos:latest\n    ports:\n      - \"8282:80/tcp\"\n    environment:\n      TZ: 'America/Toronto'\n    # Volumes store your data between container upgrades\n    volumes:\n      - './db:/var/www/html/db'\n      - './logos:/var/www/html/images/uploads/logos'\n    restart: unless-stopped\n"
  },
  {
    "path": "endpoints/admin/adduser.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$currencies = [\r\n    ['id' => 1, 'name' => 'Euro', 'symbol' => '€', 'code' => 'EUR'],\r\n    ['id' => 2, 'name' => 'US Dollar', 'symbol' => '$', 'code' => 'USD'],\r\n    ['id' => 3, 'name' => 'Japanese Yen', 'symbol' => '¥', 'code' => 'JPY'],\r\n    ['id' => 4, 'name' => 'Bulgarian Lev', 'symbol' => 'лв', 'code' => 'BGN'],\r\n    ['id' => 5, 'name' => 'Czech Republic Koruna', 'symbol' => 'Kč', 'code' => 'CZK'],\r\n    ['id' => 6, 'name' => 'Danish Krone', 'symbol' => 'kr', 'code' => 'DKK'],\r\n    ['id' => 7, 'name' => 'British Pound Sterling', 'symbol' => '£', 'code' => 'GBP'],\r\n    ['id' => 8, 'name' => 'Hungarian Forint', 'symbol' => 'Ft', 'code' => 'HUF'],\r\n    ['id' => 9, 'name' => 'Polish Zloty', 'symbol' => 'zł', 'code' => 'PLN'],\r\n    ['id' => 10, 'name' => 'Romanian Leu', 'symbol' => 'lei', 'code' => 'RON'],\r\n    ['id' => 11, 'name' => 'Swedish Krona', 'symbol' => 'kr', 'code' => 'SEK'],\r\n    ['id' => 12, 'name' => 'Swiss Franc', 'symbol' => 'Fr', 'code' => 'CHF'],\r\n    ['id' => 13, 'name' => 'Icelandic Króna', 'symbol' => 'kr', 'code' => 'ISK'],\r\n    ['id' => 14, 'name' => 'Norwegian Krone', 'symbol' => 'kr', 'code' => 'NOK'],\r\n    ['id' => 15, 'name' => 'Russian Ruble', 'symbol' => '₽', 'code' => 'RUB'],\r\n    ['id' => 16, 'name' => 'Turkish Lira', 'symbol' => '₺', 'code' => 'TRY'],\r\n    ['id' => 17, 'name' => 'Australian Dollar', 'symbol' => '$', 'code' => 'AUD'],\r\n    ['id' => 18, 'name' => 'Brazilian Real', 'symbol' => 'R$', 'code' => 'BRL'],\r\n    ['id' => 19, 'name' => 'Canadian Dollar', 'symbol' => '$', 'code' => 'CAD'],\r\n    ['id' => 20, 'name' => 'Chinese Yuan', 'symbol' => '¥', 'code' => 'CNY'],\r\n    ['id' => 21, 'name' => 'Hong Kong Dollar', 'symbol' => 'HK$', 'code' => 'HKD'],\r\n    ['id' => 22, 'name' => 'Indonesian Rupiah', 'symbol' => 'Rp', 'code' => 'IDR'],\r\n    ['id' => 23, 'name' => 'Israeli New Sheqel', 'symbol' => '₪', 'code' => 'ILS'],\r\n    ['id' => 24, 'name' => 'Indian Rupee', 'symbol' => '₹', 'code' => 'INR'],\r\n    ['id' => 25, 'name' => 'South Korean Won', 'symbol' => '₩', 'code' => 'KRW'],\r\n    ['id' => 26, 'name' => 'Mexican Peso', 'symbol' => 'Mex$', 'code' => 'MXN'],\r\n    ['id' => 27, 'name' => 'Malaysian Ringgit', 'symbol' => 'RM', 'code' => 'MYR'],\r\n    ['id' => 28, 'name' => 'New Zealand Dollar', 'symbol' => 'NZ$', 'code' => 'NZD'],\r\n    ['id' => 29, 'name' => 'Philippine Peso', 'symbol' => '₱', 'code' => 'PHP'],\r\n    ['id' => 30, 'name' => 'Singapore Dollar', 'symbol' => 'S$', 'code' => 'SGD'],\r\n    ['id' => 31, 'name' => 'Thai Baht', 'symbol' => '฿', 'code' => 'THB'],\r\n    ['id' => 32, 'name' => 'South African Rand', 'symbol' => 'R', 'code' => 'ZAR'],\r\n    ['id' => 33, 'name' => 'Ukrainian Hryvnia', 'symbol' => '₴', 'code' => 'UAH'],\r\n    ['id' => 34, 'name' => 'New Taiwan Dollar', 'symbol' => 'NT$', 'code' => 'TWD'],\r\n];\r\n\r\n$categories = [\r\n    ['id' => 1, 'name' => 'No category'],\r\n    ['id' => 2, 'name' => 'Entertainment'],\r\n    ['id' => 3, 'name' => 'Music'],\r\n    ['id' => 4, 'name' => 'Utilities'],\r\n    ['id' => 5, 'name' => 'Food & Beverages'],\r\n    ['id' => 6, 'name' => 'Health & Wellbeing'],\r\n    ['id' => 7, 'name' => 'Productivity'],\r\n    ['id' => 8, 'name' => 'Banking'],\r\n    ['id' => 9, 'name' => 'Transport'],\r\n    ['id' => 10, 'name' => 'Education'],\r\n    ['id' => 11, 'name' => 'Insurance'],\r\n    ['id' => 12, 'name' => 'Gaming'],\r\n    ['id' => 13, 'name' => 'News & Magazines'],\r\n    ['id' => 14, 'name' => 'Software'],\r\n    ['id' => 15, 'name' => 'Technology'],\r\n    ['id' => 16, 'name' => 'Cloud Services'],\r\n    ['id' => 17, 'name' => 'Charity & Donations'],\r\n];\r\n\r\n$payment_methods = [\r\n    ['id' => 1, 'name' => 'PayPal', 'icon' => 'images/uploads/icons/paypal.png'],\r\n    ['id' => 2, 'name' => 'Credit Card', 'icon' => 'images/uploads/icons/creditcard.png'],\r\n    ['id' => 3, 'name' => 'Bank Transfer', 'icon' => 'images/uploads/icons/banktransfer.png'],\r\n    ['id' => 4, 'name' => 'Direct Debit', 'icon' => 'images/uploads/icons/directdebit.png'],\r\n    ['id' => 5, 'name' => 'Money', 'icon' => 'images/uploads/icons/money.png'],\r\n    ['id' => 6, 'name' => 'Google Pay', 'icon' => 'images/uploads/icons/googlepay.png'],\r\n    ['id' => 7, 'name' => 'Samsung Pay', 'icon' => 'images/uploads/icons/samsungpay.png'],\r\n    ['id' => 8, 'name' => 'Apple Pay', 'icon' => 'images/uploads/icons/applepay.png'],\r\n    ['id' => 9, 'name' => 'Crypto', 'icon' => 'images/uploads/icons/crypto.png'],\r\n    ['id' => 10, 'name' => 'Klarna', 'icon' => 'images/uploads/icons/klarna.png'],\r\n    ['id' => 11, 'name' => 'Amazon Pay', 'icon' => 'images/uploads/icons/amazonpay.png'],\r\n    ['id' => 12, 'name' => 'SEPA', 'icon' => 'images/uploads/icons/sepa.png'],\r\n    ['id' => 13, 'name' => 'Skrill', 'icon' => 'images/uploads/icons/skrill.png'],\r\n    ['id' => 14, 'name' => 'Sofort', 'icon' => 'images/uploads/icons/sofort.png'],\r\n    ['id' => 15, 'name' => 'Stripe', 'icon' => 'images/uploads/icons/stripe.png'],\r\n    ['id' => 16, 'name' => 'Affirm', 'icon' => 'images/uploads/icons/affirm.png'],\r\n    ['id' => 17, 'name' => 'AliPay', 'icon' => 'images/uploads/icons/alipay.png'],\r\n    ['id' => 18, 'name' => 'Elo', 'icon' => 'images/uploads/icons/elo.png'],\r\n    ['id' => 19, 'name' => 'Facebook Pay', 'icon' => 'images/uploads/icons/facebookpay.png'],\r\n    ['id' => 20, 'name' => 'GiroPay', 'icon' => 'images/uploads/icons/giropay.png'],\r\n    ['id' => 21, 'name' => 'iDeal', 'icon' => 'images/uploads/icons/ideal.png'],\r\n    ['id' => 22, 'name' => 'Union Pay', 'icon' => 'images/uploads/icons/unionpay.png'],\r\n    ['id' => 23, 'name' => 'Interac', 'icon' => 'images/uploads/icons/interac.png'],\r\n    ['id' => 24, 'name' => 'WeChat', 'icon' => 'images/uploads/icons/wechat.png'],\r\n    ['id' => 25, 'name' => 'Paysafe', 'icon' => 'images/uploads/icons/paysafe.png'],\r\n    ['id' => 26, 'name' => 'Poli', 'icon' => 'images/uploads/icons/poli.png'],\r\n    ['id' => 27, 'name' => 'Qiwi', 'icon' => 'images/uploads/icons/qiwi.png'],\r\n    ['id' => 28, 'name' => 'ShopPay', 'icon' => 'images/uploads/icons/shoppay.png'],\r\n    ['id' => 29, 'name' => 'Venmo', 'icon' => 'images/uploads/icons/venmo.png'],\r\n    ['id' => 30, 'name' => 'VeriFone', 'icon' => 'images/uploads/icons/verifone.png'],\r\n    ['id' => 31, 'name' => 'WebMoney', 'icon' => 'images/uploads/icons/webmoney.png'],\r\n];\r\n\r\nfunction validate($value)\r\n{\r\n    $value = trim($value);\r\n    $value = stripslashes($value);\r\n    $value = htmlspecialchars($value);\r\n    $value = htmlentities($value);\r\n    return $value;\r\n}\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$loggedInUserId = $userId;\r\n\r\n$email = validate($data['email']);\r\n$username = validate($data['username']);\r\n$password = $data['password'];\r\n\r\nif (empty($username) || empty($password) || empty($email)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('SELECT COUNT(*) FROM user WHERE username = :username OR email = :email');\r\n$stmt->bindValue(':username', $username, SQLITE3_INTEGER);\r\n$stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray();\r\n// Error if user exist\r\nif ($row[0] > 0) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n}\r\n\r\n// Get main currency and language from admin user\r\n$stmt = $db->prepare('SELECT main_currency, language FROM user WHERE id = :id');\r\n$stmt->bindValue(':id', $loggedInUserId, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray();\r\n$currency = $row['main_currency'] ?? 1;\r\n$language = $row['language'] ?? 'en';\r\n$avatar = \"images/avatars/0.svg\";\r\n\r\n// Get code for main currency\r\n$stmt = $db->prepare('SELECT code FROM currencies WHERE id = :id');\r\n$stmt->bindValue(':id', $currency, SQLITE3_TEXT);\r\n$row = $stmt->execute();\r\n$main_currency = $row->fetchArray()['code'];\r\n\r\n$query = \"INSERT INTO user (username, email, password, main_currency, avatar, language, budget) VALUES (:username, :email, :password, :main_currency, :avatar, :language, :budget)\";\r\n$stmt = $db->prepare($query);\r\n$hashedPassword = password_hash($password, PASSWORD_DEFAULT);\r\n$stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n$stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n$stmt->bindValue(':password', $hashedPassword, SQLITE3_TEXT);\r\n$stmt->bindValue(':main_currency', 1, SQLITE3_TEXT);\r\n$stmt->bindValue(':avatar', $avatar, SQLITE3_TEXT);\r\n$stmt->bindValue(':language', $language, SQLITE3_TEXT);\r\n$stmt->bindValue(':budget', 0, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n\r\n    // Get id of the newly created user\r\n    $newUserId = $db->lastInsertRowID();\r\n\r\n    // Add username as household member for that user\r\n    $query = \"INSERT INTO household (name, user_id) VALUES (:name, :user_id)\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':name', $username, SQLITE3_TEXT);\r\n    $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n\r\n    if ($newUserId > 1) {\r\n\r\n        // Add categories for that user\r\n        $query = 'INSERT INTO categories (name, \"order\", user_id) VALUES (:name, :order, :user_id)';\r\n        $stmt = $db->prepare($query);\r\n        foreach ($categories as $index => $category) {\r\n            $stmt->bindValue(':name', $category['name'], SQLITE3_TEXT);\r\n            $stmt->bindValue(':order', $index + 1, SQLITE3_INTEGER);\r\n            $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n        }\r\n\r\n        // Add payment methods for that user\r\n        $query = 'INSERT INTO payment_methods (name, icon, \"order\", user_id) VALUES (:name, :icon, :order, :user_id)';\r\n        $stmt = $db->prepare($query);\r\n        foreach ($payment_methods as $index => $payment_method) {\r\n            $stmt->bindValue(':name', $payment_method['name'], SQLITE3_TEXT);\r\n            $stmt->bindValue(':icon', $payment_method['icon'], SQLITE3_TEXT);\r\n            $stmt->bindValue(':order', $index + 1, SQLITE3_INTEGER);\r\n            $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n        }\r\n\r\n        // Add currencies for that user\r\n        $query = \"INSERT INTO currencies (name, symbol, code, rate, user_id) VALUES (:name, :symbol, :code, :rate, :user_id)\";\r\n        $stmt = $db->prepare($query);\r\n        foreach ($currencies as $currency) {\r\n            $stmt->bindValue(':name', $currency['name'], SQLITE3_TEXT);\r\n            $stmt->bindValue(':symbol', $currency['symbol'], SQLITE3_TEXT);\r\n            $stmt->bindValue(':code', $currency['code'], SQLITE3_TEXT);\r\n            $stmt->bindValue(':rate', 1, SQLITE3_FLOAT);\r\n            $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n        }\r\n\r\n        // Retrieve main currency id\r\n        $query = \"SELECT id FROM currencies WHERE code = :code AND user_id = :user_id\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':code', $main_currency, SQLITE3_TEXT);\r\n        $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $currency = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n        // Update user main currency\r\n        $query = \"UPDATE user SET main_currency = :main_currency WHERE id = :user_id\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':main_currency', $currency['id'], SQLITE3_INTEGER);\r\n        $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n        $stmt->execute();\r\n\r\n        // Add settings for that user\r\n        $query = \"INSERT INTO settings (dark_theme, monthly_price, convert_currency, remove_background, color_theme, hide_disabled, user_id, disabled_to_bottom, show_original_price, mobile_nav) \r\n                        VALUES (2, 0, 0, 0, 'blue', 0, :user_id, 0, 0, 0)\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n        $stmt->execute();\r\n\r\n        // If email verification is required add the user to the email_verification table\r\n        $query = \"SELECT * FROM admin\";\r\n        $stmt = $db->prepare($query);\r\n        $result = $stmt->execute();\r\n        $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n    }\r\n\r\n    $db->close();\r\n\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/admin/deleteunusedlogos.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$query = 'SELECT logo FROM subscriptions';\r\n$stmt = $db->prepare($query);\r\n$result = $stmt->execute();\r\n\r\n$logosOnDB = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $logosOnDB[] = $row['logo'];\r\n}\r\n\r\n$logosOnDB = array_unique($logosOnDB);\r\n\r\n$uploadDir = '../../images/uploads/logos/';\r\n$uploadFiles = scandir($uploadDir);\r\n\r\nforeach ($uploadFiles as $file) {\r\n    if ($file != '.' && $file != '..' && $file != 'avatars') {\r\n        $logosOnDisk[] = ['logo' => $file];\r\n    }\r\n}\r\n\r\n // Get all logos in the payment_methods table\r\n $query = 'SELECT icon FROM payment_methods';\r\n $stmt = $db->prepare($query);\r\n $result = $stmt->execute();\r\n\r\n while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n     if (!strstr($row['icon'], \"images/uploads/icons/\")) {\r\n         $logosOnDB[] = $row['icon'];\r\n     }\r\n }\r\n\r\n $logosOnDB = array_unique($logosOnDB);\r\n\r\n// Find and delete unused logos\r\n$count = 0;\r\nforeach ($logosOnDisk as $disk) {\r\n    foreach ($logosOnDB as $db) {\r\n        $found = false;\r\n        if ($disk['logo'] == $db) {\r\n            $found = true;\r\n            break;\r\n        }\r\n    }\r\n    if (!$found) {\r\n        unlink($uploadDir . $disk['logo']);\r\n        $count++;\r\n    }\r\n}\r\n\r\necho json_encode([\r\n    \"success\" => true,\r\n    \"message\" => translate('success', $i18n),\r\n    'count' => $count\r\n]);\r\n\r\n\r\n?>"
  },
  {
    "path": "endpoints/admin/deleteuser.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$userId = $data['userId'];\r\n\r\nif ($userId == 1) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n} else {\r\n    // Delete user\r\n    $stmt = $db->prepare('DELETE FROM user WHERE id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete subscriptions\r\n    $stmt = $db->prepare('DELETE FROM subscriptions WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete settings\r\n    $stmt = $db->prepare('DELETE FROM settings WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete fixer\r\n    $stmt = $db->prepare('DELETE FROM fixer WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete custom colors\r\n    $stmt = $db->prepare('DELETE FROM custom_colors WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete currencies\r\n    $stmt = $db->prepare('DELETE FROM currencies WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete categories\r\n    $stmt = $db->prepare('DELETE FROM categories WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete household\r\n    $stmt = $db->prepare('DELETE FROM household WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete payment methods\r\n    $stmt = $db->prepare('DELETE FROM payment_methods WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete email notifications\r\n    $stmt = $db->prepare('DELETE FROM email_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete telegram notifications\r\n    $stmt = $db->prepare('DELETE FROM telegram_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete webhook notifications\r\n    $stmt = $db->prepare('DELETE FROM webhook_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete gotify notifications\r\n    $stmt = $db->prepare('DELETE FROM gotify_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete pushover notifications\r\n    $stmt = $db->prepare('DELETE FROM pushover_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Dele notification settings\r\n    $stmt = $db->prepare('DELETE FROM notification_settings WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete last exchange update\r\n    $stmt = $db->prepare('DELETE FROM last_exchange_update WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete email verification\r\n    $stmt = $db->prepare('DELETE FROM email_verification WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete totp\r\n    $stmt = $db->prepare('DELETE FROM totp WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete total yearly cost\r\n    $stmt = $db->prepare('DELETE FROM total_yearly_cost WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]));\r\n\r\n}"
  },
  {
    "path": "endpoints/admin/enableoidc.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$oidcEnabled = isset($data['oidcEnabled']) ? $data['oidcEnabled'] : 0;\r\n\r\n$stmt = $db->prepare('UPDATE admin SET oidc_oauth_enabled = :oidcEnabled WHERE id = 1');\r\n$stmt->bindParam(':oidcEnabled', $oidcEnabled, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n\r\nif ($db->changes() > 0) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/admin/saveoidcsettings.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$oidcName = isset($data['oidcName']) ? trim($data['oidcName']) : '';\r\n$oidcClientId = isset($data['oidcClientId']) ? trim($data['oidcClientId']) : '';\r\n$oidcClientSecret = isset($data['oidcClientSecret']) ? trim($data['oidcClientSecret']) : '';\r\n$oidcAuthUrl = isset($data['oidcAuthUrl']) ? trim($data['oidcAuthUrl']) : '';\r\n$oidcTokenUrl = isset($data['oidcTokenUrl']) ? trim($data['oidcTokenUrl']) : '';\r\n$oidcUserInfoUrl = isset($data['oidcUserInfoUrl']) ? trim($data['oidcUserInfoUrl']) : '';\r\n$oidcRedirectUrl = isset($data['oidcRedirectUrl']) ? trim($data['oidcRedirectUrl']) : '';\r\n$oidcLogoutUrl = isset($data['oidcLogoutUrl']) ? trim($data['oidcLogoutUrl']) : '';\r\n$oidcUserIdentifierField = isset($data['oidcUserIdentifierField']) ? trim($data['oidcUserIdentifierField']) : '';\r\n$oidcScopes = isset($data['oidcScopes']) ? trim($data['oidcScopes']) : '';\r\n$oidcAuthStyle = isset($data['oidcAuthStyle']) ? trim($data['oidcAuthStyle']) : '';\r\n$oidcAutoCreateUser = isset($data['oidcAutoCreateUser']) ? (int) $data['oidcAutoCreateUser'] : 0;\r\n$oidcPasswordLoginDisabled = isset($data['oidcPasswordLoginDisabled']) ? (int) $data['oidcPasswordLoginDisabled'] : 0;\r\n\r\n$checkStmt = $db->prepare('SELECT COUNT(*) as count FROM oauth_settings WHERE id = 1');\r\n$result = $checkStmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($row['count'] > 0) {\r\n    // Update existing row\r\n    $stmt = $db->prepare('UPDATE oauth_settings SET \r\n            name = :oidcName, \r\n            client_id = :oidcClientId, \r\n            client_secret = :oidcClientSecret, \r\n            authorization_url = :oidcAuthUrl, \r\n            token_url = :oidcTokenUrl, \r\n            user_info_url = :oidcUserInfoUrl, \r\n            redirect_url = :oidcRedirectUrl, \r\n            logout_url = :oidcLogoutUrl, \r\n            user_identifier_field = :oidcUserIdentifierField, \r\n            scopes = :oidcScopes, \r\n            auth_style = :oidcAuthStyle,\r\n            auto_create_user = :oidcAutoCreateUser,\r\n            password_login_disabled = :oidcPasswordLoginDisabled\r\n            WHERE id = 1');\r\n} else {\r\n    // Insert new row\r\n    $stmt = $db->prepare('INSERT INTO oauth_settings (\r\n            id, name, client_id, client_secret, authorization_url, token_url, user_info_url, redirect_url, logout_url, user_identifier_field, scopes, auth_style, auto_create_user, password_login_disabled\r\n        ) VALUES (\r\n            1, :oidcName, :oidcClientId, :oidcClientSecret, :oidcAuthUrl, :oidcTokenUrl, :oidcUserInfoUrl, :oidcRedirectUrl, :oidcLogoutUrl, :oidcUserIdentifierField, :oidcScopes, :oidcAuthStyle, :oidcAutoCreateUser, :oidcPasswordLoginDisabled \r\n        )');\r\n}\r\n\r\n$stmt->bindParam(':oidcName', $oidcName, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcClientId', $oidcClientId, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcClientSecret', $oidcClientSecret, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcAuthUrl', $oidcAuthUrl, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcTokenUrl', $oidcTokenUrl, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcUserInfoUrl', $oidcUserInfoUrl, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcRedirectUrl', $oidcRedirectUrl, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcLogoutUrl', $oidcLogoutUrl, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcUserIdentifierField', $oidcUserIdentifierField, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcScopes', $oidcScopes, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcAuthStyle', $oidcAuthStyle, SQLITE3_TEXT);\r\n$stmt->bindParam(':oidcAutoCreateUser', $oidcAutoCreateUser, SQLITE3_INTEGER);\r\n$stmt->bindParam(':oidcPasswordLoginDisabled', $oidcPasswordLoginDisabled, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n\r\nif ($db->changes() > 0) {\r\n    $db->close();\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]));\r\n} else {\r\n    $db->close();\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/admin/saveopenregistrations.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$openRegistrations = $data['open_registrations'];\r\n$maxUsers = $data['max_users'];\r\n$requireEmailVerification = $data['require_email_validation'];\r\n$serverUrl = $data['server_url'];\r\n$disableLogin = $data['disable_login'];\r\n\r\nif ($disableLogin == 1) {\r\n    if ($openRegistrations == 1) {\r\n        echo json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('error', $i18n)\r\n        ]);\r\n        die();\r\n    }\r\n\r\n    $sql = \"SELECT COUNT(*) as userCount FROM user\";\r\n    $stmt = $db->prepare($sql);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n    $userCount = $row['userCount'];\r\n\r\n    if ($userCount > 1) {\r\n        echo json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('error', $i18n)\r\n        ]);\r\n        die();\r\n    }\r\n}\r\n\r\nif ($requireEmailVerification == 1 && $serverUrl == \"\") {\r\n    echo json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_all_fields', $i18n)\r\n    ]);\r\n    die();\r\n}\r\n\r\n$sql = \"UPDATE admin SET registrations_open = :openRegistrations, max_users = :maxUsers, require_email_verification = :requireEmailVerification, server_url = :serverUrl, login_disabled = :disableLogin WHERE id = 1\";\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindParam(':openRegistrations', $openRegistrations, SQLITE3_INTEGER);\r\n$stmt->bindParam(':maxUsers', $maxUsers, SQLITE3_INTEGER);\r\n$stmt->bindParam(':requireEmailVerification', $requireEmailVerification, SQLITE3_INTEGER);\r\n$stmt->bindParam(':serverUrl', $serverUrl, SQLITE3_TEXT);\r\n$stmt->bindParam(':disableLogin', $disableLogin, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n    echo json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]);\r\n} else {\r\n    echo json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]);\r\n}"
  },
  {
    "path": "endpoints/admin/savesecuritysettings.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (!isset($data['local_webhook_notifications_allowlist'])) {\r\n    echo json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]);\r\n    die();\r\n}\r\n\r\n// Basic cleanup: trim whitespace and strip any accidental HTML tags\r\n$allowlist = trim(strip_tags($data['local_webhook_notifications_allowlist']));\r\n\r\n// Update the admin table (assuming id 1 is the primary settings row, as in your reference)\r\n$sql = \"UPDATE admin SET local_webhook_notifications_allowlist = :allowlist WHERE id = 1\";\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindParam(':allowlist', $allowlist, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n    echo json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]);\r\n} else {\r\n    echo json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]);\r\n}"
  },
  {
    "path": "endpoints/admin/savesmtpsettings.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$smtpAddress = $data['smtpaddress'];\r\n$smtpPort = $data['smtpport'];\r\n$encryption = $data['encryption'];\r\n$smtpUsername = $data['smtpusername'];\r\n$smtpPassword = $data['smtppassword'];\r\n$fromEmail = $data['fromemail'];\r\n\r\nif (empty($smtpAddress) || empty($smtpPort)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_all_fields', $i18n)\r\n    ]));\r\n}\r\n\r\n// Save settings\r\n$stmt = $db->prepare('UPDATE admin SET smtp_address = :smtp_address, smtp_port = :smtp_port, encryption = :encryption, smtp_username = :smtp_username, smtp_password = :smtp_password, from_email = :from_email');\r\n$stmt->bindValue(':smtp_address', $smtpAddress, SQLITE3_TEXT);\r\n$stmt->bindValue(':smtp_port', $smtpPort, SQLITE3_TEXT);\r\n$encryption = empty($data['encryption']) ? 'tls' : $data['encryption'];\r\n$stmt->bindValue(':encryption', $encryption, SQLITE3_TEXT);\r\n$stmt->bindValue(':smtp_username', $smtpUsername, SQLITE3_TEXT);\r\n$stmt->bindValue(':smtp_password', $smtpPassword, SQLITE3_TEXT);\r\n$stmt->bindValue(':from_email', $fromEmail, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/admin/updatenotification.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$updateNotification = $data['notificationEnabled'];\r\n\r\n// Save settings\r\n$stmt = $db->prepare('UPDATE admin SET update_notification = :update_notification');\r\n$stmt->bindValue(':update_notification', $updateNotification, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/ai/delete_recommendation.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$input = file_get_contents('php://input');\r\n$data = json_decode($input, true);\r\n\r\n$recommendationId = isset($data['id']) ? (int) $data['id'] : 0;\r\n\r\nif ($recommendationId <= 0) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n// Delete the recommendation for the user\r\n$stmt = $db->prepare(\"DELETE FROM ai_recommendations WHERE id = ? AND user_id = ?\");\r\n$stmt->bindValue(1, $recommendationId, SQLITE3_INTEGER);\r\n$stmt->bindValue(2, $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nif ($db->changes() > 0) {\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ];\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n}\r\n\r\necho json_encode($response);"
  },
  {
    "path": "endpoints/ai/fetch_models.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$chatgptModelsApiUrl = 'https://api.openai.com/v1/models';\r\n$geminiModelsApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models';\r\n$openrouterModelsApiUrl = 'https://openrouter.ai/api/v1/models';\r\n\r\n$input = file_get_contents('php://input');\r\n$data = json_decode($input, true);\r\n// Check if ai-type and ai-api-key are set\r\n$aiType = isset($data[\"type\"]) ? trim($data[\"type\"]) : '';\r\n$aiApiKey = isset($data[\"api_key\"]) ? trim($data[\"api_key\"]) : '';\r\n$aiOllamaHost = isset($data[\"ollama_host\"]) ? trim($data[\"ollama_host\"]) : '';\r\n\r\n// Validate ai-type\r\nif (!in_array($aiType, ['chatgpt', 'gemini', 'openrouter', 'ollama'])) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n// Validate ai-api-key and fetch models if ai-type is chatgpt, gemini or openrouter\r\nif ($aiType === 'chatgpt' || $aiType === 'gemini' || $aiType === 'openrouter') {\r\n    if (empty($aiApiKey)) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('invalid_api_key', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n}\r\n\r\n// Prepare the request headers\r\n$headers = [\r\n    'Content-Type: application/json',\r\n];\r\nif ($aiType === 'chatgpt') {\r\n    $headers[] = 'Authorization: Bearer ' . $aiApiKey;\r\n    $apiUrl = $chatgptModelsApiUrl;\r\n} elseif ($aiType === 'gemini') {\r\n    $apiUrl = $geminiModelsApiUrl . '?key=' . urlencode($aiApiKey);\r\n} elseif ($aiType === 'openrouter') {\r\n    $headers[] = 'Authorization: Bearer ' . $aiApiKey;\r\n    $apiUrl = $openrouterModelsApiUrl;\r\n} else {\r\n    // For ollama, no API key is needed\r\n    // Check for ollama host\r\n    if (empty($aiOllamaHost)) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('invalid_host', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n\r\n    // Scheme check\r\n    $parsedUrl = parse_url($aiOllamaHost);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($aiOllamaHost, FILTER_VALIDATE_URL)\r\n    ) {\r\n        echo json_encode([\"success\" => false, \"message\" => translate('invalid_host', $i18n)]);\r\n        exit;\r\n    }\r\n\r\n    // SSRF check — dies automatically if private IP not in allowlist\r\n    $ssrf = validate_webhook_url_for_ssrf($aiOllamaHost, $db, $i18n);\r\n\r\n    $apiUrl = $aiOllamaHost . '/api/tags';\r\n}\r\n// Initialize cURL\r\n$ch = curl_init($apiUrl);\r\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\ncurl_setopt($ch, CURLOPT_HTTPHEADER, $headers);\r\ncurl_setopt($ch, CURLOPT_TIMEOUT, 60); // Set a timeout for the request\r\n// Execute the request\r\n$response = curl_exec($ch);\r\n// Check for cURL errors\r\nif (curl_errno($ch)) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => ($aiType === 'ollama')\r\n            ? translate('invalid_host', $i18n)\r\n            : translate('error', $i18n)\r\n    ];\r\n} else {\r\n    // Decode the response\r\n    $modelsData = json_decode($response, true);\r\n    if ($aiType === 'gemini' && isset($modelsData['models']) && is_array($modelsData['models'])) {\r\n        // Normalize Gemini response\r\n        $models = array_map(function ($model) {\r\n            return [\r\n                'id' => str_replace('models/', '', $model['name']),\r\n                'name' => $model['displayName'] ?? $model['name'],\r\n            ];\r\n        }, $modelsData['models']);\r\n        $response = [\r\n            \"success\" => true,\r\n            \"models\" => $models\r\n        ];\r\n    } elseif (isset($modelsData['data']) && is_array($modelsData['data'])) {\r\n        // OpenAI format\r\n        $models = array_map(function ($model) {\r\n            return [\r\n                'id' => $model['id'],\r\n                'name' => $model['name'] ?? $model['id'],\r\n            ];\r\n        }, $modelsData['data']);\r\n        $response = [\r\n            \"success\" => true,\r\n            \"models\" => $models\r\n        ];\r\n    } elseif ($aiType === 'ollama' && isset($modelsData['models']) && is_array($modelsData['models'])) {\r\n        // Normalize Ollama response\r\n        $models = array_map(function ($model) {\r\n            return [\r\n                'id' => $model['name'],\r\n                'name' => $model['name'],\r\n            ];\r\n        }, $modelsData['models']);\r\n        $response = [\r\n            \"success\" => true,\r\n            \"models\" => $models\r\n        ];\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => ($aiType === 'ollama')\r\n                ? translate('invalid_host', $i18n)\r\n                : translate('invalid_api_key', $i18n)\r\n        ];\r\n    }\r\n}\r\n// Close cURL session\r\ncurl_close($ch);\r\n// Return the response as JSON\r\necho json_encode($response);"
  },
  {
    "path": "endpoints/ai/generate_recommendations.php",
    "content": "<?php\r\nset_time_limit(300);\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\nfunction getPricePerMonth($cycle, $frequency, $price)\r\n{\r\n    switch ($cycle) {\r\n        case 1:\r\n            return $price * (30 / $frequency);        // daily\r\n        case 2:\r\n            return $price * (4.35 / $frequency);       // weekly\r\n        case 3:\r\n            return $price / $frequency;                // monthly\r\n        case 4:\r\n            return $price / (12 * $frequency);         // yearly\r\n        default:\r\n            return $price;\r\n    }\r\n}\r\n\r\nfunction describeFrequency($cycle, $frequency)\r\n{\r\n    $unit = match ($cycle) {\r\n        1 => 'day',\r\n        2 => 'week',\r\n        3 => 'month',\r\n        4 => 'year',\r\n        default => 'unit'\r\n    };\r\n\r\n    if ($frequency == 1) {\r\n        return \"Every $unit\";\r\n    } else {\r\n        return \"Every $frequency {$unit}s\";\r\n    }\r\n}\r\n\r\nfunction describeCurrency($currencyId, $currencies)\r\n{\r\n    return $currencies[$currencyId]['code'] ?? '';\r\n}\r\n\r\n// Get AI settings for the user from the database\r\n$stmt = $db->prepare(\"SELECT * FROM ai_settings WHERE user_id = ?\");\r\n$stmt->bindValue(1, $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$aiSettings = $result->fetchArray(SQLITE3_ASSOC);\r\n$stmt->close();\r\nif (!$aiSettings) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n$type = isset($aiSettings['type']) ? $aiSettings['type'] : '';\r\n$enabled = isset($aiSettings['enabled']) ? (bool) $aiSettings['enabled'] : false;\r\n$model = isset($aiSettings['model']) ? $aiSettings['model'] : '';\r\n$host = \"\";\r\n$apiKey = \"\";\r\nif (!in_array($type, ['chatgpt', 'gemini', 'openrouter', 'ollama']) || !$enabled || empty($model)) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\nif ($type == 'ollama') {\r\n    $host = isset($aiSettings['url']) ? $aiSettings['url'] : '';\r\n    if (empty($host)) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('invalid_host', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n    $parsedUrl = parse_url($host);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($host, FILTER_VALIDATE_URL)\r\n    ) {\r\n        echo json_encode([\"success\" => false, \"message\" => translate('invalid_host', $i18n)]);\r\n        exit;\r\n    }\r\n\r\n    $ssrf = validate_webhook_url_for_ssrf($host, $db, $i18n);\r\n} else {\r\n    $ssrf = null;\r\n    $apiKey = isset($aiSettings['api_key']) ? $aiSettings['api_key'] : '';\r\n    if (empty($apiKey)) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('invalid_api_key', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n        exit;\r\n    }\r\n}\r\n\r\n// We have everything we need, fetch information from the dabase to send to the AI API\r\n// Get the categories from the database for user with ID 1\r\n$stmt = $db->prepare(\"SELECT * FROM categories WHERE user_id = :user_id\");\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$categories = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $categories[$row['id']] = $row;\r\n}\r\n\r\n// Get the currencies from the database for user with ID 1\r\n$stmt = $db->prepare(\"SELECT * FROM currencies WHERE user_id = :user_id\");\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$currencies = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $currencies[$row['id']] = $row;\r\n}\r\n\r\n// Get houswhold members from the database for user with ID 1\r\n$stmt = $db->prepare(\"SELECT * FROM household WHERE user_id = :user_id\");\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$members = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $members[$row['id']] = $row;\r\n}\r\n\r\n// Get language from the user table\r\n$stmt = $db->prepare(\"SELECT language FROM user WHERE id = :user_id\");\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$userLanguage = $result->fetchArray(SQLITE3_ASSOC)['language'] ?? 'en';\r\n\r\n// Get name from includes/i18n/languages.php\r\nrequire_once '../../includes/i18n/languages.php';\r\n$userLanguageName = $languages[$userLanguage]['name'] ?? 'English';\r\n\r\n// Get subscriptions from the database for user with ID 1\r\n$stmt = $db->prepare(\"SELECT * FROM subscriptions WHERE user_id = :user_id AND inactive = 0\");\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\n$subscriptions = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $subscriptions[] = $row;\r\n}\r\n\r\nif (!empty($subscriptions)) {\r\n    $subscriptionsForAI = [];\r\n\r\n    foreach ($subscriptions as $row) {\r\n        if ($row['inactive'])\r\n            continue;\r\n\r\n        $price = round($row['price'], 2);\r\n        $currencyCode = $currencies[$row['currency_id']]['code'] ?? '';\r\n        $priceFormatted = $currencyCode ? \"$price $currencyCode\" : \"$price\";\r\n\r\n        $payerName = $members[$row['payer_user_id']]['name'] ?? 'Unknown';\r\n\r\n        $subscriptionsForAI[] = [\r\n            'name' => $row['name'],\r\n            'price' => $priceFormatted,\r\n            'frequency' => describeFrequency($row['cycle'], $row['frequency']),\r\n            'category' => $categories[$row['category_id']]['name'] ?? 'Uncategorized',\r\n            'payer' => $payerName\r\n        ];\r\n    }\r\n\r\n    // encode\r\n    $aiDataJson = json_encode($subscriptionsForAI, JSON_PRETTY_PRINT);\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n$prompt = <<<PROMPT\r\n    You are a helpful assistant designed to help users save money on digital subscriptions.\r\n\r\n    The user has shared a list of their active subscriptions across household members. For each subscription, you are given:\r\n    - Name of the service\r\n    - Price (in original currency)\r\n    - Payment frequency (e.g., every month, every year, etc.)\r\n    - Category\r\n    - Payer (which household member pays for it)\r\n\r\n    Analyze the data and give 3 to 7 smart and specific recommendations to reduce subscription costs. If possible, include estimated savings for each suggestion.\r\n\r\n    Follow these guidelines:\r\n    - Do NOT suggest switching to family or group plans unless two or more different household members are paying for the same or similar service.\r\n    - Recognize known feature overlaps, such as:\r\n    • YouTube Premium includes YouTube Music.\r\n    • Amazon Prime includes Prime Video.\r\n    • Google One, iCloud+, and Proton all offer cloud storage.\r\n    • Real Debrid, All Debrid, and Premiumize offer similar download capabilities.\r\n    - Suggest rotating or cancelling subscriptions that serve similar purposes (e.g. multiple streaming or IPTV services).\r\n    - Recommend switching from monthly to yearly plans only if it provides clear savings and the user is likely to keep the service long-term.\r\n    - Suggest looking for promo or new customer deals if a service appears overpriced.\r\n    - Only recommend cancelling rarely used services if they do not provide unique value.\r\n\r\n    Return the result as a JSON array. Each item in the array should have:\r\n    - \"title\": a short summary of the suggestion\r\n    - \"description\": a longer explanation with reasoning\r\n    - \"savings\": a rough estimate like \"10 EUR/month\" or \"60 EUR/year\" (if possible)\r\n\r\n    If possible, all text should be in the user's language: {$userLanguageName}. Otherwise, use English.\r\n\r\n    Do not include any other text, just the JSON output. Absolutely no additional comments or explanations.\r\n\r\n    Here is the user’s data:\r\n    PROMPT;\r\n\r\n$prompt .= \"\\n\\n\" . json_encode($subscriptionsForAI, JSON_PRETTY_PRINT);\r\n\r\n// Prepare the cURL request\r\n$ch = curl_init();\r\n\r\nif ($type === 'ollama') {\r\n    curl_setopt($ch, CURLOPT_URL, $host . '/api/generate');\r\n    curl_setopt($ch, CURLOPT_RESOLVE, [\"{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}\"]);\r\n    curl_setopt($ch, CURLOPT_POST, true);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['model' => $model, 'prompt' => $prompt, 'stream' => false]));\r\n} else {\r\n    $headers = ['Content-Type: application/json'];\r\n\r\n    if ($type === 'chatgpt') {\r\n        $headers[] = 'Authorization: Bearer ' . $apiKey;\r\n        curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/chat/completions');\r\n        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([\r\n            'model' => $model,\r\n            'messages' => [['role' => 'user', 'content' => $prompt]]\r\n        ]));\r\n    } elseif ($type === 'gemini') {\r\n        curl_setopt(\r\n            $ch,\r\n            CURLOPT_URL,\r\n            'https://generativelanguage.googleapis.com/v1beta/models/' . urlencode($model) .\r\n            ':generateContent?key=' . urlencode($apiKey)\r\n        );\r\n        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([\r\n            'contents' => [\r\n                [\r\n                    'parts' => [['text' => $prompt]]\r\n                ]\r\n            ]\r\n        ]));\r\n    } elseif ($type === 'openrouter') {\r\n        $headers[] = 'Authorization: Bearer ' . $apiKey;\r\n        curl_setopt($ch, CURLOPT_URL, 'https://openrouter.ai/api/v1/chat/completions');\r\n        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([\r\n            'model' => $model,\r\n            'messages' => [['role' => 'user', 'content' => $prompt]]\r\n        ]));\r\n    }\r\n\r\n    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);\r\n}\r\n\r\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\ncurl_setopt($ch, CURLOPT_TIMEOUT, 300);\r\n\r\n// Execute the cURL request\r\n$reply = curl_exec($ch);\r\n\r\n// Check for errors\r\nif (curl_errno($ch)) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => curl_error($ch)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\n// Close the cURL session\r\ncurl_close($ch);\r\n\r\n// Try to decode the AI's JSON reply\r\n$replyData = json_decode($reply, true); // decode into array\r\nif (($type === 'chatgpt' || $type === 'openrouter') && isset($replyData['choices'][0]['message']['content'])) {\r\n    $recommendationsJson = $replyData['choices'][0]['message']['content'];\r\n    $recommendations = json_decode($recommendationsJson, true);\r\n} elseif ($type === 'gemini' && isset($replyData['candidates'][0]['content']['parts'][0]['text'])) {\r\n    $recommendationsJson = $replyData['candidates'][0]['content']['parts'][0]['text'];\r\n    // Gemini has a habit of returning the JSON wrapped in markdown syntax, no matter the prompting, strip before parsing.\r\n    $recommendationsJson = preg_replace('/^```json\\s*|\\s*```$/m', '', $recommendationsJson);\r\n    $recommendationsJson = trim($recommendationsJson);\r\n    $recommendations = json_decode($recommendationsJson, true);\r\n} else {\r\n    $recommendations = json_decode($replyData['response'], true);\r\n}\r\n\r\nif (json_last_error() === JSON_ERROR_NONE && is_array($recommendations)) {\r\n    // Remove old recommendations for this user\r\n    $stmt = $db->prepare(\"DELETE FROM ai_recommendations WHERE user_id = :user_id\");\r\n    $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n\r\n    // Insert each new recommendation\r\n    $insert = $db->prepare(\"\r\n            INSERT INTO ai_recommendations (user_id, type, title, description, savings)\r\n            VALUES (:user_id, :type, :title, :description, :savings)\r\n        \");\r\n\r\n    foreach ($recommendations as $rec) {\r\n        $insert->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n        $insert->bindValue(':type', 'subscription', SQLITE3_TEXT); // or any category you want\r\n        $insert->bindValue(':title', $rec['title'] ?? '', SQLITE3_TEXT);\r\n        $insert->bindValue(':description', $rec['description'] ?? '', SQLITE3_TEXT);\r\n        $insert->bindValue(':savings', $rec['savings'] ?? '', SQLITE3_TEXT);\r\n        $insert->execute();\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n),\r\n        \"recommendations\" => $recommendations\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n),\r\n        \"json_error\" => json_last_error_msg()\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}"
  },
  {
    "path": "endpoints/ai/save_settings.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$input = file_get_contents('php://input');\r\n$data = json_decode($input, true);\r\n\r\n$aiEnabled = isset($data['ai_enabled']) ? (bool) $data['ai_enabled'] : false;\r\n$aiType = isset($data['ai_type']) ? trim($data['ai_type']) : '';\r\n$aiApiKey = isset($data['api_key']) ? trim($data['api_key']) : '';\r\n$aiOllamaHost = isset($data['ollama_host']) ? trim($data['ollama_host']) : '';\r\n$aiModel = isset($data['model']) ? trim($data['model']) : '';\r\n\r\nif (empty($aiType) || !in_array($aiType, ['chatgpt', 'gemini', 'openrouter', 'ollama'])) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\nif (($aiType === 'chatgpt' || $aiType === 'gemini' || $aiType === 'openrouter') && empty($aiApiKey)) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('invalid_api_key', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\nif ($aiType === 'ollama' && empty($aiOllamaHost)) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('invalid_host', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\nif (empty($aiModel)) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit;\r\n}\r\n\r\nif ($aiType === 'ollama') {\r\n    $aiApiKey = ''; // Ollama does not require an API key\r\n    $parsedUrl = parse_url($aiOllamaHost);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($aiOllamaHost, FILTER_VALIDATE_URL)\r\n    ) {\r\n        echo json_encode([\"success\" => false, \"message\" => translate('invalid_host', $i18n)]);\r\n        exit;\r\n    }\r\n\r\n    // SSRF check — dies automatically if private IP not in allowlist\r\n    validate_webhook_url_for_ssrf($aiOllamaHost, $db, $i18n);\r\n} else {\r\n    $aiOllamaHost = ''; // Clear Ollama host if not using Ollama\r\n}\r\n\r\n// Remove existing AI settings for the user\r\n$stmt = $db->prepare(\"DELETE FROM ai_settings WHERE user_id = ?\");\r\n$stmt->bindValue(1, $userId, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n$stmt->close();\r\n\r\n// Insert new AI settings\r\n$stmt = $db->prepare(\"INSERT INTO ai_settings (user_id, type, enabled, api_key, model, url) VALUES (:user_id, :type, :enabled, :api_key, :model, :url)\");\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$stmt->bindValue(':type', $aiType, SQLITE3_TEXT);\r\n$stmt->bindValue(':enabled', $aiEnabled, SQLITE3_INTEGER);\r\n$stmt->bindValue(':api_key', $aiApiKey, SQLITE3_TEXT);\r\n$stmt->bindValue(':model', $aiModel, SQLITE3_TEXT);\r\n$stmt->bindValue(':url', $aiOllamaHost, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n),\r\n        \"enabled\" => $aiEnabled\r\n    ];\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ];\r\n}\r\necho json_encode($response);\r\n"
  },
  {
    "path": "endpoints/categories/category.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$action = $_POST['action'] ?? '';\r\n\r\nswitch ($action) {\r\n    case \"add\":\r\n        handleAddCategory($db, $userId, $i18n);\r\n        break;\r\n    case \"edit\":\r\n        handleEditCategory($db, $userId, $i18n);\r\n        break;\r\n    case \"delete\":\r\n        handleDeleteCategory($db, $userId, $i18n);\r\n        break;\r\n    case \"sort\":\r\n        handleSortCategories($db, $userId, $i18n);\r\n        break;\r\n    default:\r\n        echo json_encode([\"success\" => false, \"message\" => translate('error', $i18n)]);\r\n        break;\r\n}\r\n\r\nfunction handleAddCategory($db, $userId, $i18n)\r\n{\r\n    $stmt = $db->prepare('SELECT MAX(\"order\") as maxOrder FROM categories WHERE user_id = :userId');\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n    $maxOrder = $row['maxOrder'];\r\n\r\n    if ($maxOrder === NULL) {\r\n        $maxOrder = 0;\r\n    }\r\n\r\n    $order = $maxOrder + 1;\r\n\r\n    $categoryName = \"Category\";\r\n    $sqlInsert = 'INSERT INTO categories (\"name\", \"order\", \"user_id\") VALUES (:name, :order, :userId)';\r\n    $stmtInsert = $db->prepare($sqlInsert);\r\n    $stmtInsert->bindParam(':name', $categoryName, SQLITE3_TEXT);\r\n    $stmtInsert->bindParam(':order', $order, SQLITE3_INTEGER);\r\n    $stmtInsert->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $resultInsert = $stmtInsert->execute();\r\n\r\n    if ($resultInsert) {\r\n        $categoryId = $db->lastInsertRowID();\r\n        $response = [\r\n            \"success\" => true,\r\n            \"categoryId\" => $categoryId\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('failed_add_category', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}\r\n\r\nfunction handleEditCategory($db, $userId, $i18n)\r\n{\r\n    if (isset($_POST['categoryId']) && $_POST['categoryId'] != \"\" && isset($_POST['name']) && $_POST['name'] != \"\") {\r\n        $categoryId = $_POST['categoryId'];\r\n        $name = validate($_POST['name']);\r\n        $sql = \"UPDATE categories SET name = :name WHERE id = :categoryId AND user_id = :userId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindParam(':name', $name, SQLITE3_TEXT);\r\n        $stmt->bindParam(':categoryId', $categoryId, SQLITE3_INTEGER);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        if ($result) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('category_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('failed_edit_category', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('fill_all_fields', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}\r\n\r\nfunction handleDeleteCategory($db, $userId, $i18n)\r\n{\r\n    if (isset($_POST['categoryId']) && $_POST['categoryId'] != \"\" && $_POST['categoryId'] != 1) {\r\n        $categoryId = $_POST['categoryId'];\r\n        $checkCategory = \"SELECT COUNT(*) FROM subscriptions WHERE category_id = :categoryId AND user_id = :userId\";\r\n        $checkStmt = $db->prepare($checkCategory);\r\n        $checkStmt->bindParam(':categoryId', $categoryId, SQLITE3_INTEGER);\r\n        $checkStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $checkResult = $checkStmt->execute();\r\n        $row = $checkResult->fetchArray();\r\n        $count = $row[0];\r\n\r\n        if ($count > 0) {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('category_in_use', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $sql = \"DELETE FROM categories WHERE id = :categoryId AND user_id = :userId\";\r\n            $stmt = $db->prepare($sql);\r\n            $stmt->bindParam(':categoryId', $categoryId, SQLITE3_INTEGER);\r\n            $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $stmt->execute();\r\n            if ($result) {\r\n                $response = [\r\n                    \"success\" => true,\r\n                    \"message\" => translate('category_removed', $i18n)\r\n                ];\r\n                echo json_encode($response);\r\n            } else {\r\n                $response = [\r\n                    \"success\" => false,\r\n                    \"message\" => translate('failed_remove_category', $i18n)\r\n                ];\r\n                echo json_encode($response);\r\n            }\r\n        }\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('failed_remove_category', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}\r\n\r\nfunction handleSortCategories($db, $userId, $i18n)\r\n{\r\n    $categories = $_POST['categoryIds'];\r\n    $order = 2;\r\n\r\n    foreach ($categories as $categoryId) {\r\n        $sql = \"UPDATE categories SET `order` = :order WHERE id = :categoryId AND user_id = :userId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindParam(':order', $order, SQLITE3_INTEGER);\r\n        $stmt->bindParam(':categoryId', $categoryId, SQLITE3_INTEGER);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $order++;\r\n    }\r\n\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate(\"sort_order_saved\", $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n}"
  },
  {
    "path": "endpoints/cronjobs/checkforupdates.php",
    "content": "<?php\r\n\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\n\r\n$options = [\r\n    'http' => [\r\n        'header' => \"User-Agent: Wallos\\r\\n\"\r\n    ]\r\n];\r\n\r\n$repository = 'ellite/Wallos'; // Change this to your repository if you fork Wallos\r\n$url = \"https://api.github.com/repos/$repository/releases/latest\";\r\n\r\n$context = stream_context_create($options);\r\n$fetch = file_get_contents($url, false, $context);\r\n\r\nif ($fetch === false) {\r\n    die('Error fetching data from GitHub API');\r\n}\r\n\r\n$latestVersion = json_decode($fetch, true)['tag_name'];\r\n\r\n// Check that $latestVersion is a valid version number\r\nif (!preg_match('/^v\\d+\\.\\d+\\.\\d+$/', $latestVersion)) {\r\n    die('Error: Invalid version number from GitHub API');\r\n}\r\n\r\n$db->exec(\"UPDATE admin SET latest_version = '$latestVersion'\");\r\n\r\n\r\nif (php_sapi_name() !== 'cli') {\r\n    include __DIR__ . '/../../includes/version.php';\r\n    if (version_compare($latestVersion, $version) > 0) {\r\n        echo \"New version available: $latestVersion\";\r\n    } else {\r\n        echo \"No new version available, currently on $version\";\r\n    }\r\n}\r\n?>"
  },
  {
    "path": "endpoints/cronjobs/cleanupresettokens.php",
    "content": "<?php\r\n\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\n\r\n$deleted = $db->exec(\"DELETE FROM password_resets WHERE created_at <= datetime('now', '-1 hour')\");\r\n\r\nif ($deleted) {\r\n    echo \"Expired password reset tokens cleaned up successfully.\\n\";\r\n} else {\r\n    echo \"No expired password reset tokens to clean up.\\n\";\r\n}\r\n\r\n$db->close();\r\n?>"
  },
  {
    "path": "endpoints/cronjobs/createdatabase.php",
    "content": "<?php\r\n\r\n$databaseFile = __DIR__ . '/../../db/wallos.db';\r\n\r\nif (!file_exists($databaseFile)) {\r\n    echo \"Database does not exist. Creating it...\\n\";\r\n    $db = new SQLite3($databaseFile, SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);\r\n    $db->busyTimeout(5000);\r\n\r\n    $db->exec('CREATE TABLE user (\r\n        id INTEGER PRIMARY KEY,\r\n        username TEXT NOT NULL,\r\n        email TEXT NOT NULL,\r\n        password TEXT NOT NULL,\r\n        main_currency INTEGER NOT NULL,\r\n        avatar TEXT,\r\n        FOREIGN KEY(main_currency) REFERENCES currencies(id)\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE payment_methods (\r\n        id INTEGER PRIMARY KEY,\r\n        name TEXT NOT NULL,\r\n        icon TEXT\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE subscriptions (\r\n        id INTEGER PRIMARY KEY,\r\n        name TEXT NOT NULL,\r\n        logo TEXT,\r\n        price REAL NOT NULL,\r\n        currency_id INTEGER,\r\n        next_payment DATE,\r\n        cycle INTEGER,\r\n        frequency INTEGER,\r\n        notes TEXT,\r\n        payment_method_id INTEGER,\r\n        payer_user_id INTEGER,\r\n        category_id INTEGER,\r\n        notify BOOLEAN DEFAULT false,\r\n        FOREIGN KEY(currency_id) REFERENCES currencies(id),\r\n        FOREIGN KEY(cycle) REFERENCES cycles(id),\r\n        FOREIGN KEY(frequency) REFERENCES frequencies(id),\r\n        FOREIGN KEY(payment_method_id) REFERENCES payment_methods(id),\r\n        FOREIGN KEY(payer_user_id) REFERENCES household(id)\r\n        FOREIGN KEY(category_id) REFERENCES categories(id)\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE categories (\r\n        id INTEGER PRIMARY KEY,\r\n        name TEXT NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE currencies (\r\n        id INTEGER PRIMARY KEY,\r\n        name TEXT NOT NULL,\r\n        symbol TEXT NOT NULL,\r\n        code TEXT NOT NULL,\r\n        rate TEXT NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE household (\r\n        id INTEGER PRIMARY KEY,\r\n        name TEXT NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE login_tokens (\r\n        user_id INTEGER NOT NULL,\r\n        token TEXT NOT NULL,\r\n        timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\r\n        FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE cycles (\r\n        id INTEGER PRIMARY KEY,\r\n        days INTEGER NOT NULL,\r\n        name TEXT NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE frequencies (\r\n        id INTEGER PRIMARY KEY,\r\n        name INTEGER NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE fixer (\r\n        api_key TEXT NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE last_exchange_update (\r\n        date DATE NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE last_update_next_payment_date (\r\n        date DATE NOT NULL\r\n    )');\r\n\r\n    $db->exec('CREATE TABLE notifications (\r\n        id INTEGER PRIMARY KEY,\r\n        enabled BOOLEAN DEFAULT false,\r\n        days INTEGER,\r\n        smtp_address VARCHAR(255),\r\n        smtp_port INTEGER,\r\n        smtp_username VARCHAR(255),\r\n        smtp_password VARCHAR(255)\r\n    )');\r\n\r\n    $db->exec(\"INSERT INTO categories (id, name) VALUES\r\n    (1, 'No category'),\r\n    (2, 'Entertainment'),\r\n    (3, 'Music'),\r\n    (4, 'Utilities'),\r\n    (5, 'Food & Beverages'),\r\n    (6, 'Health & Wellbeing'),\r\n    (7, 'Productivity'),\r\n    (8, 'Banking'),\r\n    (9, 'Transport'),\r\n    (10, 'Education'),\r\n    (11, 'Insurance'),\r\n    (12, 'Gaming'),\r\n    (13, 'News & Magazines'),\r\n    (14, 'Software'),\r\n    (15, 'Technology'),\r\n    (16, 'Cloud Services'),\r\n    (17, 'Charity & Donations')\");\r\n\r\n    $db->exec(\"INSERT INTO cycles (id, days, name) VALUES\r\n    (1, 1, 'Daily'),\r\n    (2, 7, 'Weekly'),\r\n    (3, 30, 'Monthly'),\r\n    (4, 365, 'Yearly')\");\r\n\r\n    $db->exec(\"INSERT INTO frequencies (id, name) VALUES\r\n    (1, 1),\r\n    (2, 2),\r\n    (3, 3),\r\n    (4, 4),\r\n    (5, 5),\r\n    (6, 6),\r\n    (7, 7),\r\n    (8, 8),\r\n    (9, 9),\r\n    (10, 10),\r\n    (11, 11),\r\n    (12, 12),\r\n    (13, 13),\r\n    (14, 14),\r\n    (15, 15),\r\n    (16, 16),\r\n    (17, 17),\r\n    (18, 18),\r\n    (19, 19),\r\n    (20, 20),\r\n    (21, 21),\r\n    (22, 22),\r\n    (23, 23),\r\n    (24, 24),\r\n    (25, 25),\r\n    (26, 26),\r\n    (27, 27),\r\n    (28, 28),\r\n    (29, 29),\r\n    (30, 30),\r\n    (31, 31)\");\r\n\r\n    $db->exec(\"INSERT INTO currencies (name, symbol, code, rate) VALUES\r\n    ('Euro', '€', 'EUR', 1),\r\n    ('US Dollar', '$', 'USD', 1),\r\n    ('Japanese Yen', '¥', 'JPY', 1),\r\n    ('Bulgarian Lev', 'лв', 'BGN', 1),\r\n    ('Czech Republic Koruna', 'Kč', 'CZK', 1),\r\n    ('Danish Krone', 'kr', 'DKK', 1),\r\n    ('British Pound Sterling', '£', 'GBP', 1),\r\n    ('Hungarian Forint', 'Ft', 'HUF', 1),\r\n    ('Polish Zloty', 'zł', 'PLN', 1),\r\n    ('Romanian Leu', 'lei', 'RON', 1),\r\n    ('Swedish Krona', 'kr', 'SEK', 1),\r\n    ('Swiss Franc', 'Fr', 'CHF', 1),\r\n    ('Icelandic Króna', 'kr', 'ISK', 1),\r\n    ('Norwegian Krone', 'kr', 'NOK', 1),\r\n    ('Russian Ruble', '₽', 'RUB', 1),\r\n    ('Turkish Lira', '₺', 'TRY', 1),\r\n    ('Australian Dollar', '$', 'AUD', 1),\r\n    ('Brazilian Real', 'R$', 'BRL', 1),\r\n    ('Canadian Dollar', '$', 'CAD', 1),\r\n    ('Chinese Yuan', '¥', 'CNY', 1),\r\n    ('Hong Kong Dollar', 'HK$', 'HKD', 1),\r\n    ('Indonesian Rupiah', 'Rp', 'IDR', 1),\r\n    ('Israeli New Sheqel', '₪', 'ILS', 1),\r\n    ('Indian Rupee', '₹', 'INR', 1),\r\n    ('South Korean Won', '₩', 'KRW', 1),\r\n    ('Mexican Peso', 'Mex$', 'MXN', 1),\r\n    ('Malaysian Ringgit', 'RM', 'MYR', 1),\r\n    ('New Zealand Dollar', 'NZ$', 'NZD', 1),\r\n    ('Philippine Peso', '₱', 'PHP', 1),\r\n    ('Singapore Dollar', 'S$', 'SGD', 1),\r\n    ('Thai Baht', '฿', 'THB', 1),\r\n    ('South African Rand', 'R', 'ZAR', 1),\r\n    ('Ukrainian Hryvnia', '₴', 'UAH', 1),\r\n    ('New Taiwan Dollar', 'NT$', 'TWD', 1)\");\r\n\r\n    $db->exec(\"INSERT INTO payment_methods (id, name, icon) VALUES\r\n    (1, 'PayPal', 'paypal.png'),\r\n    (2, 'Credit Card', 'creditcard.png'),\r\n    (3, 'Bank Transfer', 'banktransfer.png'),\r\n    (4, 'Direct Debit', 'directdebit.png'),\r\n    (5, 'Money', 'money.png'),\r\n    (6, 'Google Pay', 'googlepay.png'),\r\n    (7, 'Samsung Pay', 'samsungpay.png'),\r\n    (8, 'Apple Pay', 'applepay.png'),\r\n    (9, 'Crypto', 'crypto.png'),\r\n    (10, 'Klarna', 'klarna.png'),\r\n    (11, 'Amazon Pay', 'amazonpay.png'),\r\n    (12, 'SEPA', 'sepa.png'),\r\n    (13, 'Skrill', 'skrill.png'),\r\n    (14, 'Sofort', 'sofort.png'),\r\n    (15, 'Stripe', 'stripe.png'),\r\n    (16, 'Affirm', 'affirm.png'),\r\n    (17, 'AliPay', 'alipay.png'),\r\n    (18, 'Elo', 'elo.png'),\r\n    (19, 'Facebook Pay', 'facebookpay.png'),\r\n    (20, 'GiroPay', 'giropay.png'),\r\n    (21, 'iDeal', 'ideal.png'),\r\n    (22, 'Union Pay', 'unionpay.png'),\r\n    (23, 'Interac', 'interac.png'),\r\n    (24, 'WeChat', 'wechat.png'),\r\n    (25, 'Paysafe', 'paysafe.png'),\r\n    (26, 'Poli', 'poli.png'),\r\n    (27, 'Qiwi', 'qiwi.png'),\r\n    (28, 'ShopPay', 'shoppay.png'),\r\n    (29, 'Venmo', 'venmo.png'),\r\n    (30, 'VeriFone', 'verifone.png'),\r\n    (31, 'WebMoney', 'webmoney.png')\");\r\n\r\n    echo \"Database created.\\n\";\r\n} else {\r\n    echo \"Database already exist. Checking for upgrades...\\n\";\r\n\r\n    $db = new SQLite3($databaseFile);\r\n    $db->busyTimeout(5000);\r\n\r\n    if (!$db) {\r\n        die('Connection to the database failed.');\r\n    }\r\n\r\n    # v0.9 to v1.0\r\n    # Added new notifications table\r\n    # Added notify column to subscriptions table\r\n\r\n    $result = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='notifications'\");\r\n    if (!$result->fetchArray(SQLITE3_ASSOC)) {\r\n        $db->exec('CREATE TABLE notifications (\r\n            id INTEGER PRIMARY KEY,\r\n            enabled BOOLEAN DEFAULT false,\r\n            days INTEGER,\r\n            smtp_address VARCHAR(255),\r\n            smtp_port INTEGER,\r\n            smtp_username VARCHAR(255),\r\n            smtp_password VARCHAR(255)\r\n        )');\r\n        echo \"Table 'notifications' created.\\n\";\r\n    } else {\r\n        echo \"Table 'notifications' already exists.\\n\";\r\n    }\r\n\r\n    $result = $db->query(\"PRAGMA table_info(subscriptions)\");\r\n    $notifyColumnExists = false;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        if ($row['name'] === 'notify') {\r\n            $notifyColumnExists = true;\r\n            break;\r\n        }\r\n    }\r\n    if (!$notifyColumnExists) {\r\n        $db->exec('ALTER TABLE subscriptions ADD COLUMN notify BOOLEAN DEFAULT false');\r\n        echo \"Column 'notify' added to table 'subscriptions'.\\n\";\r\n    } else {\r\n        echo \"Column 'notify' already exists in table 'subscriptions'.\\n\";\r\n    }\r\n\r\n}\r\n\r\n?>"
  },
  {
    "path": "endpoints/cronjobs/sendcancellationnotifications.php",
    "content": "<?php\r\nuse PHPMailer\\PHPMailer\\PHPMailer;\r\nuse PHPMailer\\PHPMailer\\SMTP;\r\nuse PHPMailer\\PHPMailer\\Exception;\r\n\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\nrequire_once __DIR__ . '/../../includes/ssrf_helper.php';\r\n\r\nrequire __DIR__ . '/../../libs/PHPMailer/PHPMailer.php';\r\nrequire __DIR__ . '/../../libs/PHPMailer/SMTP.php';\r\nrequire __DIR__ . '/../../libs/PHPMailer/Exception.php';\r\n\r\nrequire 'settimezone.php';\r\n\r\n// Get all user ids\r\n$query = \"SELECT id, username FROM user\";\r\n$stmt = $db->prepare($query);\r\n$usersToNotify = $stmt->execute();\r\n\r\nwhile ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) {\r\n    $userId = $userToNotify['id'];\r\n    if (php_sapi_name() !== 'cli') {\r\n        echo \"For user: \" . $userToNotify['username'] . \"<br />\";\r\n    }\r\n\r\n    $emailNotificationsEnabled = false;\r\n    $gotifyNotificationsEnabled = false;\r\n    $telegramNotificationsEnabled = false;\r\n    $pushoverNotificationsEnabled = false;\r\n    $discordNotificationsEnabled = false;\r\n    $ntfyNotificationsEnabled = false;\r\n    $webhookNotificationsEnabled = false;\r\n\r\n    // Check if email notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM email_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $emailNotificationsEnabled = $row['enabled'];\r\n        $email['smtpAddress'] = $row[\"smtp_address\"];\r\n        $email['smtpPort'] = $row[\"smtp_port\"];\r\n        $email['encryption'] = $row[\"encryption\"];\r\n        $email['smtpUsername'] = $row[\"smtp_username\"];\r\n        $email['smtpPassword'] = $row[\"smtp_password\"];\r\n        $email['fromEmail'] = $row[\"from_email\"] ? $row[\"from_email\"] : \"wallos@wallosapp.com\";\r\n        $email['otherEmails'] = $row[\"other_emails\"];\r\n    }\r\n\r\n    // Check if Discord notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM discord_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $discordNotificationsEnabled = $row['enabled'];\r\n        $discord['webhook_url'] = $row[\"webhook_url\"];\r\n        $discord['bot_username'] = $row[\"bot_username\"];\r\n        $discord['bot_avatar_url'] = $row[\"bot_avatar_url\"];\r\n    }\r\n\r\n    // Check if Gotify notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM gotify_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $gotify = [];\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $gotifyNotificationsEnabled = $row['enabled'];\r\n        $gotify['serverUrl'] = $row[\"url\"];\r\n        $gotify['appToken'] = $row[\"token\"];\r\n        $gotify['ignore_ssl'] = $row[\"ignore_ssl\"];\r\n    }\r\n\r\n    // Check if Telegram notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM telegram_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $telegramNotificationsEnabled = $row['enabled'];\r\n        $telegram['botToken'] = $row[\"bot_token\"];\r\n        $telegram['chatId'] = $row[\"chat_id\"];\r\n    }\r\n\r\n    // Check if Pushover notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM pushover_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $pushoverNotificationsEnabled = $row['enabled'];\r\n        $pushover['user_key'] = $row[\"user_key\"];\r\n        $pushover['token'] = $row[\"token\"];\r\n    }\r\n\r\n    // Check if Ntfy notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM ntfy_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $ntfyNotificationsEnabled = $row['enabled'];\r\n        $ntfy['host'] = $row[\"host\"];\r\n        $ntfy['topic'] = $row[\"topic\"];\r\n        $ntfy['headers'] = $row[\"headers\"];\r\n        $ntfy['ignore_ssl'] = $row[\"ignore_ssl\"];\r\n    }\r\n\r\n    // Check if webhook notifications are enabled and have cancelation payload set and get the settings\r\n    $query = \"SELECT * FROM webhook_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $webhook = [];\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $webhook['url'] = $row[\"url\"];\r\n        $webhook['headers'] = $row[\"headers\"];\r\n        $webhook['cancelation_payload'] = $row[\"cancelation_payload\"];\r\n        $webhook['ignore_ssl'] = $row[\"ignore_ssl\"];\r\n        $webhook['request_method'] = $row[\"request_method\"];\r\n        $webhookNotificationsEnabled = $row['enabled'] && $row['cancelation_payload'] != \"\";\r\n    }\r\n\r\n    $notificationsEnabled = $emailNotificationsEnabled || $gotifyNotificationsEnabled || $telegramNotificationsEnabled ||\r\n        $pushoverNotificationsEnabled || $discordNotificationsEnabled ||$ntfyNotificationsEnabled || $webhookNotificationsEnabled;\r\n\r\n    // If no notifications are enabled, no need to run\r\n    if (!$notificationsEnabled) {\r\n        if (php_sapi_name() !== 'cli') {\r\n            echo \"Notifications are disabled. No need to run.<br />\";\r\n        }\r\n        continue;\r\n    } else {\r\n        // Get all currencies\r\n        $currencies = array();\r\n        $query = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $currencies[$row['id']] = $row;\r\n        }\r\n\r\n        // Get all household members\r\n        $query = \"SELECT * FROM household WHERE user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n        $resultHousehold = $stmt->execute();\r\n\r\n        $household = [];\r\n        while ($rowHousehold = $resultHousehold->fetchArray(SQLITE3_ASSOC)) {\r\n            $household[$rowHousehold['id']] = $rowHousehold;\r\n        }\r\n\r\n        // Get all categories\r\n        $query = \"SELECT * FROM categories WHERE user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n        $resultCategories = $stmt->execute();\r\n\r\n        $categories = [];\r\n        while ($rowCategory = $resultCategories->fetchArray(SQLITE3_ASSOC)) {\r\n            $categories[$rowCategory['id']] = $rowCategory;\r\n        }\r\n\r\n        // Get current date to check which subscriptions are set to notify for cancellation\r\n        $currentDate = new DateTime('now');\r\n        $currentDate = $currentDate->format('Y-m-d');\r\n\r\n        $query = \"SELECT * FROM subscriptions WHERE user_id = :user_id AND inactive = :inactive AND cancellation_date = :cancellationDate ORDER BY payer_user_id ASC\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':inactive', 0, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':cancellationDate', $currentDate, SQLITE3_TEXT);\r\n        $resultSubscriptions = $stmt->execute();\r\n\r\n        $notify = [];\r\n        $i = 0;\r\n        $currentDate = new DateTime('now');\r\n        while ($rowSubscription = $resultSubscriptions->fetchArray(SQLITE3_ASSOC)) {\r\n            $notify[$rowSubscription['payer_user_id']][$i]['name'] = $rowSubscription['name'];\r\n            $notify[$rowSubscription['payer_user_id']][$i]['price'] = $rowSubscription['price'] . $currencies[$rowSubscription['currency_id']]['symbol'];\r\n            $notify[$rowSubscription['payer_user_id']][$i]['currency'] = $currencies[$rowSubscription['currency_id']]['name'];\r\n            $notify[$rowSubscription['payer_user_id']][$i]['category'] = $categories[$rowSubscription['category_id']]['name'];\r\n            $notify[$rowSubscription['payer_user_id']][$i]['payer'] = $household[$rowSubscription['payer_user_id']]['name'];\r\n            $notify[$rowSubscription['payer_user_id']][$i]['date'] = $rowSubscription['next_payment'];\r\n            $notify[$rowSubscription['payer_user_id']][$i]['url'] = $rowSubscription['url'];\r\n            $notify[$rowSubscription['payer_user_id']][$i]['notes'] = $rowSubscription['notes'];\r\n            $i++;\r\n        }\r\n\r\n        if (!empty($notify)) {\r\n\r\n            // Email notifications if enabled\r\n            if ($emailNotificationsEnabled) {\r\n\r\n                $stmt = $db->prepare('SELECT * FROM user WHERE id = :user_id');\r\n                $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                $result = $stmt->execute();\r\n                $defaultUser = $result->fetchArray(SQLITE3_ASSOC);\r\n                $defaultEmail = $defaultUser['email'];\r\n                $defaultName = $defaultUser['username'];\r\n\r\n                foreach ($notify as $userId => $perUser) {\r\n                    $message = \"The following subscriptions are up for cancellation:\\n\";\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $message .= $subscription['name'] . \" for \" . $subscription['price'] .\"\\n\";\r\n                    }\r\n\r\n                    $smtpAuth = (isset($email[\"smtpUsername\"]) && $email[\"smtpUsername\"] != \"\") || (isset($email[\"smtpPassword\"]) && $email[\"smtpPassword\"] != \"\");\r\n\r\n                    $mail = new PHPMailer(true);\r\n                    $mail->CharSet = \"UTF-8\";\r\n                    $mail->isSMTP();\r\n\r\n                    $mail->Host = $email['smtpAddress'];\r\n                    $mail->SMTPAuth = $smtpAuth;\r\n                    if ($smtpAuth) {\r\n                        $mail->Username = $email['smtpUsername'];\r\n                        $mail->Password = $email['smtpPassword'];\r\n                    }\r\n                    if ($email['encryption'] != \"none\") {\r\n                        $mail->SMTPSecure = $email['encryption'];\r\n                    }\r\n                    $mail->Port = $email['smtpPort'];\r\n\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    $emailaddress = !empty($user['email']) ? $user['email'] : $defaultEmail;\r\n                    $name = !empty($user['name']) ? $user['name'] : $defaultName;\r\n\r\n                    $mail->setFrom($email['fromEmail'], 'Wallos App');\r\n                    $mail->addAddress($emailaddress, $name);\r\n\r\n                    if (!empty($email['otherEmails'])) {\r\n                        $list = explode(';', $email['otherEmails']);\r\n\r\n                        // Avoid duplicate emails\r\n                        $list = array_unique($list);\r\n                        $list = array_filter($list, function ($value) use ($emailaddress) {\r\n                            return $value !== $emailaddress;\r\n                        });\r\n\r\n                        foreach($list as $value) {\r\n                            $mail->addCC(trim($value));\r\n                        }\r\n                    }\r\n\r\n                    $mail->Subject = 'Wallos Cancellation Notification';\r\n                    $mail->Body = $message;\r\n\r\n                    if ($mail->send()) {\r\n                        echo \"Email Notifications sent<br />\";\r\n                    } else {\r\n                        echo \"Error sending notifications: \" . $mail->ErrorInfo . \"<br />\";\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Discord notifications if enabled\r\n            if ($discordNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($discord['webhook_url'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"Discord notification skipped: URL failed SSRF validation.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                        $title = translate('wallos_notification', $i18n);\r\n\r\n                        if ($user['name']) {\r\n                            $message = $user['name'] . \", the following subscriptions are up for cancellation:\\n\";\r\n                        } else {\r\n                            $message = \"The following subscriptions are up for cancellation:\\n\";\r\n                        }\r\n\r\n                        foreach ($perUser as $subscription) {\r\n                            $message .= $subscription['name'] . \" for \" . $subscription['price'] . \"\\n\";\r\n                        }\r\n\r\n                        $postfields = [\r\n                            'content' => $message\r\n                        ];\r\n\r\n                        if (!empty($discord['bot_username'])) {\r\n                            $postfields['username'] = $discord['bot_username'];\r\n                        }\r\n\r\n                        if (!empty($discord['bot_avatar_url'])) {\r\n                            $postfields['avatar_url'] = $discord['bot_avatar_url'];\r\n                        }\r\n\r\n                        $ch = curl_init();\r\n\r\n                        curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']);\r\n                        curl_setopt($ch, CURLOPT_POST, 1);\r\n                        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));\r\n                        curl_setopt($ch, CURLOPT_HTTPHEADER, [\r\n                            'Content-Type: application/json'\r\n                        ]);\r\n                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n                        $response = curl_exec($ch);\r\n                        curl_close($ch);\r\n\r\n                        if ($response === false) {\r\n                            echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                        } else {\r\n                            echo \"Discord Notifications sent<br />\";\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Gotify notifications if enabled\r\n            if ($gotifyNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($gotify['serverUrl'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"Gotify notification skipped: URL failed SSRF validation.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                        if ($user['name']) {\r\n                            $message = $user['name'] . \", the following subscriptions are up for cancellation:\\n\";\r\n                        } else {\r\n                            $message = \"The following subscriptions are up for cancellation:\\n\";\r\n                        }\r\n\r\n                        foreach ($perUser as $subscription) {\r\n                            $message .= $subscription['name'] . \" for \" . $subscription['price'] . \"\\n\";\r\n                        }\r\n\r\n                        $data = array(\r\n                            'message' => $message,\r\n                            'priority' => 5\r\n                        );\r\n\r\n                        $data_string = json_encode($data);\r\n\r\n                        $ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']);\r\n                        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, \"POST\");\r\n                        curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);\r\n                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                        curl_setopt(\r\n                            $ch,\r\n                            CURLOPT_HTTPHEADER,\r\n                            array(\r\n                                'Content-Type: application/json',\r\n                                'Content-Length: ' . strlen($data_string)\r\n                            )\r\n                        );\r\n\r\n                        if ($gotify['ignore_ssl']) {\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n                        }\r\n\r\n                        $result = curl_exec($ch);\r\n                        if ($result === false) {\r\n                            echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                        } else {\r\n                            echo \"Gotify Notifications sent<br />\";\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Telegram notifications if enabled\r\n            if ($telegramNotificationsEnabled) {\r\n                foreach ($notify as $userId => $perUser) {\r\n                    // Get name of user from household table\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    if ($user['name']) {\r\n                        $message = $user['name'] . \", the following subscriptions are up for cancellation:\\n\";\r\n                    } else {\r\n                        $message = \"The following subscriptions are up for cancellation:\\n\";\r\n                    }\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $message .= $subscription['name'] . \" for \" . $subscription['price'] . \"\\n\";\r\n                    }\r\n\r\n                    $data = array(\r\n                        'chat_id' => $telegram['chatId'],\r\n                        'text' => $message\r\n                    );\r\n\r\n                    $data_string = json_encode($data);\r\n\r\n                    $ch = curl_init('https://api.telegram.org/bot' . $telegram['botToken'] . '/sendMessage');\r\n                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, \"POST\");\r\n                    curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);\r\n                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                    curl_setopt(\r\n                        $ch,\r\n                        CURLOPT_HTTPHEADER,\r\n                        array(\r\n                            'Content-Type: application/json',\r\n                            'Content-Length: ' . strlen($data_string)\r\n                        )\r\n                    );\r\n\r\n                    $result = curl_exec($ch);\r\n                    if ($result === false) {\r\n                        echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                    } else {\r\n                        echo \"Telegram Notifications sent<br />\";\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Pushover notifications if enabled\r\n            if ($pushoverNotificationsEnabled) {\r\n                foreach ($notify as $userId => $perUser) {\r\n                    // Get name of user from household table\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    if ($user['name']) {\r\n                        $message = $user['name'] . \", the following subscriptions are up for cancellation:\\n\";\r\n                    } else {\r\n                        $message = \"The following subscriptions are up for cancellation:\\n\";\r\n                    }\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $message .= $subscription['name'] . \" for \" . $subscription['price'] . \"\\n\";\r\n                    }\r\n\r\n                    $ch = curl_init();\r\n                    curl_setopt($ch, CURLOPT_URL, \"https://api.pushover.net/1/messages.json\");\r\n                    curl_setopt($ch, CURLOPT_POST, 1);\r\n                    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([\r\n                        'token' => $pushover['token'],\r\n                        'user' => $pushover['user_key'],\r\n                        'message' => $message,\r\n                    ]));\r\n                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n                    $result = curl_exec($ch);\r\n\r\n                    curl_close($ch);\r\n\r\n                    if ($result === false) {\r\n                        echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                    } else {\r\n                        echo \"Pushover Notifications sent<br />\";\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Ntfy notifications if enabled\r\n            if ($ntfyNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($ntfy['host'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"Ntfy notification skipped: URL failed SSRF validation.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                        if ($user['name']) {\r\n                            $message = $user['name'] . \", the following subscriptions are up for cancellation:\\n\";\r\n                        } else {\r\n                            $message = \"The following subscriptions are up for cancellation:\\n\";\r\n                        }\r\n\r\n                        foreach ($perUser as $subscription) {\r\n                            $message .= $subscription['name'] . \" for \" . $subscription['price'] . \"\\n\";\r\n                        }\r\n\r\n                        $headers = json_decode($ntfy[\"headers\"], true);\r\n                        $customheaders = array_map(function ($key, $value) {\r\n                            return \"$key: $value\";\r\n                        }, array_keys($headers), $headers);\r\n\r\n                        $ch = curl_init();\r\n\r\n                        $ntfyHost = rtrim($ntfy[\"host\"], '/');\r\n                        $ntfyTopic = $ntfy['topic'];\r\n\r\n                        curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic);\r\n                        curl_setopt($ch, CURLOPT_POST, 1);\r\n                        curl_setopt($ch, CURLOPT_POSTFIELDS, $message);\r\n                        curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);\r\n                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n                        if ($ntfy['ignore_ssl']) {\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n                        }\r\n\r\n                        $response = curl_exec($ch);\r\n                        curl_close($ch);\r\n\r\n                        if ($response === false) {\r\n                            echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                        } else {\r\n                            echo \"Ntfy Notifications sent<br />\";\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Webhook notifications if enabled\r\n            if ($webhookNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($webhook['url'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"Webhook notification skipped: URL failed SSRF validation.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n                \r\n                        if ($user['name']) {\r\n                            $payer = $user['name'];\r\n                        }\r\n                \r\n                        foreach ($perUser as $subscription) {\r\n                            // Ensure the payload is reset for each subscription\r\n                            $payload = $webhook['cancelation_payload'];\r\n                            $payload = str_replace(\"{{subscription_name}}\", $subscription['name'], $payload);\r\n                            $payload = str_replace(\"{{subscription_price}}\", $subscription['price'], $payload);\r\n                            $payload = str_replace(\"{{subscription_currency}}\", $subscription['currency'], $payload);\r\n                            $payload = str_replace(\"{{subscription_category}}\", $subscription['category'], $payload);\r\n                            $payload = str_replace(\"{{subscription_payer}}\", $payer, $payload);\r\n                            $payload = str_replace(\"{{subscription_date}}\", $subscription['date'], $payload);\r\n                            $payload = str_replace(\"{{subscription_url}}\", $subscription['url'], $payload);\r\n                            $payload = str_replace(\"{{subscription_notes}}\", $subscription['notes'], $payload);\r\n                \r\n                            // Initialize cURL for each subscription\r\n                            $ch = curl_init();\r\n                            curl_setopt($ch, CURLOPT_URL, $webhook['url']);\r\n                            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']);\r\n                            curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);\r\n                \r\n                            // Add headers if they exist\r\n                            if (!empty($webhook['headers'])) {\r\n                                $customheaders = preg_split(\"/\\r\\n|\\n|\\r/\", $webhook['headers']);\r\n                                curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);\r\n                            }\r\n                \r\n                            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                \r\n                            // Handle SSL settings\r\n                            if ($webhook['ignore_ssl']) {\r\n                                curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n                                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n                            }\r\n                \r\n                            // Execute the cURL request\r\n                            $response = curl_exec($ch);\r\n                            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n                            curl_close($ch);\r\n                \r\n                            if ($response === false || $httpCode >= 400) {\r\n                                echo \"Error sending cancellation notifications: \" . curl_error($ch) . \"<br />\";\r\n                            } else {\r\n                                echo \"Webhook Cancellation Notification sent for subscription: \" . $subscription['name'] . \"<br />\";\r\n                            }\r\n                \r\n                            usleep(1000000); // 1s delay between requests\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            \r\n\r\n        } else {\r\n            if (php_sapi_name() !== 'cli') {\r\n                echo \"Nothing to notify.<br />\";\r\n            }\r\n        }\r\n\r\n    }\r\n\r\n}\r\n\r\n?>\r\n"
  },
  {
    "path": "endpoints/cronjobs/sendnotifications.php",
    "content": "<?php\r\nuse PHPMailer\\PHPMailer\\PHPMailer;\r\nuse PHPMailer\\PHPMailer\\SMTP;\r\nuse PHPMailer\\PHPMailer\\Exception;\r\n\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\nrequire_once __DIR__ . '/../../includes/ssrf_helper.php';\r\n\r\nrequire __DIR__ . '/../../libs/PHPMailer/PHPMailer.php';\r\nrequire __DIR__ . '/../../libs/PHPMailer/SMTP.php';\r\nrequire __DIR__ . '/../../libs/PHPMailer/Exception.php';\r\n\r\nrequire __DIR__ . '/../../includes/currency_formatter.php';\r\n\r\nrequire 'settimezone.php';\r\n\r\nif (php_sapi_name() == 'cli') {\r\n    $date = new DateTime('now');\r\n    echo \"\\n\" . $date->format('Y-m-d') . \" \" . $date->format('H:i:s') . \"<br />\\n\";\r\n} else {\r\n    echo \"On Timezone: \" . date_default_timezone_get() . \"<br /><br />\";\r\n}\r\n\r\n// Get all user ids\r\n$query = \"SELECT id, username FROM user\";\r\n$stmt = $db->prepare($query);\r\n$usersToNotify = $stmt->execute();\r\n\r\nfunction getDaysText($days)\r\n{\r\n    if ($days == 0) {\r\n        return \"Today\";\r\n    } elseif ($days == 1) {\r\n        return \"Tomorrow\";\r\n    } else {\r\n        return \"In \" . $days . \" days\";\r\n    }\r\n}\r\n\r\nfunction formatPrice($price, $currencyCode, $currencySymbol)\r\n{\r\n    $formattedPrice = CurrencyFormatter::format($price, $currencyCode);\r\n\r\n    if (strpos($formattedPrice, $currencyCode) !== false) {\r\n        $formattedPrice = str_replace($currencyCode, $currencySymbol . ' ', $formattedPrice);\r\n        $formattedPrice = preg_replace('/\\s+/', ' ', $formattedPrice);\r\n    }\r\n\r\n    return $formattedPrice;\r\n}\r\n\r\nwhile ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) {\r\n    $userId = $userToNotify['id'];\r\n    if (php_sapi_name() !== 'cli') {\r\n        echo \"For user: \" . $userToNotify['username'] . \"<br /><br />\";\r\n    }\r\n\r\n    $days = 1;\r\n    $emailNotificationsEnabled = false;\r\n    $gotifyNotificationsEnabled = false;\r\n    $telegramNotificationsEnabled = false;\r\n    $webhookNotificationsEnabled = false;\r\n    $pushoverNotificationsEnabled = false;\r\n    $pushplusNotificationsEnabled = false;\r\n    $mattermostNotificationsEnabled = false;\r\n    $discordNotificationsEnabled = false;\r\n    $ntfyNotificationsEnabled = false;\r\n    $serverchanNotificationsEnabled = false;\r\n\r\n    // Get notification settings (how many days before the subscription ends should the notification be sent)\r\n    $query = \"SELECT days FROM notification_settings WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $days = $row['days'];\r\n    }\r\n\r\n    // Check if email notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM email_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $emailNotificationsEnabled = $row['enabled'];\r\n        $email['smtpAddress'] = $row[\"smtp_address\"];\r\n        $email['smtpPort'] = $row[\"smtp_port\"];\r\n        $email['encryption'] = $row[\"encryption\"];\r\n        $email['smtpUsername'] = $row[\"smtp_username\"];\r\n        $email['smtpPassword'] = $row[\"smtp_password\"];\r\n        $email['fromEmail'] = $row[\"from_email\"] ? $row[\"from_email\"] : \"wallos@wallosapp.com\";\r\n        $email['otherEmails'] = $row[\"other_emails\"];\r\n    }\r\n\r\n    // Check if Discord notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM discord_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $discordNotificationsEnabled = $row['enabled'];\r\n        $discord['webhook_url'] = $row[\"webhook_url\"];\r\n        $discord['bot_username'] = $row[\"bot_username\"];\r\n        $discord['bot_avatar_url'] = $row[\"bot_avatar_url\"];\r\n    }\r\n\r\n    // Check if Gotify notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM gotify_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $gotify = [];\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $gotifyNotificationsEnabled = $row['enabled'];\r\n        $gotify['serverUrl'] = $row[\"url\"];\r\n        $gotify['appToken'] = $row[\"token\"];\r\n        $gotify['ignore_ssl'] = $row[\"ignore_ssl\"];\r\n    }\r\n\r\n    // Check if Telegram notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM telegram_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $telegramNotificationsEnabled = $row['enabled'];\r\n        $telegram['botToken'] = $row[\"bot_token\"];\r\n        $telegram['chatId'] = $row[\"chat_id\"];\r\n    }\r\n    // Check if PushPlus notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM pushplus_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $pushplusNotificationsEnabled = $row['enabled'];\r\n        $pushplus['token'] = $row[\"token\"];\r\n    }\r\n    // Check if Mattermost notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM mattermost_notifications WHERE user_id = :userID\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userID', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $mattermostNotificationsEnabled = $row['enabled'];\r\n        $mattermost['webhook_url'] = $row['webhook_url'];\r\n        $mattermost['bot_username'] = $row['bot_username'];\r\n        $mattermost['bot_icon_emoji'] = $row['bot_icon_emoji'];\r\n    }\r\n\r\n    // Check if Pushover notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM pushover_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $pushoverNotificationsEnabled = $row['enabled'];\r\n        $pushover['user_key'] = $row[\"user_key\"];\r\n        $pushover['token'] = $row[\"token\"];\r\n    }\r\n\r\n    // Check if Ntfy notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM ntfy_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $ntfyNotificationsEnabled = $row['enabled'];\r\n        $ntfy['host'] = $row[\"host\"];\r\n        $ntfy['topic'] = $row[\"topic\"];\r\n        $ntfy['headers'] = $row[\"headers\"];\r\n        $ntfy['ignore_ssl'] = $row[\"ignore_ssl\"];\r\n    }\r\n\r\n    // Check if Webhook notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM webhook_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $webhookNotificationsEnabled = $row['enabled'];\r\n        $webhook['url'] = $row[\"url\"];\r\n        $webhook['request_method'] = $row[\"request_method\"];\r\n        $webhook['headers'] = $row[\"headers\"];\r\n        $webhook['payload'] = $row[\"payload\"];\r\n        $webhook['ignore_ssl'] = $row[\"ignore_ssl\"];\r\n    }\r\n\r\n    // Check if Serverchan notifications are enabled and get the settings\r\n    $query = \"SELECT * FROM serverchan_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $serverchanNotificationsEnabled = $row['enabled'];\r\n        $serverchan['sendkey'] = $row['sendkey'];\r\n    }\r\n\r\n    $notificationsEnabled = $emailNotificationsEnabled || $gotifyNotificationsEnabled || $telegramNotificationsEnabled ||\r\n        $webhookNotificationsEnabled || $pushoverNotificationsEnabled || $discordNotificationsEnabled || $pushplusNotificationsEnabled ||\r\n        $mattermostNotificationsEnabled || $ntfyNotificationsEnabled || $serverchanNotificationsEnabled;\r\n\r\n    // If no notifications are enabled, no need to run\r\n    if (!$notificationsEnabled) {\r\n        if (php_sapi_name() !== 'cli') {\r\n            echo \"Notifications are disabled. No need to run.<br />\";\r\n        }\r\n        continue;\r\n    } else {\r\n        // Get all currencies\r\n        $currencies = array();\r\n        $query = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $currencies[$row['id']] = $row;\r\n        }\r\n\r\n        // Get all household members\r\n        $query = \"SELECT * FROM household WHERE user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n        $resultHousehold = $stmt->execute();\r\n\r\n        $household = [];\r\n        while ($rowHousehold = $resultHousehold->fetchArray(SQLITE3_ASSOC)) {\r\n            $household[$rowHousehold['id']] = $rowHousehold;\r\n        }\r\n\r\n        // Get all categories\r\n        $query = \"SELECT * FROM categories WHERE user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n        $resultCategories = $stmt->execute();\r\n\r\n        $categories = [];\r\n        while ($rowCategory = $resultCategories->fetchArray(SQLITE3_ASSOC)) {\r\n            $categories[$rowCategory['id']] = $rowCategory;\r\n        }\r\n\r\n        $query = \"SELECT * FROM subscriptions WHERE user_id = :user_id AND notify = :notify AND inactive = :inactive ORDER BY payer_user_id ASC\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':notify', 1, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':inactive', 0, SQLITE3_INTEGER);\r\n        $resultSubscriptions = $stmt->execute();\r\n\r\n        $notify = [];\r\n        $i = 0;\r\n        $currentDate = new DateTime('now');\r\n        while ($rowSubscription = $resultSubscriptions->fetchArray(SQLITE3_ASSOC)) {\r\n            if ($rowSubscription['notify_days_before'] !== -1) {\r\n                $daysToCompare = $rowSubscription['notify_days_before'];\r\n            } else {\r\n                $daysToCompare = $days;\r\n            }\r\n            $nextPaymentDate = new DateTime($rowSubscription['next_payment']);\r\n\r\n            $difference = $currentDate->diff($nextPaymentDate)->days;\r\n            if ($nextPaymentDate > $currentDate) {\r\n                $difference += 1;\r\n            }\r\n\r\n            if ($difference === $daysToCompare && $nextPaymentDate->format('Y-m-d') >= $currentDate->format('Y-m-d')) {\r\n                echo \"Subscription: \" . $rowSubscription['name'] . \"<br />\";\r\n                echo \"Next payment date: \" . $nextPaymentDate->format('Y-m-d') . \"<br />\";\r\n                echo \"Current date: \" . $currentDate->format('Y-m-d') . \"<br />\";\r\n                echo \"Difference: \" . $difference . \"<br /><br />\";\r\n                $notify[$rowSubscription['payer_user_id']][$i]['name'] = html_entity_decode($rowSubscription['name'], ENT_QUOTES, 'UTF-8');\r\n                $notify[$rowSubscription['payer_user_id']][$i]['price'] = $rowSubscription['price'] . $currencies[$rowSubscription['currency_id']]['symbol'];\r\n                $notify[$rowSubscription['payer_user_id']][$i]['currency'] = $currencies[$rowSubscription['currency_id']]['name'];\r\n                $notify[$rowSubscription['payer_user_id']][$i]['currency_symbol'] = $currencies[$rowSubscription['currency_id']]['symbol'];\r\n                $notify[$rowSubscription['payer_user_id']][$i]['formatted_price'] = formatPrice($rowSubscription['price'], $currencies[$rowSubscription['currency_id']]['code'], $currencies[$rowSubscription['currency_id']]['symbol']);\r\n                $notify[$rowSubscription['payer_user_id']][$i]['category'] = $categories[$rowSubscription['category_id']]['name'];\r\n                $notify[$rowSubscription['payer_user_id']][$i]['payer'] = $household[$rowSubscription['payer_user_id']]['name'];\r\n                $notify[$rowSubscription['payer_user_id']][$i]['date'] = $rowSubscription['next_payment'];\r\n                $notify[$rowSubscription['payer_user_id']][$i]['days'] = $daysToCompare;\r\n                $notify[$rowSubscription['payer_user_id']][$i]['url'] = $rowSubscription['url'];\r\n                $notify[$rowSubscription['payer_user_id']][$i]['notes'] = $rowSubscription['notes'];\r\n                $i++;\r\n            }\r\n        }\r\n\r\n        if (!empty($notify)) {\r\n\r\n            // Email notifications if enabled\r\n            if ($emailNotificationsEnabled) {\r\n\r\n                $stmt = $db->prepare('SELECT * FROM user WHERE id = :user_id');\r\n                $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                $result = $stmt->execute();\r\n                $defaultUser = $result->fetchArray(SQLITE3_ASSOC);\r\n                $defaultEmail = $defaultUser['email'];\r\n                $defaultName = $defaultUser['username'];\r\n\r\n                foreach ($notify as $userId => $perUser) {\r\n                    $message = \"The following subscriptions are up for renewal:\\n\";\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $dayText = getDaysText($subscription['days']);\r\n                        $message .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                    }\r\n\r\n                    $smtpAuth = (isset($email[\"smtpUsername\"]) && $email[\"smtpUsername\"] != \"\") || (isset($email[\"smtpPassword\"]) && $email[\"smtpPassword\"] != \"\");\r\n\r\n                    $mail = new PHPMailer(true);\r\n                    $mail->CharSet = \"UTF-8\";\r\n                    $mail->isSMTP();\r\n\r\n                    $mail->Host = $email['smtpAddress'];\r\n                    $mail->SMTPAuth = $smtpAuth;\r\n\r\n                    if ($smtpAuth) {\r\n                        $mail->Username = $email['smtpUsername'];\r\n                        $mail->Password = $email['smtpPassword'];\r\n                    }\r\n\r\n                    if ($email['encryption'] != \"none\") {\r\n                        $mail->SMTPSecure = $email['encryption'];\r\n                    } else {\r\n                        $mail->SMTPSecure = false;\r\n                        $mail->SMTPAutoTLS = false;\r\n                    }\r\n\r\n                    $mail->Port = $email['smtpPort'];\r\n\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    $emailaddress = !empty($user['email']) ? $user['email'] : $defaultEmail;\r\n                    $name = !empty($user['name']) ? $user['name'] : $defaultName;\r\n\r\n                    $mail->setFrom($email['fromEmail'], 'Wallos App');\r\n                    $mail->addAddress($emailaddress, $name);\r\n\r\n                    if (!empty($email['otherEmails'])) {\r\n                        $list = explode(';', $email['otherEmails']);\r\n\r\n                        // Avoid duplicate emails\r\n                        $list = array_unique($list);\r\n                        $list = array_filter($list, function ($value) use ($emailaddress) {\r\n                            return $value !== $emailaddress;\r\n                        });\r\n\r\n                        foreach ($list as $value) {\r\n                            $mail->addCC(trim($value));\r\n                        }\r\n                    }\r\n\r\n                    $mail->Subject = 'Wallos Notification';\r\n                    $mail->Body = $message;\r\n\r\n                    if ($mail->send()) {\r\n                        echo \"Email Notifications sent<br />\";\r\n                    } else {\r\n                        echo \"Error sending notifications: \" . $mail->ErrorInfo . \"<br />\";\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Discord notifications if enabled\r\n            if ($discordNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($discord['webhook_url'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"SSRF attempt detected for Discord webhook URL. Notifications not sent.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                        $title = translate('wallos_notification', $i18n);\r\n\r\n                        if ($user['name']) {\r\n                            $message = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                        } else {\r\n                            $message = \"The following subscriptions are up for renewal:\\n\";\r\n                        }\r\n\r\n                        foreach ($perUser as $subscription) {\r\n                            $dayText = getDaysText($subscription['days']);\r\n                            $message .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                        }\r\n\r\n                        $postfields = [\r\n                            'content' => $message\r\n                        ];\r\n\r\n                        if (!empty($discord['bot_username'])) {\r\n                            $postfields['username'] = $discord['bot_username'];\r\n                        }\r\n\r\n                        if (!empty($discord['bot_avatar_url'])) {\r\n                            $postfields['avatar_url'] = $discord['bot_avatar_url'];\r\n                        }\r\n\r\n                        $ch = curl_init();\r\n\r\n                        curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']);\r\n                        curl_setopt($ch, CURLOPT_POST, 1);\r\n                        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));\r\n                        curl_setopt($ch, CURLOPT_HTTPHEADER, [\r\n                            'Content-Type: application/json'\r\n                        ]);\r\n                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n                        $response = curl_exec($ch);\r\n                        curl_close($ch);\r\n\r\n                        if ($result === false) {\r\n                            echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                        } else {\r\n                            echo \"Discord Notifications sent<br />\";\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Gotify notifications if enabled\r\n            if ($gotifyNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($gotify['serverUrl'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"SSRF attempt detected for Gotify server URL. Notifications not sent.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                        if ($user['name']) {\r\n                            $message = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                        } else {\r\n                            $message = \"The following subscriptions are up for renewal:\\n\";\r\n                        }\r\n\r\n                        foreach ($perUser as $subscription) {\r\n                            $dayText = getDaysText($subscription['days']);\r\n                            $message .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                        }\r\n\r\n                        $data = array(\r\n                            'message' => $message,\r\n                            'priority' => 5\r\n                        );\r\n\r\n                        $data_string = json_encode($data);\r\n\r\n                        $ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']);\r\n                        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, \"POST\");\r\n                        curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);\r\n                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                        curl_setopt(\r\n                            $ch,\r\n                            CURLOPT_HTTPHEADER,\r\n                            array(\r\n                                'Content-Type: application/json',\r\n                                'Content-Length: ' . strlen($data_string)\r\n                            )\r\n                        );\r\n\r\n                        if ($gotify['ignore_ssl']) {\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n                        }\r\n\r\n                        $result = curl_exec($ch);\r\n                        if ($result === false) {\r\n                            echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                        } else {\r\n                            echo \"Gotify Notifications sent<br />\";\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Telegram notifications if enabled\r\n            if ($telegramNotificationsEnabled) {\r\n                foreach ($notify as $userId => $perUser) {\r\n                    // Get name of user from household table\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    if ($user['name']) {\r\n                        $message = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                    } else {\r\n                        $message = \"The following subscriptions are up for renewal:\\n\";\r\n                    }\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $dayText = getDaysText($subscription['days']);\r\n                        $message .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                    }\r\n\r\n                    $data = array(\r\n                        'chat_id' => $telegram['chatId'],\r\n                        'text' => mb_convert_encoding($message, 'UTF-8', 'auto')\r\n                    );\r\n\r\n                    $data_string = json_encode($data);\r\n\r\n                    $ch = curl_init('https://api.telegram.org/bot' . $telegram['botToken'] . '/sendMessage');\r\n                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, \"POST\");\r\n                    curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);\r\n                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                    curl_setopt(\r\n                        $ch,\r\n                        CURLOPT_HTTPHEADER,\r\n                        array(\r\n                            'Content-Type: application/json',\r\n                            'Content-Length: ' . strlen($data_string)\r\n                        )\r\n                    );\r\n\r\n                    $result = curl_exec($ch);\r\n                    if ($result === false) {\r\n                        echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                    } else {\r\n                        echo \"Telegram Notifications sent<br />\";\r\n                    }\r\n                }\r\n            }\r\n\r\n\r\n            // PushPlus notifications if enabled\r\n            if ($pushplusNotificationsEnabled) {\r\n                foreach ($notify as $userId => $perUser) {\r\n                    // Get name of user from household table\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    // Build Message Content\r\n                    $messageContent = \"\";\r\n                    if ($user['name']) {\r\n                        $messageContent = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                    } else {\r\n                        $messageContent = \"The following subscriptions are up for renewal:\\n\";\r\n                    }\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $dayText = getDaysText($subscription['days']);\r\n                        $messageContent .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                    }\r\n\r\n                    // Prepare PushPlus Data\r\n                    $data = array(\r\n                        'token' => $pushplus['token'],\r\n                        'title' => '订阅续期提醒 - Wallos',\r\n                        'content' => mb_convert_encoding($messageContent, 'UTF-8', 'auto'),\r\n                        'template' => 'json'\r\n                    );\r\n\r\n                    $data_string = json_encode($data);\r\n\r\n                    $ch = curl_init('https://www.pushplus.plus/send');\r\n                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, \"POST\");\r\n                    curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);\r\n                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                    curl_setopt(\r\n                        $ch,\r\n                        CURLOPT_HTTPHEADER,\r\n                        array(\r\n                            'Content-Type: application/json'\r\n                        ),\r\n                    );\r\n\r\n                    $result = curl_exec($ch);\r\n                    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n\r\n                    if ($result === false) {\r\n                        echo \"Error sending PushPlus notifications: \" . curl_error($ch) . \"<br />\";\r\n                    } else {\r\n                        $resultData = json_decode($result, true);\r\n                        if (isset($resultData['code']) && $resultData['code'] == 200) {\r\n                            echo \"PushPlus Notifications sent successfully<br />\";\r\n                        } else {\r\n                            $errorMsg = isset($resultData['msg']) ? $resultData['msg'] : 'Unknown error';\r\n                            echo \"PushPlus API error: \" . $errorMsg . \"<br />\";\r\n                        }\r\n                    }\r\n                    curl_close($ch);\r\n                }\r\n            }\r\n\r\n            // Mattermost notifications if enabled\r\n            if ($mattermostNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($mattermost['webhook_url'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"SSRF attempt detected for Mattermost webhook URL. Notifications not sent.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                        // Build Message Content\r\n                        $messageContent = \"\";\r\n                        if ($user['name']) {\r\n                            $messageContent = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                        } else {\r\n                            $messageContent = \"The following subscriptions are up for renewal:\\n\";\r\n                        }\r\n\r\n                        foreach ($perUser as $subscription) {\r\n                            $dayText = getDaysText($subscription['days']);\r\n                            $messageContent .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                        }\r\n\r\n                        // Prepare Mattermost Data\r\n                        $webhook_url = $mattermost['webhook_url'];\r\n                        $data = array(\r\n                            'username' => $mattermost['bot_username'],\r\n                            'icon_emoji' => $mattermost['bot_icon_emoji'],\r\n                            'text' => mb_convert_encoding($messageContent, 'UTF-8', 'auto'),\r\n                        );\r\n\r\n                        $data_string = json_encode($data);\r\n\r\n                        $ch = curl_init();\r\n                        curl_setopt($ch, CURLOPT_URL, $webhook_url);\r\n                        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, \"POST\");\r\n                        curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);\r\n                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                        curl_setopt(\r\n                            $ch,\r\n                            CURLOPT_HTTPHEADER,\r\n                            array(\r\n                                'Content-Type: application/json'\r\n                            ),\r\n                        );\r\n\r\n                        $result = curl_exec($ch);\r\n                        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n\r\n                        if ($result === false) {\r\n                            echo \"Error sending Mattermost notifications: \" . curl_error($ch) . \"<br />\";\r\n                        } else {\r\n                            $resultData = json_decode($result, true);\r\n                            if (isset($resultData['code']) && $resultData['code'] == 200) {\r\n                                echo \"Mattermost Notifications sent successfully<br />\";\r\n                            } else {\r\n                                $errorMsg = isset($resultData['msg']) ? $resultData['msg'] : 'Unknown error';\r\n                                echo \"Mattermost API error: \" . $errorMsg . \"<br />\";\r\n                            }\r\n                        }\r\n                        curl_close($ch);\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Pushover notifications if enabled\r\n            if ($pushoverNotificationsEnabled) {\r\n                foreach ($notify as $userId => $perUser) {\r\n                    // Get name of user from household table\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    if ($user['name']) {\r\n                        $message = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                    } else {\r\n                        $message = \"The following subscriptions are up for renewal:\\n\";\r\n                    }\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $dayText = getDaysText($subscription['days']);\r\n                        $message .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                    }\r\n\r\n                    $ch = curl_init();\r\n                    curl_setopt($ch, CURLOPT_URL, \"https://api.pushover.net/1/messages.json\");\r\n                    curl_setopt($ch, CURLOPT_POST, 1);\r\n                    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([\r\n                        'token' => $pushover['token'],\r\n                        'user' => $pushover['user_key'],\r\n                        'message' => $message,\r\n                    ]));\r\n                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n                    $result = curl_exec($ch);\r\n\r\n                    curl_close($ch);\r\n\r\n                    if ($result === false) {\r\n                        echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                    } else {\r\n                        echo \"Pushover Notifications sent<br />\";\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Ntfy notifications if enabled\r\n            if ($ntfyNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($ntfy['host'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"SSRF attempt detected for Ntfy host URL. Notifications not sent.<br />\";\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                        if ($user['name']) {\r\n                            $message = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                        } else {\r\n                            $message = \"The following subscriptions are up for renewal:\\n\";\r\n                        }\r\n\r\n                        foreach ($perUser as $subscription) {\r\n                            $dayText = getDaysText($subscription['days']);\r\n                            $message .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                        }\r\n\r\n                        $headers = json_decode($ntfy[\"headers\"], true);\r\n                        $customheaders = [];\r\n\r\n                        if (is_array($headers)) {\r\n                            $customheaders = array_map(function ($key, $value) {\r\n                                return \"$key: $value\";\r\n                            }, array_keys($headers), $headers);\r\n                        }\r\n\r\n                        $ch = curl_init();\r\n\r\n                        $ntfyHost = rtrim($ntfy[\"host\"], '/');\r\n                        $ntfyTopic = $ntfy['topic'];\r\n\r\n                        curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic);\r\n                        curl_setopt($ch, CURLOPT_POST, 1);\r\n                        curl_setopt($ch, CURLOPT_POSTFIELDS, $message);\r\n                        curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);\r\n                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n                        if ($ntfy['ignore_ssl']) {\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n                            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n                        }\r\n\r\n                        $response = curl_exec($ch);\r\n                        curl_close($ch);\r\n\r\n                        if ($response === false) {\r\n                            echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                        } else {\r\n                            echo \"Ntfy Notifications sent<br />\";\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Webhook notifications if enabled\r\n            if ($webhookNotificationsEnabled) {\r\n                $ssrf = is_url_safe_for_ssrf($webhook['url'], $db);\r\n                if (!$ssrf) {\r\n                    echo \"SSRF attempt detected for webhook URL. Notifications not sent.<br />\";;\r\n                } else {\r\n                    foreach ($notify as $userId => $perUser) {\r\n                        // Get name of user from household table\r\n                        $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n                \r\n                        if ($user['name']) {\r\n                            $payer = $user['name'];\r\n                        }\r\n                \r\n                        foreach ($perUser as $subscription) {\r\n                            // Ensure the payload is reset for each subscription\r\n                            $payload = $webhook['payload'];\r\n                            $payload = str_replace(\"{{days_until}}\", $days, $payload);\r\n                            $payload = str_replace(\"{{subscription_name}}\", $subscription['name'], $payload);\r\n                            $payload = str_replace(\"{{subscription_price}}\", $subscription['formatted_price'], $payload);\r\n                            $payload = str_replace(\"{{subscription_currency}}\", $subscription['currency'], $payload);\r\n                            $payload = str_replace(\"{{subscription_category}}\", $subscription['category'], $payload);\r\n                            $payload = str_replace(\"{{subscription_payer}}\", $payer, $payload); // Use $payer instead of $subscription['payer']\r\n                            $payload = str_replace(\"{{subscription_date}}\", $subscription['date'], $payload);\r\n                            $payload = str_replace(\"{{subscription_days_until_payment}}\", $subscription['days'], $payload);\r\n                            $payload = str_replace(\"{{subscription_url}}\", $subscription['url'], $payload);\r\n                            $payload = str_replace(\"{{subscription_notes}}\", $subscription['notes'], $payload);\r\n                \r\n                            // Initialize cURL for each subscription\r\n                            $ch = curl_init();\r\n                            curl_setopt($ch, CURLOPT_URL, $webhook['url']);\r\n                            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']);\r\n                            curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);\r\n                \r\n                            // Add headers if they exist\r\n                            if (!empty($webhook['headers'])) {\r\n                                $customheaders = json_decode($webhook[\"headers\"], true);\r\n                                curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);\r\n                            }\r\n                \r\n                            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n                \r\n                            // Handle SSL settings\r\n                            if ($webhook['ignore_ssl']) {\r\n                                curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n                                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n                            }\r\n                \r\n                            // Execute the cURL request\r\n                            $response = curl_exec($ch);\r\n                            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n                            curl_close($ch);\r\n                \r\n                            if ($response === false || $httpCode >= 400) {\r\n                                echo \"Error sending notifications: \" . curl_error($ch) . \"<br />\";\r\n                            } else {\r\n                                echo \"Webhook Notification sent for subscription: \" . $subscription['name'] . \"<br />\";\r\n                            }\r\n                \r\n                            usleep(1000000); // 1s delay between requests\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            // Serverchan notifications if enabled\r\n            if ($serverchanNotificationsEnabled) {\r\n                foreach ($notify as $userId => $perUser) {\r\n                    // Get name of user from household table\r\n                    $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');\r\n                    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                    $title = 'Wallos Notification';\r\n                    if ($user['name']) {\r\n                        $message = $user['name'] . \", the following subscriptions are up for renewal:\\n\";\r\n                    } else {\r\n                        $message = \"The following subscriptions are up for renewal:\\n\";\r\n                    }\r\n\r\n                    foreach ($perUser as $subscription) {\r\n                        $dayText = getDaysText($subscription['days']);\r\n                        $message .= $subscription['name'] . \" for \" . $subscription['formatted_price'] . \" (\" . $dayText . \")\\n\";\r\n                    }\r\n\r\n                    // Build Serverchan request\r\n                    $postdata = http_build_query(array('text' => $title, 'desp' => $message));\r\n\r\n                    $sendkey = $serverchan['sendkey'];\r\n                    if (strpos($sendkey, 'sctp') === 0) {\r\n                        preg_match('/^sctp(\\d+)t/', $sendkey, $matches);\r\n                        $num = $matches[1] ?? '';\r\n                        $url = \"https://{$num}.push.ft07.com/send/{$sendkey}.send\";\r\n                    } else {\r\n                        $url = \"https://sctapi.ftqq.com/{$sendkey}.send\";\r\n                    }\r\n\r\n                    $ch = curl_init();\r\n                    curl_setopt($ch, CURLOPT_URL, $url);\r\n                    curl_setopt($ch, CURLOPT_POST, 1);\r\n                    curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);\r\n                    curl_setopt($ch, CURLOPT_HTTPHEADER, [\r\n                        'Content-Type: application/x-www-form-urlencoded'\r\n                    ]);\r\n                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n                    $response = curl_exec($ch);\r\n                    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n\r\n                    if ($response === false || $httpCode >= 400) {\r\n                        $errorMessage = $response === false ? curl_error($ch) : $httpCode;\r\n                        curl_close($ch);\r\n                        echo \"Error sending Serverchan notifications: \" . $errorMessage . \"<br />\";\r\n                    } else {\r\n                        curl_close($ch);\r\n                        echo \"Serverchan Notifications sent<br />\";\r\n                    }\r\n                }\r\n            }\r\n\r\n        } else {\r\n            if (php_sapi_name() !== 'cli') {\r\n                echo \"Nothing to notify.<br />\";\r\n            }\r\n        }\r\n\r\n    }\r\n\r\n}\r\n\r\n?>\r\n"
  },
  {
    "path": "endpoints/cronjobs/sendresetpasswordemails.php",
    "content": "<?php\r\nuse PHPMailer\\PHPMailer\\PHPMailer;\r\nuse PHPMailer\\PHPMailer\\SMTP;\r\nuse PHPMailer\\PHPMailer\\Exception;\r\n\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\n\r\nrequire 'settimezone.php';\r\n\r\n$query = \"SELECT * FROM admin\";\r\n$stmt = $db->prepare($query);\r\n$result = $stmt->execute();\r\n$admin = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n$query = \"SELECT * FROM password_resets WHERE email_sent = 0\";\r\n$stmt = $db->prepare($query);\r\n$result = $stmt->execute();\r\n\r\n$rows = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $rows[] = $row;\r\n}\r\n\r\nif ($rows) {\r\n    if ($admin['smtp_address'] && $admin['smtp_port'] && $admin['smtp_username'] && $admin['smtp_password'] && $admin['encryption']) {\r\n        // There are SMTP settings\r\n        $smtpAddress = $admin['smtp_address'];\r\n        $smtpPort = $admin['smtp_port'];\r\n        $smtpUsername = $admin['smtp_username'];\r\n        $smtpPassword = $admin['smtp_password'];\r\n        $fromEmail = empty($admin['from_email']) ? 'wallos@wallosapp.com' : $admin['from_email'];\r\n        $encryption = $admin['encryption'];\r\n        $server_url = $admin['server_url'];\r\n        $smtpAuth = (isset($admin[\"smtp_username\"]) && $admin[\"smtp_username\"] != \"\") || (isset($admin[\"smtp_password\"]) && $admin[\"smtp_password\"] != \"\");\r\n\r\n        require __DIR__ . '/../../libs/PHPMailer/PHPMailer.php';\r\n        require __DIR__ . '/../../libs/PHPMailer/SMTP.php';\r\n        require __DIR__ . '/../../libs/PHPMailer/Exception.php';\r\n\r\n        $mail = new PHPMailer(true);\r\n        $mail->isSMTP();\r\n        $mail->Host = $smtpAddress;\r\n        $mail->SMTPAuth = $smtpAuth;\r\n        if ($smtpAuth) {\r\n            $mail->Username = $smtpUsername;\r\n            $mail->Password = $smtpPassword;\r\n        }\r\n        if ($encryption != \"none\") {\r\n            $mail->SMTPSecure = $encryption;\r\n        }\r\n        $mail->Port = $smtpPort;\r\n        $mail->setFrom($fromEmail);\r\n\r\n        try {\r\n            foreach ($rows as $user) {\r\n                $mail->addAddress($user['email']);\r\n                $mail->isHTML(true);\r\n                $mail->Subject = 'Wallos - Reset Password';\r\n                $mail->Body = '<img src=\"' . $server_url . '/images/siteicons/wallos.png\" alt=\"Logo\" />\r\n                    <br>\r\n                    A password reset was requested for your account.\r\n                    <br>\r\n                    Please click the following link to reset your password: <a href=\"' . $server_url . '/passwordreset.php?email=' . $user['email'] . '&token=' . $user['token'] . '\">Reset Password</a>';\r\n\r\n                $mail->send();\r\n\r\n                $query = \"UPDATE password_resets SET email_sent = 1 WHERE id = :id\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindParam(':id', $user['id'], SQLITE3_INTEGER);\r\n                $stmt->execute();\r\n\r\n                $mail->clearAddresses();\r\n\r\n                echo \"Password reset email sent to \" . $user['email'] . \"<br>\";\r\n\r\n            }\r\n        } catch (Exception $e) {\r\n            echo \"Message could not be sent. Mailer Error: {$mail->ErrorInfo} <br>\";\r\n        }\r\n    } else {\r\n        // There are no SMTP settings\r\n        if (php_sapi_name() !== 'cli') {\r\n            echo \"SMTP settings are not configured. Please configure SMTP settings in the admin page.\";\r\n        }\r\n        exit();\r\n    }\r\n} else {\r\n    // There are no password reset emails to be sent\r\n    if (php_sapi_name() !== 'cli') {\r\n        echo \"There are no password reset emails to be sent.\";\r\n    }\r\n    exit();\r\n}\r\n\r\n?>\r\n"
  },
  {
    "path": "endpoints/cronjobs/sendverificationemails.php",
    "content": "<?php\r\nuse PHPMailer\\PHPMailer\\PHPMailer;\r\nuse PHPMailer\\PHPMailer\\SMTP;\r\nuse PHPMailer\\PHPMailer\\Exception;\r\n\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\n\r\nrequire 'settimezone.php';\r\n\r\n$query = \"SELECT * FROM admin\";\r\n$stmt = $db->prepare($query);\r\n$result = $stmt->execute();\r\n$admin = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($admin['require_email_verification'] == 0) {\r\n    if (php_sapi_name() !== 'cli') {\r\n        echo \"Email verification is not required.\";\r\n    }\r\n    die();\r\n}\r\n\r\n$query = \"SELECT * FROM email_verification WHERE email_sent = 0\";\r\n$stmt = $db->prepare($query);\r\n$result = $stmt->execute();\r\n\r\n$rows = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $rows[] = $row;\r\n}\r\n\r\nif ($rows) {\r\n    if ($admin['smtp_address'] && $admin['smtp_port'] && $admin['smtp_username'] && $admin['smtp_password'] && $admin['encryption']) {\r\n        // There are SMTP settings\r\n        $smtpAddress = $admin['smtp_address'];\r\n        $smtpPort = $admin['smtp_port'];\r\n        $smtpUsername = $admin['smtp_username'];\r\n        $smtpPassword = $admin['smtp_password'];\r\n        $fromEmail = empty($admin['from_email']) ? 'wallos@wallosapp.com' : $admin['from_email'];\r\n        $encryption = $admin['encryption'];\r\n        $server_url = $admin['server_url'];\r\n        $smtpAuth = (isset($admin[\"smtp_username\"]) && $admin[\"smtp_username\"] != \"\") || (isset($admin[\"smtp_password\"]) && $admin[\"smtp_password\"] != \"\");\r\n\r\n        require __DIR__ . '/../../libs/PHPMailer/PHPMailer.php';\r\n        require __DIR__ . '/../../libs/PHPMailer/SMTP.php';\r\n        require __DIR__ . '/../../libs/PHPMailer/Exception.php';\r\n\r\n        $mail = new PHPMailer(true);\r\n        $mail->isSMTP();\r\n        $mail->Host = $smtpAddress;\r\n        $mail->SMTPAuth = $smtpAuth;\r\n        if ($smtpAuth) {\r\n          $mail->Username = $smtpUsername;\r\n          $mail->Password = $smtpPassword;\r\n        }\r\n        if ($encryption != \"none\") {\r\n          $mail->SMTPSecure = $encryption;\r\n        }\r\n        $mail->Port = $smtpPort;\r\n        $mail->setFrom($fromEmail);\r\n\r\n        try {\r\n            foreach ($rows as $user) {\r\n                $mail->addAddress($user['email']);\r\n                $mail->isHTML(true);\r\n                $mail->Subject = 'Wallos - Email Verification';\r\n                $mail->Body = '<img src=\"' . $server_url . '/images/siteicons/wallos.png\" alt=\"Logo\" />\r\n                    <br>\r\n                    Registration on Wallos was successful.\r\n                    <br>\r\n                    Please click the following link to verify your email: <a href=\"' . $server_url . '/verifyemail.php?email=' . $user['email'] . '&token=' . $user['token'] . '\">Verify Email</a>';\r\n\r\n                $mail->send();\r\n\r\n                $query = \"UPDATE email_verification SET email_sent = 1 WHERE id = :id\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindParam(':id', $user['id'], SQLITE3_INTEGER);\r\n                $stmt->execute();\r\n\r\n                $mail->clearAddresses();\r\n\r\n                echo \"Verification email sent to \" . $user['email'] . \"<br>\";\r\n            }\r\n        } catch (Exception $e) {\r\n            echo \"Message could not be sent. Mailer Error: {$mail->ErrorInfo}\";\r\n        }\r\n    } else {\r\n        // There are no SMTP settings\r\n        if (php_sapi_name() !== 'cli') {\r\n            echo \"SMTP settings are not configured. Please configure SMTP settings in the admin page.\";\r\n        }\r\n        exit();\r\n    }\r\n} else {\r\n    // There are no verification emails to be sent\r\n    if (php_sapi_name() !== 'cli') {\r\n        echo \"No verification emails to be sent.\";\r\n    }\r\n    exit();\r\n}\r\n\r\n?>\r\n"
  },
  {
    "path": "endpoints/cronjobs/settimezone.php",
    "content": "<?php\r\n\r\n$timezone = getenv('TZ');\r\nif ($timezone == '') {\r\n    $timezone = date_default_timezone_get();\r\n    if ($timezone == '') {\r\n        $timezone = 'UTC';\r\n    }\r\n}\r\n\r\ndate_default_timezone_set($timezone);"
  },
  {
    "path": "endpoints/cronjobs/storetotalyearlycost.php",
    "content": "<?php\r\n\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\n\r\nrequire 'settimezone.php';\r\n\r\nif (php_sapi_name() == 'cli') {\r\n    $date = new DateTime('now');\r\n    echo \"\\n\" . $date->format('Y-m-d') . \" \" . $date->format('H:i:s') . \"<br />\\n\";\r\n}\r\n\r\n$currentDate = new DateTime();\r\n$currentDateString = $currentDate->format('Y-m-d');\r\n\r\nfunction getPricePerMonth($cycle, $frequency, $price)\r\n{\r\n  switch ($cycle) {\r\n    case 1:\r\n      $numberOfPaymentsPerMonth = (30 / $frequency);\r\n      return $price * $numberOfPaymentsPerMonth;\r\n    case 2:\r\n      $numberOfPaymentsPerMonth = (4.35 / $frequency);\r\n      return $price * $numberOfPaymentsPerMonth;\r\n    case 3:\r\n      $numberOfPaymentsPerMonth = (1 / $frequency);\r\n      return $price * $numberOfPaymentsPerMonth;\r\n    case 4:\r\n      $numberOfMonths = (12 * $frequency);\r\n      return $price / $numberOfMonths;\r\n  }\r\n}\r\n\r\nfunction getPriceConverted($price, $currency, $database, $userId)\r\n{\r\n  $query = \"SELECT rate FROM currencies WHERE id = :currency AND user_id = :userId\";\r\n  $stmt = $database->prepare($query);\r\n  $stmt->bindParam(':currency', $currency, SQLITE3_INTEGER);\r\n  $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n  $result = $stmt->execute();\r\n\r\n  $exchangeRate = $result->fetchArray(SQLITE3_ASSOC);\r\n  if ($exchangeRate === false) {\r\n    return $price;\r\n  } else {\r\n    $fromRate = $exchangeRate['rate'];\r\n    return $price / $fromRate;\r\n  }\r\n}\r\n\r\n// Get all users\r\n\r\n$query = \"SELECT id, main_currency FROM user\";\r\n$stmt = $db->prepare($query);\r\n$result = $stmt->execute();\r\n\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $userId = $row['id'];\r\n    $userCurrencyId = $row['main_currency'];\r\n    $totalYearlyCost = 0;\r\n\r\n    $query = \"SELECT * FROM subscriptions WHERE user_id = :userId AND inactive = 0\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $resultSubscriptions = $stmt->execute();\r\n\r\n    while ($rowSubscriptions = $resultSubscriptions->fetchArray(SQLITE3_ASSOC)) {\r\n        $originalSubscriptionPrice = getPriceConverted($rowSubscriptions['price'], $rowSubscriptions['currency_id'], $db, $userId);\r\n        $price = getPricePerMonth($rowSubscriptions['cycle'], $rowSubscriptions['frequency'], $originalSubscriptionPrice) * 12;\r\n        $totalYearlyCost += $price;\r\n    }\r\n\r\n    $query = \"INSERT INTO total_yearly_cost (user_id, date, cost, currency) VALUES (:userId, :date, :cost, :currency)\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $stmt->bindParam(':date', $currentDateString, SQLITE3_TEXT);\r\n    $stmt->bindParam(':cost', $totalYearlyCost, SQLITE3_FLOAT);\r\n    $stmt->bindParam(':currency', $userCurrencyId, SQLITE3_INTEGER);\r\n\r\n    if ($stmt->execute()) {\r\n        echo \"Inserted total yearly cost for user \" . $userId . \" with cost \" . $totalYearlyCost . \"<br />\\n\";\r\n    } else {\r\n        echo \"Error inserting total yearly cost for user \" . $userId . \"<br />\\n\";\r\n    }\r\n}\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n?>"
  },
  {
    "path": "endpoints/cronjobs/updateexchange.php",
    "content": "<?php\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\n\r\nrequire 'settimezone.php';\r\n\r\n// Get all user ids\r\n\r\nif (php_sapi_name() == 'cli') {\r\n    $date = new DateTime('now');\r\n    echo \"\\n\" . $date->format('Y-m-d') . \" \" . $date->format('H:i:s') . \"<br />\\n\";\r\n}\r\n\r\n$query = \"SELECT id, username FROM user\";\r\n$stmt = $db->prepare($query);\r\n$usersToUpdateExchange = $stmt->execute();\r\n\r\nwhile ($userToUpdateExchange = $usersToUpdateExchange->fetchArray(SQLITE3_ASSOC)) {\r\n    $userId = $userToUpdateExchange['id'];\r\n    echo \"For user: \" . $userToUpdateExchange['username'] . \"<br />\";\r\n\r\n    $query = \"SELECT api_key, provider FROM fixer WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n        if ($row) {\r\n            $apiKey = $row['api_key'];\r\n            $provider = $row['provider'];\r\n\r\n            $codes = \"\";\r\n            $query = \"SELECT id, name, symbol, code FROM currencies WHERE user_id = :userId\";\r\n            $stmt = $db->prepare($query);\r\n            $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $stmt->execute();\r\n            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n                $codes .= $row['code'] . \",\";\r\n            }\r\n            $codes = rtrim($codes, ',');\r\n            $query = \"SELECT u.main_currency, c.code FROM user u LEFT JOIN currencies c ON u.main_currency = c.id WHERE u.id = :userId\";\r\n            $stmt = $db->prepare($query);\r\n            $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $stmt->execute();\r\n            $row = $result->fetchArray(SQLITE3_ASSOC);\r\n            $mainCurrencyCode = $row['code'];\r\n            $mainCurrencyId = $row['main_currency'];\r\n\r\n            if ($provider === 1) {\r\n                $api_url = \"https://api.apilayer.com/fixer/latest?base=EUR&symbols=\" . $codes;\r\n                $context = stream_context_create([\r\n                    'http' => [\r\n                        'method' => 'GET',\r\n                        'header' => 'apikey: ' . $apiKey,\r\n                    ]\r\n                ]);\r\n                $response = file_get_contents($api_url, false, $context);\r\n            } else {\r\n                $api_url = \"http://data.fixer.io/api/latest?access_key=\" . $apiKey . \"&base=EUR&symbols=\" . $codes;\r\n                $response = file_get_contents($api_url);\r\n            }\r\n\r\n            $apiData = json_decode($response, true);\r\n\r\n            $mainCurrencyToEUR = $apiData['rates'][$mainCurrencyCode];\r\n\r\n            if ($apiData !== null && isset($apiData['rates'])) {\r\n                foreach ($apiData['rates'] as $currencyCode => $rate) {\r\n                    if ($currencyCode === $mainCurrencyCode) {\r\n                        $exchangeRate = 1.0;\r\n                    } else {\r\n                        $exchangeRate = $rate / $mainCurrencyToEUR;\r\n                    }\r\n                    $updateQuery = \"UPDATE currencies SET rate = :rate WHERE code = :code\";\r\n                    $updateStmt = $db->prepare($updateQuery);\r\n                    $updateStmt->bindParam(':rate', $exchangeRate, SQLITE3_TEXT);\r\n                    $updateStmt->bindParam(':code', $currencyCode, SQLITE3_TEXT);\r\n                    $updateResult = $updateStmt->execute();\r\n\r\n                    if (!$updateResult) {\r\n                        echo \"Error updating rate for currency: $currencyCode <br />\";\r\n                    }\r\n                }\r\n                $currentDate = new DateTime();\r\n                $formattedDate = $currentDate->format('Y-m-d');\r\n\r\n                $deleteQuery = \"DELETE FROM last_exchange_update WHERE user_id = :userId\";\r\n                $deleteStmt = $db->prepare($deleteQuery);\r\n                $deleteStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                $deleteResult = $deleteStmt->execute();\r\n\r\n                $query = \"INSERT INTO last_exchange_update (date, user_id) VALUES (:formattedDate, :userId)\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindParam(':formattedDate', $formattedDate, SQLITE3_TEXT);\r\n                $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                $result = $stmt->execute();\r\n\r\n                echo \"Rates updated successfully!<br />\";\r\n            }\r\n        } else {\r\n            echo \"Exchange rates update skipped. No fixer.io api key provided<br />\";\r\n            $apiKey = null;\r\n        }\r\n    } else {\r\n        echo \"Exchange rates update skipped. No fixer.io api key provided<br />\";\r\n        $apiKey = null;\r\n    }\r\n}\r\n$db->close();\r\n\r\n?>"
  },
  {
    "path": "endpoints/cronjobs/updatenextpayment.php",
    "content": "<?php\r\n\r\nrequire_once 'validate.php';\r\nrequire_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';\r\n\r\nrequire 'settimezone.php';\r\n\r\n$date = new DateTime('now');\r\necho \"\\n\" . $date->format('Y-m-d') . \" \" . $date->format('H:i:s') . \"<br />\\n\";\r\necho $timezone . \"<br />\\n\";\r\n\r\n$currentDate = new DateTime();\r\n$currentDateString = $currentDate->format('Y-m-d');\r\n\r\n$cycles = array();\r\n$query = \"SELECT * FROM cycles\";\r\n$result = $db->query($query);\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $cycleId = $row['id'];\r\n    $cycles[$cycleId] = $row;\r\n}\r\n\r\n$query = \"SELECT id, next_payment, frequency, cycle FROM subscriptions WHERE next_payment < :currentDate AND auto_renew = 1 AND inactive = 0\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':currentDate', $currentDate->format('Y-m-d'));\r\n$result = $stmt->execute();\r\n\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $subscriptionId = $row['id'];\r\n    $nextPaymentDate = new DateTime($row['next_payment']);\r\n    $frequency = $row['frequency'];\r\n    $cycle = $cycles[$row['cycle']]['name'];\r\n\r\n    // Calculate the interval to add based on the cycle\r\n    $intervalSpec = \"P\";\r\n    if ($cycle == 'Daily') {\r\n        $intervalSpec .= \"{$frequency}D\";\r\n    } elseif ($cycle === 'Weekly') {\r\n        $intervalSpec .= \"{$frequency}W\";\r\n    } elseif ($cycle === 'Monthly') {\r\n        $intervalSpec .= \"{$frequency}M\";\r\n    } elseif ($cycle === 'Yearly') {\r\n        $intervalSpec .= \"{$frequency}Y\";\r\n    }\r\n\r\n    $interval = new DateInterval($intervalSpec);\r\n\r\n    // Add intervals until the next payment date is in the future\r\n    while ($nextPaymentDate < $currentDate) {\r\n        $nextPaymentDate->add($interval);\r\n    }\r\n\r\n    // Update the subscription's next_payment date\r\n    $updateQuery = \"UPDATE subscriptions SET next_payment = :nextPaymentDate WHERE id = :subscriptionId\";\r\n    $updateStmt = $db->prepare($updateQuery);\r\n    $updateStmt->bindValue(':nextPaymentDate', $nextPaymentDate->format('Y-m-d'));\r\n    $updateStmt->bindValue(':subscriptionId', $subscriptionId);\r\n    $updateStmt->execute();\r\n}\r\n\r\n$formattedDate = $currentDate->format('Y-m-d');\r\n\r\n$deleteQuery = \"DELETE FROM last_update_next_payment_date\";\r\n$deleteStmt = $db->prepare($deleteQuery);\r\n$deleteResult = $deleteStmt->execute();\r\n\r\n$query = \"INSERT INTO last_update_next_payment_date (date) VALUES (:formattedDate)\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindParam(':formattedDate', $currentDateString, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n\r\necho \"Updated next payment dates\";\r\n?>"
  },
  {
    "path": "endpoints/cronjobs/validate.php",
    "content": "<?php\r\n\r\nsession_start();\r\n\r\n$userId = 0;\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    $userId = $_SESSION['userId'];\r\n}\r\n\r\nif (php_sapi_name() !== 'cli') {\r\n    if ($userId !== 1) {\r\n        die(\"Unauthorized\");\r\n    }\r\n}\r\n\r\n?>"
  },
  {
    "path": "endpoints/currency/currency.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$action = $_POST['action'] ?? '';\r\n\r\nswitch ($action) {\r\n    case \"add\":\r\n        handleAddCurrency($db, $userId, $i18n);\r\n        break;\r\n    case \"edit\":\r\n        handleEditCurrency($db, $userId, $i18n);\r\n        break;\r\n    case \"delete\":\r\n        handleDeleteCurrency($db, $userId, $i18n);\r\n        break;\r\n    default:\r\n        echo json_encode([\"success\" => false, \"message\" => translate('error', $i18n)]);\r\n        break;\r\n}\r\n\r\n\r\nfunction handleAddCurrency($db, $userId, $i18n)\r\n{\r\n    $currencyName = \"Currency\";\r\n    $currencySymbol = \"$\";\r\n    $currencyCode = \"CODE\";\r\n    $currencyRate = 1;\r\n    $sqlInsert = \"INSERT INTO currencies (name, symbol, code, rate, user_id) VALUES (:name, :symbol, :code, :rate, :userId)\";\r\n    $stmtInsert = $db->prepare($sqlInsert);\r\n    $stmtInsert->bindParam(':name', $currencyName, SQLITE3_TEXT);\r\n    $stmtInsert->bindParam(':symbol', $currencySymbol, SQLITE3_TEXT);\r\n    $stmtInsert->bindParam(':code', $currencyCode, SQLITE3_TEXT);\r\n    $stmtInsert->bindParam(':rate', $currencyRate, SQLITE3_TEXT);\r\n    $stmtInsert->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $resultInsert = $stmtInsert->execute();\r\n\r\n    if ($resultInsert) {\r\n        $currencyId = $db->lastInsertRowID();\r\n        echo json_encode([\"success\" => true, \"currencyId\" => $currencyId]);\r\n    } else {\r\n        echo translate('error_adding_currency', $i18n);\r\n    }\r\n}\r\n\r\nfunction handleEditCurrency($db, $userId, $i18n)\r\n{\r\n    if (isset($_POST['currencyId']) && $_POST['currencyId'] != \"\" && isset($_POST['name']) && $_POST['name'] != \"\" && isset($_POST['symbol']) && $_POST['symbol'] != \"\") {\r\n        $currencyId = $_POST['currencyId'];\r\n        $name = validate($_POST['name']);\r\n        $symbol = validate($_POST['symbol']);\r\n        $code = validate($_POST['code']);\r\n        $sql = \"UPDATE currencies SET name = :name, symbol = :symbol, code = :code WHERE id = :currencyId AND user_id = :userId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindParam(':name', $name, SQLITE3_TEXT);\r\n        $stmt->bindParam(':symbol', $symbol, SQLITE3_TEXT);\r\n        $stmt->bindParam(':code', $code, SQLITE3_TEXT);\r\n        $stmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        if ($result) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => $name . \" \" . translate('currency_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('failed_to_store_currency', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('fields_missing', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}\r\n\r\nfunction handleDeleteCurrency($db, $userId, $i18n)\r\n{\r\n    if (isset($_POST['currencyId']) && $_POST['currencyId'] != \"\") {\r\n        $query = \"SELECT main_currency FROM user WHERE id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n        $mainCurrencyId = $row['main_currency'];\r\n\r\n        $currencyId = $_POST['currencyId'];\r\n        $checkQuery = \"SELECT COUNT(*) FROM subscriptions WHERE currency_id = :currencyId AND user_id = :userId\";\r\n        $checkStmt = $db->prepare($checkQuery);\r\n        $checkStmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);\r\n        $checkStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $checkResult = $checkStmt->execute();\r\n        $row = $checkResult->fetchArray();\r\n        $count = $row[0];\r\n\r\n        if ($count > 0) {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('currency_in_use', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n            exit;\r\n        } else {\r\n            if ($currencyId == $mainCurrencyId) {\r\n                $response = [\r\n                    \"success\" => false,\r\n                    \"message\" => translate('currency_is_main', $i18n)\r\n                ];\r\n                echo json_encode($response);\r\n                exit;\r\n            } else {\r\n                $sql = \"DELETE FROM currencies WHERE id = :currencyId AND user_id = :userId\";\r\n                $stmt = $db->prepare($sql);\r\n                $stmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);\r\n                $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                $result = $stmt->execute();\r\n                if ($result) {\r\n                    echo json_encode([\"success\" => true, \"message\" => translate('currency_removed', $i18n)]);\r\n                } else {\r\n                    $response = [\r\n                        \"success\" => false,\r\n                        \"message\" => translate('failed_to_remove_currency', $i18n)\r\n                    ];\r\n                    echo json_encode($response);\r\n                }\r\n            }\r\n        }\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('fields_missing', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}"
  },
  {
    "path": "endpoints/currency/fixer_api_key.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$newApiKey = isset($_POST[\"api_key\"]) ? trim($_POST[\"api_key\"]) : \"\";\r\n$provider = isset($_POST[\"provider\"]) ? $_POST[\"provider\"] : 0;\r\n\r\n$removeOldKey = \"DELETE FROM fixer WHERE user_id = :userId\";\r\n$stmt = $db->prepare($removeOldKey);\r\n$stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n\r\nif ($provider == 1) {\r\n    $testKeyUrl = \"https://api.apilayer.com/fixer/latest?base=USD&symbols=EUR\";\r\n    $context = stream_context_create([\r\n        'http' => [\r\n            'method' => 'GET',\r\n            'header' => 'apikey: ' . $newApiKey,\r\n        ]\r\n    ]);\r\n    $response = file_get_contents($testKeyUrl, false, $context);\r\n} else {\r\n    $testKeyUrl = \"http://data.fixer.io/api/latest?access_key=$newApiKey\";\r\n    $response = file_get_contents($testKeyUrl);\r\n}\r\n\r\n$apiData = json_decode($response, true);\r\nif ($apiData['success'] && $apiData['success'] == 1) {\r\n    if (!empty($newApiKey)) {\r\n        $insertNewKey = \"INSERT INTO fixer (api_key, provider, user_id) VALUES (:api_key, :provider, :userId)\";\r\n        $stmt = $db->prepare($insertNewKey);\r\n        $stmt->bindParam(\":api_key\", $newApiKey, SQLITE3_TEXT);\r\n        $stmt->bindParam(\":provider\", $provider, SQLITE3_INTEGER);\r\n        $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        if ($result) {\r\n            echo json_encode([\"success\" => true, \"message\" => translate('api_key_saved', $i18n)]);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('failed_to_store_api_key', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    } else {\r\n        echo json_encode([\"success\" => true, \"message\" => translate('apy_key_saved', $i18n)]);\r\n    }\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('invalid_api_key', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n}"
  },
  {
    "path": "endpoints/currency/update_exchange.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$shouldUpdate = true;\r\n\r\nif (isset($_POST['force']) && $_POST['force'] === \"true\") {\r\n    $shouldUpdate = true;\r\n} else {\r\n    $query = \"SELECT date FROM last_exchange_update WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $lastUpdateDate = new DateTime($result);\r\n        $currentDate = new DateTime();\r\n        $lastUpdateDateString = $lastUpdateDate->format('Y-m-d');\r\n        $currentDateString = $currentDate->format('Y-m-d');\r\n        $shouldUpdate = $lastUpdateDateString < $currentDateString;\r\n    }\r\n\r\n    if (!$shouldUpdate) {\r\n        echo \"Rates are current, no need to update.\";\r\n        exit;\r\n    }\r\n}\r\n\r\n$query = \"SELECT api_key, provider FROM fixer\";\r\n$result = $db->query($query);\r\n\r\nif ($result) {\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($row) {\r\n        $apiKey = $row['api_key'];\r\n        $provider = $row['provider'];\r\n\r\n        $codes = \"\";\r\n        $query = \"SELECT id, name, symbol, code FROM currencies WHERE user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $codes .= $row['code'] . \",\";\r\n        }\r\n        $codes = rtrim($codes, ',');\r\n        $query = \"SELECT u.main_currency, c.code FROM user u LEFT JOIN currencies c ON u.main_currency = c.id WHERE u.id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n        $mainCurrencyCode = $row['code'];\r\n        $mainCurrencyId = $row['main_currency'];\r\n\r\n        if ($provider === 1) {\r\n            $api_url = \"https://api.apilayer.com/fixer/latest?base=EUR&symbols=\" . $codes;\r\n            $context = stream_context_create([\r\n                'http' => [\r\n                    'method' => 'GET',\r\n                    'header' => 'apikey: ' . $apiKey,\r\n                ]\r\n            ]);\r\n            $response = file_get_contents($api_url, false, $context);\r\n        } else {\r\n            $api_url = \"http://data.fixer.io/api/latest?access_key=\" . $apiKey . \"&base=EUR&symbols=\" . $codes;\r\n            $response = file_get_contents($api_url);\r\n        }\r\n\r\n        $apiData = json_decode($response, true);\r\n\r\n        $mainCurrencyToEUR = $apiData['rates'][$mainCurrencyCode];\r\n\r\n        if ($apiData !== null && isset($apiData['rates'])) {\r\n            foreach ($apiData['rates'] as $currencyCode => $rate) {\r\n                if ($currencyCode === $mainCurrencyCode) {\r\n                    $exchangeRate = 1.0;\r\n                } else {\r\n                    $exchangeRate = $rate / $mainCurrencyToEUR;\r\n                }\r\n                $updateQuery = \"UPDATE currencies SET rate = :rate WHERE code = :code AND user_id = :userId\";\r\n                $updateStmt = $db->prepare($updateQuery);\r\n                $updateStmt->bindParam(':rate', $exchangeRate, SQLITE3_TEXT);\r\n                $updateStmt->bindParam(':code', $currencyCode, SQLITE3_TEXT);\r\n                $updateStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                $updateResult = $updateStmt->execute();\r\n\r\n                if (!$updateResult) {\r\n                    echo \"Error updating rate for currency: $currencyCode\";\r\n                }\r\n            }\r\n            $currentDate = new DateTime();\r\n            $formattedDate = $currentDate->format('Y-m-d');\r\n\r\n            $updateQuery = \"UPDATE last_exchange_update SET date = :formattedDate WHERE user_id = :userId\";\r\n            $updateStmt = $db->prepare($updateQuery);\r\n            $updateStmt->bindParam(':formattedDate', $formattedDate, SQLITE3_TEXT);\r\n            $updateStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n            $updateResult = $updateStmt->execute();\r\n\r\n            $db->close();\r\n            echo \"Rates updated successfully!\";\r\n        }\r\n    } else {\r\n        echo \"Exchange rates update skipped. No fixer.io api key provided\";\r\n        $apiKey = null;\r\n    }\r\n} else {\r\n    echo \"Exchange rates update skipped. No fixer.io api key provided\";\r\n    $apiKey = null;\r\n}"
  },
  {
    "path": "endpoints/db/backup.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\nfunction addFolderToZip($dir, $zipArchive, $zipdir = '')\r\n{\r\n    if (is_dir($dir)) {\r\n        if ($dh = opendir($dir)) {\r\n            //Add the directory\r\n            if (!empty($zipdir))\r\n                $zipArchive->addEmptyDir($zipdir);\r\n            while (($file = readdir($dh)) !== false) {\r\n                // Skip '.' and '..'\r\n                if ($file == \".\" || $file == \"..\") {\r\n                    continue;\r\n                }\r\n                //If it's a folder, run the function again!\r\n                if (is_dir($dir . $file)) {\r\n                    $newdir = $dir . $file . '/';\r\n                    addFolderToZip($newdir, $zipArchive, $zipdir . $file . '/');\r\n                } else {\r\n                    //Add the files\r\n                    $zipArchive->addFile($dir . $file, $zipdir . $file);\r\n                }\r\n            }\r\n        }\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => \"Directory does not exist: $dir\"\r\n        ]));\r\n    }\r\n}\r\n\r\n$zip = new ZipArchive();\r\n$filename = \"backup_\" . uniqid() . \".zip\";\r\n$zipname = \"../../.tmp/\" . $filename;\r\n\r\nif ($zip->open($zipname, ZipArchive::CREATE) !== TRUE) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('cannot_open_zip', $i18n)\r\n    ]));\r\n}\r\n\r\naddFolderToZip('../../db/', $zip);\r\naddFolderToZip('../../images/uploads/', $zip);\r\n\r\n$numberOfFilesAdded = $zip->numFiles;\r\n\r\nif ($zip->close() === false) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => \"Failed to finalize the zip file\"\r\n    ]));\r\n} else {\r\n    flush();\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => \"Zip file created successfully\",\r\n        \"numFiles\" => $numberOfFilesAdded,\r\n        \"file\" => $filename\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/db/import.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\n$result = $db->query(\"SELECT COUNT(*) as count FROM user\");\r\n$row = $result->fetchArray(SQLITE3_NUM);\r\nif ($row[0] > 0) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => \"Denied\"\r\n    ]));\r\n}\r\n\r\nfunction emptyRestoreFolder() {\r\n    $files = new RecursiveIteratorIterator(\r\n        new RecursiveDirectoryIterator('../../.tmp', RecursiveDirectoryIterator::SKIP_DOTS),\r\n        RecursiveIteratorIterator::CHILD_FIRST\r\n    );\r\n\r\n    foreach ($files as $fileinfo) {\r\n        $removeFunction = ($fileinfo->isDir() ? 'rmdir' : 'unlink');\r\n        $removeFunction($fileinfo->getRealPath());\r\n    }\r\n}\r\n\r\nif ($_SERVER['REQUEST_METHOD'] === 'POST') {\r\n    if (isset($_FILES['file'])) {\r\n        $file = $_FILES['file'];\r\n        $fileTmpName = $file['tmp_name'];\r\n        $fileError = $file['error'];\r\n\r\n        if ($fileError === 0) {\r\n            $fileDestination = '../../.tmp/restore.zip';\r\n            move_uploaded_file($fileTmpName, $fileDestination);\r\n\r\n            $zip = new ZipArchive();\r\n            if ($zip->open($fileDestination) === true) {\r\n                $zip->extractTo('../../.tmp/restore/');\r\n                $zip->close();\r\n            } else {\r\n                die(json_encode([\r\n                    \"success\" => false,\r\n                    \"message\" => \"Failed to extract the uploaded file\"\r\n                ]));\r\n            }\r\n\r\n            if (file_exists('../../.tmp/restore/wallos.db')) {\r\n                if (file_exists('../../db/wallos.db')) {\r\n                    unlink('../../db/wallos.db');\r\n                }\r\n                rename('../../.tmp/restore/wallos.db', '../../db/wallos.db');\r\n\r\n                if (file_exists('../../.tmp/restore/logos/')) {\r\n                    $dir = '../../images/uploads/logos/';\r\n                    $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);\r\n                    $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);\r\n\r\n                    foreach ($ri as $file) {\r\n                        if ($file->isDir()) {\r\n                            rmdir($file->getPathname());\r\n                        } else {\r\n                            unlink($file->getPathname());\r\n                        }\r\n                    }\r\n\r\n                    $dir = new RecursiveDirectoryIterator('../../.tmp/restore/logos/');\r\n                    $ite = new RecursiveIteratorIterator($dir);\r\n                    $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];\r\n\r\n                    foreach ($ite as $filePath) {\r\n                        if (in_array(pathinfo($filePath, PATHINFO_EXTENSION), $allowedExtensions)) {\r\n                            $destination = str_replace('../../.tmp/restore/', '../../images/uploads/', $filePath);\r\n                            $destinationDir = pathinfo($destination, PATHINFO_DIRNAME);\r\n\r\n                            if (!is_dir($destinationDir)) {\r\n                                mkdir($destinationDir, 0755, true);\r\n                            }\r\n\r\n                            copy($filePath, $destination);\r\n                        }\r\n                    }\r\n                }\r\n\r\n                emptyRestoreFolder();\r\n\r\n                echo json_encode([\r\n                    \"success\" => true,\r\n                    \"message\" => translate(\"success\", $i18n)\r\n                ]);\r\n            } else {\r\n                emptyRestoreFolder();\r\n\r\n                die(json_encode([\r\n                    \"success\" => false,\r\n                    \"message\" => \"wallos.db does not exist in the backup file\"\r\n                ]));\r\n            }\r\n\r\n\r\n        } else {\r\n            echo json_encode([\r\n                \"success\" => false,\r\n                \"message\" => \"Failed to upload file\"\r\n            ]);\r\n        }\r\n    } else {\r\n        echo json_encode([\r\n            \"success\" => false,\r\n            \"message\" => \"No file uploaded\"\r\n        ]);\r\n    }\r\n} else {\r\n    echo json_encode([\r\n        \"success\" => false,\r\n        \"message\" => \"Invalid request method\"\r\n    ]);\r\n}\r\n?>"
  },
  {
    "path": "endpoints/db/migrate.php",
    "content": "<?php\nfunction errorHandler($severity, $message, $file, $line)\n{\n    throw new ErrorException($message, 0, $severity, $file, $line);\n}\n\n// Set the custom error handler\nset_error_handler('errorHandler');\n/** @var \\SQLite3 $db */\ntry {\n    require_once 'includes/connect_endpoint_crontabs.php';\n} catch (Exception $e) {\n    require_once '../../includes/connect_endpoint.php';\n} finally {\n    // Restore the default error handler\n    restore_error_handler();\n}\n\n\n$completedMigrations = [];\n\n$migrationTableExists = $db\n    ->query('SELECT name FROM sqlite_master WHERE type=\"table\" AND name=\"migrations\"')\n    ->fetchArray(SQLITE3_ASSOC) !== false;\n\nif ($migrationTableExists) {\n    $migrationQuery = $db->query('SELECT migration FROM migrations');\n    while ($row = $migrationQuery->fetchArray(SQLITE3_ASSOC)) {\n        $completedMigrations[] = $row['migration'];\n    }\n}\n\n$allMigrations = glob('migrations/*.php');\nif (count($allMigrations) == 0) {\n    $allMigrations = glob('../../migrations/*.php');\n}\n\n$allMigrations = array_map(function ($migration) {\n    return str_replace('../../', '', $migration);\n}, $allMigrations);\n\n$completedMigrations = array_map(function ($migration) {\n    return str_replace('../../', '', $migration);\n}, $completedMigrations);\n\n$requiredMigrations = array_diff($allMigrations, $completedMigrations);\n\nif (count($requiredMigrations) === 0) {\n    echo \"No migrations to run.\\n\";\n}\n\nforeach ($requiredMigrations as $migration) {\n    if (!file_exists($migration)) {\n        $migration = '../../' . $migration;\n    }\n    require_once $migration;\n\n    $stmtInsert = $db->prepare('INSERT INTO migrations (migration) VALUES (:migration)');\n    $stmtInsert->bindParam(':migration', $migration, SQLITE3_TEXT);\n    $stmtInsert->execute();\n\n    echo sprintf(\"Migration %s completed successfully.\\n\", $migration);\n}\n"
  },
  {
    "path": "endpoints/db/restore.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint_admin.php';\r\n\r\nfunction emptyRestoreFolder()\r\n{\r\n    $files = new RecursiveIteratorIterator(\r\n        new RecursiveDirectoryIterator('../../.tmp', RecursiveDirectoryIterator::SKIP_DOTS),\r\n        RecursiveIteratorIterator::CHILD_FIRST\r\n    );\r\n\r\n    foreach ($files as $fileinfo) {\r\n        $removeFunction = ($fileinfo->isDir() ? 'rmdir' : 'unlink');\r\n        $removeFunction($fileinfo->getRealPath());\r\n    }\r\n}\r\n\r\nif (isset($_FILES['file'])) {\r\n    $file = $_FILES['file'];\r\n    $fileTmpName = $file['tmp_name'];\r\n    $fileError = $file['error'];\r\n\r\n    if ($fileError === 0) {\r\n        $fileDestination = '../../.tmp/restore.zip';\r\n        move_uploaded_file($fileTmpName, $fileDestination);\r\n\r\n        $zip = new ZipArchive();\r\n        if ($zip->open($fileDestination) === true) {\r\n            $zip->extractTo('../../.tmp/restore/');\r\n            $zip->close();\r\n        } else {\r\n            die(json_encode([\r\n                \"success\" => false,\r\n                \"message\" => \"Failed to extract the uploaded file\"\r\n            ]));\r\n        }\r\n\r\n        if (file_exists('../../.tmp/restore/wallos.db')) {\r\n            if (file_exists('../../db/wallos.db')) {\r\n                unlink('../../db/wallos.db');\r\n            }\r\n            rename('../../.tmp/restore/wallos.db', '../../db/wallos.db');\r\n\r\n            if (file_exists('../../.tmp/restore/logos/')) {\r\n                $dir = '../../images/uploads/logos/';\r\n                $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);\r\n                $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);\r\n\r\n                foreach ($ri as $file) {\r\n                    if ($file->isDir()) {\r\n                        rmdir($file->getPathname());\r\n                    } else {\r\n                        unlink($file->getPathname());\r\n                    }\r\n                }\r\n\r\n                $dir = new RecursiveDirectoryIterator('../../.tmp/restore/logos/');\r\n                $ite = new RecursiveIteratorIterator($dir);\r\n                $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];\r\n\r\n                foreach ($ite as $filePath) {\r\n                    if (in_array(pathinfo($filePath, PATHINFO_EXTENSION), $allowedExtensions)) {\r\n                        $destination = str_replace('../../.tmp/restore/', '../../images/uploads/', $filePath);\r\n                        $destinationDir = pathinfo($destination, PATHINFO_DIRNAME);\r\n\r\n                        if (!is_dir($destinationDir)) {\r\n                            mkdir($destinationDir, 0755, true);\r\n                        }\r\n\r\n                        copy($filePath, $destination);\r\n                    }\r\n                }\r\n            }\r\n\r\n            emptyRestoreFolder();\r\n\r\n            echo json_encode([\r\n                \"success\" => true,\r\n                \"message\" => translate(\"success\", $i18n)\r\n            ]);\r\n        } else {\r\n            emptyRestoreFolder();\r\n\r\n            die(json_encode([\r\n                \"success\" => false,\r\n                \"message\" => \"wallos.db does not exist in the backup file\"\r\n            ]));\r\n        }\r\n\r\n\r\n    } else {\r\n        echo json_encode([\r\n            \"success\" => false,\r\n            \"message\" => \"Failed to upload file\"\r\n        ]);\r\n    }\r\n} else {\r\n    echo json_encode([\r\n        \"success\" => false,\r\n        \"message\" => \"No file uploaded\"\r\n    ]);\r\n}"
  },
  {
    "path": "endpoints/household/household.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$action = $_POST['action'] ?? '';\r\n\r\nswitch ($action) {\r\n    case 'add':\r\n        handleAddMember($db, $userId, $i18n);\r\n        break;\r\n    case 'edit':\r\n        handleEditMember($db, $userId, $i18n);\r\n        break;\r\n    case 'delete':\r\n        handleDeleteMember($db, $userId, $i18n);\r\n        break;\r\n    default:\r\n        echo translate('error', $i18n);\r\n        break;\r\n}\r\n\r\nfunction handleAddMember($db, $userId, $i18n)\r\n{\r\n    $householdName = \"Member\";\r\n    $sqlInsert = \"INSERT INTO household (name, user_id) VALUES (:name, :userId)\";\r\n    $stmtInsert = $db->prepare($sqlInsert);\r\n    $stmtInsert->bindParam(':name', $householdName, SQLITE3_TEXT);\r\n    $stmtInsert->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $resultInsert = $stmtInsert->execute();\r\n\r\n    if ($resultInsert) {\r\n        $householdId = $db->lastInsertRowID();\r\n        $response = [\r\n            \"success\" => true,\r\n            \"householdId\" => $householdId,\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('failed_add_household', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}\r\n\r\nfunction handleEditMember($db, $userId, $i18n)\r\n{\r\n    if (isset($_POST['memberId']) && $_POST['memberId'] != \"\" && isset($_POST['name']) && $_POST['name'] != \"\") {\r\n        $memberId = $_POST['memberId'];\r\n        $name = validate($_POST['name']);\r\n        $email = $_POST['email'] ? $_POST['email'] : \"\";\r\n        $email = validate($email);\r\n        $sql = \"UPDATE household SET name = :name, email = :email WHERE id = :memberId AND user_id = :userId\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindParam(':name', $name, SQLITE3_TEXT);\r\n        $stmt->bindParam(':email', $email, SQLITE3_TEXT);\r\n        $stmt->bindParam(':memberId', $memberId, SQLITE3_INTEGER);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        if ($result) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('member_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('failed_edit_household', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('fill_all_fields', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}\r\n\r\nfunction handleDeleteMember($db, $userId, $i18n)\r\n{\r\n    if (isset($_POST['memberId']) && $_POST['memberId'] != \"\" && $_POST['memberId'] != 1) {\r\n        $memberId = $_POST['memberId'];\r\n        $checkMember = \"SELECT COUNT(*) FROM subscriptions WHERE payer_user_id = :memberId AND user_id = :userId\";\r\n        $checkStmt = $db->prepare($checkMember);\r\n        $checkStmt->bindParam(':memberId', $memberId, SQLITE3_INTEGER);\r\n        $checkStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $checkResult = $checkStmt->execute();\r\n        $row = $checkResult->fetchArray();\r\n        $count = $row[0];\r\n\r\n        if ($count > 0) {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('household_in_use', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $sql = \"DELETE FROM household WHERE id = :memberId and user_id = :userId\";\r\n            $stmt = $db->prepare($sql);\r\n            $stmt->bindParam(':memberId', $memberId, SQLITE3_INTEGER);\r\n            $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $stmt->execute();\r\n            if ($result) {\r\n                $response = [\r\n                    \"success\" => true,\r\n                    \"message\" => translate('member_removed', $i18n)\r\n                ];\r\n                echo json_encode($response);\r\n            } else {\r\n                $response = [\r\n                    \"success\" => false,\r\n                    \"message\" => translate('failed_remove_household', $i18n)\r\n                ];\r\n                echo json_encode($response);\r\n            }\r\n        }\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('failed_remove_household', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n}\r\n\r\n?>"
  },
  {
    "path": "endpoints/logos/search.php",
    "content": "<?php\r\nif (isset($_GET['search'])) {\r\n    $searchTerm = urlencode($_GET['search'] . \" logo\");\r\n\r\n    function applyProxy($ch) {\r\n        $proxy = getenv('https_proxy')\r\n            ?: getenv('HTTPS_PROXY')\r\n            ?: getenv('http_proxy')\r\n            ?: getenv('HTTP_PROXY')\r\n            ?: null;\r\n\r\n        if ($proxy) {\r\n            curl_setopt($ch, CURLOPT_PROXY, $proxy);\r\n        }\r\n    }\r\n\r\n\r\n    function curlGet($url, $headers = []) {\r\n        $allowedHosts = ['duckduckgo.com', 'search.brave.com'];\r\n        $host = parse_url($url, PHP_URL_HOST);\r\n        if (!in_array($host, $allowedHosts)) return null;\r\n\r\n        $ch = curl_init();\r\n        curl_setopt($ch, CURLOPT_URL, $url);\r\n        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);\r\n        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');\r\n        \r\n        // Explicitly disable proxy by default, then re-apply only from env (not $_SERVER)\r\n        curl_setopt($ch, CURLOPT_PROXY, '');\r\n        curl_setopt($ch, CURLOPT_NOPROXY, '*');\r\n\r\n        if (!empty($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);\r\n        applyProxy($ch);\r\n        $response = curl_exec($ch);\r\n        curl_close($ch);\r\n        return $response ?: null;\r\n    }\r\n\r\n    function getVqdToken($query) {\r\n        $html = curlGet(\"https://duckduckgo.com/?q={$query}&ia=images\");\r\n        if ($html && preg_match('/vqd=\"?([\\d-]+)\"?/', $html, $matches)) {\r\n            return $matches[1];\r\n        }\r\n        return null;\r\n    }\r\n\r\n    function fetchDDGImages($query, $vqd) {\r\n        $params = http_build_query([\r\n            'l'   => 'us-en',\r\n            'o'   => 'json',\r\n            'q'   => urldecode($query),\r\n            'vqd' => $vqd,\r\n            'f'   => ',,transparent,Wide,',\r\n            'p'   => '1',\r\n        ]);\r\n\r\n        $response = curlGet(\"https://duckduckgo.com/i.js?{$params}\", [\r\n            'Accept: application/json',\r\n            'Referer: https://duckduckgo.com/',\r\n        ]);\r\n\r\n        if (!$response) return null;\r\n\r\n        $data = json_decode($response, true);\r\n        if (!isset($data['results']) || empty($data['results'])) return null;\r\n\r\n        return array_column($data['results'], 'image');\r\n    }\r\n\r\n    function fetchBraveImages($query) {\r\n    $url = \"https://search.brave.com/images?q={$query}\";\r\n    $html = curlGet($url, [\r\n        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\r\n        'Accept-Language: en-US,en;q=0.5',\r\n        'Referer: https://search.brave.com/',\r\n    ]);\r\n\r\n    if (!$html) return null;\r\n\r\n    $doc = new DOMDocument();\r\n    @$doc->loadHTML($html);\r\n\r\n    $imageUrls = [];\r\n    $imgTags = $doc->getElementsByTagName('img');\r\n    foreach ($imgTags as $imgTag) {\r\n        $src = $imgTag->getAttribute('src');\r\n        $class = $imgTag->getAttribute('class');\r\n\r\n        if (str_contains($class, 'favicon') || str_contains($class, 'logo')) continue;\r\n        if (!filter_var($src, FILTER_VALIDATE_URL)) continue;\r\n        if (str_contains($src, 'cdn.search.brave.com')) continue;  // filter Brave UI assets\r\n\r\n        $imageUrls[] = $src;\r\n    }\r\n\r\n    return !empty($imageUrls) ? $imageUrls : null;\r\n}\r\n\r\n    // --- Main flow ---\r\n\r\n    // Try DuckDuckGo first\r\n    $vqd = getVqdToken($searchTerm);\r\n    $imageUrls = $vqd ? fetchDDGImages($searchTerm, $vqd) : null;\r\n\r\n    // Fall back to Brave if DDG failed at any step\r\n    if (!$imageUrls) {\r\n        $imageUrls = fetchBraveImages($searchTerm);\r\n    }\r\n\r\n    header('Content-Type: application/json');\r\n\r\n    if ($imageUrls) {\r\n        echo json_encode(['imageUrls' => $imageUrls]);\r\n    } else {\r\n        echo json_encode(['error' => 'Failed to fetch images from both DuckDuckGo and Brave.']);\r\n    }\r\n\r\n} else {\r\n    echo json_encode(['error' => 'Invalid request.']);\r\n}\r\n?>\r\n"
  },
  {
    "path": "endpoints/notifications/savediscordnotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"url\"]) || $data[\"url\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $enabled = $data[\"enabled\"];\r\n    $webhook_url = $data[\"url\"];\r\n    $bot_username = $data[\"bot_username\"];\r\n    $bot_avatar_url = $data[\"bot_avatar\"];\r\n\r\n    validate_webhook_url_for_ssrf($webhook_url, $db, $i18n);\r\n\r\n    $query = \"SELECT COUNT(*) FROM discord_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO discord_notifications (enabled, webhook_url, bot_username, bot_avatar_url, user_id)\r\n                              VALUES (:enabled, :webhook_url, :bot_username, :bot_avatar_url, :userId)\";\r\n        } else {\r\n            $query = \"UPDATE discord_notifications\r\n                              SET enabled = :enabled, webhook_url = :webhook_url, bot_username = :bot_username, bot_avatar_url = :bot_avatar_url \r\n                              WHERE user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':webhook_url', $webhook_url, SQLITE3_TEXT);\r\n        $stmt->bindValue(':bot_username', $bot_username, SQLITE3_TEXT);\r\n        $stmt->bindValue(':bot_avatar_url', $bot_avatar_url, SQLITE3_TEXT);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/saveemailnotifications.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"smtpaddress\"]) || $data[\"smtpaddress\"] == \"\" ||\r\n    !isset($data[\"smtpport\"]) || $data[\"smtpport\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $enabled = $data[\"enabled\"];\r\n    $smtpAddress = $data[\"smtpaddress\"];\r\n    $smtpPort = $data[\"smtpport\"];\r\n    $encryption = \"tls\";\r\n    if (isset($data[\"encryption\"])) {\r\n        $encryption = $data[\"encryption\"];\r\n    }\r\n    $smtpUsername = $data[\"smtpusername\"];\r\n    $smtpPassword = $data[\"smtppassword\"];\r\n    $fromEmail = $data[\"fromemail\"];\r\n    $otherEmails = $data[\"otheremails\"];\r\n\r\n    $query = \"SELECT COUNT(*) FROM email_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO email_notifications (enabled, smtp_address, smtp_port, smtp_username, smtp_password, from_email, other_emails, encryption, user_id)\r\n                              VALUES (:enabled, :smtpAddress, :smtpPort, :smtpUsername, :smtpPassword, :fromEmail, :otherEmails, :encryption, :userId)\";\r\n        } else {\r\n            $query = \"UPDATE email_notifications\r\n                              SET enabled = :enabled, smtp_address = :smtpAddress, smtp_port = :smtpPort,\r\n                                  smtp_username = :smtpUsername, smtp_password = :smtpPassword, from_email = :fromEmail, other_emails = :otherEmails, encryption = :encryption WHERE user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':smtpAddress', $smtpAddress, SQLITE3_TEXT);\r\n        $stmt->bindValue(':smtpPort', $smtpPort, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':smtpUsername', $smtpUsername, SQLITE3_TEXT);\r\n        $stmt->bindValue(':smtpPassword', $smtpPassword, SQLITE3_TEXT);\r\n        $stmt->bindValue(':fromEmail', $fromEmail, SQLITE3_TEXT);\r\n        $stmt->bindValue(':otherEmails', $otherEmails, SQLITE3_TEXT);\r\n        $stmt->bindValue(':encryption', $encryption, SQLITE3_TEXT);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/savegotifynotifications.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"gotify_url\"]) || $data[\"gotify_url\"] == \"\" ||\r\n    !isset($data[\"token\"]) || $data[\"token\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $enabled = $data[\"enabled\"];\r\n    $url = $data[\"gotify_url\"];\r\n    $token = $data[\"token\"];\r\n    $ignore_ssl = $data[\"ignore_ssl\"];\r\n\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    validate_webhook_url_for_ssrf($url, $db, $i18n);\r\n\r\n    $query = \"SELECT COUNT(*) FROM gotify_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO gotify_notifications (enabled, url, token, user_id, ignore_ssl)\r\n                              VALUES (:enabled, :url, :token, :userId, :ignore_ssl)\";\r\n        } else {\r\n            $query = \"UPDATE gotify_notifications\r\n                              SET enabled = :enabled, url = :url, token = :token, ignore_ssl = :ignore_ssl WHERE user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':url', $url, SQLITE3_TEXT);\r\n        $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n        $stmt->bindValue(':ignore_ssl', $ignore_ssl, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/savemattermostnotifications.php",
    "content": "<?php\nrequire_once '../../includes/connect_endpoint.php';\nrequire_once '../../includes/validate_endpoint.php';\nrequire_once '../../includes/ssrf_helper.php';\n\n$postData = file_get_contents(\"php://input\");\n$data = json_decode($postData, true);\n\nif (!isset($data[\"webhook_url\"]) || $data[\"webhook_url\"] == \"\") {\n    $response = [\n        \"success\" => false,\n        \"message\" => translate('fill_mandatory_fields', $i18n)\n    ];\n    echo json_encode($response);\n} else {\n    $enabled = $data[\"enabled\"];\n    $webhook_url = $data[\"webhook_url\"];\n    $bot_username = $data[\"bot_username\"];\n    $bot_iconemoji = $data[\"bot_icon_emoji\"];\n\n    $parsedUrl = parse_url($webhook_url);\n    if (\n        !isset($parsedUrl['scheme']) ||\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\n        !filter_var($webhook_url, FILTER_VALIDATE_URL)\n    ) {\n        die(json_encode([\n            \"success\" => false,\n            \"message\" => translate(\"error\", $i18n)\n        ]));\n    }\n\n    validate_webhook_url_for_ssrf($webhook_url, $db, $i18n);\n\n    $query = \"SELECT COUNT(*) FROM mattermost_notifications WHERE user_id = :userId\";\n    $stmt = $db->prepare($query);\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\n    $result = $stmt->execute();\n\n    if ($result === false) {\n        $response = [\n            \"success\" => false,\n            \"message\" => translate('error_saving_notifications', $i18n)\n        ];\n        echo json_encode($response);\n    } else {\n        $row = $result->fetchArray();\n        $count = $row[0];\n        if ($count == 0) {\n            $query = \"INSERT INTO mattermost_notifications (enabled, webhook_url, user_id, bot_username, bot_icon_emoji)\n                          VALUES (:enabled, :webhook_url, :userId, :bot_username, :bot_icon_emoji)\";\n        } else {\n            $query = \"UPDATE mattermost_notifications\n                          SET enabled = :enabled, webhook_url = :webhook_url WHERE user_id = :userId\";\n        }\n\n        $stmt = $db->prepare($query);\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\n        $stmt->bindValue(':webhook_url', $webhook_url, SQLITE3_TEXT);\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\n        $stmt->bindValue(':bot_username', $bot_username, SQLITE3_TEXT);\n        $stmt->bindValue(':bot_icon_emoji', $bot_iconemoji, SQLITE3_TEXT);\n\n        if ($stmt->execute()) {\n            $response = [\n                \"success\" => true,\n                \"message\" => translate('notifications_settings_saved', $i18n)\n            ];\n            echo json_encode($response);\n        } else {\n            $response = [\n                \"success\" => false,\n                \"message\" => translate('error_saving_notifications', $i18n)\n            ];\n            echo json_encode($response);\n        }\n    }\n}"
  },
  {
    "path": "endpoints/notifications/savenotificationsettings.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (!isset($data[\"days\"]) || $data['days'] == \"\") {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $days = $data[\"days\"];\r\n    $query = \"SELECT COUNT(*) FROM notification_settings WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO notification_settings (days, user_id)\r\n                              VALUES (:days, :userId)\";\r\n        } else {\r\n            $query = \"UPDATE notification_settings SET days = :days WHERE user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':days', $days, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/saventfynotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"topic\"]) || $data[\"topic\"] == \"\" ||\r\n    !isset($data[\"host\"]) || $data[\"host\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $enabled = $data[\"enabled\"];\r\n    $host = $data[\"host\"];\r\n    $topic = $data[\"topic\"];\r\n    $headers = $data[\"headers\"];\r\n    $ignore_ssl = $data[\"ignore_ssl\"];\r\n\r\n    $url = rtrim($host, '/') . '/' . ltrim($topic, '/');\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    validate_webhook_url_for_ssrf($url, $db, $i18n);\r\n\r\n    $query = \"SELECT COUNT(*) FROM ntfy_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO ntfy_notifications (enabled, host, topic, headers, user_id, ignore_ssl)\r\n                              VALUES (:enabled, :host, :topic, :headers, :userId, :ignore_ssl)\";\r\n        } else {\r\n            $query = \"UPDATE ntfy_notifications\r\n                              SET enabled = :enabled, host = :host, topic = :topic, headers = :headers, ignore_ssl = :ignore_ssl WHERE user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':host', $host, SQLITE3_TEXT);\r\n        $stmt->bindValue(':topic', $topic, SQLITE3_TEXT);\r\n        $stmt->bindValue(':headers', $headers, SQLITE3_TEXT);\r\n        $stmt->bindValue(':ignore_ssl', $ignore_ssl, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/savepushovernotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"user_key\"]) || $data[\"user_key\"] == \"\" ||\r\n    !isset($data[\"token\"]) || $data[\"token\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $enabled = $data[\"enabled\"];\r\n    $user_key = $data[\"user_key\"];\r\n    $token = $data[\"token\"];\r\n\r\n    $query = \"SELECT COUNT(*) FROM pushover_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO pushover_notifications (enabled, user_key, token, user_id)\r\n                              VALUES (:enabled, :user_key, :token, :userId)\";\r\n        } else {\r\n            $query = \"UPDATE pushover_notifications\r\n                              SET enabled = :enabled, user_key = :user_key, token = :token, user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':user_key', $user_key, SQLITE3_TEXT);\r\n        $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/savepushplusnotifications.php",
    "content": "<?php\nrequire_once '../../includes/connect_endpoint.php';\nrequire_once '../../includes/validate_endpoint.php';\n\n\n    $postData = file_get_contents(\"php://input\");\n    $data = json_decode($postData, true);\n\n    if (!isset($data[\"token\"]) || $data[\"token\"] == \"\") {\n        $response = [\n            \"success\" => false,\n            \"message\" => translate('fill_mandatory_fields', $i18n)\n        ];\n        echo json_encode($response);\n    } else {\n        $enabled = $data[\"enabled\"];\n        $token = $data[\"token\"];\n\n        $query = \"SELECT COUNT(*) FROM pushplus_notifications WHERE user_id = :userId\";\n        $stmt = $db->prepare($query);\n        $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\n        $result = $stmt->execute();\n\n        if ($result === false) {\n            $response = [\n                \"success\" => false,\n                \"message\" => translate('error_saving_notifications', $i18n)\n            ];\n            echo json_encode($response);\n        } else {\n            $row = $result->fetchArray();\n            $count = $row[0];\n            if ($count == 0) {\n                $query = \"INSERT INTO pushplus_notifications (enabled, token, user_id)\n                          VALUES (:enabled, :token, :userId)\";\n            } else {\n                $query = \"UPDATE pushplus_notifications\n                          SET enabled = :enabled, token = :token WHERE user_id = :userId\";\n            }\n\n            $stmt = $db->prepare($query);\n            $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\n            $stmt->bindValue(':token', $token, SQLITE3_TEXT);\n            $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\n\n            if ($stmt->execute()) {\n                $response = [\n                    \"success\" => true,\n                    \"message\" => translate('notifications_settings_saved', $i18n)\n                ];\n                echo json_encode($response);\n            } else {\n                $response = [\n                    \"success\" => false,\n                    \"message\" => translate('error_saving_notifications', $i18n)\n                ];\n                echo json_encode($response);\n            }\n        }\n    }"
  },
  {
    "path": "endpoints/notifications/saveserverchannotifications.php",
    "content": "<?php\nrequire_once '../../includes/connect_endpoint.php';\nrequire_once '../../includes/validate_endpoint.php';\n\n$postData = file_get_contents(\"php://input\");\n$data = json_decode($postData, true);\n\nif (!isset($data[\"sendkey\"]) || $data[\"sendkey\"] == \"\") {\n    $response = [\n        \"success\" => false,\n        \"message\" => translate('fill_mandatory_fields', $i18n)\n    ];\n    echo json_encode($response);\n} else {\n    $enabled = $data[\"enabled\"];\n    $sendkey = $data[\"sendkey\"];\n\n    $query = \"SELECT COUNT(*) FROM serverchan_notifications WHERE user_id = :userId\";\n    $stmt = $db->prepare($query);\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\n    $result = $stmt->execute();\n\n    if ($result === false) {\n        $response = [\n            \"success\" => false,\n            \"message\" => translate('error_saving_notifications', $i18n)\n        ];\n        echo json_encode($response);\n    } else {\n        $row = $result->fetchArray();\n        $count = $row[0];\n        if ($count == 0) {\n            $query = \"INSERT INTO serverchan_notifications (enabled, sendkey, user_id)\n                      VALUES (:enabled, :sendkey, :userId)\";\n        } else {\n            $query = \"UPDATE serverchan_notifications\n                      SET enabled = :enabled, sendkey = :sendkey WHERE user_id = :userId\";\n        }\n\n        $stmt = $db->prepare($query);\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\n        $stmt->bindValue(':sendkey', $sendkey, SQLITE3_TEXT);\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\n\n        if ($stmt->execute()) {\n            $response = [\n                \"success\" => true,\n                \"message\" => translate('notifications_settings_saved', $i18n)\n            ];\n            echo json_encode($response);\n        } else {\n            $response = [\n                \"success\" => false,\n                \"message\" => translate('error_saving_notifications', $i18n)\n            ];\n            echo json_encode($response);\n        }\n    }\n}"
  },
  {
    "path": "endpoints/notifications/savetelegramnotifications.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"bot_token\"]) || $data[\"bot_token\"] == \"\" ||\r\n    !isset($data[\"chat_id\"]) || $data[\"chat_id\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $enabled = $data[\"enabled\"];\r\n    $bot_token = $data[\"bot_token\"];\r\n    $chat_id = $data[\"chat_id\"];\r\n\r\n    $query = \"SELECT COUNT(*) FROM telegram_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO telegram_notifications (enabled, bot_token, chat_id, user_id)\r\n                              VALUES (:enabled, :bot_token, :chat_id, :userId)\";\r\n        } else {\r\n            $query = \"UPDATE telegram_notifications\r\n                              SET enabled = :enabled, bot_token = :bot_token, chat_id = :chat_id WHERE user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':bot_token', $bot_token, SQLITE3_TEXT);\r\n        $stmt->bindValue(':chat_id', $chat_id, SQLITE3_TEXT);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/savewebhooknotifications.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"webhook_url\"]) || $data[\"webhook_url\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $enabled = $data[\"enabled\"];\r\n    $url = $data[\"webhook_url\"];\r\n    $headers = $data[\"headers\"];\r\n    $payload = $data[\"payload\"];\r\n    $cancelation_payload = $data[\"cancelation_payload\"];\r\n    $ignore_ssl = $data[\"ignore_ssl\"];\r\n\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    validate_webhook_url_for_ssrf($url, $db, $i18n);\r\n\r\n    $query = \"SELECT COUNT(*) FROM webhook_notifications WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(\":userId\", $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_saving_notifications', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $row = $result->fetchArray();\r\n        $count = $row[0];\r\n        if ($count == 0) {\r\n            $query = \"INSERT INTO webhook_notifications (enabled, url, headers, payload, cancelation_payload, user_id, ignore_ssl)\r\n                              VALUES (:enabled, :url, :headers, :payload, :cancelation_payload, :userId, :ignore_ssl)\";\r\n        } else {\r\n            $query = \"UPDATE webhook_notifications\r\n                              SET enabled = :enabled, url = :url, headers = :headers, payload = :payload, cancelation_payload = :cancelation_payload, ignore_ssl = :ignore_ssl WHERE user_id = :userId\";\r\n        }\r\n\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':url', $url, SQLITE3_TEXT);\r\n        $stmt->bindValue(':headers', $headers, SQLITE3_TEXT);\r\n        $stmt->bindValue(':payload', $payload, SQLITE3_TEXT);\r\n        $stmt->bindValue(':cancelation_payload', $cancelation_payload, SQLITE3_TEXT);\r\n        $stmt->bindValue(':ignore_ssl', $ignore_ssl, SQLITE3_INTEGER);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n        if ($stmt->execute()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notifications_settings_saved', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('error_saving_notifications', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/testdiscordnotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"url\"]) || $data[\"url\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    // Set the message parameters\r\n    $title = translate('wallos_notification', $i18n);\r\n    $message = translate('test_notification', $i18n);\r\n\r\n    $webhook_url = $data[\"url\"];\r\n    $bot_username = $data[\"bot_username\"];\r\n    $bot_avatar_url = $data[\"bot_avatar\"];\r\n\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($webhook_url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($webhook_url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    $ssrf = validate_webhook_url_for_ssrf($webhook_url, $db, $i18n);\r\n\r\n    $postfields = [\r\n        'content' => $message,\r\n        'embeds' => [\r\n            [\r\n                'title' => $title,\r\n                'description' => $message,\r\n                'color' => hexdec(\"FF0000\")\r\n            ]\r\n        ]\r\n    ];\r\n\r\n    if (!empty($bot_username)) {\r\n        $postfields['username'] = $bot_username;\r\n    }\r\n\r\n    if (!empty($bot_avatar_url)) {\r\n        $postfields['avatar_url'] = $bot_avatar_url;\r\n    }\r\n\r\n    $ch = curl_init();\r\n\r\n    // Set the URL and other options\r\n    curl_setopt($ch, CURLOPT_URL, $webhook_url);\r\n\r\n    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); \r\n    curl_setopt($ch, CURLOPT_RESOLVE, [\"{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}\"]);\r\n    curl_setopt($ch, CURLOPT_POST, 1);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));\r\n    curl_setopt($ch, CURLOPT_HTTPHEADER, [\r\n        'Content-Type: application/json'\r\n    ]);\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n    // Execute the request\r\n    $response = curl_exec($ch);\r\n\r\n    // Close the cURL session\r\n    curl_close($ch);\r\n\r\n    // Check if the message was sent successfully\r\n    if ($response === false) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('notification_failed', $i18n)\r\n        ]));\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => true,\r\n            \"message\" => translate('notification_sent_successfuly', $i18n)\r\n        ]));\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/testemailnotifications.php",
    "content": "<?php\r\n\r\nuse PHPMailer\\PHPMailer\\PHPMailer;\r\nuse PHPMailer\\PHPMailer\\SMTP;\r\nuse PHPMailer\\PHPMailer\\Exception;\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"smtpaddress\"]) || $data[\"smtpaddress\"] == \"\" ||\r\n    !isset($data[\"smtpport\"]) || $data[\"smtpport\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_all_fields', $i18n)\r\n    ];\r\n    die(json_encode($response));\r\n} else {\r\n    $encryption = \"none\";\r\n    if (isset($data[\"encryption\"])) {\r\n        $encryption = $data[\"encryption\"];\r\n    }\r\n\r\n    $smtpAuth = (isset($data[\"smtpusername\"]) && $data[\"smtpusername\"] != \"\") || (isset($data[\"smtppassword\"]) && $data[\"smtppassword\"] != \"\");\r\n\r\n    require '../../libs/PHPMailer/PHPMailer.php';\r\n    require '../../libs/PHPMailer/SMTP.php';\r\n    require '../../libs/PHPMailer/Exception.php';\r\n\r\n    $smtpAddress = $data[\"smtpaddress\"];\r\n    $smtpPort = $data[\"smtpport\"];\r\n    $smtpUsername = $data[\"smtpusername\"];\r\n    $smtpPassword = $data[\"smtppassword\"];\r\n    $fromEmail = $data[\"fromemail\"] ? $data['fromemail'] : \"wallos@wallosapp.com\";\r\n\r\n    $mail = new PHPMailer(true);\r\n    $mail->CharSet = \"UTF-8\";\r\n    $mail->isSMTP();\r\n\r\n    $mail->Host = $smtpAddress;\r\n    $mail->SMTPAuth = $smtpAuth;\r\n    if ($smtpAuth) {\r\n        $mail->Username = $smtpUsername;\r\n        $mail->Password = $smtpPassword;\r\n    }\r\n\r\n    if ($encryption != \"none\") {\r\n        $mail->SMTPSecure = $encryption;\r\n    } else {\r\n        $mail->SMTPSecure = false;\r\n        $mail->SMTPAutoTLS = false;\r\n    }\r\n\r\n    $mail->Port = $smtpPort;\r\n\r\n    $getUser = \"SELECT * FROM user WHERE id = $userId\";\r\n    $user = $db->querySingle($getUser, true);\r\n    $email = $user['email'];\r\n    $name = $user['username'];\r\n\r\n    $mail->setFrom($fromEmail, 'Wallos App');\r\n    $mail->addAddress($email, $name);\r\n\r\n    $mail->Subject = translate('wallos_notification', $i18n);\r\n    $mail->Body = translate('test_notification', $i18n);\r\n\r\n    try {\r\n        if ($mail->send()) {\r\n            $response = [\r\n                \"success\" => true,\r\n                \"message\" => translate('notification_sent_successfuly', $i18n)\r\n            ];\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('email_error', $i18n) . $mail->ErrorInfo\r\n            ];\r\n        }\r\n    } catch (Exception $e) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('email_error', $i18n) . $e->getMessage()\r\n        ];\r\n    }\r\n\r\n    die(json_encode($response));\r\n\r\n}"
  },
  {
    "path": "endpoints/notifications/testgotifynotifications.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"gotify_url\"]) || $data[\"gotify_url\"] == \"\" ||\r\n    !isset($data[\"token\"]) || $data[\"token\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    die(json_encode($response));\r\n} else {\r\n    // Set the message parameters\r\n    $title = translate('wallos_notification', $i18n);\r\n    $message = translate('test_notification', $i18n);\r\n    $priority = 5;\r\n\r\n    $url = $data[\"gotify_url\"];\r\n    $token = $data[\"token\"];\r\n    $ignore_ssl = $data[\"ignore_ssl\"];\r\n\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    $ssrf = validate_webhook_url_for_ssrf($url, $db, $i18n);\r\n\r\n    $ch = curl_init();\r\n\r\n    // Set the URL and other options\r\n    curl_setopt($ch, CURLOPT_URL, $url . \"/message?token=\" . $token);\r\n    \r\n    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); \r\n    curl_setopt($ch, CURLOPT_RESOLVE, [\"{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}\"]);\r\n    curl_setopt($ch, CURLOPT_POST, 1);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([\r\n        'title' => $title,\r\n        'message' => $message,\r\n        'priority' => $priority,\r\n    ]));\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n    if ($ignore_ssl) {\r\n        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n    }\r\n\r\n    // Execute the request\r\n    $response = curl_exec($ch);\r\n    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n\r\n    // Close the cURL session\r\n    curl_close($ch);\r\n\r\n    // Check if the message was sent successfully\r\n    if ($response === false || $httpCode < 200 || $httpCode >= 300) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('notification_failed', $i18n),\r\n            \"response\" => $response,\r\n            \"http_code\" => $httpCode\r\n        ]));\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => true,\r\n            \"message\" => translate('notification_sent_successfuly', $i18n),\r\n            \"response\" => $response\r\n        ]));\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/testmattermostnotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"webhook_url\"]) || $data[\"webhook_url\"] == \"\" ||\r\n    !isset($data[\"bot_username\"]) || $data[\"bot_username\"] == \"\" ||\r\n    !isset($data[\"bot_icon_emoji\"]) || $data[\"bot_icon_emoji\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    // Set the message parameters\r\n    $title = translate('wallos_notification', $i18n);\r\n    $message = translate('test_notification', $i18n);\r\n\r\n    $webhook_url = $data[\"webhook_url\"];\r\n    $bot_username = $data[\"bot_username\"];\r\n    $bot_icon_emoji = $data[\"bot_icon_emoji\"];\r\n\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($webhook_url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($webhook_url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    $ssrf = validate_webhook_url_for_ssrf($webhook_url, $db, $i18n);\r\n\r\n    $postfields = [\r\n        'text' => $message,\r\n    ];\r\n\r\n    if (!empty($bot_username)) {\r\n        $postfields['username'] = $bot_username;\r\n    }\r\n\r\n    if (!empty($bot_icon_emoji)) {\r\n        $postfields['icon_emoji'] = $bot_icon_emoji;\r\n    }\r\n\r\n    $ch = curl_init();\r\n\r\n    // Set the URL and other options\r\n    curl_setopt($ch, CURLOPT_URL, $webhook_url);\r\n    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); \r\n    curl_setopt($ch, CURLOPT_RESOLVE, [\"{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}\"]);\r\n    curl_setopt($ch, CURLOPT_POST, 1);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));\r\n    curl_setopt($ch, CURLOPT_HTTPHEADER, [\r\n        'Content-Type: application/json'\r\n    ]);\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n    // Execute the request\r\n    $response = curl_exec($ch);\r\n\r\n    // Close the cURL session\r\n    curl_close($ch);\r\n\r\n    // Check if the message was sent successfully\r\n    if ($response === false) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('notification_failed', $i18n)\r\n        ]));\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => true,\r\n            \"message\" => translate('notification_sent_successfuly', $i18n)\r\n        ]));\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/testntfynotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"host\"]) || $data[\"host\"] == \"\" ||\r\n    !isset($data[\"topic\"]) || $data[\"topic\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $host = rtrim($data[\"host\"], '/');\r\n    $topic = $data[\"topic\"];\r\n    $headers = json_decode($data[\"headers\"], true);\r\n    if ($headers === null) {\r\n        $headers = [];\r\n    }\r\n    $customheaders = array_map(function ($key, $value) {\r\n        return \"$key: $value\";\r\n    }, array_keys($headers), $headers);\r\n\r\n    $url = rtrim($host, '/') . '/' . ltrim($topic, '/');\r\n    $ignore_ssl = $data[\"ignore_ssl\"];\r\n\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    $ssrf = validate_webhook_url_for_ssrf($url, $db, $i18n);\r\n\r\n    // Set the message parameters\r\n    $message = translate('test_notification', $i18n);\r\n\r\n    $ch = curl_init();\r\n\r\n    // Set the URL and other options\r\n    curl_setopt($ch, CURLOPT_URL, $url);\r\n    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); \r\n    curl_setopt($ch, CURLOPT_RESOLVE, [\"{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}\"]);\r\n    curl_setopt($ch, CURLOPT_POST, 1);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, $message);\r\n    curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n    if ($ignore_ssl) {\r\n        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n    }\r\n\r\n    // Execute the request\r\n    $response = curl_exec($ch);\r\n\r\n    // Close the cURL session\r\n    curl_close($ch);\r\n\r\n    // Check if the message was sent successfully\r\n    if ($response === false) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('notification_failed', $i18n)\r\n        ]));\r\n    }\r\n\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('notification_sent_successfuly', $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/notifications/testpushovernotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"user_key\"]) || $data[\"user_key\"] == \"\" ||\r\n    !isset($data[\"token\"]) || $data[\"token\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    // Set the message parameters\r\n    $message = translate('test_notification', $i18n);\r\n\r\n    $user_key = $data[\"user_key\"];\r\n    $token = $data[\"token\"];\r\n\r\n    $ch = curl_init();\r\n\r\n    // Set the URL and other options\r\n    curl_setopt($ch, CURLOPT_URL, \"https://api.pushover.net/1/messages.json\");\r\n    curl_setopt($ch, CURLOPT_POST, 1);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([\r\n        'token' => $token,\r\n        'user' => $user_key,\r\n        'message' => $message,\r\n    ]));\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n    // Execute the request\r\n    $response = curl_exec($ch);\r\n\r\n    // Close the cURL session\r\n    curl_close($ch);\r\n\r\n    // Check if the message was sent successfully\r\n    if ($response === false) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('notification_failed', $i18n)\r\n        ]));\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => true,\r\n            \"message\" => translate('notification_sent_successfuly', $i18n)\r\n        ]));\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/testpushplusnotifications.php",
    "content": "<?php\nrequire_once '../../includes/connect_endpoint.php';\nrequire_once '../../includes/validate_endpoint.php';\n\n$postData = file_get_contents(\"php://input\");\n$data = json_decode($postData, true);\n\nif (!isset($data[\"token\"]) || $data[\"token\"] == \"\") {\n    $response = [\n        \"success\" => false,\n        \"message\" => translate('fill_mandatory_fields', $i18n)\n    ];\n    echo json_encode($response);\n} else {\n    // Set the message parameters\n    $title = translate('wallos_notification', $i18n);\n    $message = translate('test_notification', $i18n);\n\n    $token = $data[\"token\"];\n\n    $ch = curl_init();\n\n    // Set the URL and other options for PushPlus\n    $postData = [\n        \"token\" => $token,\n        \"title\" => \"您的订阅到期拉\",\n        \"content\" => $message,\n        \"template\" => \"json\"\n    ];\n\n    curl_setopt_array($ch, [\n        CURLOPT_URL => 'https://www.pushplus.plus/send',\n        CURLOPT_RETURNTRANSFER => true,\n        CURLOPT_POST => true,\n        CURLOPT_POSTFIELDS => json_encode($postData),\n        CURLOPT_HTTPHEADER => [\n            'Content-Type: application/json'\n        ],\n        CURLOPT_TIMEOUT => 10\n    ]);\n\n    // Execute the request\n    $response = curl_exec($ch);\n    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n    $curlError = curl_error($ch);\n\n    // Close the cURL session\n    curl_close($ch);\n\n    // Check if the message was sent successfully\n    if ($response === false) {\n        die(json_encode([\n            \"success\" => false,\n            \"message\" => translate('notification_failed', $i18n) . \": \" . $curlError\n        ]));\n    } else {\n        $responseData = json_decode($response, true);\n        if (isset($responseData['code']) && $responseData['code'] == 200) {\n            die(json_encode([\n                \"success\" => true,\n                \"message\" => translate('notification_sent_successfuly', $i18n)\n            ]));\n        } else {\n            $errorMsg = isset($responseData['msg']) ? $responseData['msg'] : translate('notification_failed', $i18n);\n            die(json_encode([\n                \"success\" => false,\n                \"message\" => $errorMsg\n            ]));\n        }\n    }\n}"
  },
  {
    "path": "endpoints/notifications/testserverchannotifications.php",
    "content": "<?php\nrequire_once '../../includes/connect_endpoint.php';\nrequire_once '../../includes/validate_endpoint.php';\n\n$postData = file_get_contents(\"php://input\");\n$data = json_decode($postData, true);\n\n$enabled = $data[\"enabled\"] ?? 0;\n$sendkey = $data[\"sendkey\"] ?? \"\";\n\nif (!$enabled || $sendkey === \"\") {\n    echo json_encode([\n        \"success\" => false,\n        \"message\" => translate('fill_mandatory_fields', $i18n)\n    ]);\n    exit;\n}\n\nfunction sc_send($text, $desp = '', $key = '') {\n    $postdata = http_build_query(array('text' => $text, 'desp' => $desp));\n\n    if (strpos($key, 'sctp') === 0) {\n        preg_match('/^sctp(\\d+)t/', $key, $matches);\n        $num = $matches[1] ?? '';\n        $url = \"https://{$num}.push.ft07.com/send/{$key}.send\";\n    } else {\n        $url = \"https://sctapi.ftqq.com/{$key}.send\";\n    }\n\n    $opts = array('http' => array(\n        'method'  => 'POST',\n        'header'  => 'Content-type: application/x-www-form-urlencoded',\n        'content' => $postdata\n    ));\n\n    $context = stream_context_create($opts);\n    $result = @file_get_contents($url, false, $context);\n    return $result !== false ? $result : '';\n}\n\n$title = 'Wallos Notification Test';\n$body = 'This is a test notification from Wallos via Serverchan.';\n\n$result = sc_send($title, $body, $sendkey);\n$info = json_decode($result, true);\n$code = (is_array($info) && array_key_exists('code', $info)) ? $info['code'] : null;\nif ($code === 0) {\n    echo json_encode([\n        \"success\" => true,\n        \"message\" => translate('notification_sent_successfuly', $i18n)\n    ]);\n} else {\n    echo json_encode([\n        \"success\" => false,\n        \"message\" => translate('notification_failed', $i18n)\n    ]);\n}"
  },
  {
    "path": "endpoints/notifications/testtelegramnotifications.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"bottoken\"]) || $data[\"bottoken\"] == \"\" ||\r\n    !isset($data[\"chatid\"]) || $data[\"chatid\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    // Set the message parameters\r\n    $title = translate('wallos_notification', $i18n);\r\n    $message = translate('test_notification', $i18n);\r\n\r\n    $botToken = $data[\"bottoken\"];\r\n    $chatId = $data[\"chatid\"];\r\n\r\n    $ch = curl_init();\r\n\r\n    // Set the URL and other options\r\n    curl_setopt($ch, CURLOPT_URL, \"https://api.telegram.org/bot\" . $botToken . \"/sendMessage\");\r\n    curl_setopt($ch, CURLOPT_POST, 1);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([\r\n        'chat_id' => $chatId,\r\n        'text' => $message,\r\n    ]));\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n    // Execute the request\r\n    $response = curl_exec($ch);\r\n\r\n    // Close the cURL session\r\n    curl_close($ch);\r\n\r\n    // Check if the message was sent successfully\r\n    if ($response === false) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('notification_failed', $i18n)\r\n        ]));\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => true,\r\n            \"message\" => translate('notification_sent_successfuly', $i18n)\r\n        ]));\r\n    }\r\n}"
  },
  {
    "path": "endpoints/notifications/testwebhooknotifications.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/ssrf_helper.php';\r\n\r\n// Variables available: {{days_until}}, {{subscription_name}}, {{subscription_price}}, {{subscription_currency}}, {{subscription_category}}, {{subscription_date}}, {{subscription_payer}}, {{subscription_days_until_payment}}, {{subscription_notes}}, {{subscription_url}}\r\n$fakeSubscription = [\r\n    \"days_until\" => 5,\r\n    \"subscription_name\" => \"Test Subscription\",\r\n    \"subscription_price\" => 10.00,\r\n    \"subscription_currency\" => \"USD\",\r\n    \"subscription_category\" => \"Test Category\",\r\n    \"subscription_date\" => date(\"Y-m-d H:i:s\"),\r\n    \"subscription_payer\" => \"Test Payer\",\r\n    \"subscription_days_until_payment\" => 30,\r\n    \"subscription_notes\" => \"Test Notes\",\r\n    \"subscription_url\" => \"https://example.com/test-subscription\"\r\n];\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (\r\n    !isset($data[\"requestmethod\"]) || $data[\"requestmethod\"] == \"\" ||\r\n    !isset($data[\"url\"]) || $data[\"url\"] == \"\" ||\r\n    !isset($data[\"payload\"]) || $data[\"payload\"] == \"\"\r\n) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_mandatory_fields', $i18n)\r\n    ];\r\n    die(json_encode($response));\r\n} else {\r\n    $requestmethod = $data[\"requestmethod\"];\r\n    $url = $data[\"url\"];\r\n    $payload = $data[\"payload\"];\r\n\r\n    // Validate URL scheme\r\n    $parsedUrl = parse_url($url);\r\n    if (\r\n        !isset($parsedUrl['scheme']) ||\r\n        !in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||\r\n        !filter_var($url, FILTER_VALIDATE_URL)\r\n    ) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    $ssrf = validate_webhook_url_for_ssrf($url, $db, $i18n);\r\n\r\n    // Replace placeholders in the payload with fake subscription data\r\n    foreach ($fakeSubscription as $key => $value) {\r\n        $placeholder = \"{{\" . $key . \"}}\";\r\n        $payload = str_replace($placeholder, $value, $payload);\r\n    }\r\n\r\n    $customheaders = json_decode($data[\"customheaders\"], true);\r\n    $ignore_ssl = $data[\"ignore_ssl\"];\r\n\r\n    $ch = curl_init();\r\n\r\n    // Set the URL and other options\r\n    curl_setopt($ch, CURLOPT_URL, $url);\r\n    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);\r\n    curl_setopt($ch, CURLOPT_RESOLVE, [\"{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}\"]);\r\n    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $requestmethod);\r\n    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);\r\n    if (!empty($customheaders)) {\r\n        curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);\r\n    }\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n\r\n    if ($ignore_ssl) {\r\n        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\r\n        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);\r\n    }\r\n\r\n    // Execute the request\r\n    $response = curl_exec($ch);\r\n    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n\r\n    // Close the cURL session\r\n    curl_close($ch);\r\n\r\n    // Check if the message was sent successfully\r\n    if ($response === false || $httpCode >= 400) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('notification_failed', $i18n),\r\n            \"response\" => curl_error($ch)\r\n        ]));\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => true,\r\n            \"message\" => translate('notification_sent_successfuly', $i18n),\r\n            \"response\" => $response\r\n        ]));\r\n    }\r\n}"
  },
  {
    "path": "endpoints/payments/add.php",
    "content": "<?php\r\nerror_reporting(E_ERROR | E_PARSE);\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/getsettings.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\nif (!file_exists('../../images/uploads/logos')) {\r\n    mkdir('../../images/uploads/logos', 0777, true);\r\n    mkdir('../../images/uploads/logos/avatars', 0777, true);\r\n}\r\n\r\nfunction sanitizeFilename($filename)\r\n{\r\n    $filename = preg_replace(\"/[^a-zA-Z0-9\\s]/\", \"\", $filename);\r\n    $filename = str_replace(\" \", \"-\", $filename);\r\n    $filename = str_replace(\".\", \"\", $filename);\r\n    return $filename;\r\n}\r\n\r\nfunction validateFileExtension($fileExtension)\r\n{\r\n    $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'jtif', 'webp'];\r\n    return in_array($fileExtension, $allowedExtensions);\r\n}\r\n\r\nfunction getLogoFromUrl($url, $uploadDir, $name, $i18n, $settings)\r\n{\r\n    if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('/^https?:\\/\\//i', $url)) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => \"Invalid URL format.\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit();\r\n    }\r\n\r\n    $host = parse_url($url, PHP_URL_HOST);\r\n    $ip = gethostbyname($host);\r\n    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => \"Invalid IP Address.\"\r\n        ];\r\n        echo json_encode($response);\r\n        exit();\r\n    }\r\n\r\n    $ch = curl_init($url);\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\r\n    curl_setopt($ch, CURLOPT_TIMEOUT, 5);\r\n    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);\r\n    curl_setopt($ch, CURLOPT_MAXREDIRS, 3);\r\n\r\n    $imageData = curl_exec($ch);\r\n\r\n    if ($imageData !== false) {\r\n        $timestamp = time();\r\n        $fileName = $timestamp . '-payments-' . sanitizeFilename($name) . '.png';\r\n        $uploadDir = '../../images/uploads/logos/';\r\n        $uploadFile = $uploadDir . $fileName;\r\n\r\n        if (saveLogo($imageData, $uploadFile, $name, $settings)) {\r\n            curl_close($ch);\r\n            return $fileName;\r\n        } else {\r\n            curl_close($ch);\r\n            echo translate('error_fetching_image', $i18n) . \": \" . curl_error($ch);\r\n            return \"\";\r\n        }\r\n    } else {\r\n        echo translate('error_fetching_image', $i18n) . \": \" . curl_error($ch);\r\n        return \"\";\r\n    }\r\n}\r\n\r\n\r\nfunction saveLogo($imageData, $uploadFile, $name, $settings)\r\n{\r\n    $image = imagecreatefromstring($imageData);\r\n    $removeBackground = isset($settings['removeBackground']) && $settings['removeBackground'] === 'true';\r\n    if ($image !== false) {\r\n        $tempFile = tempnam(sys_get_temp_dir(), 'logo');\r\n        imagepng($image, $tempFile);\r\n        imagedestroy($image);\r\n\r\n        if (extension_loaded('imagick')) {\r\n            $imagick = new Imagick($tempFile);\r\n            if ($removeBackground) {\r\n                $fuzz = Imagick::getQuantum() * 0.1; // 10%\r\n                $imagick->transparentPaintImage(\"rgb(247, 247, 247)\", 0, $fuzz, false);\r\n            }\r\n            $imagick->setImageFormat('png');\r\n            $imagick->writeImage($uploadFile);\r\n\r\n            $imagick->clear();\r\n            $imagick->destroy();\r\n        } else {\r\n            // Alternative method if Imagick is not available\r\n            $newImage = imagecreatefrompng($tempFile);\r\n            if ($removeBackground) {\r\n                imagealphablending($newImage, false);\r\n                imagesavealpha($newImage, true);\r\n                $transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127);\r\n                imagefill($newImage, 0, 0, $transparent);  // Fill the entire image with transparency\r\n                imagepng($newImage, $uploadFile);\r\n                imagedestroy($newImage);\r\n            }\r\n            imagepng($newImage, $uploadFile);\r\n            imagedestroy($newImage);\r\n        }\r\n        unlink($tempFile);\r\n\r\n        return true;\r\n    } else {\r\n        return false;\r\n    }\r\n}\r\n\r\nfunction resizeAndUploadLogo($uploadedFile, $uploadDir, $name)\r\n{\r\n    $targetWidth = 70;\r\n    $targetHeight = 48;\r\n\r\n    $timestamp = time();\r\n    $originalFileName = $uploadedFile['name'];\r\n    $fileExtension = pathinfo($originalFileName, PATHINFO_EXTENSION);\r\n    $fileExtension = validateFileExtension($fileExtension) ? $fileExtension : 'png';\r\n    $fileName = $timestamp . '-payments-' . sanitizeFilename($name) . '.' . $fileExtension;\r\n    $uploadFile = $uploadDir . $fileName;\r\n\r\n    if (move_uploaded_file($uploadedFile['tmp_name'], $uploadFile)) {\r\n        $fileInfo = getimagesize($uploadFile);\r\n\r\n        if ($fileInfo !== false) {\r\n            $width = $fileInfo[0];\r\n            $height = $fileInfo[1];\r\n\r\n            // Load the image based on its format\r\n            if ($fileExtension === 'png') {\r\n                $image = imagecreatefrompng($uploadFile);\r\n            } elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {\r\n                $image = imagecreatefromjpeg($uploadFile);\r\n            } elseif ($fileExtension === 'gif') {\r\n                $image = imagecreatefromgif($uploadFile);\r\n            } elseif ($fileExtension === 'webp') {\r\n                $image = imagecreatefromwebp($uploadFile);\r\n            } else {\r\n                // Handle other image formats as needed\r\n                return \"\";\r\n            }\r\n\r\n            // Enable alpha channel (transparency) for PNG images\r\n            if ($fileExtension === 'png') {\r\n                imagesavealpha($image, true);\r\n            }\r\n\r\n            $newWidth = $width;\r\n            $newHeight = $height;\r\n\r\n            if ($width > $targetWidth) {\r\n                $newWidth = (int) $targetWidth;\r\n                $newHeight = (int) (($targetWidth / $width) * $height);\r\n            }\r\n\r\n            if ($newHeight > $targetHeight) {\r\n                $newWidth = (int) (($targetHeight / $newHeight) * $newWidth);\r\n                $newHeight = (int) $targetHeight;\r\n            }\r\n\r\n            $resizedImage = imagecreatetruecolor($newWidth, $newHeight);\r\n            imagesavealpha($resizedImage, true);\r\n            $transparency = imagecolorallocatealpha($resizedImage, 0, 0, 0, 127);\r\n            imagefill($resizedImage, 0, 0, $transparency);\r\n            imagecopyresampled($resizedImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);\r\n\r\n            if ($fileExtension === 'png') {\r\n                imagepng($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {\r\n                imagejpeg($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'gif') {\r\n                imagegif($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'webp') {\r\n                imagewebp($resizedImage, $uploadFile);\r\n            } else {\r\n                return \"\";\r\n            }\r\n\r\n            imagedestroy($image);\r\n            imagedestroy($resizedImage);\r\n            return $fileName;\r\n        }\r\n    }\r\n\r\n    return \"\";\r\n}\r\n\r\n$enabled = 1;\r\n$name = validate($_POST[\"paymentname\"]);\r\n$iconUrl = validate($_POST['icon-url']);\r\n\r\nif ($name === \"\" || ($iconUrl === \"\" && empty($_FILES['paymenticon']['name']))) {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_all_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit();\r\n}\r\n\r\n\r\n$icon = \"\";\r\n\r\nif ($iconUrl !== \"\") {\r\n    $icon = getLogoFromUrl($iconUrl, '../../images/uploads/logos/', $name, $i18n, $settings);\r\n} else {\r\n    if (!empty($_FILES['paymenticon']['name'])) {\r\n        $fileType = mime_content_type($_FILES['paymenticon']['tmp_name']);\r\n        if (strpos($fileType, 'image') === false) {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('fill_all_fields', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n            exit();\r\n        }\r\n        $icon = resizeAndUploadLogo($_FILES['paymenticon'], '../../images/uploads/logos/', $name);\r\n    }\r\n}\r\n\r\n// Get the maximum existing ID\r\n$stmt = $db->prepare(\"SELECT MAX(id) as maxID FROM payment_methods\");\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n$maxID = $row['maxID'];\r\n\r\n// Ensure the new ID is greater than 31\r\n$newID = max($maxID + 1, 32);\r\n\r\n// Insert the new record with the new ID\r\n$sql = \"INSERT INTO payment_methods (id, name, icon, enabled, user_id) VALUES (:id, :name, :icon, :enabled, :userId)\";\r\n$stmt = $db->prepare($sql);\r\n\r\n$stmt->bindParam(':id', $newID, SQLITE3_INTEGER);\r\n$stmt->bindParam(':name', $name, SQLITE3_TEXT);\r\n$stmt->bindParam(':icon', $icon, SQLITE3_TEXT);\r\n$stmt->bindParam(':enabled', $enabled, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    $success['success'] = true;\r\n    $success['message'] = translate('payment_method_added_successfuly', $i18n);\r\n    $json = json_encode($success);\r\n    header('Content-Type: application/json');\r\n    echo $json;\r\n    exit();\r\n} else {\r\n    echo translate('error', $i18n) . \": \" . $db->lastErrorMsg();\r\n}\r\n\r\n$db->close();\r\n\r\n?>"
  },
  {
    "path": "endpoints/payments/delete.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$input = file_get_contents('php://input');\r\n$data = json_decode($input, true);\r\n\r\n$paymentMethodId = $data[\"id\"];\r\n$deleteQuery = \"DELETE FROM payment_methods WHERE id = :paymentMethodId and user_id = :userId\";\r\n$deleteStmt = $db->prepare($deleteQuery);\r\n$deleteStmt->bindParam(':paymentMethodId', $paymentMethodId, SQLITE3_INTEGER);\r\n$deleteStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($deleteStmt->execute()) {\r\n    $success['success'] = true;\r\n    $success['message'] = translate('payment_method_removed', $i18n);\r\n    $json = json_encode($success);\r\n    header('Content-Type: application/json');\r\n    echo $json;\r\n} else {\r\n    http_response_code(500);\r\n    echo json_encode(array(\"message\" => translate('error', $i18n)));\r\n}\r\n\r\n$db->close();\r\n\r\n?>"
  },
  {
    "path": "endpoints/payments/get.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    $paymentsInUseQuery = $db->prepare('SELECT id FROM payment_methods WHERE id IN (SELECT DISTINCT payment_method_id FROM subscriptions) AND user_id = :userId');\r\n    $paymentsInUseQuery->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $paymentsInUseQuery->execute();\r\n\r\n    $paymentsInUse = [];\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $paymentsInUse[] = $row['id'];\r\n    }\r\n\r\n    $sql = \"SELECT * FROM payment_methods WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $payments = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $payments[] = $row;\r\n        }\r\n    } else {\r\n        http_response_code(500);\r\n        echo json_encode(array(\"message\" => translate('error', $i18n)));\r\n        exit();\r\n    }\r\n\r\n    foreach ($payments as $payment) {\r\n        $paymentIconFolder = (strpos($payment['icon'], 'images/uploads/icons/') !== false) ? \"\" : \"images/uploads/logos/\";\r\n        $inUse = in_array($payment['id'], $paymentsInUse);\r\n        ?>\r\n        <div class=\"payments-payment\" data-enabled=\"<?= $payment['enabled']; ?>\" data-in-use=\"<?= $inUse ? 'yes' : 'no' ?>\"\r\n            data-paymentid=\"<?= $payment['id'] ?>\"\r\n            title=\"<?= $inUse ? translate('cant_delete_payment_method_in_use', $i18n) : ($payment['enabled'] ? translate('disable', $i18n) : translate('enable', $i18n)) ?>\"\r\n            onClick=\"togglePayment(<?= $payment['id'] ?>)\">\r\n            <img src=\"<?= $paymentIconFolder . $payment['icon'] ?>\" alt=\"Logo\" />\r\n            <span class=\"payment-name\">\r\n                <?= $payment['name'] ?>\r\n            </span>\r\n            <?php\r\n            if (!$inUse) {\r\n                ?>\r\n                <div class=\"delete-payment-method\" title=\"<?= translate('delete', $i18n) ?>\" data-paymentid=\"<?= $payment['id'] ?>\"\r\n                    onclick=\"deletePaymentMethod(<?= $payment['id'] ?>)\">\r\n                    x\r\n                </div>\r\n                <?php\r\n            }\r\n            ?>\r\n        </div>\r\n        <?php\r\n    }\r\n} else {\r\n    http_response_code(401);\r\n    echo json_encode(array(\"message\" => translate('error', $i18n)));\r\n    exit();\r\n}\r\n\r\n?>"
  },
  {
    "path": "endpoints/payments/rename.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\nfunction validate($value)\r\n{\r\n    $value = trim($value);\r\n    $value = stripslashes($value);\r\n    $value = htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');\r\n    return $value;\r\n}\r\n\r\n\r\nif (!isset($_POST['paymentId']) || !isset($_POST['name']) || $_POST['paymentId'] === '' || $_POST['name'] === '') {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('fields_missing', $i18n)\r\n    ]));\r\n}\r\n\r\n$paymentId = $_POST['paymentId'];\r\n$name = validate($_POST['name']);\r\nif (strlen($name) > 255) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('fields_missing', $i18n)\r\n    ]));\r\n}\r\n\r\n\r\n$sql = \"UPDATE payment_methods SET name = :name WHERE id = :paymentId and user_id = :userId\";\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindParam(':name', $name, SQLITE3_TEXT);\r\n$stmt->bindParam(':paymentId', $paymentId, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nif ($result && $db->changes() > 0) {\r\n    echo json_encode([\"success\" => true, \"message\" => translate('payment_renamed', $i18n)]);\r\n} else {\r\n    echo json_encode([\"success\" => false, \"message\" => translate('payment_not_renamed', $i18n)]);\r\n}\r\n\r\n\r\n?>"
  },
  {
    "path": "endpoints/payments/search.php",
    "content": "<?php\r\n\r\nif (isset($_GET['search'])) {\r\n    function applyProxy($ch) {\r\n        $proxy = getenv('https_proxy')\r\n            ?: getenv('HTTPS_PROXY')\r\n            ?: getenv('http_proxy')\r\n            ?: getenv('HTTP_PROXY')\r\n            ?: null;\r\n\r\n        if ($proxy) {\r\n            curl_setopt($ch, CURLOPT_PROXY, $proxy);\r\n        }\r\n    }\r\n\r\n    function curlGet($url, $headers = []) {\r\n        $allowedHosts = ['duckduckgo.com', 'search.brave.com'];\r\n\r\n        $host = parse_url($url, PHP_URL_HOST);\r\n        if (!in_array($host, $allowedHosts, true)) {\r\n            return null;\r\n        }\r\n\r\n        $ch = curl_init();\r\n        curl_setopt($ch, CURLOPT_URL, $url);\r\n        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);\r\n        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36');\r\n\r\n        if (!empty($headers)) {\r\n            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);\r\n        }\r\n\r\n        applyProxy($ch);\r\n        $response = curl_exec($ch);\r\n        curl_close($ch);\r\n\r\n        return $response ?: null;\r\n    }\r\n\r\n    $searchTermRaw = $_GET['search'] . \" logo\";\r\n    $searchTerm    = urlencode($searchTermRaw);\r\n\r\n    function getVqdToken($query) {\r\n        $html = curlGet(\"https://duckduckgo.com/?q={$query}&ia=images\");\r\n        if ($html && preg_match('/vqd=\"?([\\d-]+)\"?/', $html, $matches)) {\r\n            return $matches[1];\r\n        }\r\n        return null;\r\n    }\r\n\r\n    function fetchDDGImages($query, $vqd) {\r\n        $params = http_build_query([\r\n            'l'   => 'us-en',\r\n            'o'   => 'json',\r\n            'q'   => urldecode($query),\r\n            'vqd' => $vqd,\r\n            'f'   => ',,,,',   // size,color,type,layout,license → all unset\r\n            'p'   => '1',      // safesearch on\r\n        ]);\r\n\r\n        $response = curlGet(\"https://duckduckgo.com/i.js?{$params}\", [\r\n            'Accept: application/json',\r\n            'Referer: https://duckduckgo.com/',\r\n        ]);\r\n\r\n        if (!$response) return null;\r\n\r\n        $data = json_decode($response, true);\r\n        if (!isset($data['results']) || empty($data['results'])) return null;\r\n\r\n        return array_column($data['results'], 'image');\r\n    }\r\n\r\n    function fetchBraveImages($query) {\r\n        $url  = \"https://search.brave.com/images?q={$query}\";\r\n        $html = curlGet($url, [\r\n            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\r\n            'Accept-Language: en-US,en;q=0.5',\r\n            'Referer: https://search.brave.com/',\r\n        ]);\r\n\r\n        if (!$html) return null;\r\n\r\n        $doc = new DOMDocument();\r\n        @$doc->loadHTML($html);\r\n\r\n        $blockedDomains = ['cdn.search.brave.com', 'search.brave.com/static'];\r\n\r\n        $imageUrls = [];\r\n        $imgTags   = $doc->getElementsByTagName('img');\r\n\r\n        foreach ($imgTags as $imgTag) {\r\n            $src   = $imgTag->getAttribute('src');\r\n            $class = $imgTag->getAttribute('class');\r\n\r\n            if (str_contains($class, 'favicon') || str_contains($class, 'logo')) continue;\r\n            if (!filter_var($src, FILTER_VALIDATE_URL)) continue;\r\n\r\n            foreach ($blockedDomains as $blocked) {\r\n                if (str_contains($src, $blocked)) {\r\n                    continue 2; // skip to next <img>\r\n                }\r\n            }\r\n\r\n            $imageUrls[] = $src;\r\n        }\r\n\r\n        return !empty($imageUrls) ? $imageUrls : null;\r\n    }\r\n\r\n    // Main flow: DDG first, Brave fallback\r\n    $vqd       = getVqdToken($searchTerm);\r\n    $imageUrls = $vqd ? fetchDDGImages($searchTerm, $vqd) : null;\r\n\r\n    if (!$imageUrls) {\r\n        $imageUrls = fetchBraveImages($searchTerm);\r\n    }\r\n\r\n    header('Content-Type: application/json');\r\n\r\n    if ($imageUrls) {\r\n        echo json_encode(['imageUrls' => $imageUrls]);\r\n    } else {\r\n        echo json_encode(['error' => 'Failed to fetch images from DuckDuckGo and Brave.']);\r\n    }\r\n\r\n} else {\r\n    echo json_encode(['error' => 'Invalid request.']);\r\n}\r\n"
  },
  {
    "path": "endpoints/payments/sort.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$paymentMethods = $_POST['paymentMethodIds'];\r\n$order = 1;\r\n\r\nforeach ($paymentMethods as $paymentMethodId) {\r\n    $sql = \"UPDATE payment_methods SET `order` = :order WHERE id = :paymentMethodId and user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindParam(':order', $order, SQLITE3_INTEGER);\r\n    $stmt->bindParam(':paymentMethodId', $paymentMethodId, SQLITE3_INTEGER);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    $order++;\r\n}\r\n\r\n$response = [\r\n    \"success\" => true,\r\n    \"message\" => translate(\"sort_order_saved\", $i18n)\r\n];\r\necho json_encode($response);\r\n\r\n?>"
  },
  {
    "path": "endpoints/payments/toggle.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\nif (!isset($_POST['paymentId']) || !isset($_POST['enabled'])) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('fields_missing', $i18n)\r\n    ]));\r\n}\r\n\r\n$paymentId = $_POST['paymentId'];\r\n\r\n$stmt = $db->prepare('SELECT COUNT(*) as count FROM subscriptions WHERE payment_method_id=:paymentId and user_id=:userId');\r\n$stmt->bindValue(':paymentId', $paymentId, SQLITE3_INTEGER);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray();\r\n$inUse = $row['count'] === 1;\r\n\r\nif ($inUse) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('payment_in_use', $i18n)\r\n    ]));\r\n}\r\n\r\n$enabled = $_POST['enabled'];\r\n\r\n$sqlUpdate = 'UPDATE payment_methods SET enabled=:enabled WHERE id=:id and user_id=:userId';\r\n$stmtUpdate = $db->prepare($sqlUpdate);\r\n$stmtUpdate->bindParam(':enabled', $enabled);\r\n$stmtUpdate->bindParam(':id', $paymentId);\r\n$stmtUpdate->bindParam(':userId', $userId);\r\n$resultUpdate = $stmtUpdate->execute();\r\n\r\n$text = $enabled ? \"enabled\" : \"disabled\";\r\n\r\nif ($resultUpdate) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate($text, $i18n)\r\n    ]));\r\n}\r\n\r\ndie(json_encode([\r\n    \"success\" => false,\r\n    \"message\" => translate('failed_update_payment', $i18n)\r\n]));\r\n"
  },
  {
    "path": "endpoints/settings/colortheme.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n// Valiudate input, should be a color from the allowed list\r\n$allowedColors = ['blue', 'red', 'green', 'yellow', 'purple'];\r\nif (!isset($data['color']) || !in_array($data['color'], $allowedColors)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$color = $data['color'];\r\n\r\n$stmt = $db->prepare('UPDATE settings SET color_theme = :color WHERE user_id = :userId');\r\n$stmt->bindParam(':color', $color, SQLITE3_TEXT);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/convert_currency.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$convert_currency = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($convert_currency) || !is_bool($convert_currency)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET convert_currency = :convert_currency WHERE user_id = :userId');\r\n$stmt->bindParam(':convert_currency', $convert_currency, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/customcss.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$customCss = $data['customCss'];\r\n\r\n$stmt = $db->prepare('DELETE FROM custom_css_style WHERE user_id = :userId');\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n\r\n$stmt = $db->prepare('INSERT INTO custom_css_style (css, user_id) VALUES (:customCss, :userId)');\r\n$stmt->bindParam(':customCss', $customCss, SQLITE3_TEXT);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n"
  },
  {
    "path": "endpoints/settings/customtheme.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$main_color = $data['mainColor'];\r\n$accent_color = $data['accentColor'];\r\n$hover_color = $data['hoverColor'];\r\n\r\n// Validate input, should be a color in #RRGGBB format\r\nif (!preg_match('/^#[0-9A-Fa-f]{6}$/', $main_color) || !preg_match('/^#[0-9A-Fa-f]{6}$/', $accent_color) || !preg_match('/^#[0-9A-Fa-f]{6}$/', $hover_color)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\nif ($main_color == $accent_color) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"main_accent_color_error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('DELETE FROM custom_colors WHERE user_id = :userId');\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n\r\n$stmt = $db->prepare('INSERT INTO custom_colors (main_color, accent_color, hover_color, user_id) VALUES (:main_color, :accent_color, :hover_color, :userId)');\r\n$stmt->bindParam(':main_color', $main_color, SQLITE3_TEXT);\r\n$stmt->bindParam(':accent_color', $accent_color, SQLITE3_TEXT);\r\n$stmt->bindParam(':hover_color', $hover_color, SQLITE3_TEXT);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/deleteaccount.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$userIdToDelete = $data['userId'];\r\n\r\nif ($userIdToDelete == 1 || $userIdToDelete != $userId) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n} else {\r\n    // Delete user\r\n    $stmt = $db->prepare('DELETE FROM user WHERE id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete subscriptions\r\n    $stmt = $db->prepare('DELETE FROM subscriptions WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete settings\r\n    $stmt = $db->prepare('DELETE FROM settings WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete fixer\r\n    $stmt = $db->prepare('DELETE FROM fixer WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete custom colors\r\n    $stmt = $db->prepare('DELETE FROM custom_colors WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete currencies\r\n    $stmt = $db->prepare('DELETE FROM currencies WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete categories\r\n    $stmt = $db->prepare('DELETE FROM categories WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete household\r\n    $stmt = $db->prepare('DELETE FROM household WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete payment methods\r\n    $stmt = $db->prepare('DELETE FROM payment_methods WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete email notifications\r\n    $stmt = $db->prepare('DELETE FROM email_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete telegram notifications\r\n    $stmt = $db->prepare('DELETE FROM telegram_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete webhook notifications\r\n    $stmt = $db->prepare('DELETE FROM webhook_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete gotify notifications\r\n    $stmt = $db->prepare('DELETE FROM gotify_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete pushover notifications\r\n    $stmt = $db->prepare('DELETE FROM pushover_notifications WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Dele notification settings\r\n    $stmt = $db->prepare('DELETE FROM notification_settings WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete last exchange update\r\n    $stmt = $db->prepare('DELETE FROM last_exchange_update WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete email verification\r\n    $stmt = $db->prepare('DELETE FROM email_verification WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete totp\r\n    $stmt = $db->prepare('DELETE FROM totp WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    // Delete total yearly cost\r\n    $stmt = $db->prepare('DELETE FROM total_yearly_cost WHERE user_id = :id');\r\n    $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n)\r\n    ]));\r\n\r\n}"
  },
  {
    "path": "endpoints/settings/disabled_to_bottom.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$disabled_to_bottom = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($disabled_to_bottom) || !is_bool($disabled_to_bottom)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET disabled_to_bottom = :disabled_to_bottom WHERE user_id = :userId');\r\n$stmt->bindParam(':disabled_to_bottom', $disabled_to_bottom, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/hide_disabled.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$hide_disabled = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($hide_disabled) || !is_bool($hide_disabled)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET hide_disabled = :hide_disabled WHERE user_id = :userId');\r\n$stmt->bindParam(':hide_disabled', $hide_disabled, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/mobile_navigation.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$mobile_nav = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($mobile_nav) || !is_bool($mobile_nav)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET mobile_nav = :mobile_nav WHERE user_id = :userId');\r\n$stmt->bindParam(':mobile_nav', $mobile_nav, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/monthly_price.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$monthly_price = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($monthly_price) || !is_bool($monthly_price)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET monthly_price = :monthly_price WHERE user_id = :userId');\r\n$stmt->bindParam(':monthly_price', $monthly_price, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/remove_background.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$remove_background = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($remove_background) || !is_bool($remove_background)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET remove_background = :remove_background WHERE user_id = :userId');\r\n$stmt->bindParam(':remove_background', $remove_background, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/resettheme.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$stmt = $db->prepare('DELETE FROM custom_colors WHERE user_id = :userId');\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/show_original_price.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$show_original_price = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($show_original_price) || !is_bool($show_original_price)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET show_original_price = :show_original_price WHERE user_id = :userId');\r\n$stmt->bindParam(':show_original_price', $show_original_price, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/subscription_progress.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$show_subscription_progress = $data['value'];\r\n\r\n// Validate input\r\nif (!isset($show_subscription_progress) || !is_bool($show_subscription_progress)) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET show_subscription_progress = :show_subscription_progress WHERE user_id = :userId');\r\n$stmt->bindParam(':show_subscription_progress', $show_subscription_progress, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/settings/theme.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$theme = (int) $data['theme'];\r\n\r\n// Validate input, should be an integer (0, 1 or 2)\r\nif (!isset($theme) || !is_int($theme) || $theme < 0 || $theme > 2) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$stmt = $db->prepare('UPDATE settings SET dark_theme = :theme WHERE user_id = :userId');\r\n$stmt->bindParam(':theme', $theme, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    die(json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate(\"success\", $i18n)\r\n    ]));\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/subscription/add.php",
    "content": "<?php\r\nerror_reporting(E_ERROR | E_PARSE);\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/getsettings.php';\r\n\r\nif (!file_exists('../../images/uploads/logos')) {\r\n    mkdir('../../images/uploads/logos', 0777, true);\r\n    mkdir('../../images/uploads/logos/avatars', 0777, true);\r\n}\r\n\r\nfunction sanitizeFilename($filename)\r\n{\r\n    $filename = preg_replace(\"/[^a-zA-Z0-9\\s]/\", \"\", $filename);\r\n    $filename = str_replace(\" \", \"-\", $filename);\r\n    $filename = str_replace(\".\", \"\", $filename);\r\n    return $filename;\r\n}\r\n\r\nfunction validateFileExtension($fileExtension)\r\n{\r\n    $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];\r\n    return in_array($fileExtension, $allowedExtensions);\r\n}\r\n\r\nfunction getLogoFromUrl($url, $uploadDir, $name, $settings, $i18n)\r\n{\r\n    $maxRedirects = 3;\r\n    $currentUrl = $url;\r\n\r\n    for ($i = 0; $i <= $maxRedirects; $i++) {\r\n        if (!filter_var($currentUrl, FILTER_VALIDATE_URL) || !preg_match('/^https?:\\/\\//i', $currentUrl)) {\r\n            $response = [\"success\" => false, \"message\" => \"Invalid URL format.\"];\r\n            echo json_encode($response);\r\n            exit();\r\n        }\r\n\r\n        $parts = parse_url($currentUrl);\r\n        $host = $parts['host'];\r\n        $port = $parts['port'] ?? ($parts['scheme'] === 'https' ? 443 : 80);\r\n        $ip = gethostbyname($host);\r\n\r\n        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {\r\n            $response = [\"success\" => false, \"message\" => \"Invalid IP Address.\"];\r\n            echo json_encode($response);\r\n            exit();\r\n        }\r\n\r\n        $ch = curl_init($currentUrl);\r\n        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\r\n        curl_setopt($ch, CURLOPT_TIMEOUT, 5);\r\n        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);\r\n        \r\n        curl_setopt($ch, CURLOPT_RESOLVE, [\"$host:$port:$ip\"]);\r\n\r\n        $imageData = curl_exec($ch);\r\n        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\r\n\r\n        if ($httpCode >= 300 && $httpCode < 400) {\r\n            $redirectUrl = curl_getinfo($ch, CURLINFO_REDIRECT_URL);\r\n            curl_close($ch);\r\n\r\n            if (!$redirectUrl) {\r\n                break;\r\n            }\r\n\r\n            $currentUrl = $redirectUrl;\r\n            continue;\r\n        }\r\n\r\n        if ($imageData !== false && $httpCode === 200) {\r\n            $timestamp = time();\r\n            $fileName = $timestamp . '-' . sanitizeFilename($name) . '.png';\r\n            $uploadDir = '../../images/uploads/logos/';\r\n            $uploadFile = $uploadDir . $fileName;\r\n\r\n            if (saveLogo($imageData, $uploadFile, $name, $settings)) {\r\n                curl_close($ch);\r\n                return $fileName;\r\n            }\r\n        }\r\n\r\n        echo translate('error_fetching_image', $i18n) . \": \" . curl_error($ch);\r\n        curl_close($ch);\r\n        return \"\";\r\n    }\r\n\r\n    return \"\";\r\n}\r\n\r\nfunction saveLogo($imageData, $uploadFile, $name, $settings)\r\n{\r\n    $image = imagecreatefromstring($imageData);\r\n    $removeBackground = isset($settings['removeBackground']) && $settings['removeBackground'] === 'true';\r\n    if ($image !== false) {\r\n        $tempFile = tempnam(sys_get_temp_dir(), 'logo');\r\n        imagepng($image, $tempFile);\r\n        imagedestroy($image);\r\n\r\n        if (extension_loaded('imagick')) {\r\n            $imagick = new Imagick($tempFile);\r\n            if ($removeBackground) {\r\n                $fuzz = Imagick::getQuantum() * 0.1; // 10%\r\n                $imagick->transparentPaintImage(\"rgb(247, 247, 247)\", 0, $fuzz, false);\r\n            }\r\n            $imagick->setImageFormat('png');\r\n            $imagick->writeImage($uploadFile);\r\n\r\n            $imagick->clear();\r\n            $imagick->destroy();\r\n        } else {\r\n            // Alternative method if Imagick is not available\r\n            $newImage = imagecreatefrompng($tempFile);\r\n            if ($newImage !== false) {\r\n                if ($removeBackground) {\r\n                    imagealphablending($newImage, false);\r\n                    imagesavealpha($newImage, true);\r\n                    $transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127);\r\n                    imagefill($newImage, 0, 0, $transparent);  // Fill the entire image with transparency\r\n                    imagepng($newImage, $uploadFile);\r\n                    imagedestroy($newImage);\r\n                }\r\n                imagepng($newImage, $uploadFile);\r\n                imagedestroy($newImage);\r\n            } else {\r\n                unlink($tempFile);\r\n                return false;\r\n            }\r\n        }\r\n        unlink($tempFile);\r\n\r\n        return true;\r\n    } else {\r\n        return false;\r\n    }\r\n}\r\n\r\nfunction resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings)\r\n{\r\n    $targetWidth = 135;\r\n    $targetHeight = 42;\r\n\r\n    $timestamp = time();\r\n    $originalFileName = $uploadedFile['name'];\r\n    $fileExtension = pathinfo($originalFileName, PATHINFO_EXTENSION);\r\n    $fileExtension = validateFileExtension($fileExtension) ? $fileExtension : 'png';\r\n    $fileName = $timestamp . '-' . sanitizeFilename($name) . '.' . $fileExtension;\r\n    $uploadFile = $uploadDir . $fileName;\r\n\r\n    if (move_uploaded_file($uploadedFile['tmp_name'], $uploadFile)) {\r\n        $fileInfo = getimagesize($uploadFile);\r\n\r\n        if ($fileInfo !== false) {\r\n            $width = $fileInfo[0];\r\n            $height = $fileInfo[1];\r\n\r\n            // Load the image based on its format\r\n            if ($fileExtension === 'png') {\r\n                $image = imagecreatefrompng($uploadFile);\r\n            } elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {\r\n                $image = imagecreatefromjpeg($uploadFile);\r\n            } elseif ($fileExtension === 'gif') {\r\n                $image = imagecreatefromgif($uploadFile);\r\n            } elseif ($fileExtension === 'webp') {\r\n                $image = imagecreatefromwebp($uploadFile);\r\n            } else {\r\n                // Handle other image formats as needed\r\n                return \"\";\r\n            }\r\n\r\n            // Enable alpha channel (transparency) for PNG images\r\n            if ($fileExtension === 'png') {\r\n                imagesavealpha($image, true);\r\n            }\r\n\r\n            $newWidth = $width;\r\n            $newHeight = $height;\r\n\r\n            if ($width > $targetWidth) {\r\n                $newWidth = (int) $targetWidth;\r\n                $newHeight = (int) (($targetWidth / $width) * $height);\r\n            }\r\n\r\n            if ($newHeight > $targetHeight) {\r\n                $newWidth = (int) (($targetHeight / $newHeight) * $newWidth);\r\n                $newHeight = (int) $targetHeight;\r\n            }\r\n\r\n            $resizedImage = imagecreatetruecolor($newWidth, $newHeight);\r\n            imagesavealpha($resizedImage, true);\r\n            $transparency = imagecolorallocatealpha($resizedImage, 0, 0, 0, 127);\r\n            imagefill($resizedImage, 0, 0, $transparency);\r\n            imagecopyresampled($resizedImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);\r\n\r\n            if ($fileExtension === 'png') {\r\n                imagepng($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {\r\n                imagejpeg($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'gif') {\r\n                imagegif($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'webp') {\r\n                imagewebp($resizedImage, $uploadFile);\r\n            } else {\r\n                return \"\";\r\n            }\r\n\r\n            imagedestroy($image);\r\n            imagedestroy($resizedImage);\r\n\r\n            return $fileName;\r\n        }\r\n    }\r\n\r\n    return \"\";\r\n}\r\n\r\n$isEdit = isset($_POST['id']) && $_POST['id'] != \"\";\r\n$name = validate($_POST[\"name\"]);\r\n$price = $_POST['price'];\r\n$currencyId = $_POST[\"currency_id\"];\r\n$frequency = $_POST[\"frequency\"];\r\n$cycle = $_POST[\"cycle\"];\r\n$nextPayment = $_POST[\"next_payment\"];\r\n$autoRenew = isset($_POST['auto_renew']) ? true : false;\r\n$startDate = $_POST[\"start_date\"];\r\n$paymentMethodId = $_POST[\"payment_method_id\"];\r\n$payerUserId = $_POST[\"payer_user_id\"];\r\n$categoryId = $_POST['category_id'];\r\n$notes = validate($_POST[\"notes\"]);\r\n$url = validate($_POST['url']);\r\n$logoUrl = validate($_POST['logo-url']);\r\n$logo = \"\";\r\n$notify = isset($_POST['notifications']) ? true : false;\r\n$notifyDaysBefore = $_POST['notify_days_before'];\r\n$inactive = isset($_POST['inactive']) ? true : false;\r\n$cancellationDate = $_POST['cancellation_date'] ?? null;\r\n$replacementSubscriptionId = $_POST['replacement_subscription_id'];\r\n\r\nif ($replacementSubscriptionId == 0 || $inactive == 0) {\r\n    $replacementSubscriptionId = null;\r\n}\r\n\r\nif ($logoUrl !== \"\") {\r\n    $logo = getLogoFromUrl($logoUrl, '../../images/uploads/logos/', $name, $settings, $i18n);\r\n} else {\r\n    if (!empty($_FILES['logo']['name'])) {\r\n        $fileType = mime_content_type($_FILES['logo']['tmp_name']);\r\n        if (strpos($fileType, 'image') === false) {\r\n            echo translate(\"fill_all_fields\", $i18n);\r\n            exit();\r\n        }\r\n        $logo = resizeAndUploadLogo($_FILES['logo'], '../../images/uploads/logos/', $name, $settings);\r\n    }\r\n}\r\n\r\nif (!$isEdit) {\r\n    $sql = \"INSERT INTO subscriptions (\r\n                        name, logo, price, currency_id, next_payment, cycle, frequency, notes, \r\n                        payment_method_id, payer_user_id, category_id, notify, inactive, url, \r\n                        notify_days_before, user_id, cancellation_date, replacement_subscription_id,\r\n                        auto_renew, start_date\r\n                    ) VALUES (\r\n                        :name, :logo, :price, :currencyId, :nextPayment, :cycle, :frequency, :notes, \r\n                        :paymentMethodId, :payerUserId, :categoryId, :notify, :inactive, :url, \r\n                        :notifyDaysBefore, :userId, :cancellationDate, :replacement_subscription_id,\r\n                        :autoRenew, :startDate\r\n                    )\";\r\n} else {\r\n    $id = $_POST['id'];\r\n    $sql = \"UPDATE subscriptions SET \r\n                        name = :name, \r\n                        price = :price, \r\n                        currency_id = :currencyId,\r\n                        next_payment = :nextPayment, \r\n                        auto_renew = :autoRenew,\r\n                        start_date = :startDate,\r\n                        cycle = :cycle, \r\n                        frequency = :frequency, \r\n                        notes = :notes, \r\n                        payment_method_id = :paymentMethodId,\r\n                        payer_user_id = :payerUserId, \r\n                        category_id = :categoryId, \r\n                        notify = :notify, \r\n                        inactive = :inactive, \r\n                        url = :url, \r\n                        notify_days_before = :notifyDaysBefore, \r\n                        cancellation_date = :cancellationDate, \r\n                        replacement_subscription_id = :replacement_subscription_id\";\r\n\r\n    if ($logo != \"\") {\r\n        $sql .= \", logo = :logo\";\r\n    }\r\n\r\n    $sql .= \" WHERE id = :id AND user_id = :userId\";\r\n}\r\n\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindParam(':name', $name, SQLITE3_TEXT);\r\nif ($logo != \"\") {\r\n    $stmt->bindParam(':logo', $logo, SQLITE3_TEXT);\r\n}\r\n$stmt->bindParam(':price', $price, SQLITE3_FLOAT);\r\n$stmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);\r\n$stmt->bindParam(':nextPayment', $nextPayment, SQLITE3_TEXT);\r\n$stmt->bindParam(':autoRenew', $autoRenew, SQLITE3_INTEGER);\r\n$stmt->bindParam(':startDate', $startDate, SQLITE3_TEXT);\r\n$stmt->bindParam(':cycle', $cycle, SQLITE3_INTEGER);\r\n$stmt->bindParam(':frequency', $frequency, SQLITE3_INTEGER);\r\n$stmt->bindParam(':notes', $notes, SQLITE3_TEXT);\r\n$stmt->bindParam(':paymentMethodId', $paymentMethodId, SQLITE3_INTEGER);\r\n$stmt->bindParam(':payerUserId', $payerUserId, SQLITE3_INTEGER);\r\n$stmt->bindParam(':categoryId', $categoryId, SQLITE3_INTEGER);\r\n$stmt->bindParam(':notify', $notify, SQLITE3_INTEGER);\r\n$stmt->bindParam(':inactive', $inactive, SQLITE3_INTEGER);\r\n$stmt->bindParam(':url', $url, SQLITE3_TEXT);\r\n$stmt->bindParam(':notifyDaysBefore', $notifyDaysBefore, SQLITE3_INTEGER);\r\n$stmt->bindParam(':cancellationDate', $cancellationDate, SQLITE3_TEXT);\r\nif ($isEdit) {\r\n    $stmt->bindParam(':id', $id, SQLITE3_INTEGER);\r\n}\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n$stmt->bindParam(':replacement_subscription_id', $replacementSubscriptionId, SQLITE3_INTEGER);\r\n\r\nif ($stmt->execute()) {\r\n    $success['status'] = \"Success\";\r\n    $text = $isEdit ? \"updated\" : \"added\";\r\n    $success['message'] = translate('subscription_' . $text . '_successfuly', $i18n);\r\n    $json = json_encode($success);\r\n    header('Content-Type: application/json');\r\n    echo $json;\r\n    exit();\r\n} else {\r\n    echo translate('error', $i18n) . \": \" . $db->lastErrorMsg();\r\n}\r\n$db->close();\r\n?>"
  },
  {
    "path": "endpoints/subscription/clone.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$subscriptionId = $data[\"id\"];\r\n$query = \"SELECT * FROM subscriptions WHERE id = :id AND user_id = :user_id\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':id', $subscriptionId, SQLITE3_INTEGER);\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$subscriptionToClone = $result->fetchArray(SQLITE3_ASSOC);\r\nif ($subscriptionToClone === false) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$query = \"INSERT INTO subscriptions (name, logo, price, currency_id, next_payment, cycle, frequency, notes, payment_method_id, payer_user_id, category_id, notify, url, inactive, notify_days_before, user_id, cancellation_date, replacement_subscription_id) VALUES (:name, :logo, :price, :currency_id, :next_payment, :cycle, :frequency, :notes, :payment_method_id, :payer_user_id, :category_id, :notify, :url, :inactive, :notify_days_before, :user_id, :cancellation_date, :replacement_subscription_id)\";\r\n$cloneStmt = $db->prepare($query);\r\n$cloneStmt->bindValue(':name', $subscriptionToClone['name'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':logo', $subscriptionToClone['logo'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':price', $subscriptionToClone['price'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':currency_id', $subscriptionToClone['currency_id'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':next_payment', $subscriptionToClone['next_payment'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':auto_renew', $subscriptionToClone['auto_renew'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':start_date', $subscriptionToClone['start_date'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':cycle', $subscriptionToClone['cycle'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':frequency', $subscriptionToClone['frequency'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':notes', $subscriptionToClone['notes'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':payment_method_id', $subscriptionToClone['payment_method_id'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':payer_user_id', $subscriptionToClone['payer_user_id'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':category_id', $subscriptionToClone['category_id'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':notify', $subscriptionToClone['notify'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':url', $subscriptionToClone['url'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':inactive', $subscriptionToClone['inactive'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':notify_days_before', $subscriptionToClone['notify_days_before'], SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$cloneStmt->bindValue(':cancellation_date', $subscriptionToClone['cancellation_date'], SQLITE3_TEXT);\r\n$cloneStmt->bindValue(':replacement_subscription_id', $subscriptionToClone['replacement_subscription_id'], SQLITE3_INTEGER);\r\n\r\nif ($cloneStmt->execute()) {\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n),\r\n        \"id\" => $db->lastInsertRowID()\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$db->close();\r\n?>"
  },
  {
    "path": "endpoints/subscription/delete.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$subscriptionId = $data[\"id\"];\r\n$deleteQuery = \"DELETE FROM subscriptions WHERE id = :subscriptionId AND user_id = :userId\";\r\n$deleteStmt = $db->prepare($deleteQuery);\r\n$deleteStmt->bindParam(':subscriptionId', $subscriptionId, SQLITE3_INTEGER);\r\n$deleteStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif ($deleteStmt->execute()) {\r\n    $query = \"UPDATE subscriptions SET replacement_subscription_id = NULL WHERE replacement_subscription_id = :subscriptionId AND user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(':subscriptionId', $subscriptionId, SQLITE3_INTEGER);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n\r\n    echo json_encode([\r\n        \"success\" => true,\r\n        \"message\" => translate('subscription_deleted', $i18n)\r\n    ]);\r\n} else {\r\n    echo json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error_deleting_subscription', $i18n)\r\n    ]);\r\n}\r\n$db->close();"
  },
  {
    "path": "endpoints/subscription/exportcalendar.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\nrequire_once '../../includes/getdbkeys.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$id = $data['id'];\r\n\r\n$stmt = $db->prepare('SELECT * FROM subscriptions WHERE id = :id AND user_id = :userId');\r\n$stmt->bindParam(':id', $id, SQLITE3_INTEGER);\r\n$stmt->bindParam(':userId', $_SESSION['userId'], SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nif ($result === false) {\r\n    die(json_encode([\r\n        'success' => false,\r\n        'message' => \"Subscription not found\"\r\n    ]));\r\n}\r\n\r\n$subscription = $result->fetchArray(SQLITE3_ASSOC); // Fetch the subscription details as an associative array\r\n\r\nif ($subscription) {\r\n    $subscription['payer_user'] = $members[$subscription['payer_user_id']]['name'];\r\n    $subscription['category'] = $categories[$subscription['category_id']]['name'];\r\n    $subscription['payment_method'] = $payment_methods[$subscription['payment_method_id']]['name'];\r\n    $subscription['currency'] = $currencies[$subscription['currency_id']]['symbol'];\r\n    $subscription['trigger'] = $subscription['notify_days_before'] ? $subscription['notify_days_before'] : 1;\r\n    $subscription['price'] = number_format($subscription['price'], 2);\r\n\r\n    // Create ICS from subscription information\r\n    $uid = 'wallos-subscription-' . $subscription['id'] . '@wallos';\r\n    $summary = html_entity_decode($subscription['name'], ENT_QUOTES, 'UTF-8');\r\n    $description = \"Price: {$subscription['currency']}{$subscription['price']}\\nCategory: {$subscription['category']}\\nPayment Method: {$subscription['payment_method']}\\nPayer: {$subscription['payer_user']}\\n\\nNotes: {$subscription['notes']}\";\r\n\r\n    $dtstamp = gmdate('Ymd\\THis\\Z');\r\n    $dtstart = (new DateTime($subscription['next_payment']))->format('Ymd');\r\n    $dtend = (new DateTime($subscription['next_payment']))->format('Ymd');\r\n    $location = isset($subscription['url']) ? $subscription['url'] : '';\r\n    $alarm_trigger = '-P' . $subscription['trigger'] . 'D';\r\n\r\n    $icsContent = <<<ICS\r\n        BEGIN:VCALENDAR\r\n        VERSION:2.0\r\n        PRODID:-//Your Organization//Your Application//EN\r\n        CALSCALE:GREGORIAN\r\n        METHOD:PUBLISH\r\n        BEGIN:VEVENT\r\n        UID:$uid\r\n        DTSTAMP:$dtstamp\r\n        SUMMARY:$summary\r\n        DESCRIPTION:$description\r\n        DTSTART;VALUE=DATE:$dtstart\r\n        DTEND;VALUE=DATE:$dtend\r\n        LOCATION:$location\r\n        STATUS:CONFIRMED\r\n        TRANSP:OPAQUE\r\n        BEGIN:VALARM\r\n        ACTION:DISPLAY\r\n        DESCRIPTION:Reminder\r\n        TRIGGER:$alarm_trigger\r\n        END:VALARM\r\n        END:VEVENT\r\n        END:VCALENDAR\r\n        ICS;\r\n\r\n    echo json_encode([\r\n        'success' => true,\r\n        'ics' => $icsContent,\r\n        'name' => $subscription['name']\r\n    ]);\r\n} else {\r\n    echo json_encode([\r\n        'success' => false,\r\n        'message' => \"Subscription not found\"\r\n    ]);\r\n}"
  },
  {
    "path": "endpoints/subscription/get.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    if (isset($_GET['id']) && $_GET['id'] != \"\") {\r\n        $subscriptionId = intval($_GET['id']);\r\n        $query = \"SELECT * FROM subscriptions WHERE id = :subscriptionId AND user_id = :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindParam(':subscriptionId', $subscriptionId, SQLITE3_INTEGER);\r\n        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        $subscriptionData = array();\r\n\r\n        if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $subscriptionData['id'] = $subscriptionId;\r\n            $subscriptionData['name'] = htmlspecialchars_decode($row['name'] ?? \"\");\r\n            $subscriptionData['logo'] = $row['logo'];\r\n            $subscriptionData['price'] = $row['price'];\r\n            $subscriptionData['currency_id'] = $row['currency_id'];\r\n            $subscriptionData['auto_renew'] = $row['auto_renew'];\r\n            $subscriptionData['start_date'] = $row['start_date'];\r\n            $subscriptionData['next_payment'] = $row['next_payment'];\r\n            $subscriptionData['frequency'] = $row['frequency'];\r\n            $subscriptionData['cycle'] = $row['cycle'];\r\n            $subscriptionData['notes'] = htmlspecialchars_decode($row['notes'] ?? \"\");\r\n            $subscriptionData['payment_method_id'] = $row['payment_method_id'];\r\n            $subscriptionData['payer_user_id'] = $row['payer_user_id'];\r\n            $subscriptionData['category_id'] = $row['category_id'];\r\n            $subscriptionData['notify'] = $row['notify'];\r\n            $subscriptionData['inactive'] = $row['inactive'];\r\n            $subscriptionData['url'] = htmlspecialchars_decode($row['url'] ?? \"\");\r\n            $subscriptionData['notify_days_before'] = $row['notify_days_before'];\r\n            $subscriptionData['cancellation_date'] = $row['cancellation_date'];\r\n            $subscriptionData['replacement_subscription_id'] = $row['replacement_subscription_id'];\r\n\r\n            $subscriptionJson = json_encode($subscriptionData);\r\n            header('Content-Type: application/json');\r\n            echo $subscriptionJson;\r\n        } else {\r\n            echo translate('error', $i18n);\r\n        }\r\n    } else {\r\n        echo translate('error', $i18n);\r\n    }\r\n}\r\n$db->close();\r\n?>"
  },
  {
    "path": "endpoints/subscription/getcalendar.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/getdbkeys.php';\r\n\r\nif (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('session_expired', $i18n)\r\n    ]));\r\n}\r\n\r\nif ($_SERVER[\"REQUEST_METHOD\"] === \"POST\") {\r\n    $postData = file_get_contents(\"php://input\");\r\n    $data = json_decode($postData, true);\r\n\r\n    $id = $data['id'];\r\n\r\n    $stmt = $db->prepare('SELECT * FROM subscriptions WHERE id = :id AND user_id = :userId');\r\n    $stmt->bindParam(':id', $id, SQLITE3_INTEGER);\r\n    $stmt->bindParam(':userId', $_SESSION['userId'], SQLITE3_INTEGER); // Assuming $_SESSION['userId'] holds the logged-in user's ID\r\n    $result = $stmt->execute();\r\n\r\n    if ($result === false) {\r\n        die(json_encode([\r\n            'success' => false,\r\n            'message' => \"Subscription not found\"\r\n        ]));\r\n    }\r\n\r\n    $subscription = $result->fetchArray(SQLITE3_ASSOC); // Fetch the subscription details as an associative array\r\n\r\n    if ($subscription) {\r\n        // get payer name from household object\r\n        $subscription['payer_user'] = $members[$subscription['payer_user_id']]['name'];\r\n        $subscription['category'] = $categories[$subscription['category_id']]['name'];\r\n        $subscription['payment_method'] = $payment_methods[$subscription['payment_method_id']]['name'];\r\n        $subscription['currency'] = $currencies[$subscription['currency_id']]['symbol'];\r\n        $subscription['price'] = number_format($subscription['price'], 2);\r\n\r\n        echo json_encode([\r\n            'success' => true,\r\n            'data' => $subscription\r\n        ]);\r\n    } else {\r\n        echo json_encode([\r\n            'success' => false,\r\n            'message' => \"Subscription not found\"\r\n        ]);\r\n    }\r\n}\r\n?>"
  },
  {
    "path": "endpoints/subscription/renew.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$currentDate = new DateTime();\r\n$currentDateString = $currentDate->format('Y-m-d');\r\n\r\n$cycles = array();\r\n$query = \"SELECT * FROM cycles\";\r\n$result = $db->query($query);\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $cycleId = $row['id'];\r\n    $cycles[$cycleId] = $row;\r\n}\r\n\r\n$subscriptionId = $data[\"id\"];\r\n$query = \"SELECT * FROM subscriptions WHERE id = :id AND user_id = :user_id AND auto_renew = 0\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':id', $subscriptionId, SQLITE3_INTEGER);\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$subscriptionToRenew = $result->fetchArray(SQLITE3_ASSOC);\r\nif ($subscriptionToRenew === false) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}\r\n\r\n$nextPaymentDate = new DateTime($subscriptionToRenew['next_payment']);\r\n$frequency = $subscriptionToRenew['frequency'];\r\n$cycle = $cycles[$subscriptionToRenew['cycle']]['name'];\r\n\r\n// Calculate the interval to add based on the cycle\r\n$intervalSpec = \"P\";\r\nif ($cycle == 'Daily') {\r\n    $intervalSpec .= \"{$frequency}D\";\r\n} elseif ($cycle === 'Weekly') {\r\n    $intervalSpec .= \"{$frequency}W\";\r\n} elseif ($cycle === 'Monthly') {\r\n    $intervalSpec .= \"{$frequency}M\";\r\n} elseif ($cycle === 'Yearly') {\r\n    $intervalSpec .= \"{$frequency}Y\";\r\n}\r\n\r\n$interval = new DateInterval($intervalSpec);\r\n\r\n// Add intervals until the next payment date is in the future and after current next payment date\r\nwhile ($nextPaymentDate < $currentDate || $nextPaymentDate == new DateTime($subscriptionToRenew['next_payment'])) {\r\n    $nextPaymentDate->add($interval);\r\n}\r\n\r\n// Update the subscription's next_payment date\r\n$updateQuery = \"UPDATE subscriptions SET next_payment = :nextPaymentDate WHERE id = :subscriptionId\";\r\n$updateStmt = $db->prepare($updateQuery);\r\n$updateStmt->bindValue(':nextPaymentDate', $nextPaymentDate->format('Y-m-d'));\r\n$updateStmt->bindValue(':subscriptionId', $subscriptionId);\r\n$updateStmt->execute();\r\n\r\nif ($updateStmt->execute()) {\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate('success', $i18n),\r\n        \"id\" => $subscriptionId\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate(\"error\", $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/subscriptions/export.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nif (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('session_expired', $i18n)\r\n    ]));\r\n}\r\n\r\nrequire_once '../../includes/getdbkeys.php';\r\n\r\n$subscriptions = array();\r\n\r\n$query = \"SELECT * FROM subscriptions WHERE user_id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $cycle = $cycles[$row['cycle']]['name'];\r\n    $frequency =$row['frequency'];\r\n\r\n    $cyclesMap = array(\r\n        'Daily' => 'Days',\r\n        'Weekly' => 'Weeks',\r\n        'Monthly' => 'Months',\r\n        'Yearly' => 'Years'\r\n    );\r\n\r\n    if ($frequency == 1) {\r\n        $cyclePrint = $cycle;\r\n    } else {\r\n        $cyclePrint = \"Every \" . $frequency . \" \" . $cyclesMap[$cycle];\r\n    }\r\n\r\n    $subscriptionDetails = array(\r\n        'Name' => str_replace(',', ' ', $row['name']),\r\n        'Payment Cycle' => $cyclePrint,\r\n        'Next Payment' => $row['next_payment'],\r\n        'Renewal' => $row['auto_renew'] ? 'Automatic' : 'Manual',\r\n        'Category' => str_replace(',', ' ', $categories[$row['category_id']]['name']),\r\n        'Payment Method' => str_replace(',', ' ', $payment_methods[$row['payment_method_id']]['name']),\r\n        'Paid By' => str_replace(',', ' ', $members[$row['payer_user_id']]['name']),\r\n        'Price' => $currencies[$row['currency_id']]['symbol'] . $row['price'],\r\n        'Notes' => str_replace(',', ' ', $row['notes']),\r\n        'URL' => $row['url'],\r\n        'State' => $row['inactive'] ? 'Disabled' : 'Enabled',\r\n        'Notifications' => $row['notify'] ? 'Enabled' : 'Disabled',\r\n        'Cancellation Date' => $row['cancellation_date'],\r\n        'Active' => $row['inactive'] ? 'No' : 'Yes',\r\n    );\r\n\r\n    $subscriptions[] = $subscriptionDetails;\r\n}\r\n\r\ndie(json_encode([\r\n    \"success\" => true,\r\n    \"subscriptions\" => $subscriptions\r\n]));\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "endpoints/subscriptions/get.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\n\r\nrequire_once '../../includes/currency_formatter.php';\r\nrequire_once '../../includes/getdbkeys.php';\r\n\r\ninclude_once '../../includes/list_subscriptions.php';\r\n\r\nrequire_once '../../includes/getsettings.php';\r\n\r\n$theme = \"light\";\r\nif (isset($settings['theme'])) {\r\n  $theme = $settings['theme'];\r\n}\r\n\r\n$colorTheme = \"blue\";\r\nif (isset($settings['color_theme'])) {\r\n  $colorTheme = $settings['color_theme'];\r\n}\r\n\r\n$formatter = new IntlDateFormatter(\r\n  'en', // Force English locale\r\n  IntlDateFormatter::SHORT,\r\n  IntlDateFormatter::NONE,\r\n  null,\r\n  null,\r\n  'MMM d, yyyy'\r\n);\r\n\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n\r\n\r\n  $sort = \"next_payment\";\r\n  $sortOrder = $sort;\r\n  $order = \"ASC\";\r\n\r\n  $params = array();\r\n  $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId\";\r\n\r\n  if (isset($_GET['categories']) && $_GET['categories'] != \"\") {\r\n    $allCategories = explode(',', $_GET['categories']);\r\n    $placeholders = array_map(function ($idx) {\r\n      return \":categories{$idx}\";\r\n    }, array_keys($allCategories));\r\n\r\n    $sql .= \" AND (\" . implode(' OR ', array_map(function ($placeholder) {\r\n      return \"category_id = {$placeholder}\";\r\n    }, $placeholders)) . \")\";\r\n\r\n    foreach ($allCategories as $idx => $category) {\r\n      $params[\":categories{$idx}\"] = $category;\r\n    }\r\n  }\r\n\r\n  if (isset($_GET['payments']) && $_GET['payments'] !== \"\") {\r\n    $allPayments = explode(',', $_GET['payments']);\r\n    $placeholders = array_map(function ($idx) {\r\n      return \":payments{$idx}\";\r\n    }, array_keys($allPayments));\r\n\r\n    $sql .= \" AND (\" . implode(' OR ', array_map(function ($placeholder) {\r\n      return \"payment_method_id = {$placeholder}\";\r\n    }, $placeholders)) . \")\";\r\n\r\n    foreach ($allPayments as $idx => $payment) {\r\n      $params[\":payments{$idx}\"] = $payment;\r\n    }\r\n  }\r\n\r\n  if (isset($_GET['members']) && $_GET['members'] != \"\") {\r\n    $allMembers = explode(',', $_GET['members']);\r\n    $placeholders = array_map(function ($idx) {\r\n      return \":members{$idx}\";\r\n    }, array_keys($allMembers));\r\n\r\n    $sql .= \" AND (\" . implode(' OR ', array_map(function ($placeholder) {\r\n      return \"payer_user_id = {$placeholder}\";\r\n    }, $placeholders)) . \")\";\r\n\r\n    foreach ($allMembers as $idx => $member) {\r\n      $params[\":members{$idx}\"] = $member;\r\n    }\r\n  }\r\n\r\n  if (isset($_GET['state']) && $_GET['state'] != \"\") {\r\n    $sql .= \" AND inactive = :inactive\";\r\n    $params[':inactive'] = $_GET['state'];\r\n  }\r\n\r\n  if (isset($_GET['renewalType']) && $_GET['renewalType'] != \"\") {\r\n    $sql .= \" AND auto_renew = :auto_renew\";\r\n    $params[':auto_renew'] = $_GET['renewalType'];\r\n  }\r\n\r\n  if (isset($_COOKIE['sortOrder']) && $_COOKIE['sortOrder'] != \"\") {\r\n    $sort = $_COOKIE['sortOrder'];\r\n  }\r\n\r\n  $sortOrder = $sort;\r\n  $allowedSortCriteria = ['name', 'id', 'next_payment', 'price', 'payer_user_id', 'category_id', 'payment_method_id', 'inactive', 'alphanumeric', 'renewal_type'];\r\n  $order = ($sort == \"price\" || $sort == \"id\") ? \"DESC\" : \"ASC\";\r\n\r\n  if ($sort == \"alphanumeric\") {\r\n    $sort = \"name\";\r\n  }\r\n\r\n  if (!in_array($sort, $allowedSortCriteria)) {\r\n    $sort = \"next_payment\";\r\n  }\r\n\r\n  if ($sort == \"renewal_type\") {\r\n    $sort = \"auto_renew\";\r\n  }\r\n\r\n  $orderByClauses = [];\r\n\r\n  if ($settings['disabledToBottom'] === 'true') {\r\n    if (in_array($sort, [\"payer_user_id\", \"category_id\", \"payment_method_id\"])) {\r\n      $orderByClauses[] = \"$sort $order\";\r\n      $orderByClauses[] = \"inactive ASC\";\r\n    } else {\r\n      $orderByClauses[] = \"inactive ASC\";\r\n      $orderByClauses[] = \"$sort $order\";\r\n    }\r\n  } else {\r\n    $orderByClauses[] = \"$sort $order\";\r\n    if ($sort != \"inactive\") {\r\n      $orderByClauses[] = \"inactive ASC\";\r\n    }\r\n  }\r\n\r\n  if ($sort != \"next_payment\") {\r\n    $orderByClauses[] = \"next_payment ASC\";\r\n  }\r\n\r\n  $sql .= \" ORDER BY \" . implode(\", \", $orderByClauses);\r\n\r\n  $stmt = $db->prepare($sql);\r\n  $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n  foreach ($params as $key => $value) {\r\n    $stmt->bindValue($key, $value);\r\n  }\r\n\r\n  $result = $stmt->execute();\r\n  if ($result) {\r\n    $subscriptions = array();\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n      $subscriptions[] = $row;\r\n    }\r\n  }\r\n\r\n  foreach ($subscriptions as $subscription) {\r\n    if ($subscription['inactive'] == 1 && isset($settings['hideDisabledSubscriptions']) && $settings['hideDisabledSubscriptions'] === 'true') {\r\n      continue;\r\n    }\r\n    $id = $subscription['id'];\r\n    $print[$id]['id'] = $id;\r\n    $print[$id]['logo'] = $subscription['logo'] != \"\" ? \"images/uploads/logos/\" . $subscription['logo'] : \"\";\r\n    $print[$id]['name'] = $subscription['name'] ?? \"\";\r\n    $cycle = $subscription['cycle'];\r\n    $frequency = $subscription['frequency'];\r\n    $print[$id]['billing_cycle'] = getBillingCycle($cycle, $frequency, $i18n);\r\n    $paymentMethodId = $subscription['payment_method_id'];\r\n    $print[$id]['currency_code'] = $currencies[$subscription['currency_id']]['code'];\r\n    $currencyId = $subscription['currency_id'];\r\n    $next_payment_timestamp = strtotime($subscription['next_payment']);\r\n    $formatted_date = $formatter->format($next_payment_timestamp);\r\n    $print[$id]['next_payment'] = $formatted_date;\r\n    $print[$id]['auto_renew'] = $subscription['auto_renew'];\r\n    $paymentIconFolder = (strpos($payment_methods[$paymentMethodId]['icon'], 'images/uploads/icons/') !== false) ? \"\" : \"images/uploads/logos/\";\r\n    $print[$id]['payment_method_icon'] = $paymentIconFolder . $payment_methods[$paymentMethodId]['icon'];\r\n    $print[$id]['payment_method_name'] = $payment_methods[$paymentMethodId]['name'];\r\n    $print[$id]['payment_method_id'] = $paymentMethodId;\r\n    $print[$id]['category_id'] = $subscription['category_id'];\r\n    $print[$id]['payer_user_id'] = $subscription['payer_user_id'];\r\n    $print[$id]['price'] = floatval($subscription['price']);\r\n    $print[$id]['progress'] = getSubscriptionProgress($cycle, $frequency, $subscription['next_payment']);\r\n    $print[$id]['inactive'] = $subscription['inactive'];\r\n    $print[$id]['url'] = $subscription['url'] ?? \"\";\r\n    $print[$id]['notes'] = $subscription['notes'] ?? \"\";\r\n    $print[$id]['replacement_subscription_id'] = $subscription['replacement_subscription_id'];\r\n\r\n    if (isset($settings['convertCurrency']) && $settings['convertCurrency'] === 'true' && $currencyId != $mainCurrencyId) {\r\n      $print[$id]['price'] = getPriceConverted($print[$id]['price'], $currencyId, $db);\r\n      $print[$id]['currency_code'] = $currencies[$mainCurrencyId]['code'];\r\n    }\r\n    if (isset($settings['showMonthlyPrice']) && $settings['showMonthlyPrice'] === 'true') {\r\n      $print[$id]['price'] = getPricePerMonth($cycle, $frequency, $print[$id]['price']);\r\n    }\r\n    if (isset($settings['showOriginalPrice']) && $settings['showOriginalPrice'] === 'true') {\r\n      $print[$id]['original_price'] = floatval($subscription['price']);\r\n      $print[$id]['original_currency_code'] = $currencies[$subscription['currency_id']]['code'];\r\n    }\r\n  }\r\n\r\n  if ($sortOrder == \"alphanumeric\") {\r\n    usort($print, function ($a, $b) {\r\n      return strnatcmp(strtolower($a['name']), strtolower($b['name']));\r\n    });\r\n    if ($settings['disabledToBottom'] === 'true') {\r\n      usort($print, function ($a, $b) {\r\n        return $a['inactive'] - $b['inactive'];\r\n      });\r\n    }\r\n  }\r\n\r\n  if ($sortOrder == \"category_id\") {\r\n    usort($print, function ($a, $b) use ($categories) {\r\n      return $categories[$a['category_id']]['order'] - $categories[$b['category_id']]['order'];\r\n    });\r\n  }\r\n  \r\n  if ($sortOrder == \"payment_method_id\") {\r\n    usort($print, function ($a, $b) use ($payment_methods) {\r\n      return $payment_methods[$a['payment_method_id']]['order'] - $payment_methods[$b['payment_method_id']]['order'];\r\n    });\r\n  }\r\n\r\n  if (isset($print)) {\r\n    printSubscriptions($print, $sort, $categories, $members, $i18n, $colorTheme, \"../../\", $settings['disabledToBottom'], $settings['mobileNavigation'], $settings['showSubscriptionProgress'], $currencies, $lang);\r\n  }\r\n\r\n  if (count($subscriptions) == 0) {\r\n    ?>\r\n    <div class=\"no-matching-subscriptions\">\r\n      <p>\r\n        <?= translate('no_matching_subscriptions', $i18n) ?>\r\n      </p>\r\n      <button class=\"button\" onClick=\"clearFilters()\">\r\n        <span clasS=\"fa-solid fa-minus-circle\"></span>\r\n        <?= translate('clear_filters', $i18n) ?>\r\n      </button>\r\n      <img src=\"images/siteimages/empty.png\" alt=\"<?= translate('empty_page', $i18n) ?>\" />\r\n    </div>\r\n    <?php\r\n  }\r\n}\r\n\r\n$db->close();\r\n?>"
  },
  {
    "path": "endpoints/user/budget.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$budget = $data[\"budget\"];\r\n\r\n$sql = \"UPDATE user SET budget = :budget WHERE id = :userId\";\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindValue(':budget', $budget, SQLITE3_TEXT);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate('user_details_saved', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error_updating_user_data', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n}\r\n\r\n\r\n?>"
  },
  {
    "path": "endpoints/user/delete_avatar.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$input = json_decode(file_get_contents('php://input'), true);\r\nif (isset($input['avatar'])) {\r\n    $baseDir = realpath(\"../../images/uploads/logos/avatars/\");\r\n    $avatar = $input['avatar'];\r\n    $avatarPath = \"images/uploads/logos/avatars/\" . $avatar;\r\n\r\n    $stmt = $db->prepare(\"SELECT id FROM uploaded_avatars WHERE user_id = :userId AND path = :path\");\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $stmt->bindValue(':path', $avatarPath, SQLITE3_TEXT);\r\n    $result = $stmt->execute();\r\n    $ownership = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if (!$ownership) {\r\n        echo json_encode([\r\n            \"success\" => false,\r\n            \"message\" => \"Security Error: You do not have permission to delete this file.\"\r\n        ]);\r\n        exit;\r\n    }\r\n\r\n    $cleanAvatar = rawurldecode($avatar);\r\n    $cleanAvatar = preg_replace('/[^a-zA-Z0-9_\\-\\.]/', '', $cleanAvatar);\r\n\r\n    $filePath = realpath($baseDir . DIRECTORY_SEPARATOR . $cleanAvatar);\r\n\r\n    if ($filePath === false || strpos($filePath, $baseDir) !== 0) {\r\n        echo json_encode([\r\n            \"success\" => false,\r\n            \"message\" => \"Invalid file path.\"\r\n        ]);\r\n        exit;\r\n    }\r\n\r\n    $sql = \"SELECT avatar FROM user WHERE id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    $userAvatar = $result->fetchArray(SQLITE3_ASSOC)['avatar'];\r\n\r\n    // Check if $avatarPath matches the avatar in the user table\r\n    if ($avatarPath === $userAvatar) {\r\n        echo json_encode(array(\"success\" => false, \"message\" => \"Avatar in use\"));\r\n    } else {\r\n        if (file_exists($filePath)) {\r\n            unlink($filePath);\r\n            $delStmt = $db->prepare(\"DELETE FROM uploaded_avatars WHERE id = :id\");\r\n            $delStmt->bindValue(':id', $ownership['id'], SQLITE3_INTEGER);\r\n            $delStmt->execute();\r\n            echo json_encode(array(\"success\" => true, \"message\" => translate(\"success\", $i18n)));\r\n        } else {\r\n            echo json_encode(array(\"success\" => false, \"message\" => translate(\"error\", $i18n)));\r\n        }\r\n    }\r\n} else {\r\n    echo json_encode(array(\"success\" => false, \"message\" => translate(\"error\", $i18n)));\r\n}\r\n\r\n?>"
  },
  {
    "path": "endpoints/user/disable_totp.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\nif (!function_exists('trigger_deprecation')) {\r\n    function trigger_deprecation($package, $version, $message, ...$args)\r\n    {\r\n        if (PHP_VERSION_ID >= 80000) {\r\n            trigger_error(sprintf($message, ...$args), E_USER_DEPRECATED);\r\n        }\r\n    }\r\n}\r\n\r\n$statement = $db->prepare('SELECT totp_enabled FROM user WHERE id = :id');\r\n$statement->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n$result = $statement->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($row['totp_enabled'] == 0) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => \"2FA is not enabled for this user\",\r\n        \"reload\" => true\r\n    ]));\r\n}\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\nif (isset($data['totpCode']) && $data['totpCode'] != \"\") {\r\n    require_once __DIR__ . '/../../libs/OTPHP/FactoryInterface.php';\r\n    require_once __DIR__ . '/../../libs/OTPHP/Factory.php';\r\n    require_once __DIR__ . '/../../libs/OTPHP/ParameterTrait.php';\r\n    require_once __DIR__ . '/../../libs/OTPHP/OTPInterface.php';\r\n    require_once __DIR__ . '/../../libs/OTPHP/OTP.php';\r\n    require_once __DIR__ . '/../../libs/OTPHP/TOTPInterface.php';\r\n    require_once __DIR__ . '/../../libs/OTPHP/TOTP.php';\r\n    require_once __DIR__ . '/../../libs/Psr/Clock/ClockInterface.php';\r\n    require_once __DIR__ . '/../../libs/OTPHP/InternalClock.php';\r\n    require_once __DIR__ . '/../../libs/constant_time_encoding/Binary.php';\r\n    require_once __DIR__ . '/../../libs/constant_time_encoding/EncoderInterface.php';\r\n    require_once __DIR__ . '/../../libs/constant_time_encoding/Base32.php';\r\n\r\n    $totp_code = $data['totpCode'];\r\n\r\n    $statement = $db->prepare('SELECT totp_secret FROM totp WHERE user_id = :id');\r\n    $statement->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $statement->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n    $secret = $row['totp_secret'];\r\n\r\n    $statement = $db->prepare('SELECT backup_codes FROM totp WHERE user_id = :id');\r\n    $statement->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n    $result = $statement->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n    $backupCodes = $row['backup_codes'];\r\n\r\n    $clock = new OTPHP\\InternalClock();\r\n    $totp = OTPHP\\TOTP::createFromSecret($secret, $clock);\r\n    $totp->setPeriod(30);\r\n\r\n    if ($totp->verify($totp_code, null, 15)) {\r\n        $statement = $db->prepare('UPDATE user SET totp_enabled = 0 WHERE id = :id');\r\n        $statement->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n        $statement->execute();\r\n\r\n        $statement = $db->prepare('DELETE FROM totp WHERE user_id = :id');\r\n        $statement->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n        $statement->execute();\r\n\r\n        die(json_encode([\r\n            \"success\" => true,\r\n            \"message\" => translate('success', $i18n),\r\n            \"reload\" => true\r\n        ]));\r\n    } else {\r\n        // Compare the TOTP code agains the backup codes\r\n        // Normalize TOTP input\r\n        $totp_code = strtolower(trim((string) $totp_code));\r\n\r\n        // Decode and normalize backup codes\r\n        $backupCodes = json_decode($backupCodes, true);\r\n        $normalizedBackupCodes = array_map(function ($code) {\r\n            return strtolower(trim((string) $code));\r\n        }, $backupCodes);\r\n\r\n        // Search for the normalized code\r\n        if (($key = array_search($totp_code, $normalizedBackupCodes)) !== false) {\r\n            // Match found, disable TOTP\r\n            $statement = $db->prepare('UPDATE user SET totp_enabled = 0 WHERE id = :id');\r\n            $statement->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n            $statement->execute();\r\n\r\n            $statement = $db->prepare('DELETE FROM totp WHERE user_id = :id');\r\n            $statement->bindValue(':id', $userId, SQLITE3_INTEGER);\r\n            $statement->execute();\r\n\r\n            die(json_encode([\r\n                \"success\" => true,\r\n                \"message\" => translate('success', $i18n),\r\n                \"reload\" => true\r\n            ]));\r\n        } else {\r\n            die(json_encode([\r\n                \"success\" => false,\r\n                \"message\" => translate('totp_code_incorrect', $i18n),\r\n                \"reload\" => false\r\n            ]));\r\n        }\r\n\r\n    }\r\n\r\n} else {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('fields_missing', $i18n),\r\n        \"reload\" => false\r\n    ]));\r\n}"
  },
  {
    "path": "endpoints/user/enable_totp.php",
    "content": "<?php\r\n\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\nif (!function_exists('trigger_deprecation')) {\r\n    function trigger_deprecation($package, $version, $message, ...$args)\r\n    {\r\n        if (PHP_VERSION_ID >= 80000) {\r\n            trigger_error(sprintf($message, ...$args), E_USER_DEPRECATED);\r\n        }\r\n    }\r\n}\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true) ?? [];\r\n\r\n$action = $data['action'] ?? '';\r\n\r\nif ($action === 'generate') {\r\n\r\n    function base32_encode($hex)\r\n    {\r\n        $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\r\n        $bin = '';\r\n        foreach (str_split($hex) as $char) {\r\n            $bin .= str_pad(base_convert($char, 16, 2), 4, '0', STR_PAD_LEFT);\r\n        }\r\n\r\n        $chunks = str_split($bin, 5);\r\n        $base32 = '';\r\n        foreach ($chunks as $chunk) {\r\n            $chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);\r\n            $index = bindec($chunk);\r\n            $base32 .= $alphabet[$index];\r\n        }\r\n\r\n        return $base32;\r\n    }\r\n\r\n\r\n    $secret = base32_encode(bin2hex(random_bytes(20)));\r\n    $qrCodeUrl = \"otpauth://totp/Wallos:\" . $_SESSION['username'] . \"?secret=\" . $secret . \"&issuer=Wallos\";\r\n\r\n    echo json_encode([\r\n        \"success\" => true,\r\n        \"secret\" => $secret,\r\n        \"qrCodeUrl\" => $qrCodeUrl,\r\n    ]);\r\n    exit;\r\n}\r\n\r\nif ($action === 'verify') {\r\n    if (isset($data['totpSecret']) && $data['totpSecret'] != \"\" && isset($data['totpCode']) && $data['totpCode'] != \"\") {\r\n        require_once __DIR__ . '/../../libs/OTPHP/FactoryInterface.php';\r\n        require_once __DIR__ . '/../../libs/OTPHP/Factory.php';\r\n        require_once __DIR__ . '/../../libs/OTPHP/ParameterTrait.php';\r\n        require_once __DIR__ . '/../../libs/OTPHP/OTPInterface.php';\r\n        require_once __DIR__ . '/../../libs/OTPHP/OTP.php';\r\n        require_once __DIR__ . '/../../libs/OTPHP/TOTPInterface.php';\r\n        require_once __DIR__ . '/../../libs/OTPHP/TOTP.php';\r\n        require_once __DIR__ . '/../../libs/Psr/Clock/ClockInterface.php';\r\n        require_once __DIR__ . '/../../libs/OTPHP/InternalClock.php';\r\n        require_once __DIR__ . '/../../libs/constant_time_encoding/Binary.php';\r\n        require_once __DIR__ . '/../../libs/constant_time_encoding/EncoderInterface.php';\r\n        require_once __DIR__ . '/../../libs/constant_time_encoding/Base32.php';\r\n\r\n        $secret = $data['totpSecret'];\r\n        $totp_code = $data['totpCode'];\r\n\r\n        // Check if user already has TOTP enabled\r\n        $stmt = $db->prepare(\"SELECT totp_enabled FROM user WHERE id = :user_id\");\r\n        $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($row['totp_enabled'] == 1) {\r\n            die(json_encode([\r\n                \"success\" => false,\r\n                \"message\" => translate('2fa_already_enabled', $i18n)\r\n            ]));\r\n        }\r\n\r\n        $clock = new OTPHP\\InternalClock();\r\n        \r\n        $totp = OTPHP\\TOTP::createFromSecret($secret, $clock);\r\n        $totp->setPeriod(30);\r\n\r\n        if ($totp->verify($totp_code, null, 15)) {\r\n            // Generate 10 backup codes\r\n            $backupCodes = [];\r\n            for ($i = 0; $i < 10; $i++) {\r\n                $backupCode = bin2hex(random_bytes(10));\r\n                $backupCodes[] = $backupCode;\r\n            }\r\n\r\n            // Remove old TOTP data\r\n            $stmt = $db->prepare(\"DELETE FROM totp WHERE user_id = :user_id\");\r\n            $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n\r\n            $stmt = $db->prepare(\"INSERT INTO totp (user_id, totp_secret, backup_codes, last_totp_used) VALUES (:user_id, :totp_secret, :backup_codes, :last_totp_used)\");\r\n            $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n            $stmt->bindValue(':totp_secret', $secret, SQLITE3_TEXT);\r\n            $stmt->bindValue(':backup_codes', json_encode($backupCodes), SQLITE3_TEXT);\r\n            $stmt->bindValue(':last_totp_used', time(), SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n\r\n            // Update user totp_enabled\r\n\r\n            $stmt = $db->prepare(\"UPDATE user SET totp_enabled = 1 WHERE id = :user_id\");\r\n            $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n\r\n            die(json_encode([\r\n                \"success\" => true,\r\n                \"backupCodes\" => $backupCodes,\r\n                \"message\" => translate('success', $i18n)\r\n            ]));\r\n        } else {\r\n            die(json_encode([\r\n                \"success\" => false,\r\n                \"message\" => translate('totp_code_incorrect', $i18n)\r\n            ]));\r\n        }\r\n\r\n    } else {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate('totp_code_incorrect', $i18n)\r\n        ]));\r\n    }\r\n}"
  },
  {
    "path": "endpoints/user/regenerateapikey.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\n$postData = file_get_contents(\"php://input\");\r\n$data = json_decode($postData, true);\r\n\r\n$apiKey = bin2hex(random_bytes(32));\r\n\r\n$sql = \"UPDATE user SET api_key = :apiKey WHERE id = :userId\";\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindValue(':apiKey', $apiKey, SQLITE3_TEXT);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n\r\nif ($result) {\r\n    $response = [\r\n        \"success\" => true,\r\n        \"message\" => translate('user_details_saved', $i18n),\r\n        \"apiKey\" => $apiKey\r\n    ];\r\n    echo json_encode($response);\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('error_updating_user_data', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n}"
  },
  {
    "path": "endpoints/user/save_user.php",
    "content": "<?php\r\nrequire_once '../../includes/connect_endpoint.php';\r\nrequire_once '../../includes/inputvalidation.php';\r\nrequire_once '../../includes/validate_endpoint.php';\r\n\r\nif (!file_exists('../../images/uploads/logos')) {\r\n    mkdir('../../images/uploads/logos', 0777, true);\r\n    mkdir('../../images/uploads/logos/avatars', 0777, true);\r\n}\r\n\r\nfunction update_exchange_rate($db, $userId)\r\n{\r\n    $query = \"SELECT api_key, provider FROM fixer WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n        if ($row) {\r\n            $apiKey = $row['api_key'];\r\n            $provider = $row['provider'];\r\n\r\n            $codes = \"\";\r\n            $query = \"SELECT id, name, symbol, code FROM currencies\";\r\n            $result = $db->query($query);\r\n            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n                $codes .= $row['code'] . \",\";\r\n            }\r\n            $codes = rtrim($codes, ',');\r\n\r\n            $query = \"SELECT u.main_currency, c.code FROM user u LEFT JOIN currencies c ON u.main_currency = c.id WHERE u.id = :userId\";\r\n            $stmt = $db->prepare($query);\r\n            $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $stmt->execute();\r\n            $row = $result->fetchArray(SQLITE3_ASSOC);\r\n            $mainCurrencyCode = $row['code'];\r\n            $mainCurrencyId = $row['main_currency'];\r\n\r\n            if ($provider === 1) {\r\n                $api_url = \"https://api.apilayer.com/fixer/latest?base=EUR&symbols=\" . $codes;\r\n                $context = stream_context_create([\r\n                    'http' => [\r\n                        'method' => 'GET',\r\n                        'header' => 'apikey: ' . $apiKey,\r\n                    ]\r\n                ]);\r\n                $response = file_get_contents($api_url, false, $context);\r\n            } else {\r\n                $api_url = \"http://data.fixer.io/api/latest?access_key=\" . $apiKey . \"&base=EUR&symbols=\" . $codes;\r\n                $response = file_get_contents($api_url);\r\n            }\r\n\r\n            $apiData = json_decode($response, true);\r\n\r\n            $mainCurrencyToEUR = $apiData['rates'][$mainCurrencyCode];\r\n\r\n            if ($apiData !== null && isset($apiData['rates'])) {\r\n                foreach ($apiData['rates'] as $currencyCode => $rate) {\r\n                    if ($currencyCode === $mainCurrencyCode) {\r\n                        $exchangeRate = 1.0;\r\n                    } else {\r\n                        $exchangeRate = $rate / $mainCurrencyToEUR;\r\n                    }\r\n                    $updateQuery = \"UPDATE currencies SET rate = :rate WHERE code = :code AND user_id = :userId\";\r\n                    $updateStmt = $db->prepare($updateQuery);\r\n                    $updateStmt->bindParam(':rate', $exchangeRate, SQLITE3_TEXT);\r\n                    $updateStmt->bindParam(':code', $currencyCode, SQLITE3_TEXT);\r\n                    $updateStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                    $updateResult = $updateStmt->execute();\r\n                }\r\n                $currentDate = new DateTime();\r\n                $formattedDate = $currentDate->format('Y-m-d');\r\n\r\n                $query = \"SELECT * FROM last_exchange_update WHERE user_id = :userId\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                $result = $stmt->execute();\r\n                $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                if ($row) {\r\n                    $query = \"UPDATE last_exchange_update SET date = :formattedDate WHERE user_id = :userId\";\r\n                } else {\r\n                    $query = \"INSERT INTO last_exchange_update (date, user_id) VALUES (:formattedDate, :userId)\";\r\n                }\r\n\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindParam(':formattedDate', $formattedDate, SQLITE3_TEXT);\r\n                $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                $resutl = $stmt->execute();\r\n\r\n                $db->close();\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n$demoMode = getenv('DEMO_MODE');\r\n\r\n$query = \"SELECT main_currency FROM user WHERE id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n$mainCurrencyId = $row['main_currency'];\r\n\r\nfunction sanitizeFilename($filename)\r\n{\r\n    $filename = preg_replace(\"/[^a-zA-Z0-9\\s]/\", \"\", $filename);\r\n    $filename = str_replace(\" \", \"-\", $filename);\r\n    $filename = str_replace(\".\", \"\", $filename);\r\n    return $filename;\r\n}\r\n\r\nfunction validateFileExtension($fileExtension)\r\n{\r\n    $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'jtif', 'webp'];\r\n    return in_array($fileExtension, $allowedExtensions);\r\n}\r\n\r\nfunction resizeAndUploadAvatar($uploadedFile, $uploadDir, $name)\r\n{\r\n    $targetWidth = 80;\r\n    $targetHeight = 80;\r\n\r\n    $timestamp = time();\r\n    $originalFileName = $uploadedFile['name'];\r\n    $fileExtension = strtolower(pathinfo($originalFileName, PATHINFO_EXTENSION));\r\n    $fileExtension = validateFileExtension($fileExtension) ? $fileExtension : 'png';\r\n    $fileName = $timestamp . '-avatars-' . sanitizeFilename($name) . '.' . $fileExtension;\r\n    $uploadFile = $uploadDir . $fileName;\r\n\r\n    if (move_uploaded_file($uploadedFile['tmp_name'], $uploadFile)) {\r\n        $fileInfo = getimagesize($uploadFile);\r\n\r\n        if ($fileInfo !== false) {\r\n            $width = $fileInfo[0];\r\n            $height = $fileInfo[1];\r\n\r\n            // Load the image based on its format\r\n            if ($fileExtension === 'png') {\r\n                $image = imagecreatefrompng($uploadFile);\r\n            } elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {\r\n                $image = imagecreatefromjpeg($uploadFile);\r\n            } elseif ($fileExtension === 'gif') {\r\n                $image = imagecreatefromgif($uploadFile);\r\n            } elseif ($fileExtension === 'webp') {\r\n                $image = imagecreatefromwebp($uploadFile);\r\n            } else {\r\n                // Handle other image formats as needed\r\n                return \"\";\r\n            }\r\n\r\n            // Enable alpha channel (transparency) for PNG images\r\n            if ($fileExtension === 'png') {\r\n                imagesavealpha($image, true);\r\n            }\r\n\r\n            $newWidth = $width;\r\n            $newHeight = $height;\r\n\r\n            if ($width > $targetWidth) {\r\n                $newWidth = (int)$targetWidth;\r\n                $newHeight = (int)(($targetWidth / $width) * $height);\r\n            }\r\n\r\n            if ($newHeight > $targetHeight) {\r\n                $newWidth = (int)(($targetHeight / $newHeight) * $newWidth);\r\n                $newHeight = (int)$targetHeight;\r\n            }\r\n\r\n            $resizedImage = imagecreatetruecolor($newWidth, $newHeight);\r\n            imagesavealpha($resizedImage, true);\r\n            $transparency = imagecolorallocatealpha($resizedImage, 0, 0, 0, 127);\r\n            imagefill($resizedImage, 0, 0, $transparency);\r\n            imagecopyresampled($resizedImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);\r\n\r\n            if ($fileExtension === 'png') {\r\n                imagepng($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {\r\n                imagejpeg($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'gif') {\r\n                imagegif($resizedImage, $uploadFile);\r\n            } elseif ($fileExtension === 'webp') {\r\n                imagewebp($resizedImage, $uploadFile);\r\n            } else {\r\n                return \"\";\r\n            }\r\n\r\n            imagedestroy($image);\r\n            imagedestroy($resizedImage);\r\n            return \"images/uploads/logos/avatars/\" . $fileName;\r\n        }\r\n    }\r\n\r\n    return \"\";\r\n}\r\n\r\nif (\r\n    isset($_SESSION['username']) \r\n    && isset($_POST['firstname'])\r\n    && isset($_POST['lastname'])\r\n    && isset($_POST['email']) && $_POST['email'] !== \"\"\r\n    && isset($_POST['avatar']) && $_POST['avatar'] !== \"\"\r\n    && isset($_POST['main_currency']) && $_POST['main_currency'] !== \"\"\r\n    && isset($_POST['language']) && $_POST['language'] !== \"\"\r\n) {\r\n\r\n    $firstname = validate($_POST['firstname']);\r\n    $lastname = validate($_POST['lastname']);\r\n    $email = validate($_POST['email']);\r\n\r\n    $query = \"SELECT email FROM user WHERE id = :user_id\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':user_id', $userId, SQLITE3_TEXT);\r\n    $result = $stmt->execute();\r\n    $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    $oldEmail = $user['email'];\r\n\r\n    if ($oldEmail != $email) {\r\n        $query = \"SELECT email FROM user WHERE email = :email AND id != :userId\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n        $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $otherUser = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n        if ($otherUser) {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('email_exists', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n            exit();\r\n        }\r\n    }\r\n\r\n    $avatar = filter_var($_POST['avatar'], FILTER_SANITIZE_URL);\r\n    $main_currency = $_POST['main_currency'];\r\n    $language = $_POST['language'];\r\n\r\n    if (!empty($_FILES['profile_pic'][\"name\"])) {\r\n        $file = $_FILES['profile_pic'];\r\n\r\n        $fileType = mime_content_type($_FILES['profile_pic']['tmp_name']);\r\n        if (strpos($fileType, 'image') === false) {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('fill_all_fields', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n            exit();\r\n        }\r\n        $name = $file['name'];\r\n        $avatar = resizeAndUploadAvatar($_FILES['profile_pic'], '../../images/uploads/logos/avatars/', $name);\r\n\r\n        if ($avatar !== \"\") {\r\n            $stmt = $db->prepare(\"INSERT INTO uploaded_avatars (user_id, path) VALUES (:userId, :path)\");\r\n            $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n            $stmt->bindParam(':path', $avatar, SQLITE3_TEXT);\r\n            $stmt->execute();\r\n        }\r\n    }\r\n\r\n    if (isset($_POST['password']) && $_POST['password'] != \"\" && !$demoMode) {\r\n        $password = $_POST['password'];\r\n        if (isset($_POST['confirm_password'])) {\r\n            $confirm = $_POST['confirm_password'];\r\n            if ($password != $confirm) {\r\n                $response = [\r\n                    \"success\" => false,\r\n                    \"message\" => translate('passwords_dont_match', $i18n)\r\n                ];\r\n                echo json_encode($response);\r\n                exit();\r\n            }\r\n        } else {\r\n            $response = [\r\n                \"success\" => false,\r\n                \"message\" => translate('passwords_dont_match', $i18n)\r\n            ];\r\n            echo json_encode($response);\r\n            exit();\r\n        }\r\n    }\r\n\r\n    if (isset($_POST['password']) && $_POST['password'] != \"\" && !$demoMode) {\r\n        $sql = \"UPDATE user SET avatar = :avatar, firstname = :firstname, lastname = :lastname, email = :email, password = :password, main_currency = :main_currency, language = :language WHERE id = :userId\";\r\n    } else {\r\n        $sql = \"UPDATE user SET avatar = :avatar, firstname = :firstname, lastname = :lastname, email = :email, main_currency = :main_currency, language = :language WHERE id = :userId\";\r\n    }\r\n\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindParam(':avatar', $avatar, SQLITE3_TEXT);\r\n    $stmt->bindParam(':firstname', $firstname, SQLITE3_TEXT);\r\n    $stmt->bindParam(':lastname', $lastname, SQLITE3_TEXT);\r\n    $stmt->bindParam(':email', $email, SQLITE3_TEXT);\r\n    $stmt->bindParam(':main_currency', $main_currency, SQLITE3_INTEGER);\r\n    $stmt->bindParam(':language', $language, SQLITE3_TEXT);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n\r\n    if (isset($_POST['password']) && $_POST['password'] != \"\" && !$demoMode) {\r\n        $hashedPassword = password_hash($password, PASSWORD_DEFAULT);\r\n        $stmt->bindParam(':password', $hashedPassword, SQLITE3_TEXT);\r\n    }\r\n\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $cookieExpire = time() + (30 * 24 * 60 * 60);\r\n        $oldLanguage = isset($_COOKIE['language']) ? $_COOKIE['language'] : \"en\";\r\n        $root = str_replace('/endpoints/user', '', dirname($_SERVER['PHP_SELF']));\r\n        $root = $root == '' ? '/' : $root;\r\n        setcookie('language', $language, [\r\n            'path' => $root,\r\n            'expires' => $cookieExpire,\r\n            'samesite' => 'Strict'\r\n        ]);\r\n        $_SESSION['firstname'] = $firstname;\r\n        $_SESSION['avatar'] = $avatar;\r\n        $_SESSION['main_currency'] = $main_currency;\r\n\r\n        if ($main_currency != $mainCurrencyId) {\r\n            update_exchange_rate($db, $userId);\r\n        }\r\n\r\n        $reload = $oldLanguage != $language;\r\n\r\n        $response = [\r\n            \"success\" => true,\r\n            \"message\" => translate('user_details_saved', $i18n),\r\n            \"reload\" => $reload\r\n        ];\r\n        echo json_encode($response);\r\n    } else {\r\n        $response = [\r\n            \"success\" => false,\r\n            \"message\" => translate('error_updating_user_data', $i18n)\r\n        ];\r\n        echo json_encode($response);\r\n    }\r\n\r\n    exit();\r\n} else {\r\n    $response = [\r\n        \"success\" => false,\r\n        \"message\" => translate('fill_all_fields', $i18n)\r\n    ];\r\n    echo json_encode($response);\r\n    exit();\r\n}\r\n"
  },
  {
    "path": "health.php",
    "content": "<?php\n\nheader('Content-Type: text/plain');\nhttp_response_code(200);\n\necho 'OK';\nexit;\n"
  },
  {
    "path": "images/siteicons/svg/automatic.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n<path fill=\"currentColor\" d=\"M 25 5 C 13.964844 5 5 13.964844 5 25 C 4.996094 25.359375 5.183594 25.695313 5.496094 25.878906 C 5.808594 26.058594 6.191406 26.058594 6.503906 25.878906 C 6.816406 25.695313 7.003906 25.359375 7 25 C 7 15.046875 15.046875 7 25 7 C 31.246094 7 36.726563 10.179688 39.957031 15 L 33 15 C 32.640625 14.996094 32.304688 15.183594 32.121094 15.496094 C 31.941406 15.808594 31.941406 16.191406 32.121094 16.503906 C 32.304688 16.816406 32.640625 17.003906 33 17 L 43 17 L 43 7 C 43.003906 6.730469 42.898438 6.46875 42.707031 6.277344 C 42.515625 6.085938 42.253906 5.980469 41.984375 5.984375 C 41.433594 5.996094 40.992188 6.449219 41 7 L 41 13.011719 C 37.347656 8.148438 31.539063 5 25 5 Z M 43.984375 23.984375 C 43.433594 23.996094 42.992188 24.449219 43 25 C 43 34.953125 34.953125 43 25 43 C 18.753906 43 13.269531 39.820313 10.042969 35 L 17 35 C 17.359375 35.007813 17.695313 34.816406 17.878906 34.507813 C 18.058594 34.195313 18.058594 33.808594 17.878906 33.496094 C 17.695313 33.1875 17.359375 32.996094 17 33 L 8.445313 33 C 8.316406 32.976563 8.1875 32.976563 8.058594 33 L 7 33 L 7 43 C 6.996094 43.359375 7.183594 43.695313 7.496094 43.878906 C 7.808594 44.058594 8.191406 44.058594 8.503906 43.878906 C 8.816406 43.695313 9.003906 43.359375 9 43 L 9 36.984375 C 12.648438 41.847656 18.460938 45 25 45 C 36.035156 45 45 36.035156 45 25 C 45.003906 24.730469 44.898438 24.46875 44.707031 24.277344 C 44.515625 24.085938 44.253906 23.980469 43.984375 23.984375 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/category.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Page-Setting--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>Page Setting Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"page-setting--page-setting-square-triangle-circle-line-combination-variation\">\r\n    <path id=\"Union\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M28 27a2 2 0 1 0 0 4h16a2 2 0 1 0 0 -4H28Zm-2 9a2 2 0 0 1 2 -2h16a2 2 0 1 1 0 4H28a2 2 0 0 1 -2 -2Zm0 7a2 2 0 0 1 2 -2h16a2 2 0 1 1 0 4H28a2 2 0 0 1 -2 -2Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n    <path id=\"Union_2\" class=\"accent-color\" fill-rule=\"evenodd\" d=\"M18.122 1.829C16.724 1.646 14.748 1.5 12 1.5c-2.748 0 -4.724 0.146 -6.122 0.329 -2.5 0.326 -4.097 2.314 -4.24 4.677C1.56 7.776 1.5 9.55 1.5 12c0 2.45 0.06 4.225 0.137 5.494 0.144 2.363 1.742 4.351 4.241 4.677 1.398 0.183 3.374 0.329 6.122 0.329 2.748 0 4.724 -0.146 6.122 -0.329 2.5 -0.326 4.097 -2.314 4.24 -4.677 0.078 -1.27 0.138 -3.044 0.138 -5.494 0 -2.45 -0.06 -4.225 -0.137 -5.494 -0.144 -2.363 -1.742 -4.351 -4.241 -4.677ZM1.5 36c0 -5.799 4.701 -10.5 10.5 -10.5S22.5 30.201 22.5 36 17.799 46.5 12 46.5 1.5 41.799 1.5 36ZM32.793 3.45a4.02 4.02 0 0 1 6.384 0 51.43 51.43 0 0 1 4.057 5.997 51.492 51.492 0 0 1 3.165 6.51 4.021 4.021 0 0 1 -3.193 5.53c-1.83 0.261 -4.338 0.513 -7.22 0.513a51.48 51.48 0 0 1 -7.22 -0.514 4.02 4.02 0 0 1 -3.193 -5.529 51.447 51.447 0 0 1 3.165 -6.51 51.494 51.494 0 0 1 4.055 -5.996Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/check.php",
    "content": "<?php\r\nheader('Content-Type: image/svg+xml');\r\necho '\r\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"-0.5 -0.5 16 16\" id=\"Check-Thick--Streamline-Plump.svg\" height=\"16\" width=\"16\">\r\n  <desc>Check Thick Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"check-thick--check-form-validation-checkmark-success-add-addition-1-tick\">\r\n    <path id=\"Union\" fill=\"currentColor\" d=\"M6.320125 7.069593749999999C6.423687500000001 7.188750000000001 6.516437499999999 7.296375 6.59834375 7.3925C6.787875 7.11834375 7.0450937499999995 6.747062499999999 7.36459375 6.289125C8.392468749999999 4.815781250000001 9.428374999999999 3.3480937500000003 10.472312500000001 1.8861249999999998C10.985093749999999 1.1675625 11.975093750000001 0.74753125 12.868593749999999 1.2433750000000001C13.4003125 1.5385312500000001 14.034593749999999 1.9957812499999998 14.53415625 2.67675C15.02965625 3.35196875 14.883781250000002 4.20115625 14.49759375 4.786031250000001C11.599375 9.17684375 9.50771875 11.851 8.34309375 13.24271875C7.5815624999999995 14.1531875 6.2510625 14.216125 5.3961250000000005 13.4051875C3.6985312500000003 11.7910625 2.10321875 10.072656250000001 0.61946875 8.259968749999999C0.09556250000000001 7.61825 -0.05946875 6.6445 0.5169374999999999 5.90565625C0.8909374999999999 5.426375 1.3136562500000002 5.0205625 1.6896875 4.70309375C2.5205937499999997 4.00146875 3.6814999999999998 4.1906875 4.36959375 4.91971875C5.03440625 5.6230625 5.68471875 6.3399375000000004 6.320125 7.0699375Z\" stroke-width=\"1\"></path>\r\n  </g>\r\n</svg>';\r\n?>"
  },
  {
    "path": "images/siteicons/svg/clone.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" height=\"48\" width=\"48\">\r\n  <g id=\"add-layer-2--layer-add-design-plus-layers-square-box\">\r\n    <path id=\"Union\" class=\"main-color\" d=\"M28.5 1.5c5.166 0 8.966 0.172 11.56 0.357 3.3 0.236 5.847 2.782 6.083 6.084 0.185 2.593 0.357 6.393 0.357 11.559 0 5.166 -0.172 8.966 -0.357 11.56 -0.236 3.3 -2.782 5.847 -6.084 6.083 -2.593 0.185 -6.393 0.357 -11.559 0.357 -5.166 0 -8.966 -0.172 -11.56 -0.357 -3.3 -0.236 -5.847 -2.782 -6.083 -6.084 -0.185 -2.593 -0.357 -6.393 -0.357 -11.559 0 -5.166 0.172 -8.966 0.357 -11.56 0.236 -3.3 2.782 -5.847 6.084 -6.083C19.534 1.672 23.334 1.5 28.5 1.5Z\" stroke-width=\"1\"/>\r\n    <path id=\"Union_2\" class=\"accent-color\" d=\"M19.5 10.5c5.166 0 8.966 0.172 11.56 0.357 3.3 0.236 5.847 2.782 6.083 6.084 0.185 2.593 0.357 6.393 0.357 11.559 0 5.166 -0.172 8.966 -0.357 11.56 -0.236 3.3 -2.782 5.847 -6.084 6.083 -2.593 0.185 -6.393 0.357 -11.559 0.357 -5.166 0 -8.966 -0.172 -11.56 -0.357 -3.3 -0.236 -5.847 -2.782 -6.083 -6.084C1.672 37.466 1.5 33.666 1.5 28.5c0 -5.166 0.172 -8.966 0.357 -11.56 0.236 -3.3 2.782 -5.847 6.084 -6.083 2.593 -0.185 6.393 -0.357 11.559 -0.357Z\" stroke-width=\"1\"/>\r\n    <path id=\"Subtract\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M16.113 21.628c0.047 -1.067 0.733 -1.95 1.795 -2.055 0.436 -0.043 0.963 -0.073 1.592 -0.073 0.628 0 1.156 0.03 1.592 0.073 1.062 0.106 1.748 0.988 1.795 2.055 0.034 0.788 0.068 1.89 0.09 3.395 1.504 0.022 2.607 0.056 3.395 0.09 1.067 0.047 1.95 0.733 2.055 1.795 0.043 0.436 0.073 0.963 0.073 1.592 0 0.628 -0.03 1.156 -0.073 1.592 -0.106 1.062 -0.989 1.748 -2.055 1.795 -0.788 0.034 -1.89 0.068 -3.395 0.09a119.605 119.605 0 0 1 -0.09 3.395c-0.047 1.066 -0.733 1.95 -1.795 2.055 -0.436 0.043 -0.963 0.073 -1.592 0.073 -0.628 0 -1.156 -0.03 -1.592 -0.073 -1.062 -0.106 -1.748 -0.989 -1.795 -2.055 -0.034 -0.788 -0.068 -1.89 -0.09 -3.395a119.605 119.605 0 0 1 -3.395 -0.09c-1.066 -0.047 -1.95 -0.733 -2.055 -1.795A16.127 16.127 0 0 1 10.5 28.5c0 -0.628 0.03 -1.156 0.073 -1.592 0.106 -1.062 0.989 -1.748 2.055 -1.795 0.788 -0.034 1.89 -0.068 3.395 -0.09 0.022 -1.504 0.056 -2.607 0.09 -3.395Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/delete.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Recycle-Bin-2--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <g id=\"recycle-bin-2--remove-delete-empty-bin-trash-garbage\">\r\n    <path id=\"Union\" class=\"accent-color\" d=\"M43.318 15.934a1.5 1.5 0 0 0 -1.618 -1.591c-3.016 0.246 -8.46 0.52 -17.721 0.52 -9.215 0 -14.65 -0.271 -17.675 -0.516a1.5 1.5 0 0 0 -1.618 1.59c0.888 13.84 1.74 21.07 2.253 24.547 0.332 2.252 1.85 4.217 4.226 4.788 2.445 0.588 6.55 1.227 12.837 1.227 6.286 0 10.392 -0.64 12.837 -1.227 2.375 -0.57 3.894 -2.536 4.226 -4.788 0.513 -3.477 1.365 -10.708 2.253 -24.55Z\" stroke-width=\"1\"/>\r\n    <path id=\"Union_2\" class=\"main-color\" d=\"M23.37 1a8 8 0 0 0 -7.034 4.188c-3.411 0.072 -6 0.182 -7.814 0.282 -2.312 0.127 -4.692 1.242 -5.7 3.605 -0.244 0.57 -0.475 1.212 -0.663 1.919 -0.68 2.548 1.302 4.622 3.657 4.822 3.057 0.258 8.614 0.548 18.161 0.548 9.549 0 15.106 -0.29 18.162 -0.549 2.374 -0.2 4.291 -2.261 3.751 -4.785a16.68 16.68 0 0 0 -0.294 -1.167c-0.824 -2.831 -3.517 -4.277 -6.188 -4.411a260.66 260.66 0 0 0 -7.744 -0.264A8 8 0 0 0 24.631 1H23.37Z\" stroke-width=\"1\"/>\r\n    <path id=\"Vector_831_Stroke\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M17.8 23.01a2 2 0 0 1 2.19 1.791l1 10a2 2 0 0 1 -3.98 0.398l-1 -10a2 2 0 0 1 1.79 -2.189Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n    <path id=\"Vector_832_Stroke\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M30.2 23.01a2 2 0 0 0 -2.19 1.791l-1 10a2 2 0 0 0 3.98 0.398l1 -10a2 2 0 0 0 -1.79 -2.189Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/edit.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Pencil-Square--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>Pencil Square Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"pencil-square--change-document-edit-modify-paper-pencil-write-writing\">\r\n    <path id=\"Rectangle_1098\" class=\"main-color\" d=\"M44.42 14.557c1.546 -1.546 2.146 -3.798 0.937 -5.62 -0.655 -0.986 -1.574 -2.156 -2.856 -3.438 -1.282 -1.282 -2.452 -2.201 -3.438 -2.856 -1.822 -1.21 -4.074 -0.61 -5.62 0.937L16.198 20.825c-0.578 0.578 -0.974 1.312 -1.053 2.126 -0.133 1.381 -0.256 3.962 0.024 7.967a2.063 2.063 0 0 0 1.913 1.913c4.005 0.28 6.586 0.157 7.967 0.024 0.814 -0.079 1.548 -0.475 2.126 -1.053L44.42 14.557Z\" stroke-width=\"1\"/>\r\n    <path id=\"Subtract\" class=\"accent-color\" d=\"m29.206 3.574 -15.13 15.13c-0.982 0.982 -1.76 2.337 -1.917 3.958 -0.15 1.562 -0.273 4.31 0.018 8.465a5.063 5.063 0 0 0 4.695 4.696c4.156 0.29 6.904 0.169 8.465 0.018 1.622 -0.157 2.977 -0.935 3.96 -1.918l15.129 -15.13c0.046 1.854 0.074 3.92 0.074 6.207 0 6.899 -0.254 11.783 -0.502 14.91 -0.261 3.291 -2.797 5.827 -6.087 6.088 -3.128 0.248 -8.012 0.502 -14.911 0.502 -6.899 0 -11.783 -0.254 -14.91 -0.502 -3.291 -0.261 -5.827 -2.797 -6.088 -6.087C1.754 36.782 1.5 31.898 1.5 25c0 -6.899 0.254 -11.783 0.502 -14.91 0.261 -3.291 2.797 -5.827 6.087 -6.088C11.217 3.754 16.101 3.5 23 3.5c2.287 0 4.352 0.028 6.206 0.074Z\" stroke-width=\"1\"/>\r\n    <path id=\"Intersect\" class=\"accent-color\" d=\"M43.775 15.202c-1.083 -1.72 -2.654 -3.79 -4.92 -6.057 -2.267 -2.266 -4.337 -3.838 -6.057 -4.92L29.15 7.873c1.516 0.84 3.668 2.307 6.169 4.808 2.5 2.5 3.967 4.653 4.808 6.169l3.648 -3.648Z\" stroke-width=\"1\"/>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/export_ical.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"mdi-calendar-export\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n    <path d=\"M12,22L16,18H13V12H11V18H8M19,4H18V2H16V4H8V2H6V4H5A2,2 0 0,0 3,6V20A2,2 0 0,0 5,22H8V20H5V9H19V20H16V22H19A2,2 0 0,0 21,20V6A2,2 0 0,0 19,4Z\" fill=\"currentColor\"/>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/logo.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2146\" height=\"798\" viewBox=\"0 0 2146 798\"\r\n    preserveAspectRatio=\"xMidYMid meet\">\r\n    <path\r\n        d=\"M0 0 C0.76655914 -0.01243744 1.53311829 -0.02487488 2.32290649 -0.03768921 C63.37476978 -0.83358072 63.37476978 -0.83358072 82.09375 16.875 C84.97342812 19.89873706 87.67151866 23.06656261 90.34375 26.2734375 C91.17519531 27.25699219 92.00664063 28.24054687 92.86328125 29.25390625 C104.34375 43.05712324 104.34375 43.05712324 104.34375 46.2734375 C105.21386719 46.65628906 105.21386719 46.65628906 106.1015625 47.046875 C116.77422212 52.88522887 122.85483536 64.93315104 127.94921875 75.40625 C130.76891834 81.18645209 133.74473121 86.88401076 136.71875 92.5859375 C137.56614746 94.21269409 137.56614746 94.21269409 138.43066406 95.87231445 C142.04093355 102.79545773 145.67691529 109.70470745 149.32373047 116.60864258 C153.73879587 124.98813102 158.06679845 133.41262347 162.40444946 141.83233643 C166.35311724 149.49591359 170.311297 157.15349182 174.34375 164.7734375 C178.73731325 173.07575701 183.04193906 181.42335853 187.34375 189.7734375 C192.49878639 199.77883769 197.68495073 209.76617733 202.94140625 219.71875 C210.69555882 234.40303561 218.32102238 249.15471703 225.94238281 263.90820312 C230.16902416 272.08852411 234.42067912 280.25479158 238.71850586 288.39794922 C246.87898084 303.86007838 254.92313639 319.38262285 262.94641113 334.91625977 C266.69645693 342.17535432 270.46141688 349.4257765 274.28125 356.6484375 C279.05520521 365.67518266 283.72744131 374.75399725 288.4039917 383.8314209 C292.74677593 392.25959449 297.11347552 400.67374053 301.546875 409.0546875 C305.693482 416.90706621 309.7600578 424.80072056 313.83380127 432.69104004 C317.62111084 440.02535897 321.42148637 447.35189803 325.28125 454.6484375 C329.69452565 462.99299282 334.02033082 471.38212154 338.34375 479.7734375 C342.88119547 488.57828636 347.42114228 497.38132157 352.03125 506.1484375 C352.40443359 506.85935547 352.77761719 507.57027344 353.16210938 508.30273438 C356.50681719 514.64361556 359.97649421 520.91291482 363.5546875 527.125 C370.61928174 539.40041581 376.62470814 551.64646926 381.15625 565.0859375 C381.4464502 565.94533936 381.73665039 566.80474121 382.03564453 567.69018555 C385.72598417 579.01702446 386.6507061 589.26788202 386.21875 601.1484375 C386.19452972 601.87820801 386.17030945 602.60797852 386.14535522 603.35986328 C385.21433697 629.90350969 378.18779951 660.80845406 361.34375 682.2734375 C360.35375 682.6034375 359.36375 682.9334375 358.34375 683.2734375 C358.034375 684.0778125 357.725 684.8821875 357.40625 685.7109375 C356.12432087 688.80264894 355.25503311 688.90877354 352.34375 690.2734375 C352.06789063 690.86253906 351.79203125 691.45164062 351.5078125 692.05859375 C345.89672537 702.73472263 327.17815705 714.6993958 316.34375 719.2734375 C313.94877625 719.52232647 313.94877625 719.52232647 311.2331543 719.52067566 C309.68128575 719.52697117 309.68128575 719.52697117 308.09806633 719.53339386 C306.40016803 719.52496082 306.40016803 719.52496082 304.66796875 719.51635742 C303.46832552 719.51823311 302.26868229 719.5201088 301.03268623 719.52204132 C297.726094 719.52507355 294.41974766 719.51911024 291.11318445 719.50905585 C287.60466258 719.50037763 284.09614605 719.50274958 280.58761597 719.50361633 C273.8367131 719.50334341 267.08587469 719.49181568 260.33499205 719.47602159 C254.90319918 719.46353135 249.47141657 719.45564993 244.03961182 719.45068359 C206.57185984 719.41526062 169.11266754 719.25683772 131.65052795 718.55973816 C130.44714279 718.53744644 129.24375763 718.51515472 128.00390625 718.4921875 C126.84600082 718.47066116 125.6880954 718.44913483 124.49510193 718.42695618 C111.148675 718.19400404 97.80365433 718.24092617 84.45581055 718.28369141 C69.51285802 718.32733388 54.58695131 718.16345594 39.64746094 717.84619141 C13.81278848 717.29800482 -12.00426219 717.22701112 -37.84375 717.2734375 C-69.0981899 717.32950771 -100.33974407 717.16109857 -131.59134293 716.73699951 C-151.31392652 716.46935927 -171.0331003 716.22884403 -190.7578125 716.2421875 C-191.96491127 716.24295036 -191.96491127 716.24295036 -193.19639587 716.24372864 C-200.78547434 716.25000682 -208.37445194 716.26682622 -215.9634552 716.30132294 C-228.67918678 716.35791115 -241.39092669 716.36991292 -254.1015625 715.9609375 C-255.06005096 715.93845947 -256.01853943 715.91598145 -257.006073 715.89282227 C-264.75541003 715.58530682 -270.77388775 713.59001122 -277.50390625 709.63671875 C-278.34904785 709.14606934 -279.19418945 708.65541992 -280.06494141 708.14990234 C-281.37857666 707.37573975 -281.37857666 707.37573975 -282.71875 706.5859375 C-288.39112194 703.27514883 -294.05020064 700.02202987 -299.92407227 697.08178711 C-306.64691753 693.70313094 -311.89843452 689.84338785 -317.2890625 684.5703125 C-319.61086532 682.2356244 -319.61086532 682.2356244 -322.2109375 680.27734375 C-324.75367154 678.19360164 -326.63539209 676.04812455 -328.65625 673.4609375 C-329.6153125 672.24470703 -329.6153125 672.24470703 -330.59375 671.00390625 C-331.274375 670.10285156 -331.955 669.20179688 -332.65625 668.2734375 C-333.46835937 667.25121094 -334.28046875 666.22898437 -335.1171875 665.17578125 C-338.47893327 660.41802361 -340.33613438 655.10311834 -342.28125 649.6484375 C-342.67368896 648.54902832 -343.06612793 647.44961914 -343.47045898 646.31689453 C-346.95090936 636.14033638 -349.05849584 626.01366762 -349.65625 615.2734375 C-349.7176416 614.24476563 -349.7790332 613.21609375 -349.84228516 612.15625 C-350.17853242 601.10154537 -348.37103992 590.52020065 -346.45800781 579.66552734 C-345.90529368 576.51799154 -345.37716777 573.36700719 -344.8515625 570.21484375 C-344.50145151 568.17683345 -344.14993707 566.13906364 -343.796875 564.1015625 C-343.64252991 563.17732483 -343.48818481 562.25308716 -343.3291626 561.30084229 C-341.51572514 551.18539354 -337.8773061 542.13616631 -332.96875 533.1484375 C-332.07938425 531.4909084 -331.19006221 529.83335585 -330.30078125 528.17578125 C-329.85686035 527.3544873 -329.41293945 526.53319336 -328.95556641 525.68701172 C-327.74180723 523.43236625 -326.54444393 521.17013677 -325.35546875 518.90234375 C-320.29967684 509.25971589 -315.05374609 499.73150426 -309.74609375 490.22558594 C-306.12805911 483.74489486 -302.62450656 477.24482467 -299.34375 470.5859375 C-296.39549052 464.63071425 -293.20546915 458.85401433 -289.91601562 453.08178711 C-287.78013488 449.31815853 -285.74407774 445.53852017 -283.84375 441.6484375 C-280.57424109 434.97394279 -276.97462962 428.48854639 -273.39794922 421.97558594 C-269.84374468 415.48891637 -266.37808275 408.96227863 -262.96875 402.3984375 C-259.34904034 395.43188661 -255.60684561 388.55639209 -251.71801758 381.73681641 C-249.05075505 377.03824286 -246.62417323 372.25230277 -244.25 367.3984375 C-241.96442237 362.91691273 -239.46193975 358.56593568 -236.97265625 354.1953125 C-234.7490888 350.27225073 -232.63849911 346.32603567 -230.65625 342.2734375 C-228.32268595 337.50259545 -225.78321142 332.88618896 -223.15625 328.2734375 C-220.60028724 323.78228324 -218.11286757 319.29376963 -215.84375 314.6484375 C-213.11292088 309.0745534 -210.14819614 303.64724628 -207.15625 298.2109375 C-201.78058718 288.44087689 -196.62134982 278.56824043 -191.55273438 268.63647461 C-188.53583245 262.75836759 -185.38009243 257.03722991 -181.93359375 251.40039062 C-180.42032629 248.88059205 -179.10636802 246.30193951 -177.84375 243.6484375 C-175.44597236 238.75218815 -172.85456024 234.00737403 -170.15625 229.2734375 C-167.52895443 224.6608934 -164.98979941 220.04424962 -162.65625 215.2734375 C-160.32268043 210.50258416 -157.78309577 205.88626318 -155.15625 201.2734375 C-152.03670057 195.78901654 -149.09479804 190.2627242 -146.3125 184.59765625 C-141.50648711 174.95162566 -136.55095355 165.34438761 -131.03125 156.0859375 C-127.95012392 150.91456391 -125.28559515 145.59413494 -122.63623047 140.19458008 C-120.12676842 135.1168878 -117.34420685 130.26056532 -114.4375 125.3984375 C-112.36150885 121.75634776 -110.50225627 118.03576987 -108.65625 114.2734375 C-105.76383234 108.38356249 -102.66152652 102.6927522 -99.296875 97.0546875 C-97.72385869 94.38805032 -96.24222907 91.69023907 -94.78125 88.9609375 C-91.32058926 82.57202537 -87.59800711 76.35718767 -83.79882812 70.16552734 C-82.71457669 68.3700255 -81.67417328 66.55548669 -80.640625 64.73046875 C-75.52966768 55.96124591 -69.09949667 49.55394715 -61.5234375 42.84375 C-56.2916332 38.15146937 -51.78346813 32.936375 -47.71875 27.2109375 C-37.58824601 13.52540176 -17.19372546 0.25306864 0 0 Z M-5.65625 317.2734375 C-6.88472656 317.61375 -8.11320312 317.9540625 -9.37890625 318.3046875 C-45.14701043 329.25335227 -73.58383706 355.1875031 -91.03125 387.8359375 C-102.68871392 410.50791506 -107.88605557 434.19934572 -107.90625 459.6484375 C-107.90692474 460.41043457 -107.90759949 461.17243164 -107.90829468 461.95751953 C-107.89229312 473.32550186 -107.41189941 484.20744382 -104.65625 495.2734375 C-104.48996094 495.99273438 -104.32367187 496.71203125 -104.15234375 497.453125 C-96.1055296 531.90541075 -75.07697562 565.94383794 -44.94921875 585.29296875 C-43.86253906 585.94652344 -42.77585937 586.60007812 -41.65625 587.2734375 C-41.04990723 587.64903809 -40.44356445 588.02463867 -39.81884766 588.41162109 C-12.67051817 605.14425046 21.60319019 611.53519416 52.90234375 604.2421875 C54.03800781 603.9225 55.17367187 603.6028125 56.34375 603.2734375 C57.57222656 602.933125 58.80070313 602.5928125 60.06640625 602.2421875 C95.83451043 591.29352273 124.27133706 565.3593719 141.71875 532.7109375 C153.37621392 510.03895994 158.57355557 486.34752928 158.59375 460.8984375 C158.59442474 460.13644043 158.59509949 459.37444336 158.59579468 458.58935547 C158.57979312 447.22137314 158.09939941 436.33943118 155.34375 425.2734375 C155.17746094 424.55414063 155.01117187 423.83484375 154.83984375 423.09375 C146.7930296 388.64146425 125.76447562 354.60303706 95.63671875 335.25390625 C94.55003906 334.60035156 93.46335937 333.94679688 92.34375 333.2734375 C91.73740723 332.89783691 91.13106445 332.52223633 90.50634766 332.13525391 C62.55579679 314.90818259 26.29678072 308.27871376 -5.65625 317.2734375 Z \"\r\n        fill=\"#8EBEF9\" transform=\"translate(876.65625,31.7265625)\" class=\"accent-color\" />\r\n    <path\r\n        d=\"M0 0 C10.81138995 7.3905986 14.39230898 20.38000632 17.52734375 32.46875 C17.90318604 33.91697395 17.90318604 33.91697395 18.28662109 35.39445496 C22.83811775 53.29244117 26.72671162 71.35896137 30.76063156 89.37862015 C32.01130145 94.96250137 33.26573326 100.54553746 34.51977539 106.12866211 C36.08333521 113.09095539 37.64634038 120.05337175 39.20729065 127.01625061 C42.6607968 142.41922583 46.13234482 157.81803861 49.61523056 173.21439552 C51.15298227 180.01492448 52.68763712 186.81615286 54.22208977 193.61742687 C55.44892253 199.05433334 56.67752289 204.49082755 57.91015625 209.92642212 C59.06413516 215.01568601 60.21307407 220.10606151 61.3581562 225.19733429 C61.77880496 227.06341029 62.20131317 228.92906825 62.62580681 230.79427338 C63.20396518 233.33580469 63.77552854 235.87873215 64.34545898 238.42211914 C64.51509407 239.16050176 64.68472916 239.89888437 64.8595047 240.65964222 C65.66564395 244.2906227 66.19214369 247.64461268 66 251.3671875 C66.66 251.3671875 67.32 251.3671875 68 251.3671875 C68.08763611 250.78816177 68.17527222 250.20913605 68.26556396 249.61256409 C69.82861978 240.1821088 72.38643633 231.04501273 74.890625 221.83203125 C75.38865247 219.9864127 75.88626126 218.14068113 76.38348389 216.29484558 C77.72230126 211.3289555 79.06647693 206.36453064 80.41168213 201.40036774 C81.82051709 196.19792283 83.2246265 190.99420333 84.62890625 185.79052734 C85.20128175 183.66973548 85.7736695 181.54894692 86.34606934 179.42816162 C86.77371834 177.84368351 86.77371834 177.84368351 87.21000671 176.22719574 C88.08243492 172.99575498 88.95550778 169.76448899 89.82873535 166.53326416 C93.39903998 153.31928587 96.95147938 140.10066826 100.48375702 126.87647438 C101.22973114 124.08379133 101.97615535 121.29122896 102.72271729 118.498703 C105.53667204 107.9734392 105.53667204 107.9734392 108.34436035 97.44650269 C112.53498514 81.71727462 116.74794336 65.99502318 121.24136353 50.34905624 C121.78730576 48.44714475 122.32870003 46.54392954 122.86987305 44.64065552 C127.38155573 29.10708898 132.74264547 11.8126888 147.41796875 2.8984375 C160.57004633 -3.94621407 177.55821584 -5.38572092 191.875 -1.0703125 C202.97103217 3.54499363 211.53510148 12.40895075 216.27392578 23.38037109 C219.73061508 31.80236479 222.09387829 40.51428606 224.5390625 49.26953125 C225.06301417 51.12590768 225.58732474 52.98218284 226.11196899 54.83836365 C231.74461068 74.82476722 237.1634315 94.86996409 242.56391144 114.92007446 C243.95349952 120.07625914 245.34670075 125.23146724 246.73974609 130.38671875 C247.31046326 132.49892974 247.88117246 134.61114288 248.45187378 136.72335815 C248.73559719 137.77343929 249.0193206 138.82352043 249.31164169 139.90542221 C250.17065055 143.08531108 251.02924965 146.26531031 251.88772583 149.44534302 C257.53523914 170.36349771 263.21250578 191.27353943 268.8952713 212.18214035 C270.09627749 216.60125976 271.29689905 221.02048366 272.49755859 225.43969727 C274.66461584 233.41564239 276.83211814 241.39146646 279 249.3671875 C280.70371662 246.2779687 281.5824788 243.37328033 282.34768677 239.93510437 C282.59656647 238.82898804 282.84544617 237.7228717 283.10186768 236.58323669 C283.36876404 235.37200638 283.6356604 234.16077606 283.91064453 232.9128418 C284.19735016 231.63154388 284.48405579 230.35024597 284.77944946 229.03012085 C285.5701973 225.4957451 286.35442662 221.95995038 287.13761377 218.42389274 C287.97503632 214.64857098 288.81864664 210.8746322 289.66159058 207.10054016 C291.27575543 199.86923943 292.88467345 192.63678206 294.49161136 185.40387231 C296.25612523 177.46246414 298.02519397 169.5220742 299.79526567 161.58190322 C300.31774255 159.23814618 300.84012381 156.89436785 301.36248398 154.55058479 C303.1124322 146.69905599 304.86316189 138.84770189 306.61480713 130.99655151 C306.78920106 130.21484537 306.96359499 129.43313924 307.14327359 128.62774503 C308.02522759 124.6744949 308.90731091 120.7212737 309.78958511 116.76809502 C311.7925133 107.79184998 313.78935714 98.81428002 315.78053439 89.83542103 C317.2378087 83.26606116 318.70093684 76.69802066 320.16812742 70.13086849 C321.03308894 66.25825038 321.89539364 62.38508917 322.7506752 58.51032066 C323.54569609 54.90874251 324.34823152 51.30891746 325.1562314 47.71022964 C325.58439558 45.79449551 326.00482541 43.87703765 326.42486191 41.95950508 C330.04449189 25.92488135 334.20398548 8.7828331 349 -0.6328125 C357.77321472 -4.27376379 370.59061667 -4.32903252 379.5625 -1.1953125 C386.81311309 2.39824881 394.20572937 8.48579595 397.31640625 16.12109375 C403.61822959 36.82708472 395.65485072 59.15063472 390.60549545 79.43080902 C389.85961643 82.4341009 389.12162872 85.439311 388.38356876 88.44453239 C385.85976055 98.72042318 383.31275349 108.99051723 380.7635498 119.26013184 C378.96909932 126.48981388 377.17802373 133.72032992 375.38722932 140.9509182 C368.94465014 166.96324723 362.49496074 192.97381274 356.03344727 218.98144531 C355.84453261 219.74185057 355.65561796 220.50225582 355.46097863 221.28570366 C353.40877425 229.54548616 351.35466307 237.8047893 349.29658508 246.06311035 C348.75325565 248.24356139 348.20994522 250.42401715 347.66665852 252.60447884 C347.31040402 254.03428469 346.95411875 255.46408287 346.59780204 256.89387321 C344.43460474 265.57587114 342.2849872 274.2609967 340.15405941 282.95097923 C338.98834278 287.70206104 337.81648139 292.45161609 336.64383888 297.20099258 C336.11623604 299.34505433 335.59157374 301.48984258 335.07034874 303.63546371 C332.52733438 314.10137191 329.80779914 324.47721649 326.51049805 334.73388672 C326.24944432 335.57230165 325.9883906 336.41071659 325.71942616 337.27453804 C322.16160791 347.95726891 315.04158075 358.4706606 305.03515625 364.0078125 C299.35147597 366.5020379 294.19013817 368.06766469 288 368.3671875 C287.23300781 368.40585937 286.46601563 368.44453125 285.67578125 368.484375 C272.53167161 368.85579411 261.49228462 365.87709903 251.75 356.8046875 C243.11124047 348.44132466 239.64213837 336.17246933 236.2578125 324.95703125 C235.82843613 323.55832708 235.39900129 322.15964086 234.96951294 320.76097107 C230.10013672 304.77776023 225.81999069 288.63259617 221.55051041 272.4811821 C220.38697676 268.08635742 219.21515243 263.69374501 218.04370117 259.30102539 C217.55642673 257.47349962 217.06916795 255.64596968 216.58192444 253.81843567 C216.21565288 252.44466735 216.21565288 252.44466735 215.84198189 251.04314613 C215.09422802 248.23713529 214.34739496 245.43088084 213.60081482 242.6245575 C208.14326137 222.11510371 202.61114676 201.62584525 197.07736969 181.13685226 C194.90073901 173.07720917 192.72562582 165.01715647 190.55078125 156.95703125 C190.3231031 156.11334299 190.09542496 155.26965473 189.86084747 154.40040016 C184.5526313 134.72837166 179.27416959 115.04836718 174 95.3671875 C169.52599619 109.03461432 165.79066871 122.88938409 162.04961967 136.77177691 C161.15031108 140.10606425 160.24727364 143.43934153 159.34460449 146.77272034 C157.81048793 152.43952088 156.27833416 158.10684871 154.74749756 163.77453613 C152.36591403 172.59137238 149.9788339 181.40671544 147.59033203 190.22167969 C147.39325079 190.94904414 147.19616954 191.67640859 146.99311614 192.42581439 C146.00325597 196.07900969 145.01331641 199.73218347 144.02331924 203.38534164 C143.04157283 207.0081185 142.06007052 210.63096144 141.07864082 214.25382411 C135.41085847 235.17564603 129.74171723 256.09707579 124.00421143 276.99990845 C123.26748959 279.68415967 122.53303499 282.36901152 121.80096436 285.05453491 C118.33815948 297.74399621 114.77713603 310.40325093 110.9558754 322.98987579 C110.40632804 324.80513614 109.86602535 326.62318744 109.32614136 328.44134521 C104.68851032 343.497614 98.52664845 356.36515049 84.31640625 364.21875 C73.21930253 369.72051812 59.7800719 369.69389266 48 366.3671875 C35.36673866 361.64372477 28.41667851 353.89444249 22.80810547 341.78735352 C20.74174421 336.94697731 19.25465392 331.98357283 17.8046875 326.9296875 C17.50581131 325.89480698 17.20693512 324.85992645 16.89900208 323.79368591 C13.02593829 310.18770001 9.6504894 296.45268434 6.25495529 282.72177505 C5.47645512 279.5772268 4.69422211 276.43361162 3.91199589 273.28998852 C1.26482675 262.6514855 -1.36948582 252.0098192 -4 241.3671875 C-4.18543365 240.61710419 -4.37086731 239.86702087 -4.56192017 239.09420776 C-7.95740326 225.35911132 -11.34725677 211.62263094 -14.7265625 197.88354492 C-19.01885916 180.43524147 -23.33060358 162.99190255 -27.66301155 145.55351067 C-28.51515953 142.1229676 -29.36685198 138.69231179 -30.21836853 135.26161194 C-30.64040605 133.56126313 -30.64040605 133.56126313 -31.07096958 131.82656384 C-31.63940133 129.536337 -32.20781347 127.24610529 -32.77620602 124.95586872 C-34.44218076 118.24426445 -36.11103937 111.53338558 -37.78198051 104.82301617 C-38.83362408 100.5966341 -39.88244027 96.36955376 -40.93058014 92.14230156 C-41.41561422 90.18858008 -41.90178636 88.23514067 -42.3892746 86.28203011 C-44.98863538 75.86532718 -47.47554762 65.43230959 -49.77026367 54.94384766 C-49.97988141 53.98950018 -50.18949915 53.0351527 -50.40546894 52.0518856 C-53.22087054 39.00599999 -55.56087831 25.68322223 -49 13.3671875 C-46.90336451 10.41599916 -44.60697316 7.87297677 -42 5.3671875 C-41.236875 4.6246875 -40.47375 3.8821875 -39.6875 3.1171875 C-28.69273247 -4.04219601 -12.08968599 -6.08682107 0 0 Z \"\r\n        fill=\"#FFFFFF\" transform=\"translate(145,193.6328125)\" class=\"text-color\" />\r\n    <path\r\n        d=\"M0 0 C35.84808566 -0.85992409 68.72655009 13.86279043 94.48046875 38.125 C95.14433594 38.72441406 95.80820313 39.32382812 96.4921875 39.94140625 C103.33897079 46.37154958 110.59668849 54.85225169 114.55078125 63.453125 C115.35939464 65.3010285 115.35939464 65.3010285 117.3984375 65.98046875 C124.48837726 69.87794032 127.8219759 85.60425199 130.29296875 92.9375 C130.58276611 93.79577393 130.87256348 94.65404785 131.17114258 95.53833008 C134.86233892 106.86633441 135.78747466 117.11807461 135.35546875 129 C135.31913834 130.09465576 135.31913834 130.09465576 135.28207397 131.21142578 C134.35105572 157.75507219 127.32451826 188.66001656 110.48046875 210.125 C109.49046875 210.455 108.50046875 210.785 107.48046875 211.125 C107.17109375 211.929375 106.86171875 212.73375 106.54296875 213.5625 C105.26103962 216.65421144 104.39175186 216.76033604 101.48046875 218.125 C101.20460938 218.71410156 100.92875 219.30320312 100.64453125 219.91015625 C95.00525687 230.63991656 76.33511235 242.46766159 65.48046875 247.125 C63.31721878 247.37109566 63.31721878 247.37109566 60.93731689 247.36550903 C60.03150375 247.36825836 59.12569061 247.37100769 58.19242859 247.37384033 C56.71320671 247.3630545 56.71320671 247.3630545 55.20410156 247.35205078 C53.63777733 247.35248886 53.63777733 247.35248886 52.03981018 247.35293579 C48.58765357 247.35154224 45.13571267 247.33597962 41.68359375 247.3203125 C39.29034544 247.31658247 36.8970956 247.31373545 34.50384521 247.31173706 C28.20407904 247.30409988 21.90440504 247.28445055 15.60467529 247.2623291 C9.17656705 247.24187258 2.74844391 247.23273575 -3.6796875 247.22265625 C-16.29300808 247.20120483 -28.90626476 247.16745207 -41.51953125 247.125 C-41.51953125 246.465 -41.51953125 245.805 -41.51953125 245.125 C-42.50050781 245.22941406 -43.48148438 245.33382812 -44.4921875 245.44140625 C-59.29491892 246.52714951 -68.26655763 239.93034489 -79.26953125 230.75 C-81.38479825 228.90870313 -83.45804965 227.02615899 -85.51953125 225.125 C-86.93878906 223.82949219 -86.93878906 223.82949219 -88.38671875 222.5078125 C-109.24372713 202.59616427 -121.7959598 176.36733277 -126.51953125 148.125 C-126.65230469 147.36316406 -126.78507813 146.60132813 -126.921875 145.81640625 C-130.26684608 122.30996564 -125.28951 98.92580104 -116.328125 77.2421875 C-115.35487445 74.9621093 -115.35487445 74.9621093 -115.14453125 71.6875 C-114.42571806 67.59026481 -113.12222345 65.34009036 -110.51953125 62.125 C-109.03453125 61.63 -109.03453125 61.63 -107.51953125 61.125 C-105.84881628 58.94606055 -104.29783365 56.77460841 -102.76953125 54.5 C-99.08526211 49.17349964 -95.32685735 44.49415836 -90.51953125 40.125 C-89.75640625 39.34125 -88.99328125 38.5575 -88.20703125 37.75 C-87.32015625 36.88375 -86.43328125 36.0175 -85.51953125 35.125 C-84.84664062 34.46757813 -84.17375 33.81015625 -83.48046875 33.1328125 C-60.90972496 11.8854348 -30.58239742 1.51726623 0 0 Z \"\r\n        fill=\"#2858C5\" transform=\"translate(1127.51953125,503.875)\" class=\"main-color\" />\r\n    <path\r\n        d=\"M0 0 C0.73283203 0.19335938 1.46566406 0.38671875 2.22070312 0.5859375 C19.41256843 5.32650291 33.55631276 13.2396321 46 26 C46.73734375 26.72832031 47.4746875 27.45664062 48.234375 28.20703125 C68.25055796 49.11540841 73.13407704 79.44066264 73.203125 107.1796875 C73.20736607 108.33980032 73.20736607 108.33980032 73.21169281 109.52334976 C73.22661969 113.62364279 73.23588782 117.72390128 73.24023438 121.82421875 C73.2446041 125.17361433 73.25844598 128.52275017 73.28125 131.87207031 C73.50792641 165.8372186 72.30852983 200.53986772 48 227 C47.26394531 227.82757812 46.52789063 228.65515625 45.76953125 229.5078125 C29.13542329 247.25447907 4.232332 257.07553786 -19.8737793 258.17700195 C-21.27383018 258.20317356 -22.67400309 258.22333081 -24.07421875 258.23828125 C-24.83183502 258.2480751 -25.58945129 258.25786896 -26.37002563 258.26795959 C-58.19429958 258.59487255 -58.19429958 258.59487255 -73 254 C-73.70801758 253.78569336 -74.41603516 253.57138672 -75.14550781 253.35058594 C-98.49212752 246.04346624 -117.71527471 230.34618212 -129.1875 208.6796875 C-139.51939592 188.18006774 -142.14680045 166.04884842 -142.16796875 143.3359375 C-142.17129715 142.0638443 -142.17462555 140.7917511 -142.17805481 139.48110962 C-142.18308866 136.80361397 -142.18590045 134.12622004 -142.18530273 131.44873047 C-142.18522169 128.74358402 -142.19213844 126.03860768 -142.20581055 123.33349609 C-142.39149625 86.00721507 -139.30880584 50.71482947 -111.80859375 22.7734375 C-83.33435517 -3.61235067 -36.74168409 -9.74131105 0 0 Z M-57 58 C-57.87785156 58.4125 -58.75570313 58.825 -59.66015625 59.25 C-69.96210483 64.71951116 -76.26377919 74.1244115 -80 85 C-82.24534705 93.03957091 -82.31008219 100.80830756 -82.30078125 109.11328125 C-82.3046783 110.57382832 -82.309015 112.03437428 -82.31376648 113.49491882 C-82.3211921 116.54507126 -82.32096463 119.59510787 -82.31567383 122.64526367 C-82.30989074 126.52155778 -82.32679968 130.39737226 -82.3500185 134.27358723 C-82.36487315 137.28766066 -82.36561165 140.30162176 -82.36250877 143.31572533 C-82.36301634 144.74322647 -82.36814576 146.17073487 -82.37832069 147.59819984 C-82.47978577 163.82363387 -79.31314901 178.05575996 -67.97265625 190.21875 C-57.7778462 199.42436066 -45.40018765 201.98538693 -32 202 C-18.01242667 201.10648486 -6.44194831 196.53788873 3 186 C12.76533669 173.35136346 14.14422936 158.15442025 14.203125 142.75 C14.20882507 141.57848389 14.21452515 140.40696777 14.22039795 139.19995117 C14.22981475 136.72559502 14.23636867 134.25122652 14.24023438 131.77685547 C14.24989707 128.03980894 14.2809357 124.303323 14.3125 120.56640625 C14.40386608 100.20978974 14.11914888 80.60463981 -0.80859375 64.96875 C-12.90121867 54.1047875 -27.01877662 52.98456374 -42.59179688 53.78857422 C-47.82476893 54.24799776 -52.30468799 55.71309004 -57 58 Z \"\r\n        fill=\"#FFFFFF\" transform=\"translate(1734,306)\" class=\"text-color\" />\r\n    <path\r\n        d=\"M0 0 C-1.52848492 5.07882646 -4.65105172 8.33386385 -8.125 12.1875 C-9.31131538 13.52869363 -10.49621382 14.87114193 -11.6796875 16.21484375 C-12.25299805 16.86437012 -12.82630859 17.51389648 -13.41699219 18.18310547 C-16.07640665 21.2354439 -18.58234535 24.40193654 -21.0859375 27.58203125 C-24.0421601 31.3165247 -27.08425348 34.98006301 -30.1171875 38.65234375 C-32.09774658 41.05145848 -32.09774658 41.05145848 -34 44 C-37.35200764 43.31556885 -40.08493261 42.05033835 -43.12109375 40.48828125 C-56.48224196 33.75216032 -69.38230745 28.28472769 -84 25 C-85.13953125 24.73703125 -86.2790625 24.4740625 -87.453125 24.203125 C-104.44429851 20.97915874 -126.50646013 20.91775213 -141.9453125 29.546875 C-146.64751407 32.87238637 -150.61353984 37.31551333 -152 43 C-152.44172322 48.56571258 -152.33863108 52.30505005 -149 57 C-139.85862181 64.88667922 -127.11019634 65.55741089 -115.61221313 66.70678711 C-112.44906757 67.02412167 -109.28850032 67.3643314 -106.12760925 67.70327759 C-104.49783233 67.87798062 -102.86795885 68.05178512 -101.23799133 68.22470093 C-30.88944085 75.69396589 -30.88944085 75.69396589 -12 98 C1.91562321 116.44033505 3.02544534 140.35173998 0.5703125 162.61328125 C-1.69787949 176.7684794 -7.18739546 189.48649513 -17 200 C-17.62648438 200.70511719 -18.25296875 201.41023437 -18.8984375 202.13671875 C-36.02784411 220.48965441 -63.88515335 227.95308369 -88.27560425 229.10772705 C-90.62682595 229.16308415 -92.97628887 229.19027737 -95.328125 229.203125 C-96.19106674 229.21018463 -97.05400848 229.21724426 -97.94309998 229.22451782 C-133.84742069 229.45166205 -133.84742069 229.45166205 -149 226 C-150.35883301 225.70198486 -150.35883301 225.70198486 -151.74511719 225.39794922 C-174.45383332 220.29937269 -194.66902165 211.99265624 -213.8125 198.75 C-214.41819824 198.33766113 -215.02389648 197.92532227 -215.64794922 197.50048828 C-217.8937693 195.89540086 -219.7305273 194.43933834 -221.18359375 192.0703125 C-220.8780923 188.62529611 -218.49856179 186.56592643 -216.25 184.125 C-215.21911266 182.9590092 -214.19053274 181.79097499 -213.1640625 180.62109375 C-212.36983887 179.72173096 -212.36983887 179.72173096 -211.55957031 178.80419922 C-209.14518223 176.01109842 -206.8840037 173.10664822 -204.625 170.1875 C-198.53702753 162.36026619 -192.27302148 154.67921548 -186 147 C-181.68506687 149.38229957 -177.37504446 151.76961865 -173.11328125 154.24609375 C-152.14861033 166.38453752 -130.23909393 174.43240005 -105.8125 174.3125 C-104.62648193 174.30758545 -103.44046387 174.3026709 -102.21850586 174.29760742 C-87.16327555 174.09728194 -70.72725633 172.45729963 -59 162 C-54.60436719 156.10468071 -53.27667122 150.25959069 -54 143 C-54.94127487 138.90878956 -56.21244544 135.85915042 -59.0625 132.75 C-65.55716307 128.88083902 -72.03496754 126.82055204 -79.4765625 125.53125 C-80.57470779 125.33963539 -80.57470779 125.33963539 -81.69503784 125.14414978 C-90.48609802 123.67110739 -99.33961773 122.77949322 -108.20281982 121.8659668 C-176.01531195 114.82367695 -176.01531195 114.82367695 -194 95 C-208.18646826 76.37515491 -209.01167584 53.85718733 -206.72753906 31.31054688 C-204.46253745 16.43725019 -196.82911525 2.29434413 -186 -8 C-185.34128906 -8.64324219 -184.68257812 -9.28648437 -184.00390625 -9.94921875 C-138.1887483 -52.44577371 -45.06825336 -33.9337437 0 0 Z \"\r\n        fill=\"#FFFFFF\" transform=\"translate(2071,335)\" class=\"text-color\" />\r\n    <path\r\n        d=\"M0 0 C19.47 0 38.94 0 59 0 C59.01458252 8.59160156 59.02916504 17.18320313 59.04418945 26.03515625 C59.09295165 53.52794707 59.15636208 81.02068666 59.22898628 108.5134244 C59.24025439 112.78417717 59.25139285 117.05493027 59.26245117 121.32568359 C59.26465486 122.17591032 59.26685854 123.02613705 59.269129 123.90212822 C59.3044972 137.66718227 59.32936403 151.43223627 59.35034702 165.19731891 C59.37211287 179.32254891 59.40517076 193.44771996 59.44870156 207.57289982 C59.47520476 216.28823456 59.49290994 225.00349587 59.49934604 233.71886891 C59.504604 239.69608641 59.52083467 245.67321151 59.54566566 251.65037948 C59.55966672 255.0984193 59.56885671 258.54629377 59.56500816 261.99436378 C59.56335858 265.73585686 59.58002914 269.47683871 59.60127258 273.21826172 C59.59642501 274.30387925 59.59157743 275.38949678 59.58658296 276.50801182 C59.644664 282.70331354 60.31171664 287.38421297 63 293 C72.33004539 301.87492123 77.72610297 298.64384191 94 300 C94 318.48 94 336.96 94 356 C83.605 356.12375 83.605 356.12375 73 356.25 C70.84855469 356.28641602 68.69710938 356.32283203 66.48046875 356.36035156 C47.42165918 356.54843263 31.00968748 353.82298501 16.625 340.1875 C3.25486262 325.98172903 -0.33724668 308.19247314 -0.24050903 289.21211243 C-0.24221788 287.96948094 -0.24392673 286.72684945 -0.24568737 285.44656247 C-0.24839518 282.02805291 -0.24220934 278.60977645 -0.23213124 275.19129527 C-0.22344461 271.49343116 -0.22642057 267.79557347 -0.22793579 264.09770203 C-0.22898136 257.70105114 -0.22273915 251.30444125 -0.21146011 244.90780067 C-0.19516346 235.65948921 -0.18999286 226.41119744 -0.18748413 217.16287317 C-0.18311355 202.1558528 -0.16982176 187.14885424 -0.15087891 172.1418457 C-0.1325036 157.56925566 -0.11835518 142.99667156 -0.10986328 128.42407227 C-0.10933783 127.52488956 -0.10881239 126.62570685 -0.10827102 125.69927619 C-0.10566083 121.18806368 -0.10313344 116.67685114 -0.10064721 112.16563857 C-0.07993455 74.77707299 -0.04307551 37.38854781 0 0 Z \"\r\n        fill=\"#FFFFFF\" transform=\"translate(1454,205)\" class=\"text-color\" />\r\n    <path\r\n        d=\"M0 0 C19.47 0 38.94 0 59 0 C59.01458252 8.55607178 59.02916504 17.11214355 59.04418945 25.92749023 C59.09295956 53.31175397 59.15637219 80.69596632 59.22898628 108.0801768 C59.24025428 112.33461952 59.25139276 116.58906258 59.26245117 120.84350586 C59.26465486 121.69047966 59.26685854 122.53745346 59.269129 123.41009314 C59.3044898 137.11950612 59.32936081 150.82891905 59.35034702 164.53836071 C59.37211877 178.60822504 59.40518226 192.67803025 59.44870156 206.74784428 C59.4751946 215.42786026 59.49290726 224.1078026 59.49934604 232.787857 C59.50460711 238.7421882 59.52084821 244.69642677 59.54566566 250.65070831 C59.55965608 254.08469527 59.56886071 257.5185165 59.56500816 260.95253372 C59.56335647 264.68053993 59.58004717 268.40803498 59.60127258 272.13597107 C59.59642501 273.21478481 59.59157743 274.29359856 59.58658296 275.40510362 C59.64512868 281.64073186 60.18758294 287.34099656 63 293 C72.11563772 301.89330509 78.33841876 298.69486823 94 300 C94 318.48 94 336.96 94 356 C83.5121875 356.0928125 83.5121875 356.0928125 72.8125 356.1875 C70.63374268 356.21481201 68.45498535 356.24212402 66.21020508 356.27026367 C47.44195845 356.40865513 31.38471469 353.97857628 17.25 340.41015625 C-0.63937627 321.24740424 -0.45881262 297.44118597 -0.34819686 272.78005314 C-0.33510095 269.11022624 -0.33963542 265.44041258 -0.34190369 261.77056885 C-0.34346796 255.43018766 -0.33414801 249.08989992 -0.31719017 242.74954224 C-0.29268888 233.58274608 -0.28498359 224.41599458 -0.2812262 215.24916923 C-0.27467906 200.37246749 -0.25476534 185.49581543 -0.22631836 170.61914062 C-0.19873261 156.17773688 -0.1775212 141.73634669 -0.16479492 127.29492188 C-0.16400675 126.40302678 -0.16321858 125.51113168 -0.16240653 124.59220946 C-0.15849109 120.11731832 -0.15470004 115.64242708 -0.15097082 111.16753578 C-0.11992908 74.11164639 -0.06465543 37.05584898 0 0 Z \"\r\n        fill=\"#FFFFFF\" transform=\"translate(1308,205)\" class=\"text-color\" />\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/manual.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n<path fill=\"currentColor\" d=\"M 32 1 C 31.0625 1 30.085938 1.324219 29.3125 2 C 28.539063 2.675781 28 3.734375 28 5 L 28 5.40625 C 27.519531 5.1875 27.0625 5 26.5 5 C 24.578125 5 23 6.578125 23 8.5 C 23 10.421875 24.578125 12 26.5 12 C 27.089844 12 27.625 11.824219 28.25 11.625 C 28.875 11.425781 29.539063 11.195313 30.15625 10.9375 C 31 10.585938 31.605469 10.28125 32 10.09375 C 32.394531 10.28125 33 10.585938 33.84375 10.9375 C 34.460938 11.195313 35.125 11.425781 35.75 11.625 C 36.375 11.824219 36.910156 12 37.5 12 C 39.421875 12 41 10.421875 41 8.5 C 41 6.578125 39.421875 5 37.5 5 C 36.9375 5 36.480469 5.1875 36 5.40625 L 36 4.90625 C 36 3.675781 35.429688 2.625 34.65625 1.96875 C 33.882813 1.3125 32.9375 1 32 1 Z M 32 3 C 32.460938 3 33 3.179688 33.375 3.5 C 33.75 3.820313 34 4.238281 34 4.90625 L 34 6.46875 C 33.894531 6.53125 33.789063 6.59375 33.6875 6.65625 C 32.984375 7.09375 32.421875 7.488281 32 7.78125 C 31.578125 7.488281 31.015625 7.09375 30.3125 6.65625 C 30.210938 6.59375 30.105469 6.53125 30 6.46875 L 30 5 C 30 4.265625 30.25 3.824219 30.625 3.5 C 31 3.175781 31.539063 3 32 3 Z M 26.5 7 C 26.492188 7 26.972656 7.121094 27.5 7.375 C 28.027344 7.628906 28.667969 7.980469 29.25 8.34375 C 29.621094 8.574219 29.625 8.605469 29.9375 8.8125 C 29.671875 8.929688 29.675781 8.9375 29.375 9.0625 C 28.789063 9.304688 28.195313 9.574219 27.65625 9.75 C 27.117188 9.925781 26.613281 10 26.5 10 C 25.625 10 25 9.375 25 8.5 C 25 7.625 25.625 7 26.5 7 Z M 37.5 7 C 38.375 7 39 7.625 39 8.5 C 39 9.375 38.375 10 37.5 10 C 37.386719 10 36.882813 9.925781 36.34375 9.75 C 35.804688 9.574219 35.210938 9.304688 34.625 9.0625 C 34.324219 8.9375 34.328125 8.929688 34.0625 8.8125 C 34.375 8.605469 34.378906 8.574219 34.75 8.34375 C 35.332031 7.980469 35.972656 7.628906 36.5 7.375 C 37.027344 7.121094 37.507813 7 37.5 7 Z M 30 12.1875 L 28 13 L 28 15.5 C 27.398438 15.199219 26.800781 15 26 15 C 24.398438 15 23.292969 15.710938 22.59375 16.8125 C 21.992188 16.3125 21.101563 16 20 16 C 18.601563 16 17.011719 16.488281 16.3125 18.6875 C 15.511719 18.085938 14.699219 18 14 18 C 12.5 18 10 18.601563 10 23 L 10 41 C 10 46 14 50 19 50 L 28.09375 50 C 31.492188 50 34.59375 48.101563 36.09375 45 L 39 39.5 C 39.101563 39.199219 40.09375 37.207031 40.09375 34.90625 C 40.09375 31.007813 37.792969 29.011719 36.09375 28.3125 L 36.09375 13 L 34.09375 12.1875 L 34.09375 28.90625 C 34.09375 29.40625 34.507813 29.804688 34.90625 29.90625 C 35.007813 29.90625 38.09375 30.8125 38.09375 34.8125 C 38.09375 36.3125 37.488281 37.898438 37.1875 38.5 L 34.40625 44.09375 C 33.207031 46.492188 30.792969 48 28.09375 48 L 19 48 C 15.101563 48 12 44.898438 12 41 L 12 23 C 12 20.398438 13 20 14 20 C 15 20 16 20.398438 16 23 C 16 23.601563 16.398438 24 17 24 C 17.601563 24 18 23.601563 18 23 L 18 21 C 18 18.199219 19 18 20 18 C 21.699219 18 22 19.101563 22 20 L 22 22 C 22 22.601563 22.398438 23 23 23 C 23.601563 23 24 22.601563 24 22 L 24 19 C 24 18.199219 24.5 17 26 17 C 27.5 17 28 18.199219 28 19 L 28 23 C 28 23.601563 28.398438 24 29 24 C 29.601563 24 30 23.601563 30 23 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/about.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 9 4 C 6.2504839 4 4 6.2504839 4 9 L 4 41 C 4 43.749516 6.2504839 46 9 46 L 41 46 C 43.749516 46 46 43.749516 46 41 L 46 9 C 46 6.2504839 43.749516 4 41 4 L 9 4 z M 9 6 L 41 6 C 42.668484 6 44 7.3315161 44 9 L 44 41 C 44 42.668484 42.668484 44 41 44 L 9 44 C 7.3315161 44 6 42.668484 6 41 L 6 9 C 6 7.3315161 7.3315161 6 9 6 z M 23.451172 12 C 23.179172 12 23 12.171688 23 12.429688 L 23 15.570312 C 23 15.827312 23.178172 16 23.451172 16 L 23.451172 15.998047 L 26.544922 15.998047 C 26.816922 15.998047 27 15.826359 27 15.568359 L 27 12.429688 C 27 12.172688 26.816922 12 26.544922 12 L 23.451172 12 z M 23.474609 20 C 23.190609 20 23 20.151953 23 20.376953 L 23 37.623047 C 23 37.848047 23.188609 37.998047 23.474609 37.998047 L 23.474609 38 L 26.523438 38 C 26.809437 38 27 37.848047 27 37.623047 L 27 20.376953 C 27 20.151953 26.810437 20 26.523438 20 L 23.474609 20 z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/admin.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n<path fill=\"currentColor\" d=\"M 27 4 C 22.042969 4 18.449219 5.945313 16.5625 8.8125 C 14.789063 11.507813 14.617188 14.933594 15.8125 18.09375 C 15.371094 18.640625 15 19.351563 15.09375 20.46875 C 15.09375 20.488281 15.09375 20.511719 15.09375 20.53125 C 15.261719 21.75 15.628906 22.609375 16.09375 23.21875 C 16.320313 23.515625 16.65625 23.523438 16.9375 23.6875 C 17.105469 24.769531 17.425781 25.867188 17.875 26.75 C 18.128906 27.25 18.382813 27.691406 18.65625 28.0625 C 18.78125 28.234375 18.960938 28.328125 19.09375 28.46875 C 19.105469 29.761719 19.15625 30.78125 19 32.09375 C 18.691406 32.910156 17.949219 33.53125 16.75 34.15625 C 15.5 34.804688 13.855469 35.398438 12.1875 36.125 C 10.519531 36.851563 8.828125 37.734375 7.46875 39.125 C 6.109375 40.515625 5.136719 42.421875 5 44.9375 C 4.984375 45.214844 5.082031 45.484375 5.269531 45.683594 C 5.460938 45.886719 5.722656 46 6 46 L 44 46 C 44.277344 46 44.539063 45.886719 44.730469 45.683594 C 44.917969 45.484375 45.015625 45.214844 45 44.9375 C 44.863281 42.421875 43.914063 40.515625 42.5625 39.125 C 41.210938 37.734375 39.511719 36.847656 37.84375 36.125 C 36.175781 35.402344 34.523438 34.859375 33.25 34.21875 C 32.738281 33.960938 32.324219 33.683594 31.96875 33.40625 C 31.949219 33.382813 31.929688 33.363281 31.90625 33.34375 C 31.820313 33.238281 31.714844 33.15625 31.59375 33.09375 C 31.289063 32.800781 31.050781 32.496094 30.90625 32.15625 C 30.828125 30.816406 30.8125 29.722656 30.8125 28.53125 C 30.933594 28.390625 31.105469 28.289063 31.21875 28.125 C 31.484375 27.746094 31.757813 27.28125 32 26.78125 C 32.429688 25.898438 32.765625 24.859375 32.96875 23.84375 C 33.289063 23.679688 33.65625 23.667969 33.90625 23.34375 C 34.386719 22.714844 34.667969 21.820313 34.78125 20.59375 C 34.84375 19.867188 34.792969 19.210938 34.5625 18.65625 C 34.488281 18.480469 34.363281 18.316406 34.25 18.15625 C 34.574219 17.457031 34.78125 16.65625 34.875 15.75 C 35.011719 14.453125 34.957031 13.007813 34.59375 11.59375 C 34.230469 10.179688 33.554688 8.804688 32.46875 7.75 C 31.53125 6.839844 30.214844 6.28125 28.6875 6.125 L 27.90625 4.5625 C 27.738281 4.214844 27.386719 3.996094 27 4 Z M 26.40625 6.0625 L 27.09375 7.4375 C 27.261719 7.785156 27.613281 8.003906 28 8 C 29.390625 8 30.351563 8.464844 31.09375 9.1875 C 31.835938 9.910156 32.359375 10.945313 32.65625 12.09375 C 32.953125 13.242188 33.015625 14.472656 32.90625 15.53125 C 32.796875 16.589844 32.464844 17.46875 32.21875 17.78125 C 32.019531 18.035156 31.957031 18.375 32.050781 18.683594 C 32.144531 18.992188 32.378906 19.238281 32.6875 19.34375 C 32.683594 19.351563 32.667969 19.363281 32.6875 19.40625 C 32.757813 19.578125 32.851563 19.933594 32.8125 20.40625 C 32.722656 21.378906 32.480469 21.90625 32.3125 22.125 C 32.144531 22.34375 32.058594 22.3125 32.09375 22.3125 C 31.609375 22.320313 31.199219 22.675781 31.125 23.15625 C 30.996094 24.007813 30.601563 25.058594 30.1875 25.90625 C 29.980469 26.332031 29.777344 26.703125 29.59375 26.96875 C 29.410156 27.234375 29.214844 27.398438 29.3125 27.34375 C 29.011719 27.519531 28.820313 27.839844 28.8125 28.1875 C 28.8125 29.585938 28.804688 30.835938 28.90625 32.46875 C 28.914063 32.566406 28.933594 32.660156 28.96875 32.75 C 29.078125 33.039063 29.21875 33.3125 29.375 33.5625 L 25.875 35 L 24.125 35 L 20.53125 33.53125 C 20.699219 33.253906 20.832031 32.949219 20.9375 32.625 C 20.964844 32.566406 20.984375 32.5 21 32.4375 C 21.210938 30.742188 21.09375 29.550781 21.09375 28.09375 C 21.097656 27.734375 20.90625 27.398438 20.59375 27.21875 C 20.667969 27.261719 20.472656 27.132813 20.28125 26.875 C 20.089844 26.617188 19.863281 26.257813 19.65625 25.84375 C 19.238281 25.019531 18.863281 23.960938 18.78125 23.09375 C 18.738281 22.613281 18.355469 22.230469 17.875 22.1875 C 17.910156 22.195313 17.886719 22.1875 17.8125 22.1875 C 17.773438 22.164063 17.71875 22.117188 17.65625 22.03125 C 17.480469 21.800781 17.230469 21.234375 17.09375 20.3125 C 17.09375 20.292969 17.09375 20.300781 17.09375 20.28125 C 17.089844 20.265625 17.097656 20.265625 17.09375 20.25 C 17.078125 19.492188 17.546875 19.070313 17.46875 19.125 C 17.859375 18.851563 18.003906 18.34375 17.8125 17.90625 C 16.585938 15.132813 16.761719 12.164063 18.25 9.90625 C 19.664063 7.757813 22.332031 6.214844 26.40625 6.0625 Z M 19 35.09375 L 23.09375 36.75 L 24 39 L 22.34375 44 L 7.25 44 C 7.523438 42.550781 8.023438 41.402344 8.875 40.53125 C 9.9375 39.441406 11.429688 38.65625 13 37.96875 C 14.570313 37.28125 16.207031 36.691406 17.65625 35.9375 C 18.128906 35.691406 18.578125 35.398438 19 35.09375 Z M 30.875 35.125 C 31.328125 35.457031 31.828125 35.742188 32.34375 36 C 33.804688 36.734375 35.460938 37.289063 37.03125 37.96875 C 38.601563 38.648438 40.066406 39.445313 41.125 40.53125 C 41.96875 41.398438 42.476563 42.550781 42.75 44 L 27.65625 44 L 26 39 L 26.90625 36.75 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/calendar.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 12 0 C 10.90625 0 10 0.90625 10 2 L 10 4 L 4 4 C 3.476563 4 2.945313 4.191406 2.570313 4.570313 C 2.191406 4.945313 2 5.476563 2 6 L 2 46 C 2 46.523438 2.191406 47.054688 2.570313 47.433594 C 2.945313 47.808594 3.476563 48 4 48 L 46 48 C 46.523438 48 47.054688 47.808594 47.433594 47.433594 C 47.808594 47.054688 48 46.523438 48 46 L 48 6 C 48 5.476563 47.808594 4.945313 47.433594 4.570313 C 47.054688 4.191406 46.523438 4 46 4 L 40 4 L 40 2 C 40 0.90625 39.09375 0 38 0 L 36 0 C 34.90625 0 34 0.90625 34 2 L 34 4 L 16 4 L 16 2 C 16 0.90625 15.09375 0 14 0 Z M 12 2 L 14 2 L 14 8 L 12 8 Z M 36 2 L 38 2 L 38 8 L 36 8 Z M 4 6 L 10 6 L 10 8 C 10 9.09375 10.90625 10 12 10 L 14 10 C 15.09375 10 16 9.09375 16 8 L 16 6 L 34 6 L 34 8 C 34 9.09375 34.90625 10 36 10 L 38 10 C 39.09375 10 40 9.09375 40 8 L 40 6 L 46 6 L 46 13 L 4 13 Z M 4 15 L 46 15 L 46 46 L 4 46 Z M 10 19 L 10 42 L 40 42 L 40 19 Z M 12 21 L 17 21 L 17 26 L 12 26 Z M 19 21 L 24 21 L 24 26 L 19 26 Z M 26 21 L 31 21 L 31 26 L 26 26 Z M 33 21 L 38 21 L 38 26 L 33 26 Z M 12 28 L 17 28 L 17 33 L 12 33 Z M 19 28 L 24 28 L 24 33 L 19 33 Z M 26 28 L 31 28 L 31 33 L 26 33 Z M 33 28 L 38 28 L 38 33 L 33 33 Z M 12 35 L 17 35 L 17 40 L 12 40 Z M 19 35 L 24 35 L 24 40 L 19 40 Z M 26 35 L 31 35 L 31 40 L 26 40 Z M 33 35 L 38 35 L 38 40 L 33 40 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/clone.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n<path fill=\"currentColor\" d=\"M 10 0 C 6.699219 0 4 2.699219 4 6 C 4 9.300781 6.699219 12 10 12 C 13.300781 12 16 9.300781 16 6 C 16 2.699219 13.300781 0 10 0 Z M 40 0 C 36.699219 0 34 2.699219 34 6 C 34 9.300781 36.699219 12 40 12 C 43.300781 12 46 9.300781 46 6 C 46 2.699219 43.300781 0 40 0 Z M 10 2 C 12.222656 2 14 3.777344 14 6 C 14 8.222656 12.222656 10 10 10 C 7.777344 10 6 8.222656 6 6 C 6 3.777344 7.777344 2 10 2 Z M 40 2 C 42.222656 2 44 3.777344 44 6 C 44 8.222656 42.222656 10 40 10 C 37.777344 10 36 8.222656 36 6 C 36 3.777344 37.777344 2 40 2 Z M 6.09375 13 C 2.746094 13 0 15.746094 0 19.09375 L 0 32.8125 C 0 34.078125 1.042969 35 2.3125 35 L 4 35 L 4 47.6875 C 4 48.957031 5.042969 50 6.3125 50 L 13.8125 50 C 15.082031 50 16.09375 48.957031 16.09375 47.6875 L 16.09375 35 L 17.8125 35 C 19.082031 35 20.09375 33.957031 20.09375 32.6875 L 20.09375 29 C 20.097656 28.640625 19.910156 28.304688 19.597656 28.121094 C 19.285156 27.941406 18.902344 27.941406 18.589844 28.121094 C 18.277344 28.304688 18.089844 28.640625 18.09375 29 L 18.09375 32.6875 C 18.09375 32.816406 17.941406 33 17.8125 33 L 15.09375 33 C 14.542969 33 14.09375 33.449219 14.09375 34 L 14.09375 47.6875 C 14.09375 47.816406 13.941406 48 13.8125 48 L 6.3125 48 C 6.183594 48 6 47.816406 6 47.6875 L 6 34 C 6 33.449219 5.550781 33 5 33 L 2.3125 33 C 2.183594 33 2 32.746094 2 32.8125 L 2 19.09375 C 2 16.839844 3.839844 15 6.09375 15 L 13.90625 15 C 16.160156 15 18 16.839844 18 19.09375 L 18 21 C 17.996094 21.359375 18.183594 21.695313 18.496094 21.878906 C 18.808594 22.058594 19.191406 22.058594 19.503906 21.878906 C 19.816406 21.695313 20.003906 21.359375 20 21 L 20 19.09375 C 20 15.746094 17.253906 13 13.90625 13 Z M 36.09375 13 C 34.671875 13 33.347656 13.484375 32.28125 14.3125 C 31.945313 14.507813 31.75 14.882813 31.789063 15.269531 C 31.824219 15.660156 32.082031 15.992188 32.449219 16.121094 C 32.816406 16.25 33.226563 16.15625 33.5 15.875 C 34.234375 15.304688 35.117188 15 36.09375 15 L 44 15 C 46.253906 15 48.09375 16.839844 48.09375 19.09375 L 48.09375 32.6875 C 48.09375 32.816406 47.941406 33 47.8125 33 L 45.09375 33 C 44.542969 33 44.09375 33.449219 44.09375 34 L 44.09375 47.6875 C 44.09375 47.816406 43.941406 48 43.8125 48 L 36.3125 48 C 36.183594 48 36 47.816406 36 47.6875 L 36 34 C 36 33.449219 35.550781 33 35 33 L 32.3125 33 C 31.953125 32.996094 31.617188 33.183594 31.433594 33.496094 C 31.253906 33.808594 31.253906 34.191406 31.433594 34.503906 C 31.617188 34.816406 31.953125 35.003906 32.3125 35 L 34 35 L 34 47.6875 C 34 48.957031 35.042969 50 36.3125 50 L 43.8125 50 C 45.082031 50 46.09375 48.957031 46.09375 47.6875 L 46.09375 35 L 47.8125 35 C 49.082031 35 50.09375 33.957031 50.09375 32.6875 L 50.09375 19.09375 C 50.09375 15.746094 47.347656 13 44 13 Z M 27.90625 17.96875 C 27.863281 17.976563 27.820313 17.988281 27.78125 18 C 27.40625 18.066406 27.105469 18.339844 27 18.703125 C 26.894531 19.070313 27.003906 19.460938 27.28125 19.71875 L 31.5625 24 L 14 24 C 13.96875 24 13.9375 24 13.90625 24 C 13.355469 24.027344 12.925781 24.496094 12.953125 25.046875 C 12.980469 25.597656 13.449219 26.027344 14 26 L 31.5625 26 L 27.28125 30.28125 C 26.882813 30.679688 26.882813 31.320313 27.28125 31.71875 C 27.679688 32.117188 28.320313 32.117188 28.71875 31.71875 L 34.5625 25.84375 C 34.617188 25.808594 34.671875 25.765625 34.71875 25.71875 L 34.78125 25.625 C 34.804688 25.605469 34.824219 25.585938 34.84375 25.5625 L 35.40625 25 L 34.84375 24.4375 C 34.808594 24.382813 34.765625 24.328125 34.71875 24.28125 L 34.6875 24.28125 C 34.667969 24.257813 34.648438 24.238281 34.625 24.21875 L 28.71875 18.28125 C 28.511719 18.058594 28.210938 17.945313 27.90625 17.96875 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/delete.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n<path fill=\"currentColor\" d=\"M 21 2 C 19.354545 2 18 3.3545455 18 5 L 18 7 L 10 7 L 8 7 A 1.0001 1.0001 0 1 0 8 9 L 9 9 L 9 45 C 9 46.654 10.346 48 12 48 L 31.074219 48 C 30.523219 47.386 30.033187 46.718 29.617188 46 L 12 46 C 11.448 46 11 45.551 11 45 L 11 9 L 18.832031 9 A 1.0001 1.0001 0 0 0 19.158203 9 L 30.832031 9 A 1.0001 1.0001 0 0 0 31.158203 9 L 39 9 L 39 28.050781 C 39.331 28.023781 39.662 28 40 28 C 40.338 28 40.669 28.023781 41 28.050781 L 41 9 L 42 9 A 1.0001 1.0001 0 1 0 42 7 L 40 7 L 32 7 L 32 5 C 32 3.3545455 30.645455 2 29 2 L 21 2 z M 21 4 L 29 4 C 29.554545 4 30 4.4454545 30 5 L 30 7 L 20 7 L 20 5 C 20 4.4454545 20.445455 4 21 4 z M 18.984375 13.986328 A 1.0001 1.0001 0 0 0 18 15 L 18 40 A 1.0001 1.0001 0 1 0 20 40 L 20 15 A 1.0001 1.0001 0 0 0 18.984375 13.986328 z M 24.984375 13.986328 A 1.0001 1.0001 0 0 0 24 15 L 24 40 A 1.0001 1.0001 0 1 0 26 40 L 26 15 A 1.0001 1.0001 0 0 0 24.984375 13.986328 z M 31 14 C 30.447 14 30 14.448 30 15 L 30 33.371094 C 30.565 32.520094 31.242 31.753219 32 31.074219 L 32 15 C 32 14.448 31.553 14 31 14 z M 40 30 C 34.5 30 30 34.5 30 40 C 30 45.5 34.5 50 40 50 C 45.5 50 50 45.5 50 40 C 50 34.5 45.5 30 40 30 z M 40 32 C 44.4 32 48 35.6 48 40 C 48 44.4 44.4 48 40 48 C 35.6 48 32 44.4 32 40 C 32 35.6 35.6 32 40 32 z M 36.5 35.5 C 36.25 35.5 36.000781 35.600781 35.800781 35.800781 C 35.400781 36.200781 35.400781 36.799219 35.800781 37.199219 L 38.599609 40 L 35.800781 42.800781 C 35.400781 43.200781 35.400781 43.799219 35.800781 44.199219 C 36.200781 44.599219 36.799219 44.599219 37.199219 44.199219 L 40 41.400391 L 42.800781 44.199219 C 43.200781 44.599219 43.799219 44.599219 44.199219 44.199219 C 44.399219 43.999219 44.5 43.7 44.5 43.5 C 44.5 43.3 44.399219 43.000781 44.199219 42.800781 L 41.400391 40 L 44.199219 37.199219 C 44.399219 36.999219 44.5 36.7 44.5 36.5 C 44.5 36.3 44.399219 36.000781 44.199219 35.800781 C 43.799219 35.400781 43.200781 35.400781 42.800781 35.800781 L 40 38.599609 L 37.199219 35.800781 C 36.999219 35.600781 36.75 35.5 36.5 35.5 z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/edit.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n<path fill=\"currentColor\" d=\"M 44.34375 2 C 43.402344 2 42.433594 2.347656 41.71875 3.0625 L 40.375 4.4375 L 45.5625 9.625 C 45.558594 9.628906 46.9375 8.28125 46.9375 8.28125 C 48.371094 6.847656 48.371094 4.496094 46.9375 3.0625 C 46.21875 2.34375 45.285156 2 44.34375 2 Z M 38.75 5.9375 L 16.03125 28.65625 L 15.96875 28.96875 L 15.03125 33.8125 L 14.71875 35.28125 L 16.1875 34.96875 L 21.03125 34.03125 L 21.34375 33.96875 L 44.0625 11.25 L 42.625 9.84375 L 20.375 32.0625 L 17.9375 29.625 L 40.15625 7.375 Z M 3 10 C 2.445313 10 2 10.449219 2 11 L 2 47 C 2 47.550781 2.445313 48 3 48 L 39 48 C 39.554688 48 40 47.550781 40 47 L 40 18 L 38 20 L 38 46 L 4 46 L 4 12 L 30 12 L 32 10 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/home.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 24.962891 1.0546875 A 1.0001 1.0001 0 0 0 24.384766 1.2636719 L 1.3847656 19.210938 A 1.0005659 1.0005659 0 0 0 2.6152344 20.789062 L 4 19.708984 L 4 46 C 4 46.552 4.448 47 5 47 L 45 47 C 45.552 47 46 46.552 46 46 L 46 19.708984 L 47.384766 20.789062 A 1.0005657 1.0005657 0 1 0 48.615234 19.210938 L 41 13.269531 C 40.999922 13.061489 41.00006 12.820623 41 12.507812 C 40.999859 11.776319 41.000062 10.809744 41 9.8457031 C 40.999875 7.9176211 41 6 41 6 A 1.0001 1.0001 0 0 0 40 5 L 36 5 A 1.0001 1.0001 0 0 0 35 6 C 35 6 35.000625 7.1570629 35 8.3144531 C 34.999946 8.4138611 35.000063 8.4892153 35 8.5878906 L 25.615234 1.2636719 A 1.0001 1.0001 0 0 0 24.962891 1.0546875 z M 25 3.3222656 L 35.378906 11.419922 A 1.0001 1.0001 0 0 0 36.367188 11.560547 C 36.367188 11.560547 36.986309 10.761867 36.986328 10.761719 C 36.986348 10.76157 36.991388 10.708386 36.992188 10.697266 C 36.993688 10.675006 36.993841 10.668839 36.994141 10.662109 C 36.994592 10.648659 36.995894 10.642016 36.996094 10.634766 C 36.996335 10.620266 36.995945 10.606331 36.996094 10.587891 C 36.996392 10.551011 36.995852 10.499661 36.996094 10.435547 C 36.996578 10.307318 36.997647 10.125735 36.998047 9.9082031 C 36.99857 9.4731324 36.999687 8.8954613 37 8.3164062 C 37.000355 7.6582563 36.999934 7.4454733 37 7 L 39 7 C 39.000006 7.6422969 38.999908 8.4189176 39 9.8457031 C 39.000063 10.809787 38.999859 11.77615 39 12.507812 C 39.00007 12.873644 38.999904 13.179924 39 13.398438 C 39.000096 13.61695 39.000933 13.681016 39.001953 13.769531 A 1.0001 1.0001 0 0 0 39.386719 14.546875 L 44 18.146484 L 44 45 L 6 45 L 6 18.148438 L 25 3.3222656 z M 15 27 A 1.0001 1.0001 0 1 0 15 29 L 35 29 A 1.0001 1.0001 0 1 0 35 27 L 15 27 z M 15 33 A 1.0001 1.0001 0 1 0 15 35 L 35 35 A 1.0001 1.0001 0 1 0 35 33 L 15 33 z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/logout.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 9.0175781 0 A 1.0001 1.0001 0 0 0 8 1 L 8 44 A 1.0001 1.0001 0 0 0 9.40625 44.914062 L 18.265625 40.976562 A 1.0001 1.0001 0 0 0 19 40 L 19 6 L 39 6 L 39 40 A 1.0001 1.0001 0 1 0 41 40 L 41 5 A 1.0001 1.0001 0 0 0 40 4 L 18.212891 4 L 9.40625 0.0859375 A 1.0001 1.0001 0 0 0 9.0175781 0 z M 10 2.5390625 L 17 5.6503906 L 17 39.349609 L 10 42.460938 L 10 2.5390625 z M 29.023438 8 C 26.83483 8 25.048828 9.8070917 25.048828 12 C 25.048828 14.192908 26.834831 16 29.023438 16 C 31.212044 16 33 14.192908 33 12 C 33 9.8070917 31.212044 8 29.023438 8 z M 29.023438 10 C 30.122831 10 31 10.878908 31 12 C 31 13.121092 30.122831 14 29.023438 14 C 27.924043 14 27.048828 13.121092 27.048828 12 C 27.048828 10.878908 27.924044 10 29.023438 10 z M 29 17 C 25.698375 17 23 19.698375 23 23 L 23 28 A 1.0001 1.0001 0 0 0 23.292969 28.707031 L 25.050781 30.464844 L 26.007812 38.125 A 1.0001 1.0001 0 0 0 27 39 L 31 39 A 1.0001 1.0001 0 0 0 31.992188 38.125 L 32.949219 30.464844 L 34.707031 28.707031 A 1.0001 1.0001 0 0 0 35 28 L 35 23 C 35 19.69757 32.301384 17 29 17 z M 29 19 C 31.220616 19 33 20.77843 33 23 L 33 27.585938 L 31.292969 29.292969 A 1.0001 1.0001 0 0 0 31.007812 29.875 L 30.117188 37 L 27.882812 37 L 26.992188 29.875 A 1.0001 1.0001 0 0 0 26.707031 29.292969 L 25 27.585938 L 25 23 C 25 20.779625 26.779625 19 29 19 z M 12 21 C 11.448 21 11 21.672 11 22.5 C 11 23.328 11.448 24 12 24 C 12.552 24 13 23.328 13 22.5 C 13 21.672 12.552 21 12 21 z M 22 42 A 1.0001 1.0001 0 0 0 21.050781 42.683594 L 19.050781 48.683594 A 1.0001 1.0001 0 0 0 20 50 L 38 50 A 1.0001 1.0001 0 0 0 38.949219 48.683594 L 36.949219 42.683594 A 1.0001 1.0001 0 0 0 36 42 L 22 42 z M 22.720703 44 L 35.279297 44 L 36.611328 48 L 21.388672 48 L 22.720703 44 z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/profile.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 25 2.0078125 C 12.309296 2.0078125 2.0000002 12.317108 2 25.007812 C 2 37.698518 12.309295 48.007812 25 48.007812 C 37.690705 48.007812 48 37.698518 48 25.007812 C 48 12.317108 37.690704 2.0078125 25 2.0078125 z M 25 4.0078125 C 36.609824 4.0078125 46 13.397988 46 25.007812 C 46 30.740509 43.703999 35.925856 39.988281 39.712891 C 38.158498 38.369571 35.928049 37.69558 34.039062 37.023438 C 32.975192 36.644889 32.018651 36.269758 31.320312 35.851562 C 30.651504 35.451051 30.280089 35.039466 30.083984 34.566406 C 29.992134 33.419545 30.010738 32.496253 30.017578 31.40625 C 30.13873 31.285594 30.294155 31.200823 30.417969 31.054688 C 30.709957 30.710058 31.007253 30.29128 31.291016 29.820312 C 31.777604 29.012711 32.131673 28.024913 32.330078 27.023438 C 32.63305 26.869 32.956699 26.835578 33.203125 26.521484 C 33.658098 25.941577 33.965233 25.125482 34.101562 23.988281 C 34.222454 22.984232 33.898957 22.29366 33.482422 21.763672 C 33.930529 20.298851 34.48532 17.969341 34.296875 15.558594 C 34.193203 14.232288 33.859467 12.897267 33.056641 11.787109 C 32.290173 10.727229 31.045786 9.9653642 29.453125 9.6894531 C 28.441568 8.5409775 26.834704 8 24.914062 8 L 24.904297 8 L 24.896484 8 C 20.593741 8.078993 17.817552 9.8598398 16.628906 12.576172 C 15.498615 15.159149 15.741603 18.37477 16.552734 21.722656 C 16.116708 22.25268 15.775146 22.95643 15.898438 23.988281 C 16.035282 25.125098 16.34224 25.94153 16.796875 26.521484 C 17.043118 26.835604 17.366808 26.868911 17.669922 27.023438 C 17.868296 28.024134 18.222437 29.01059 18.708984 29.818359 C 18.992747 30.289465 19.289737 30.707821 19.582031 31.052734 C 19.705876 31.198874 19.861128 31.285522 19.982422 31.40625 C 19.988922 32.49568 20.007396 33.418614 19.916016 34.566406 C 19.720294 35.037723 19.34937 35.449526 18.681641 35.851562 C 17.984409 36.271364 17.029015 36.648577 15.966797 37.029297 C 14.079805 37.705631 11.85061 38.384459 10.015625 39.716797 C 6.2976298 35.929423 4 30.742497 4 25.007812 C 4.0000002 13.397989 13.390176 4.0078125 25 4.0078125 z M 24.921875 10.001953 C 26.766001 10.003853 27.92628 10.549863 28.244141 11.107422 L 28.488281 11.535156 L 28.974609 11.601562 C 30.230788 11.776108 30.932655 12.263579 31.435547 12.958984 C 31.938439 13.654389 32.217535 14.624895 32.302734 15.714844 C 32.473134 17.894741 31.849129 20.468905 31.453125 21.660156 L 31.201172 22.416016 L 31.882812 22.830078 C 31.813472 22.787858 32.203297 23.018609 32.115234 23.75 C 32.008564 24.639799 31.781184 25.093017 31.628906 25.287109 C 31.476629 25.481202 31.411442 25.45641 31.427734 25.455078 L 30.603516 25.523438 L 30.515625 26.345703 C 30.440195 27.052169 30.04285 28.015793 29.578125 28.787109 C 29.345762 29.172767 29.098543 29.516317 28.890625 29.761719 C 28.682707 30.00712 28.461282 30.159117 28.544922 30.115234 L 28.009766 30.394531 L 28.009766 31 C 28.009766 32.324321 27.955813 33.407291 28.095703 34.949219 L 28.107422 35.082031 L 28.154297 35.207031 C 28.547829 36.266071 29.369275 37.013258 30.292969 37.566406 C 31.216662 38.119555 32.276387 38.519377 33.369141 38.908203 C 35.170096 39.549023 37.047465 40.179657 38.478516 41.111328 C 34.832228 44.16545 30.135566 46.007812 25 46.007812 C 19.866422 46.007812 15.171083 44.167232 11.525391 41.115234 C 12.964568 40.188909 14.844735 39.556492 16.642578 38.912109 C 17.73461 38.520704 18.79156 38.119183 19.712891 37.564453 C 20.634221 37.009723 21.452728 36.262662 21.845703 35.207031 L 21.892578 35.082031 L 21.904297 34.949219 C 22.043042 33.408482 21.990234 32.325309 21.990234 31 L 21.990234 30.394531 L 21.455078 30.113281 C 21.538828 30.157091 21.317362 30.005196 21.109375 29.759766 C 20.901388 29.514336 20.654237 29.172879 20.421875 28.787109 C 19.957151 28.015571 19.559775 27.05118 19.484375 26.345703 L 19.396484 25.523438 L 18.572266 25.455078 C 18.587716 25.456378 18.523206 25.481158 18.371094 25.287109 C 18.218979 25.093064 17.991921 24.640183 17.884766 23.75 C 17.797356 23.01846 18.191557 22.784891 18.117188 22.830078 L 18.751953 22.445312 L 18.566406 21.724609 C 17.705952 18.412902 17.575833 15.399621 18.460938 13.376953 C 19.345167 11.356284 21.116417 10.074289 24.921875 10.001953 z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/renew.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 8.8125 2 C 8.335938 2.089844 7.992188 2.511719 8 3 L 8 11 C 8 11.03125 8 11.0625 8 11.09375 L 8 36 C 8 37.65625 6.65625 39 5 39 C 4.96875 39 4.9375 39 4.90625 39 C 3.296875 38.949219 2 37.621094 2 36 L 2 14 L 6 14 L 6 12 L 1 12 C 0.449219 12 0 12.449219 0 13 L 0 36 C 0 38.628906 2.074219 40.785156 4.65625 40.96875 C 4.667969 40.96875 4.675781 40.96875 4.6875 40.96875 C 4.707031 40.980469 4.730469 40.992188 4.75 41 C 4.835938 41.003906 4.914063 41 5 41 L 25 41 C 25.359375 41.003906 25.695313 40.816406 25.878906 40.503906 C 26.058594 40.191406 26.058594 39.808594 25.878906 39.496094 C 25.695313 39.183594 25.359375 38.996094 25 39 L 8.96875 39 C 9.609375 38.160156 10 37.128906 10 36 L 10 11.15625 C 10.003906 11.105469 10.003906 11.050781 10 11 L 10 4 L 44 4 L 44 25 C 43.996094 25.359375 44.183594 25.695313 44.496094 25.878906 C 44.808594 26.058594 45.191406 26.058594 45.503906 25.878906 C 45.816406 25.695313 46.003906 25.359375 46 25 L 46 3 C 46 2.449219 45.550781 2 45 2 L 9 2 C 8.96875 2 8.9375 2 8.90625 2 C 8.875 2 8.84375 2 8.8125 2 Z M 16 10 L 16 12 L 38 12 L 38 10 Z M 16 14 L 16 16 L 38 16 L 38 14 Z M 16 22 L 16 24 L 26 24 L 26 22 Z M 28 22 L 28 24 L 38 24 L 38 22 Z M 16 26 L 16 28 L 26 28 L 26 26 Z M 28 26 L 28 28 L 30 28 L 33 26 Z M 39 27 C 32.953125 27 28 31.953125 28 38 C 28 44.046875 32.953125 49 39 49 L 39 47 C 34.046875 47 30 42.953125 30 38 C 30 33.046875 34.046875 29 39 29 C 43.953125 29 48 33.046875 48 38 C 48 40.660156 46.851563 43 45 44.625 L 45 41 L 43 41 L 43 48 L 50 48 L 50 46 L 46.4375 46 C 48.621094 44.011719 50 41.171875 50 38 C 50 31.953125 45.046875 27 39 27 Z M 16 30 L 16 32 L 26 32 L 26 30 Z M 16 34 L 16 36 L 26 36 L 26 34 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/settings.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 22.205078 2 A 1.0001 1.0001 0 0 0 21.21875 2.8378906 L 20.246094 8.7929688 C 19.076509 9.1331971 17.961243 9.5922728 16.910156 10.164062 L 11.996094 6.6542969 A 1.0001 1.0001 0 0 0 10.708984 6.7597656 L 6.8183594 10.646484 A 1.0001 1.0001 0 0 0 6.7070312 11.927734 L 10.164062 16.873047 C 9.583454 17.930271 9.1142098 19.051824 8.765625 20.232422 L 2.8359375 21.21875 A 1.0001 1.0001 0 0 0 2.0019531 22.205078 L 2.0019531 27.705078 A 1.0001 1.0001 0 0 0 2.8261719 28.691406 L 8.7597656 29.742188 C 9.1064607 30.920739 9.5727226 32.043065 10.154297 33.101562 L 6.6542969 37.998047 A 1.0001 1.0001 0 0 0 6.7597656 39.285156 L 10.648438 43.175781 A 1.0001 1.0001 0 0 0 11.927734 43.289062 L 16.882812 39.820312 C 17.936999 40.39548 19.054994 40.857928 20.228516 41.201172 L 21.21875 47.164062 A 1.0001 1.0001 0 0 0 22.205078 48 L 27.705078 48 A 1.0001 1.0001 0 0 0 28.691406 47.173828 L 29.751953 41.1875 C 30.920633 40.838997 32.033372 40.369697 33.082031 39.791016 L 38.070312 43.291016 A 1.0001 1.0001 0 0 0 39.351562 43.179688 L 43.240234 39.287109 A 1.0001 1.0001 0 0 0 43.34375 37.996094 L 39.787109 33.058594 C 40.355783 32.014958 40.813915 30.908875 41.154297 29.748047 L 47.171875 28.693359 A 1.0001 1.0001 0 0 0 47.998047 27.707031 L 47.998047 22.207031 A 1.0001 1.0001 0 0 0 47.160156 21.220703 L 41.152344 20.238281 C 40.80968 19.078827 40.350281 17.974723 39.78125 16.931641 L 43.289062 11.933594 A 1.0001 1.0001 0 0 0 43.177734 10.652344 L 39.287109 6.7636719 A 1.0001 1.0001 0 0 0 37.996094 6.6601562 L 33.072266 10.201172 C 32.023186 9.6248101 30.909713 9.1579916 29.738281 8.8125 L 28.691406 2.828125 A 1.0001 1.0001 0 0 0 27.705078 2 L 22.205078 2 z M 23.056641 4 L 26.865234 4 L 27.861328 9.6855469 A 1.0001 1.0001 0 0 0 28.603516 10.484375 C 30.066026 10.848832 31.439607 11.426549 32.693359 12.185547 A 1.0001 1.0001 0 0 0 33.794922 12.142578 L 38.474609 8.7792969 L 41.167969 11.472656 L 37.835938 16.220703 A 1.0001 1.0001 0 0 0 37.796875 17.310547 C 38.548366 18.561471 39.118333 19.926379 39.482422 21.380859 A 1.0001 1.0001 0 0 0 40.291016 22.125 L 45.998047 23.058594 L 45.998047 26.867188 L 40.279297 27.871094 A 1.0001 1.0001 0 0 0 39.482422 28.617188 C 39.122545 30.069817 38.552234 31.434687 37.800781 32.685547 A 1.0001 1.0001 0 0 0 37.845703 33.785156 L 41.224609 38.474609 L 38.53125 41.169922 L 33.791016 37.84375 A 1.0001 1.0001 0 0 0 32.697266 37.808594 C 31.44975 38.567585 30.074755 39.148028 28.617188 39.517578 A 1.0001 1.0001 0 0 0 27.876953 40.3125 L 26.867188 46 L 23.052734 46 L 22.111328 40.337891 A 1.0001 1.0001 0 0 0 21.365234 39.53125 C 19.90185 39.170557 18.522094 38.59371 17.259766 37.835938 A 1.0001 1.0001 0 0 0 16.171875 37.875 L 11.46875 41.169922 L 8.7734375 38.470703 L 12.097656 33.824219 A 1.0001 1.0001 0 0 0 12.138672 32.724609 C 11.372652 31.458855 10.793319 30.079213 10.427734 28.609375 A 1.0001 1.0001 0 0 0 9.6328125 27.867188 L 4.0019531 26.867188 L 4.0019531 23.052734 L 9.6289062 22.117188 A 1.0001 1.0001 0 0 0 10.435547 21.373047 C 10.804273 19.898143 11.383325 18.518729 12.146484 17.255859 A 1.0001 1.0001 0 0 0 12.111328 16.164062 L 8.8261719 11.46875 L 11.523438 8.7734375 L 16.185547 12.105469 A 1.0001 1.0001 0 0 0 17.28125 12.148438 C 18.536908 11.394293 19.919867 10.822081 21.384766 10.462891 A 1.0001 1.0001 0 0 0 22.132812 9.6523438 L 23.056641 4 z M 25 17 C 20.593567 17 17 20.593567 17 25 C 17 29.406433 20.593567 33 25 33 C 29.406433 33 33 29.406433 33 25 C 33 20.593567 29.406433 17 25 17 z M 25 19 C 28.325553 19 31 21.674447 31 25 C 31 28.325553 28.325553 31 25 31 C 21.674447 31 19 28.325553 19 25 C 19 21.674447 21.674447 19 25 19 z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/statistics.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 34 4 L 34 50 L 48 50 L 48 4 Z M 36 6 L 46 6 L 46 48 L 36 48 Z M 2 17 L 2 50 L 16 50 L 16 17 Z M 4 19 L 14 19 L 14 48 L 4 48 Z M 18 28 L 18 50 L 32 50 L 32 28 Z M 20 30 L 30 30 L 30 48 L 20 48 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/mobile-menu/subscriptions.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 50 50\">\r\n    <path fill=\"currentColor\" d=\"M 7 7 C 5.355469 7 4 8.355469 4 10 L 4 32 C 4 32.75 4.285156 33.441406 4.75 33.96875 L 0.40625 39.78125 C 0.125 40.160156 0 40.609375 0 41 C 0 41.007813 0 41.023438 0 41.03125 L 0 42 C -0.015625 43.644531 1.355469 45 3 45 L 47 45 C 48.644531 45 50.015625 43.644531 50 42 L 50 41.03125 C 50 41.023438 50 41.007813 50 41 C 50 40.609375 49.878906 40.160156 49.59375 39.78125 L 45.25 33.96875 C 45.714844 33.441406 46 32.75 46 32 L 46 10 C 46 8.355469 44.644531 7 43 7 Z M 7 9 L 43 9 C 43.566406 9 44 9.433594 44 10 L 44 32 C 44 32.566406 43.566406 33 43 33 L 7 33 C 6.433594 33 6 32.566406 6 32 L 6 10 C 6 9.433594 6.433594 9 7 9 Z M 24 11 L 24 31 L 26 31 L 26 11 Z M 9.71875 12 C 9.167969 12.078125 8.78125 12.589844 8.859375 13.140625 C 8.9375 13.691406 9.449219 14.078125 10 14 L 20 14 C 20.359375 14.003906 20.695313 13.816406 20.878906 13.503906 C 21.058594 13.191406 21.058594 12.808594 20.878906 12.496094 C 20.695313 12.183594 20.359375 11.996094 20 12 L 10 12 C 9.96875 12 9.9375 12 9.90625 12 C 9.875 12 9.84375 12 9.8125 12 C 9.78125 12 9.75 12 9.71875 12 Z M 29.71875 12 C 29.167969 12.078125 28.78125 12.589844 28.859375 13.140625 C 28.9375 13.691406 29.449219 14.078125 30 14 L 40 14 C 40.359375 14.003906 40.695313 13.816406 40.878906 13.503906 C 41.058594 13.191406 41.058594 12.808594 40.878906 12.496094 C 40.695313 12.183594 40.359375 11.996094 40 12 L 30 12 C 29.96875 12 29.9375 12 29.90625 12 C 29.875 12 29.84375 12 29.8125 12 C 29.78125 12 29.75 12 29.71875 12 Z M 9.71875 16 C 9.167969 16.078125 8.78125 16.589844 8.859375 17.140625 C 8.9375 17.691406 9.449219 18.078125 10 18 L 17 18 C 17.359375 18.003906 17.695313 17.816406 17.878906 17.503906 C 18.058594 17.191406 18.058594 16.808594 17.878906 16.496094 C 17.695313 16.183594 17.359375 15.996094 17 16 L 10 16 C 9.96875 16 9.9375 16 9.90625 16 C 9.875 16 9.84375 16 9.8125 16 C 9.78125 16 9.75 16 9.71875 16 Z M 29.71875 16 C 29.167969 16.078125 28.78125 16.589844 28.859375 17.140625 C 28.9375 17.691406 29.449219 18.078125 30 18 L 37 18 C 37.359375 18.003906 37.695313 17.816406 37.878906 17.503906 C 38.058594 17.191406 38.058594 16.808594 37.878906 16.496094 C 37.695313 16.183594 37.359375 15.996094 37 16 L 30 16 C 29.96875 16 29.9375 16 29.90625 16 C 29.875 16 29.84375 16 29.8125 16 C 29.78125 16 29.75 16 29.71875 16 Z M 9.71875 20 C 9.167969 20.078125 8.78125 20.589844 8.859375 21.140625 C 8.9375 21.691406 9.449219 22.078125 10 22 L 20 22 C 20.359375 22.003906 20.695313 21.816406 20.878906 21.503906 C 21.058594 21.191406 21.058594 20.808594 20.878906 20.496094 C 20.695313 20.183594 20.359375 19.996094 20 20 L 10 20 C 9.96875 20 9.9375 20 9.90625 20 C 9.875 20 9.84375 20 9.8125 20 C 9.78125 20 9.75 20 9.71875 20 Z M 29.71875 20 C 29.167969 20.078125 28.78125 20.589844 28.859375 21.140625 C 28.9375 21.691406 29.449219 22.078125 30 22 L 40 22 C 40.359375 22.003906 40.695313 21.816406 40.878906 21.503906 C 41.058594 21.191406 41.058594 20.808594 40.878906 20.496094 C 40.695313 20.183594 40.359375 19.996094 40 20 L 30 20 C 29.96875 20 29.9375 20 29.90625 20 C 29.875 20 29.84375 20 29.8125 20 C 29.78125 20 29.75 20 29.71875 20 Z M 9.71875 24 C 9.167969 24.078125 8.78125 24.589844 8.859375 25.140625 C 8.9375 25.691406 9.449219 26.078125 10 26 L 17 26 C 17.359375 26.003906 17.695313 25.816406 17.878906 25.503906 C 18.058594 25.191406 18.058594 24.808594 17.878906 24.496094 C 17.695313 24.183594 17.359375 23.996094 17 24 L 10 24 C 9.96875 24 9.9375 24 9.90625 24 C 9.875 24 9.84375 24 9.8125 24 C 9.78125 24 9.75 24 9.71875 24 Z M 29.71875 24 C 29.167969 24.078125 28.78125 24.589844 28.859375 25.140625 C 28.9375 25.691406 29.449219 26.078125 30 26 L 37 26 C 37.359375 26.003906 37.695313 25.816406 37.878906 25.503906 C 38.058594 25.191406 38.058594 24.808594 37.878906 24.496094 C 37.695313 24.183594 37.359375 23.996094 37 24 L 30 24 C 29.96875 24 29.9375 24 29.90625 24 C 29.875 24 29.84375 24 29.8125 24 C 29.78125 24 29.75 24 29.71875 24 Z M 9.71875 28 C 9.167969 28.078125 8.78125 28.589844 8.859375 29.140625 C 8.9375 29.691406 9.449219 30.078125 10 30 L 20 30 C 20.359375 30.003906 20.695313 29.816406 20.878906 29.503906 C 21.058594 29.191406 21.058594 28.808594 20.878906 28.496094 C 20.695313 28.183594 20.359375 27.996094 20 28 L 10 28 C 9.96875 28 9.9375 28 9.90625 28 C 9.875 28 9.84375 28 9.8125 28 C 9.78125 28 9.75 28 9.71875 28 Z M 29.71875 28 C 29.167969 28.078125 28.78125 28.589844 28.859375 29.140625 C 28.9375 29.691406 29.449219 30.078125 30 30 L 40 30 C 40.359375 30.003906 40.695313 29.816406 40.878906 29.503906 C 41.058594 29.191406 41.058594 28.808594 40.878906 28.496094 C 40.695313 28.183594 40.359375 27.996094 40 28 L 30 28 C 29.96875 28 29.9375 28 29.90625 28 C 29.875 28 29.84375 28 29.8125 28 C 29.78125 28 29.75 28 29.71875 28 Z M 6.75 35 L 43.5 35 C 43.507813 35 43.527344 34.992188 43.53125 35 L 48 40.96875 L 48 42 C 48.003906 42.558594 47.558594 43 47 43 L 3 43 C 2.441406 43 1.996094 42.558594 2 42 L 2 40.96875 L 6.3125 35.21875 C 6.457031 35.144531 6.601563 35.074219 6.75 35 Z M 19.5 38 C 19.160156 38 18.839844 38.183594 18.65625 38.46875 L 17 41 L 33 41 L 31.34375 38.46875 C 31.160156 38.183594 30.839844 38 30.5 38 Z\"></path>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/notes.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Notepad-Text--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>Notepad Text Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"notepad-text--content-notes-book-notepad-notebook\">\r\n    <path id=\"Union\" class=\"accent-color\" d=\"M24 5.531c-7.4 0 -12.588 0.252 -15.855 0.492 -3.277 0.241 -5.853 2.735 -6.135 6.033 -0.256 2.984 -0.51 7.566 -0.51 13.975 0 6.41 0.254 10.991 0.51 13.976 0.282 3.297 2.858 5.791 6.135 6.032 3.267 0.24 8.455 0.492 15.855 0.492s12.588 -0.251 15.855 -0.492c3.277 -0.24 5.853 -2.735 6.135 -6.032 0.256 -2.985 0.51 -7.567 0.51 -13.976 0 -6.409 -0.254 -10.99 -0.51 -13.975 -0.282 -3.298 -2.858 -5.792 -6.135 -6.033 -3.267 -0.24 -8.455 -0.492 -15.855 -0.492Z\" stroke-width=\"1\"/>\r\n    <path id=\"Subtract\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M12.75 24A2.25 2.25 0 0 1 15 21.75h18a2.25 2.25 0 0 1 0 4.5H15A2.25 2.25 0 0 1 12.75 24ZM15 32.75a2.25 2.25 0 0 0 0 4.5h9a2.25 2.25 0 0 0 0 -4.5h-9Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n    <path id=\"Union_2\" class=\"main-color\" d=\"M13 12.531c-0.35 0 -0.665 -0.009 -0.944 -0.025 -1.58 -0.09 -2.465 -1.399 -2.513 -2.83a79.157 79.157 0 0 1 0 -5.29c0.048 -1.43 0.933 -2.74 2.513 -2.83a17.564 17.564 0 0 1 1.888 0c1.58 0.09 2.465 1.4 2.513 2.83a78.983 78.983 0 0 1 0 5.29c-0.048 1.431 -0.933 2.74 -2.513 2.83 -0.28 0.016 -0.593 0.025 -0.944 0.025Z\" stroke-width=\"1\"/>\r\n    <path id=\"Union_3\" class=\"main-color\" d=\"M24 12.531c-0.35 0 -0.665 -0.009 -0.944 -0.025 -1.58 -0.09 -2.465 -1.399 -2.513 -2.83a78.983 78.983 0 0 1 0 -5.29c0.048 -1.43 0.933 -2.74 2.513 -2.83a17.564 17.564 0 0 1 1.888 0c1.58 0.09 2.465 1.4 2.513 2.83a78.983 78.983 0 0 1 0 5.29c-0.048 1.431 -0.933 2.74 -2.513 2.83 -0.28 0.016 -0.593 0.025 -0.944 0.025Z\" stroke-width=\"1\"/>\r\n    <path id=\"Union_4\" class=\"main-color\" d=\"M35 12.531c-0.35 0 -0.665 -0.009 -0.944 -0.025 -1.58 -0.09 -2.465 -1.399 -2.513 -2.83a78.983 78.983 0 0 1 0 -5.29c0.048 -1.43 0.933 -2.74 2.513 -2.83a17.564 17.564 0 0 1 1.888 0c1.58 0.09 2.465 1.4 2.513 2.83a78.983 78.983 0 0 1 0 5.29c-0.048 1.431 -0.933 2.74 -2.513 2.83 -0.28 0.016 -0.593 0.025 -0.944 0.025Z\" stroke-width=\"1\"/>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/payment.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Payment-Recieve-7--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>Payment Recieve 7 Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"payment-recieve-7--deposit-payment-finance-atm-withdraw-dollar\">\r\n    <path id=\"Union\" class=\"accent-color\" fill=\"#8fbffa\" d=\"M24 47.52C16.1987 47.52 10.7556 47.3224 7.3573 47.1364C4.0247 46.9545 1.2421 44.5335 0.9034 41.0985C0.6765 38.7988 0.48 35.575 0.48 31.3173C0.48 27.0597 0.6765 23.8359 0.9034 21.5362C1.2421 18.1001 4.0247 15.6802 7.3573 15.4983C10.7546 15.3122 16.1987 15.1147 24 15.1147C31.8013 15.1147 37.2444 15.3122 40.6428 15.4983C43.9753 15.6802 46.758 18.1012 47.0966 21.5362C47.3235 23.8359 47.52 27.0597 47.52 31.3173C47.52 35.575 47.3235 38.7988 47.0966 41.0985C46.758 44.5345 43.9753 46.9545 40.6428 47.1364C37.2454 47.3224 31.8013 47.52 24 47.52Z\" stroke-width=\"1\"/>\r\n    <path id=\"Vector_1565_Stroke\" class=\"main-color\" fill=\"#2859c5\" fill-rule=\"evenodd\" d=\"M23.4773 10.9333C24.6319 10.9333 25.568 9.9973 25.568 8.8427V2.5707C25.5715 0.9613 23.8315 -0.0484 22.436 0.7532C21.7855 1.1269 21.385 1.8205 21.3867 2.5707V8.8427C21.3867 9.9973 22.3227 10.9333 23.4773 10.9333Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n    <path id=\"Vector_1566_Stroke\" class=\"main-color\" fill=\"#2859c5\" fill-rule=\"evenodd\" d=\"M31.84 13.024C32.9946 13.0239 33.9307 12.0879 33.9307 10.9333V5.7067C33.9342 4.0973 32.1942 3.0876 30.7986 3.8892C30.1481 4.2629 29.7477 4.9565 29.7493 5.7067V10.9333C29.7493 12.088 30.6853 13.0241 31.84 13.024Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n    <path id=\"Vector_1567_Stroke\" class=\"main-color\" fill=\"#2859c5\" fill-rule=\"evenodd\" d=\"M15.1147 13.024C13.96 13.024 13.024 12.088 13.024 10.9333V5.7067C13.0205 4.0973 14.7605 3.0876 16.156 3.8892C16.8065 4.2629 17.207 4.9565 17.2053 5.7067V10.9333C17.2053 12.0879 16.2693 13.024 15.1147 13.024Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n    <path id=\"Subtract\" class=\"main-color\" fill=\"#2859c5\" fill-rule=\"evenodd\" d=\"M25.0453 22.432C25.0453 21.2249 23.7387 20.4705 22.6933 21.0741C22.2082 21.3542 21.9093 21.8718 21.9093 22.432V23.5725C20.8692 23.7502 19.8145 24.1139 18.9542 24.7704C17.9015 25.5732 17.2053 26.7733 17.2053 28.312C17.2053 29.1984 17.4165 29.9992 17.8503 30.6891C18.2778 31.3706 18.8622 31.8494 19.4653 32.1933C20.5629 32.8205 21.9428 33.1153 23.031 33.3484L23.1491 33.3735C24.3972 33.6401 25.3067 33.8512 25.9339 34.2098C26.2119 34.3687 26.3625 34.5202 26.4492 34.6593C26.5297 34.7868 26.6133 34.9938 26.6133 35.368C26.6133 35.8551 26.4335 36.1604 26.098 36.4165C25.7039 36.7175 25.0568 36.9611 24.1965 37.0614C23.327 37.1554 22.448 37.1081 21.5936 36.9214C20.7354 36.7332 20.102 36.4405 19.7559 36.1614C18.8149 35.405 17.4078 35.951 17.2233 37.1442C17.1377 37.698 17.354 38.2554 17.7907 38.6064C18.6468 39.2932 19.7894 39.7364 20.9215 39.9852C21.2424 40.0553 21.5727 40.1117 21.9093 40.1546V41.248C21.9093 42.455 23.216 43.2094 24.2613 42.6059C24.7465 42.3258 25.0453 41.8082 25.0453 41.248V40.1075C26.0854 39.9309 27.1402 39.5661 28.0005 38.9096C29.0531 38.1068 29.7493 36.9067 29.7493 35.368C29.7493 34.4816 29.5392 33.6808 29.1054 32.9899C28.6768 32.3094 28.0914 31.8306 27.4893 31.4867C26.3917 30.8595 25.0119 30.5647 23.9237 30.3316L23.8056 30.3065C22.5585 30.0389 21.648 29.8288 21.0218 29.4702C20.7427 29.3113 20.5922 29.1598 20.5055 29.0207C20.425 28.8932 20.3413 28.6862 20.3413 28.312C20.3413 27.8249 20.5211 27.5196 20.8567 27.2635C21.2508 26.9625 21.8978 26.7189 22.7581 26.6186C23.628 26.5246 24.5073 26.5719 25.3621 26.7586C26.2192 26.9468 26.8527 27.2395 27.1998 27.5186C28.1409 28.2746 29.5475 27.7283 29.7316 26.5353C29.8171 25.9816 29.6007 25.4244 29.164 25.0736C28.3078 24.3868 27.1653 23.9435 26.0332 23.6948C25.7066 23.6236 25.377 23.5671 25.0453 23.5254V22.432Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n    <path id=\"Subtract_2\" class=\"main-color\" fill=\"#2859c5\" d=\"M11.9787 31.84C11.9787 30.6854 11.0426 29.7494 9.888 29.7493H8.8427C7.2333 29.7458 6.2236 31.4858 7.0252 32.8814C7.3989 33.5319 8.0925 33.9323 8.8427 33.9307H9.888C11.0426 33.9307 11.9787 32.9946 11.9787 31.84Z\" stroke-width=\"1\"/>\r\n    <path id=\"Subtract_3\" class=\"main-color\" fill=\"#2859c5\" d=\"M37.0667 29.7493C35.4573 29.7458 34.4476 31.4858 35.2492 32.8814C35.6229 33.5319 36.3165 33.9323 37.0667 33.9307H38.112C39.7214 33.9342 40.7311 32.1942 39.9294 30.7986C39.5557 30.1481 38.8622 29.7477 38.112 29.7493H37.0667Z\" stroke-width=\"1\"/>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/renew.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Rotate-Left--Streamline-Plump\" height=\"48\" width=\"48\">\r\n    <g id=\"rotate-left\">\r\n        <path id=\"Union\" class=\"accent-color\" fill-rule=\"evenodd\" d=\"M46.002 24.252C46.002 12.235 36.277 2 24.282 2c-6.843 0 -13.34 2.924 -17.495 7.768A73.686 73.686 0 0 0 4.274 8.47c-1.207 -0.59 -2.41 0.295 -2.262 1.632 0.58 5.209 1.925 8.672 2.66 10.248 0.289 0.62 0.912 0.98 1.594 0.92 1.73 -0.15 5.397 -0.715 10.19 -2.817 1.23 -0.54 1.396 -2.022 0.283 -2.774a71.485 71.485 0 0 0 -2.14 -1.383c2.48 -2.629 5.77 -4.384 9.682 -4.384 8.18 0 14.81 6.147 14.81 14.34 0 3.568 -1.258 6.842 -3.354 9.401 -0.626 0.765 -0.74 1.858 -0.167 2.664l1.758 2.474c0.687 0.967 2.067 1.13 2.874 0.26a21.7 21.7 0 0 0 5.8 -14.8ZM20.105 43.407c-0.165 1.149 -1.276 1.925 -2.377 1.558a19.778 19.778 0 0 1 -7.423 -4.488c-0.798 -0.763 -0.698 -2.034 0.138 -2.755l1.74 -1.501c0.738 -0.636 1.82 -0.638 2.63 -0.095A17.801 17.801 0 0 0 19 38.193c0.99 0.335 1.668 1.298 1.52 2.332l-0.415 2.882ZM8.891 35.146c-0.92 0.793 -2.335 0.591 -2.885 -0.492a23.216 23.216 0 0 1 -1.433 -3.517c-0.473 -1.499 0.722 -2.93 2.294 -2.93 1.132 0 2.102 0.77 2.573 1.8 0.223 0.489 0.48 0.965 0.765 1.426 0.573 0.927 0.485 2.16 -0.34 2.873l-0.974 0.84Zm25.595 8.315c0.998 -0.53 1.232 -1.82 0.578 -2.74l-1.711 -2.408c-0.618 -0.87 -1.806 -1.088 -2.772 -0.634a14.894 14.894 0 0 1 -5.063 1.354c-1.019 0.086 -1.907 0.81 -2.053 1.823l-0.427 2.972c-0.165 1.148 0.681 2.205 1.841 2.171a21.847 21.847 0 0 0 9.607 -2.538Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n    </g>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/save.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"File-Check-Alternate--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>File Check Alternate Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"file-check-alternate--file-common-check\">\r\n    <path id=\"Subtract\" class=\"accent-color\" d=\"M13.582 2.137C16.326 1.823 20.685 1.5 27 1.5a165 165 0 0 1 5.13 0.077 1.5 1.5 0 0 1 0.4 0.068c1.098 0.343 4.029 1.564 8.123 5.578 3.862 3.787 5.195 6.563 5.63 7.781a1.5 1.5 0 0 1 0.087 0.45c0.08 2.153 0.13 4.655 0.13 7.546 0 7.57 -0.343 12.478 -0.669 15.432 -0.32 2.9 -2.518 5.1 -5.413 5.431 -2.744 0.314 -7.103 0.637 -13.418 0.637 -1.044 0 -2.035 -0.009 -2.974 -0.025A14.458 14.458 0 0 0 28.5 34c0 -8.008 -6.492 -14.5 -14.5 -14.5a14.44 14.44 0 0 0 -6.492 1.531c0.053 -6.464 0.364 -10.773 0.66 -13.463 0.32 -2.9 2.519 -5.1 5.414 -5.431Z\" stroke-width=\"1\"></path>\r\n    <path id=\"Intersect\" class=\"main-color\" d=\"M46.348 15.25c-2.42 -0.001 -6.57 -0.04 -8.948 -0.268 -2.598 -0.249 -4.641 -2.321 -4.896 -4.975 -0.214 -2.233 -0.253 -5.99 -0.254 -8.421 0.095 0.01 0.188 0.03 0.28 0.059 1.098 0.343 4.029 1.564 8.123 5.578 3.862 3.787 5.195 6.563 5.63 7.781 0.029 0.08 0.05 0.163 0.065 0.246Z\" stroke-width=\"1\"></path>\r\n    <path id=\"Subtract_2\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M14 46c6.627 0 12 -5.373 12 -12s-5.373 -12 -12 -12S2 27.373 2 34s5.373 12 12 12Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n    <path id=\"Subtract_3\" class=\"accent-color\" fill-rule=\"evenodd\" d=\"M20.611 31.185a2 2 0 1 0 -3.222 -2.37l-4.413 6.002L10.5 32.01a2 2 0 0 0 -3 2.647l4.118 4.666a2 2 0 0 0 3.111 -0.138l5.882 -8Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/subscription.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Layers-1--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>Layers 1 Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"layers-1--design-layer-layers-pile-stack-align\">\r\n    <path id=\"Union\" class=\"accent-color\" d=\"M25.65 22.818a7.074 7.074 0 0 0 -3.3 0c-2.005 0.48 -6.01 1.62 -10.995 3.994 -3.882 1.848 -6.57 3.453 -8.228 4.542 -1.945 1.28 -1.945 4.012 0 5.29 1.657 1.09 4.346 2.695 8.228 4.543 4.984 2.373 8.99 3.514 10.995 3.994 1.087 0.26 2.212 0.26 3.3 0 2.005 -0.48 6.01 -1.62 10.995 -3.994 3.881 -1.848 6.57 -3.453 8.228 -4.543 1.945 -1.278 1.945 -4.01 0 -5.29 -1.658 -1.09 -4.346 -2.694 -8.228 -4.542 -4.985 -2.374 -8.99 -3.515 -10.995 -3.994Z\" stroke-width=\"1\"></path>\r\n    <path id=\"Union_2\" class=\"main-color\" d=\"M25.65 12.818a7.074 7.074 0 0 0 -3.3 0c-2.005 0.48 -6.01 1.62 -10.995 3.994 -3.882 1.848 -6.57 3.453 -8.228 4.542 -1.945 1.28 -1.945 4.012 0 5.29 1.657 1.09 4.346 2.695 8.228 4.543 4.984 2.373 8.99 3.514 10.995 3.994 1.087 0.26 2.212 0.26 3.3 0 2.005 -0.48 6.01 -1.62 10.995 -3.994 3.881 -1.849 6.57 -3.453 8.228 -4.543 1.945 -1.278 1.945 -4.01 0 -5.29 -1.658 -1.09 -4.346 -2.694 -8.228 -4.542 -4.985 -2.374 -8.99 -3.515 -10.995 -3.994Z\" stroke-width=\"1\"></path>\r\n    <path id=\"Union_3\" class=\"accent-color\" d=\"M25.65 2.818a7.076 7.076 0 0 0 -3.3 0c-2.005 0.48 -6.01 1.62 -10.995 3.994 -3.882 1.848 -6.57 3.453 -8.228 4.542 -1.945 1.28 -1.945 4.012 0 5.29 1.658 1.09 4.346 2.695 8.228 4.543 4.984 2.373 8.99 3.514 10.995 3.993 1.088 0.26 2.212 0.26 3.3 0 2.005 -0.479 6.01 -1.62 10.995 -3.993 3.882 -1.849 6.57 -3.453 8.228 -4.543 1.945 -1.278 1.945 -4.01 0 -5.29 -1.658 -1.09 -4.346 -2.694 -8.228 -4.542 -4.984 -2.374 -8.99 -3.514 -10.995 -3.994Z\" stroke-width=\"1\"></path>\r\n  </g>\r\n</svg>"
  },
  {
    "path": "images/siteicons/svg/web.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Web--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>Web Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"web--server-world-internet-earth-www-globe-worldwide-web-network\">\r\n    <path id=\"Union\" class=\"accent-color\" d=\"M24 1.5C11.574 1.5 1.5 11.574 1.5 24S11.574 46.5 24 46.5 46.5 36.426 46.5 24 36.426 1.5 24 1.5Z\" stroke-width=\"1\"></path>\r\n    <path id=\"Intersect\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M17.091 32.5c0.443 3.068 1.116 5.815 1.952 8.076 0.779 2.106 1.67 3.713 2.585 4.765 0.486 0.558 0.94 0.92 1.355 1.137a22.473 22.473 0 0 1 -4.769 -0.729c-0.75 -1.184 -1.412 -2.584 -1.985 -4.133 -0.957 -2.589 -1.7 -5.694 -2.167 -9.116H3.16a22.332 22.332 0 0 1 -0.984 -3H13.73a64.97 64.97 0 0 1 -0.23 -5.5c0 -1.881 0.08 -3.722 0.231 -5.5H2.177c0.259 -1.03 0.588 -2.032 0.984 -3h10.903c0.469 -3.422 1.214 -6.527 2.173 -9.116 0.574 -1.55 1.237 -2.951 1.988 -4.136a22.475 22.475 0 0 1 4.764 -0.726c-0.413 0.217 -0.866 0.579 -1.352 1.137 -0.914 1.053 -1.807 2.66 -2.587 4.766 -0.837 2.262 -1.512 5.008 -1.957 8.075h13.813c-0.445 -3.067 -1.12 -5.813 -1.957 -8.075 -0.78 -2.106 -1.672 -3.713 -2.587 -4.766 -0.485 -0.558 -0.938 -0.92 -1.352 -1.137 1.64 0.073 3.234 0.32 4.764 0.726 0.751 1.185 1.415 2.585 1.989 4.136 0.958 2.589 1.703 5.694 2.172 9.116H44.84c0.395 0.968 0.725 1.97 0.984 3H34.268c0.152 1.778 0.232 3.619 0.232 5.5 0 1.881 -0.08 3.723 -0.231 5.5h11.554a22.319 22.319 0 0 1 -0.984 3H33.938c-0.468 3.422 -1.21 6.527 -2.167 9.116 -0.573 1.549 -1.236 2.95 -1.986 4.133a22.473 22.473 0 0 1 -4.769 0.729c0.416 -0.218 0.87 -0.58 1.355 -1.137 0.915 -1.052 1.807 -2.659 2.586 -4.765 0.836 -2.261 1.508 -5.008 1.951 -8.076H17.091ZM31.5 24c0 -1.894 -0.085 -3.736 -0.244 -5.5H16.743A61.529 61.529 0 0 0 16.5 24a61.8 61.8 0 0 0 0.242 5.5h14.516A61.8 61.8 0 0 0 31.5 24Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n  </g>\r\n</svg>\r\n"
  },
  {
    "path": "images/siteicons/svg/websearch.php",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Search-Visual--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <desc>Search Visual Streamline Icon: https://streamlinehq.com</desc>\r\n  <g id=\"search-visual\">\r\n    <path id=\"Subtract\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M35 22c0 2.542 -0.73 4.913 -1.99 6.916l0.55 0.456c1.205 1 2.393 1.984 3.564 3.078 1.158 1.082 1.302 2.718 0.207 3.863a32.717 32.717 0 0 1 -1.018 1.018c-1.145 1.095 -2.781 0.951 -3.863 -0.207 -1.094 -1.171 -2.078 -2.358 -3.078 -3.565l-0.456 -0.55A12.939 12.939 0 0 1 22 35c-7.18 0 -13 -5.82 -13 -13S14.82 9 22 9s13 5.82 13 13Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n    <path id=\"Subtract_2\" class=\"accent-color\" fill-rule=\"evenodd\" d=\"M14.5 22a7.5 7.5 0 1 1 15 0 7.5 7.5 0 0 1 -15 0Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n    <path id=\"Union\" class=\"accent-color\" fill-rule=\"evenodd\" d=\"M13.924 1.501a2.5 2.5 0 1 1 0.152 4.998l-6.31 0.192A1.159 1.159 0 0 0 6.69 7.765l-0.19 6.311a2.5 2.5 0 1 1 -4.998 -0.152l0.194 -6.357 0.003 -0.072a6.16 6.16 0 0 1 5.87 -5.8l6.356 -0.194Zm27.577 32.423a2.5 2.5 0 1 1 4.998 0.152l-0.194 6.357 -0.003 0.072a6.16 6.16 0 0 1 -5.87 5.8l-6.356 0.194a2.5 2.5 0 0 1 -0.152 -4.998l6.31 -0.192a1.158 1.158 0 0 0 1.075 -1.074l0.192 -6.311ZM3.924 31.5a2.5 2.5 0 0 1 2.575 2.423l0.192 6.311a1.159 1.159 0 0 0 1.074 1.074l6.311 0.192a2.5 2.5 0 1 1 -0.152 4.998l-6.356 -0.194 -0.072 -0.003a6.16 6.16 0 0 1 -5.801 -5.87L1.5 34.077a2.5 2.5 0 0 1 2.423 -2.575ZM31.5 3.924a2.5 2.5 0 0 1 2.575 -2.423l6.356 0.194 0.072 0.003a6.16 6.16 0 0 1 5.801 5.87l0.194 6.356a2.5 2.5 0 0 1 -4.998 0.152l-0.192 -6.311a1.158 1.158 0 0 0 -1.074 -1.074l-6.311 -0.192A2.5 2.5 0 0 1 31.5 3.924Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n  </g>\r\n</svg>"
  },
  {
    "path": "includes/checkredirect.php",
    "content": "<?php\r\n\r\n$currentPage = basename($_SERVER['PHP_SELF']);\r\nif ($currentPage == 'index.php') {\r\n    // Redirect to subscriptions page if no subscriptions exist\r\n    $stmt = $db->prepare(\"SELECT COUNT(*) FROM subscriptions WHERE user_id = :userId\");\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_NUM);\r\n    $subscriptionCount = $row[0];\r\n\r\n    if ($subscriptionCount === 0) {\r\n        header('Location: subscriptions.php');\r\n        exit;\r\n    }\r\n}"
  },
  {
    "path": "includes/checksession.php",
    "content": "<?php\r\n// Handle OIDC first\r\nif (session_status() === PHP_SESSION_NONE) {\r\n    session_start();\r\n}\r\n\r\nif (isset($_GET['code']) && isset($_GET['state'])) {\r\n    // This request is coming from the OIDC login flow\r\n    $code = $_GET['code'];\r\n    $state = $_GET['state'];\r\n\r\n    require_once 'includes/oidc/handle_oidc_callback.php';\r\n\r\n} else {\r\n    if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n        $username = $_SESSION['username'];\r\n        $main_currency = $_SESSION['main_currency'];\r\n        $sql = \"SELECT * FROM user WHERE username = :username\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n        $result = $stmt->execute();\r\n        $userData = $result->fetchArray(SQLITE3_ASSOC);\r\n        $userId = $userData['id'];\r\n\r\n        if ($userData === false) {\r\n            header('Location: logout.php');\r\n            exit();\r\n        } else {\r\n            $_SESSION['userId'] = $userData['id'];\r\n        }\r\n\r\n        if ($userData['avatar'] == \"\") {\r\n            $userData['avatar'] = \"0\";\r\n        }\r\n    } else {\r\n\r\n        if (isset($_COOKIE['wallos_login'])) {\r\n            $cookie = explode('|', $_COOKIE['wallos_login'], 3);\r\n            $username = $cookie[0];\r\n            $token = $cookie[1];\r\n            $main_currency = $cookie[2];\r\n\r\n            $sql = \"SELECT * FROM user WHERE username = :username\";\r\n            $stmt = $db->prepare($sql);\r\n            $stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n            $result = $stmt->execute();\r\n\r\n            if ($result) {\r\n                $userData = $result->fetchArray(SQLITE3_ASSOC);\r\n                if (!isset($userData['id'])) {\r\n                    $db->close();\r\n                    header(\"Location: logout.php\");\r\n                    exit();\r\n                }\r\n\r\n                if ($userData['avatar'] == \"\") {\r\n                    $userData['avatar'] = \"0\";\r\n                }\r\n                $userId = $userData['id'];\r\n                $main_currency = $userData['main_currency'];\r\n\r\n                $adminQuery = \"SELECT login_disabled FROM admin\";\r\n                $adminResult = $db->query($adminQuery);\r\n                $adminRow = $adminResult->fetchArray(SQLITE3_ASSOC);\r\n                if ($adminRow['login_disabled'] == 1) {\r\n                    $sql = \"SELECT * FROM login_tokens WHERE user_id = :userId\";\r\n                    $stmt = $db->prepare($sql);\r\n                    $stmt->bindParam(':userId', $userId, SQLITE3_TEXT);\r\n                } else {\r\n                    $sql = \"SELECT * FROM login_tokens WHERE user_id = :userId AND token = :token\";\r\n                    $stmt = $db->prepare($sql);\r\n                    $stmt->bindParam(':userId', $userId, SQLITE3_TEXT);\r\n                    $stmt->bindParam(':token', $token, SQLITE3_TEXT);\r\n                }\r\n                $result = $stmt->execute();\r\n                $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                if ($row != false) {\r\n                    $_SESSION['username'] = $username;\r\n                    $_SESSION['token'] = $token;\r\n                    $_SESSION['loggedin'] = true;\r\n                    $_SESSION['main_currency'] = $main_currency;\r\n                    $_SESSION['userId'] = $userId;\r\n                } else {\r\n                    $db->close();\r\n                    header(\"Location: logout.php\");\r\n                    exit();\r\n                }\r\n            } else {\r\n                $db->close();\r\n                header(\"Location: logout.php\");\r\n                exit();\r\n            }\r\n\r\n\r\n        } else {\r\n            $db->close();\r\n            header(\"Location: login.php\");\r\n            exit();\r\n        }\r\n    }\r\n}\r\n\r\n\r\n?>"
  },
  {
    "path": "includes/checkuser.php",
    "content": "<?php\r\n$query = \"SELECT COUNT(*) as count FROM user\";\r\n$result = $db->query($query);\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n$userCount = $row['count'];\r\n?>"
  },
  {
    "path": "includes/connect.php",
    "content": "<?php\r\n\r\n$databaseFile = 'db/wallos.db';\r\n\r\n$db = new SQLite3($databaseFile);\r\n$db->busyTimeout(5000);\r\n\r\nif (!$db) {\r\n    die('Connection to the database failed.');\r\n}\r\n\r\n?>"
  },
  {
    "path": "includes/connect_endpoint.php",
    "content": "<?php\r\n\r\n$databaseFile = '../../db/wallos.db';\r\n$db = new SQLite3($databaseFile);\r\n$db->busyTimeout(5000);\r\n\r\nif (!$db) {\r\n    die('Connection to the database failed.');\r\n}\r\n\r\nrequire_once 'i18n/languages.php';\r\nrequire_once 'i18n/getlang.php';\r\nrequire_once 'i18n/' . $lang . '.php';\r\n\r\nsession_start();\r\n\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    $userId = $_SESSION['userId'];\r\n} else {\r\n    $userId = 0;\r\n}\r\n\r\n?>"
  },
  {
    "path": "includes/connect_endpoint_crontabs.php",
    "content": "<?php\r\n\r\n$databaseFile = __DIR__ . '/../db/wallos.db';\r\n$db = new SQLite3($databaseFile);\r\n$db->busyTimeout(5000);\r\n\r\nif (!$db) {\r\n    die('Connection to the database failed.');\r\n}\r\n\r\nrequire_once __DIR__ . '/../includes/i18n/languages.php';\r\nrequire_once __DIR__ . '/../includes/i18n/getlang.php';\r\nrequire_once __DIR__ . '/../includes/i18n/' . $lang . '.php';\r\n\r\n?>"
  },
  {
    "path": "includes/currency_formatter.php",
    "content": "<?php\n\nfinal class CurrencyFormatter\n{\n    private static $instance;\n\n    private static function getInstance()\n    {\n        if (self::$instance === null) {\n            if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {\n                self::$instance = new NumberFormatter(Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']), NumberFormatter::CURRENCY);\n            } else {\n                self::$instance = new NumberFormatter('en_US', NumberFormatter::CURRENCY);\n            }\n        }\n\n        return self::$instance;\n    }\n\n    public static function format($amount, $currency)\n    {\n        return self::getInstance()->formatCurrency($amount, $currency);\n    }\n}\n"
  },
  {
    "path": "includes/filters_menu.php",
    "content": "<div class=\"filtermenu-content\">\r\n  <?php\r\n  if (count($members) > 1) {\r\n    ?>\r\n    <div class=\"filtermenu-submenu\">\r\n      <div class=\"filter-title\" onClick=\"toggleSubMenu('member')\"><?= translate(\"member\", $i18n) ?></div>\r\n      <div class=\"filtermenu-submenu-content\" id=\"filter-member\">\r\n        <?php\r\n        foreach ($members as $member) {\r\n          if ($member['count'] == 0) {\r\n            continue;\r\n          }\r\n          $selectedClass = '';\r\n          if (isset($_GET['member'])) {\r\n            $memberIds = explode(',', $_GET['member']);\r\n            if (in_array($member['id'], $memberIds)) {\r\n              $selectedClass = 'selected';\r\n            }\r\n          }\r\n          ?>\r\n          <div class=\"filter-item <?= $selectedClass ?>\" data-memberid=\"<?= $member['id'] ?>\"><?= $member['name'] ?>\r\n          </div>\r\n          <?php\r\n        }\r\n        ?>\r\n      </div>\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n  <?php\r\n  if (count($categories) > 1) {\r\n    ?>\r\n    <div class=\"filtermenu-submenu\">\r\n      <div class=\"filter-title\" onClick=\"toggleSubMenu('category')\"><?= translate(\"category\", $i18n) ?></div>\r\n      <div class=\"filtermenu-submenu-content\" id=\"filter-category\">\r\n        <?php\r\n        foreach ($categories as $category) {\r\n          if ($category['count'] == 0) {\r\n            continue;\r\n          }\r\n          if ($category['name'] == \"No category\") {\r\n            $category['name'] = translate(\"no_category\", $i18n);\r\n          }\r\n          $selectedClass = '';\r\n          if (isset($_GET['category'])) {\r\n            $categoryIds = explode(',', $_GET['category']);\r\n            if (in_array($category['id'], $categoryIds)) {\r\n              $selectedClass = 'selected';\r\n            }\r\n          }\r\n          ?>\r\n          <div class=\"filter-item <?= $selectedClass ?>\" data-categoryid=\"<?= $category['id'] ?>\">\r\n            <?= $category['name'] ?>\r\n          </div>\r\n          <?php\r\n        }\r\n        ?>\r\n      </div>\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n  <?php\r\n  if (count($payment_methods) > 1) {\r\n    ?>\r\n    <div class=\"filtermenu-submenu\">\r\n      <div class=\"filter-title\" onClick=\"toggleSubMenu('payment')\"><?= translate(\"payment_method\", $i18n) ?></div>\r\n      <div class=\"filtermenu-submenu-content\" id=\"filter-payment\">\r\n        <?php\r\n        foreach ($payment_methods as $payment) {\r\n          if ($payment['count'] == 0) {\r\n            continue;\r\n          }\r\n          $selectedClass = '';\r\n          if (isset($_GET['payment'])) {\r\n            $paymentIds = explode(',', $_GET['payment']);\r\n            if (in_array($payment['id'], $paymentIds)) {\r\n              $selectedClass = 'selected';\r\n            }\r\n          }\r\n          ?>\r\n          <div class=\"filter-item <?= $selectedClass ?>\" data-paymentid=\"<?= $payment['id'] ?>\">\r\n            <?= $payment['name'] ?>\r\n          </div>\r\n          <?php\r\n        }\r\n        ?>\r\n      </div>\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n  <?php\r\n  if (!isset($settings['hideDisabledSubscriptions']) || $settings['hideDisabledSubscriptions'] !== 'true') {\r\n    ?>\r\n    <div class=\"filtermenu-submenu\">\r\n      <div class=\"filter-title\" onClick=\"toggleSubMenu('state')\"><?= translate(\"state\", $i18n) ?></div>\r\n      <div class=\"filtermenu-submenu-content\" id=\"filter-state\">\r\n        <div class=\"filter-item capitalize\" data-state=\"0\"><?= translate(\"enabled\", $i18n) ?></div>\r\n        <div class=\"filter-item capitalize\" data-state=\"1\"><?= translate(\"disabled\", $i18n) ?></div>\r\n      </div>\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n\r\n  <div class=\"filtermenu-submenu\">\r\n    <div class=\"filter-title\" onClick=\"toggleSubMenu('renewal_type')\"><?= translate(\"renewal_type\", $i18n) ?></div>\r\n    <div class=\"filtermenu-submenu-content\" id=\"filter-renewal_type\">\r\n      <div class=\"filter-item capitalize\" data-renewaltype=\"1\"><?= translate(\"auto_renewal\", $i18n) ?></div>\r\n      <div class=\"filter-item capitalize\" data-renewaltype=\"0\"><?= translate(\"manual_renewal\", $i18n) ?></div>\r\n    </div>\r\n  </div>\r\n\r\n  <div class=\"filtermenu-submenu hide\" id=\"clear-filters\">\r\n    <div class=\"filter-title filter-clear\" onClick=\"clearFilters()\">\r\n      <i class=\"fa-solid fa-times-circle\"></i> <?= translate(\"clear\", $i18n) ?>\r\n    </div>\r\n  </div>\r\n</div>"
  },
  {
    "path": "includes/footer.php",
    "content": "</main>\r\n\r\n<div class=\"toast\" id=\"errorToast\">\r\n  <div class=\"toast-content\">\r\n    <i class=\"fas fa-solid fa-x toast-icon error\"></i>\r\n    <div class=\"message\">\r\n      <span class=\"text text-1\"><?= translate(\"error\", $i18n) ?></span>\r\n      <span class=\"text text-2 errorMessage\"></span>\r\n    </div>\r\n  </div>\r\n  <i class=\"fa-solid fa-xmark close close-error\"></i>\r\n  <div class=\"progress error\"></div>\r\n</div>\r\n\r\n<div class=\"toast\" id=\"successToast\">\r\n  <div class=\"toast-content\">\r\n    <i class=\"fas fa-solid fa-check toast-icon success\"></i>\r\n    <div class=\"message\">\r\n      <span class=\"text text-1\"><?= translate(\"success\", $i18n) ?></span>\r\n      <span class=\"text text-2 successMessage\"></span>\r\n    </div>\r\n  </div>\r\n  <i class=\"fa-solid fa-xmark close close-success\"></i>\r\n  <div class=\"progress success\"></div>\r\n</div>\r\n\r\n<?php\r\nif (isset($db)) {\r\n  $db->close();\r\n}\r\n?>\r\n\r\n</body>\r\n\r\n</html>"
  },
  {
    "path": "includes/getdbkeys.php",
    "content": "<?php\r\n\r\n    $currencies = array();\r\n    $query = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $currencyId = $row['id'];\r\n        $currencies[$currencyId] = $row;\r\n    }\r\n\r\n    $members = array();\r\n    $query = \"SELECT * FROM household WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $memberId = $row['id'];\r\n        $members[$memberId] = $row;\r\n        $members[$memberId]['count'] = 0;\r\n    }\r\n\r\n    $payment_methods = array();\r\n    $query = $db->prepare(\"SELECT * FROM payment_methods WHERE enabled=:enabled AND user_id = :userId ORDER BY `order` ASC\");\r\n    $query->bindValue(':enabled', 1, SQLITE3_INTEGER);\r\n    $query->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $query->execute();\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $payment_methodId = $row['id'];\r\n        $payment_methods[$payment_methodId] = $row;\r\n        $payment_methods[$payment_methodId]['count'] = 0;\r\n    }\r\n\r\n    $categories = array();\r\n    $query = \"SELECT * FROM categories WHERE user_id = :userId ORDER BY `order` ASC\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $categoryId = $row['id'];\r\n        $categories[$categoryId] = $row;\r\n        $categories[$categoryId]['count'] = 0;\r\n    }\r\n\r\n    $cycles = array();\r\n    $query = \"SELECT * FROM cycles\";\r\n    $result = $db->query($query);\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $cycleId = $row['id'];\r\n        $cycles[$cycleId] = $row;\r\n    }\r\n\r\n    $frequencies = array();\r\n    for ($i = 1; $i <= 366; $i++) {\r\n        $frequencies[$i] = array('id' => $i, 'name' => $i);\r\n    }\r\n\r\n?>"
  },
  {
    "path": "includes/getsettings.php",
    "content": "<?php\r\n\r\n$query = \"SELECT * FROM settings WHERE user_id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\n$settings = $result->fetchArray(SQLITE3_ASSOC);\r\nif ($settings !== false) {\r\n    $themeMapping = array(0 => 'light', 1 => 'dark', 2 => 'automatic');\r\n    $themeKey = isset($settings['dark_theme']) ? $settings['dark_theme'] : 2;\r\n    $themeValue = $themeMapping[$themeKey];\r\n    $settings['update_theme_setttings'] = false;\r\n    if (isset($_COOKIE['inUseTheme']) && $settings['dark_theme'] == 2) {\r\n        $inUseTheme = $_COOKIE['inUseTheme'];\r\n        $settings['theme'] = $inUseTheme;\r\n    } else {\r\n        $settings['theme'] = $themeValue;\r\n    }\r\n    if ($themeValue == \"automatic\") {\r\n        $settings['update_theme_setttings'] = true;\r\n    }\r\n    $settings['color_theme'] = $settings['color_theme'] ? $settings['color_theme'] : \"blue\";\r\n    $settings['showMonthlyPrice'] = $settings['monthly_price'] ? 'true': 'false';\r\n    $settings['convertCurrency'] = $settings['convert_currency'] ? 'true': 'false';\r\n    $settings['removeBackground'] = $settings['remove_background'] ? 'true': 'false';\r\n    $settings['hideDisabledSubscriptions'] = $settings['hide_disabled'] ? 'true': 'false';\r\n    $settings['disabledToBottom'] = $settings['disabled_to_bottom'] ? 'true': 'false';\r\n    $settings['showOriginalPrice'] = $settings['show_original_price'] ? 'true': 'false';\r\n    $settings['mobileNavigation'] = $settings['mobile_nav'] ? 'true': 'false';\r\n    $settings['showSubscriptionProgress'] = $settings['show_subscription_progress'] ? 'true': 'false';\r\n}\r\n\r\n$query = \"SELECT * FROM custom_colors WHERE user_id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$customColors = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($customColors !== false) {\r\n    $settings['customColors'] = $customColors;\r\n}\r\n\r\n$query = \"SELECT * FROM custom_css_style WHERE user_id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$customCss = $result->fetchArray(SQLITE3_ASSOC);\r\nif ($customCss !== false) {\r\n    $settings['customCss'] = $customCss['css'];\r\n}\r\n\r\n$query = \"SELECT * FROM admin\";\r\n$result = $db->query($query);\r\n$adminSettings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($adminSettings !== false) {\r\n    $settings['disableLogin'] = $adminSettings['login_disabled'];\r\n    $settings['update_notification'] = $adminSettings['update_notification'];\r\n    $settings['latest_version'] = $adminSettings['latest_version'];\r\n}\r\n\r\n?>"
  },
  {
    "path": "includes/header.php",
    "content": "<?php\r\nrequire_once 'connect.php';\r\nrequire_once 'checkuser.php';\r\nrequire_once 'checksession.php';\r\nrequire_once 'checkredirect.php';\r\nrequire_once 'currency_formatter.php';\r\n\r\nrequire_once 'libs/csrf.php';\r\n\r\nrequire_once 'i18n/languages.php';\r\nrequire_once 'i18n/getlang.php';\r\nrequire_once 'i18n/' . $lang . '.php';\r\n\r\nrequire_once 'getsettings.php';\r\n\r\nrequire_once 'version.php';\r\n\r\nif ($userCount == 0) {\r\n  $db->close();\r\n  header(\"Location: registration.php\");\r\n  exit();\r\n}\r\n\r\n$demoMode = getenv('DEMO_MODE');\r\n\r\n$theme = \"automatic\";\r\nif (isset($settings['theme'])) {\r\n  $theme = $settings['theme'];\r\n}\r\n\r\n$updateThemeSettings = false;\r\nif (isset($settings['update_theme_setttings'])) {\r\n  $updateThemeSettings = $settings['update_theme_setttings'];\r\n}\r\n\r\n$colorTheme = \"blue\";\r\nif (isset($settings['color_theme'])) {\r\n  $colorTheme = $settings['color_theme'];\r\n}\r\n\r\n$customCss = \"\";\r\nif (isset($settings['customCss'])) {\r\n  $customCss = $settings['customCss'];\r\n}\r\n\r\nif (isset($themeValue)) {\r\n  $cookieExpire = time() + (30 * 24 * 60 * 60);\r\n  setcookie('theme', $themeValue, [\r\n    'expires' => $cookieExpire,\r\n    'samesite' => 'Strict'\r\n  ]);\r\n}\r\n\r\n$isAdmin = $_SESSION['userId'] == 1;\r\n\r\n$locale = isset($_COOKIE['user_locale']) ? $_COOKIE['user_locale'] : 'en_US';\r\n$formatter = new IntlDateFormatter(\r\n  $locale, \r\n  IntlDateFormatter::MEDIUM,\r\n  IntlDateFormatter::NONE\r\n);\r\n\r\nfunction hex2rgb($hex)\r\n{\r\n  $hex = str_replace(\"#\", \"\", $hex);\r\n  if (strlen($hex) == 3) {\r\n    $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1));\r\n    $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1));\r\n    $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1));\r\n  } else {\r\n    $r = hexdec(substr($hex, 0, 2));\r\n    $g = hexdec(substr($hex, 2, 2));\r\n    $b = hexdec(substr($hex, 4, 2));\r\n  }\r\n  return \"$r, $g, $b\";\r\n}\r\n\r\n$mobileNavigation = $settings['mobile_nav'] ? \"mobile-navigation\" : \"\";\r\n\r\n?>\r\n<!DOCTYPE html>\r\n<html dir=\"<?= $languages[$lang]['dir'] ?>\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\r\n  <title>Wallos - Subscription Tracker</title>\r\n  <meta name=\"apple-mobile-web-app-title\" content=\"Wallos\">\r\n  <meta name=\"theme-color\" content=\"<?= $theme == \"light\" ? \"#FFFFFF\" : \"#222222\" ?>\" id=\"theme-color\" />\r\n  <meta name=\"referrer\" content=\"no-referrer\">\r\n  <link rel=\"icon\" type=\"image/png\" href=\"images/icon/favicon.ico\" sizes=\"16x16\">\r\n  <link rel=\"apple-touch-icon\" href=\"images/icon/apple-touch-icon.png\">\r\n  <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"images/icon/apple-touch-icon-152.png\">\r\n  <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"images/icon/apple-touch-icon-180.png\">\r\n  <link rel=\"manifest\" href=\"manifest.json\" crossorigin=\"use-credentials\">\r\n  <link rel=\"stylesheet\" href=\"styles/theme.css?<?= $version ?>\">\r\n  <link rel=\"stylesheet\" href=\"styles/styles.css?<?= $version ?>\">\r\n  <link rel=\"stylesheet\" href=\"styles/dark-theme.css?<?= $version ?>\" id=\"dark-theme\" <?= $theme != \"dark\" ? \"disabled\" : \"\" ?>>\r\n  <link rel=\"stylesheet\" href=\"styles/themes/red.css?<?= $version ?>\" id=\"red-theme\" <?= $colorTheme != \"red\" ? \"disabled\" : \"\" ?>>\r\n  <link rel=\"stylesheet\" href=\"styles/themes/green.css?<?= $version ?>\" id=\"green-theme\" <?= $colorTheme != \"green\" ? \"disabled\" : \"\" ?>>\r\n  <link rel=\"stylesheet\" href=\"styles/themes/yellow.css?<?= $version ?>\" id=\"yellow-theme\" <?= $colorTheme != \"yellow\" ? \"disabled\" : \"\" ?>>\r\n  <link rel=\"stylesheet\" href=\"styles/themes/purple.css?<?= $version ?>\" id=\"purple-theme\" <?= $colorTheme != \"purple\" ? \"disabled\" : \"\" ?>>\r\n  <link rel=\"stylesheet\" href=\"styles/barlow.css\">\r\n  <link rel=\"stylesheet\" href=\"styles/font-awesome.min.css\">\r\n  <link rel=\"stylesheet\" href=\"styles/brands.css\">\r\n  <script type=\"text/javascript\" src=\"scripts/all.js?<?= $version ?>\"></script>\r\n  <script type=\"text/javascript\" src=\"scripts/common.js?<?= $version ?>\"></script>\r\n  <script type=\"text/javascript\">\r\n    window.theme = \"<?= $theme ?>\";\r\n    window.update_theme_settings = \"<?= $updateThemeSettings ?>\";\r\n    window.lang = \"<?= $lang ?>\";\r\n    window.colorTheme = \"<?= $colorTheme ?>\";\r\n    window.mobileNavigation = \"<?= $settings['mobileNavigation'] == \"true\" ?>\";\r\n    window.csrfToken = \"<?= htmlspecialchars(generate_csrf_token()) ?>\";\r\n  </script>\r\n  <style>\r\n    <?= htmlspecialchars($customCss, ENT_QUOTES, 'UTF-8') ?>\r\n  </style>\r\n  <?php\r\n  if (isset($settings['customColors'])) {\r\n    ?>\r\n    <style id=\"custom_theme_colors\">\r\n      :root {\r\n        <?php if (isset($settings['customColors']['main_color']) && !empty($settings['customColors']['main_color'])): ?>\r\n          --main-color:\r\n            <?= $settings['customColors']['main_color'] ?>\r\n          ;\r\n          --main-color-rgb:\r\n            <?= hex2rgb($settings['customColors']['main_color']) ?>\r\n          ;\r\n        <?php endif; ?>\r\n        <?php if (isset($settings['customColors']['accent_color']) && !empty($settings['customColors']['accent_color'])): ?>\r\n          --accent-color:\r\n            <?= $settings['customColors']['accent_color'] ?>\r\n          ;\r\n          --accent-color-rgb:\r\n            <?= hex2rgb($settings['customColors']['accent_color']) ?>\r\n          ;\r\n        <?php endif; ?>\r\n        <?php if (isset($settings['customColors']['hover_color']) && !empty($settings['customColors']['hover_color'])): ?>\r\n          --hover-color:\r\n            <?= $settings['customColors']['hover_color'] ?>\r\n          ;\r\n          --hover-color-rgb:\r\n            <?= hex2rgb($settings['customColors']['hover_color']) ?>\r\n          ;\r\n        <?php endif; ?>\r\n      }\r\n    </style>\r\n    <?php\r\n  }\r\n  ?>\r\n  <script type=\"text/javascript\" src=\"scripts/i18n/<?= $lang ?>.js?<?= $version ?>\"></script>\r\n  <script type=\"text/javascript\" src=\"scripts/i18n/getlang.js?<?= $version ?>\"></script>\r\n  <script>\r\n    if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {\r\n      if (!sessionStorage.getItem('sw_prefetched')) {\r\n        navigator.serviceWorker.controller.postMessage({ type: 'PREFETCH_PAGES' });\r\n        sessionStorage.setItem('sw_prefetched', '1');\r\n      }\r\n    }\r\n  </script>\r\n</head>\r\n\r\n<body class=\"<?= $theme ?> <?= $languages[$lang]['dir'] ?> <?= $mobileNavigation ?>\">\r\n  <header>\r\n    <div class=\"contain\">\r\n      <div class=\"logo\">\r\n        <a href=\".\">\r\n          <div class=\"logo-image\" title=\"Wallos - Subscription Tracker\">\r\n            <?php include \"images/siteicons/svg/logo.php\"; ?>\r\n          </div>\r\n        </a>\r\n      </div>\r\n      <nav>\r\n        <div class=\"dropdown\">\r\n          <button class=\"dropbtn\" onClick=\"toggleDropdown()\">\r\n            <img src=\"<?= htmlspecialchars($userData['avatar'], ENT_QUOTES, 'UTF-8') ?>\" alt=\"me\" id=\"avatar\">\r\n            <span id=\"user\" class=\"mobileNavigationHideOnMobile\"><?= $userData['username'] ?></span>\r\n          </button>\r\n          <div class=\"dropdown-content\">\r\n            <a href=\".\" class=\"mobileNavigationHideOnMobile\">\r\n              <?php include \"images/siteicons/svg/mobile-menu/home.php\"; ?>\r\n              <?= translate('dashboard', $i18n) ?></a>\r\n            <a href=\"subscriptions.php\" class=\"mobileNavigationHideOnMobile\">\r\n              <?php include \"images/siteicons/svg/mobile-menu/subscriptions.php\"; ?>\r\n              <?= translate('subscriptions', $i18n) ?></a>  \r\n            <a href=\"calendar.php\" class=\"mobileNavigationHideOnMobile\">\r\n                <?php include \"images/siteicons/svg/mobile-menu/calendar.php\"; ?>\r\n                <?= translate('calendar', $i18n) ?></a>\r\n            <a href=\"stats.php\" class=\"mobileNavigationHideOnMobile\">\r\n              <?php include \"images/siteicons/svg/mobile-menu/statistics.php\"; ?>\r\n              <?= translate('stats', $i18n) ?></a>\r\n            <a href=\"settings.php\" class=\"mobileNavigationHideOnMobile\">\r\n              <?php include \"images/siteicons/svg/mobile-menu/settings.php\"; ?>\r\n              <?= translate('settings', $i18n) ?></a>\r\n            <a href=\"profile.php\">\r\n              <?php include \"images/siteicons/svg/mobile-menu/profile.php\"; ?>\r\n              <?= translate('profile', $i18n) ?></a>  \r\n            <?php if ($isAdmin): ?>\r\n              <a href=\"admin.php\">\r\n                <?php include \"images/siteicons/svg/mobile-menu/admin.php\"; ?>\r\n                <?= translate('admin', $i18n) ?>\r\n              </a>\r\n            <?php endif; ?>\r\n            <a href=\"about.php\">\r\n              <?php include \"images/siteicons/svg/mobile-menu/about.php\"; ?>\r\n              <?= translate('about', $i18n) ?>\r\n            </a>\r\n            <?php\r\n            if ($settings['disableLogin'] == 0) {\r\n              ?>\r\n              <a href=\"logout.php\">\r\n                <?php include \"images/siteicons/svg/mobile-menu/logout.php\"; ?>\r\n                <?= translate('logout', $i18n) ?></a>\r\n              <?php\r\n            }\r\n            ?>\r\n          </div>\r\n        </div>\r\n      </nav>\r\n    </div>\r\n  </header>\r\n\r\n  <?php\r\n  // find out which page is being viewed\r\n  $page = basename($_SERVER['PHP_SELF']);\r\n  $dashboardClass = $page === 'index.php' ? 'active' : '';\r\n  $subscriptionsClass = $page === 'subscriptions.php' ? 'active' : '';\r\n  $calendarClass = $page === 'calendar.php' ? 'active' : '';\r\n  $statsClass = $page === 'stats.php' ? 'active' : '';\r\n  $settingsClass = $page === 'settings.php' ? 'active' : '';\r\n  $profileClass = $page === 'profile.php' ? 'active' : '';\r\n  ?>\r\n\r\n  <?php\r\n  if ($settings['mobile_nav'] == 1) {\r\n    ?>\r\n    <nav class=\"mobile-nav\">\r\n        <a href=\".\" class=\"nav-link <?= $dashboardClass ?>\" title=\"<?= translate('dashboard', $i18n) ?>\">\r\n          <?php include \"images/siteicons/svg/mobile-menu/home.php\"; ?>\r\n          <?= translate('dashboard', $i18n) ?>\r\n        </a>\r\n        <a href=\"subscriptions.php\" class=\"nav-link <?= $subscriptionsClass ?>\" title=\"<?= translate('subscriptions', $i18n) ?>\">\r\n          <?php include \"images/siteicons/svg/mobile-menu/subscriptions.php\"; ?>\r\n          <?= translate('subscriptions', $i18n) ?>\r\n        </a>\r\n        <a href=\"calendar.php\" class=\"nav-link <?= $calendarClass ?>\" title=\"<?= translate('calendar', $i18n) ?>\">\r\n          <?php include \"images/siteicons/svg/mobile-menu/calendar.php\"; ?>\r\n          <?= translate('calendar', $i18n) ?>\r\n        </a>\r\n        <a href=\"stats.php\" class=\"nav-link <?= $statsClass ?>\" title=\"<?= translate('stats', $i18n) ?>\">\r\n          <?php include \"images/siteicons/svg/mobile-menu/statistics.php\"; ?>\r\n          <?= translate('stats', $i18n) ?>\r\n        </a>\r\n        <a href=\"settings.php\" class=\"nav-link <?= $settingsClass ?>\" title=\"<?= translate('settings', $i18n) ?>\">\r\n          <?php include \"images/siteicons/svg/mobile-menu/settings.php\"; ?>\r\n          <?= translate('settings', $i18n) ?>\r\n        </a>\r\n    </nav>\r\n    <?php\r\n  }\r\n  ?>\r\n\r\n  <main>\r\n"
  },
  {
    "path": "includes/i18n/ca.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"Necessites crear un compte abans de poder iniciar sessió\",\r\n    \"username\" => \"Nom d'Usuari\",\r\n    \"password\" => \"Contrasenya\",\r\n    \"email\" => \"Correu Electrònic\",\r\n    \"firstname\" => \"Nom\",\r\n    \"lastname\" => \"Cognom\",\r\n    \"confirm_password\" => \"Confirmar Contrasenya\",\r\n    \"main_currency\" => \"Moneda Principal\",\r\n    \"language\" => \"Idioma\",\r\n    \"passwords_dont_match\" => \"Les contrasenyes no coincideixen\",\r\n    \"username_exists\" => \"El nom d'usuari ja existeix\",\r\n    \"email_exists\" => \"El correu electrònic ja està registrat\",\r\n    \"registration_failed\" => \"Error en el registre, intenti-ho de nou.\",\r\n    \"register\" => \"Registrar\",\r\n    \"restore_database\" => \"Restaurar Base de Dades\",\r\n    // Login Page\r\n    \"please_login\" => \"Si us plau, inicia sessió\",\r\n    \"stay_logged_in\" => \"Mantenir sessió iniciada (30 dies)\",\r\n    \"login\" => \"Iniciar Sessió\",\r\n    \"login_with\" => \"Iniciar sessió amb\",\r\n    \"or\" => \"o\",\r\n    \"login_failed\" => \"Els detalls d'inici de sessió son incorrectes\",\r\n    \"registration_successful\" => \"Registre completat amb èxit\",\r\n    \"user_email_waiting_verification\" => \"S'ha de confirmar el teu correu electrònic. Comprova la teva bústia\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Has oblidat la contrasenya?\",\r\n    \"reset_password\" => \"Restablir Contrasenya\",\r\n    \"reset_sent_check_email\" => \"S'ha enviat un correu electrònic per restablir la contrasenya. Comprova la teva bústia.\",\r\n    \"password_reset_successful\" => \"Contrasenya restablerta correctament\",\r\n    // Header\r\n    \"profile\" => \"Perfil\",\r\n    \"dashboard\" => \"Taulell\",\r\n    \"subscriptions\" => \"Subscripcions\",\r\n    \"stats\" => \"Estadístiques\",\r\n    \"settings\" => \"Configuració\",\r\n    \"admin\" => \"Admin\",\r\n    \"about\" => \"Quant a\",\r\n    \"logout\" => \"Tanca la Sessió\",\r\n    // Dashboard\r\n    \"hello\" => \"Hola\",\r\n    \"upcoming_payments\" => \"Pròxims Pagaments\",\r\n    \"no_upcoming_payments\" => \"No tens pròxims pagaments\",\r\n    \"overdue_renewals\" => \"Renovacions Endarrerides\",\r\n    \"ai_recommendations\" => \"Recomanacions per IA\",\r\n    \"your_budget\" => \"El Teu Pressupost\",\r\n    \"budget\" => \"Pressupost\",\r\n    \"budget_used\" => \"Pressupost Utilitzat\",\r\n    \"over_budget\" => \"Sobre Pressupost\",\r\n    \"your_subscriptions\" => \"Les Teves Subscripcions\",\r\n    \"your_savings\" => \"El Teu Estalvi\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Subscripció\",\r\n    \"no_subscriptions_yet\" => \"Encara no tens cap subscripció\",\r\n    \"add_first_subscription\" => \"Afegir la primera subscripció\",\r\n    \"new_subscription\" => \"Nova Subscripció\",\r\n    \"search\" => \"Buscar\",\r\n    \"state\" => \"Estat\",\r\n    \"alphanumeric\" => \"Alfabètic\",\r\n    \"sort\" => \"Ordenar\",\r\n    \"name\" => \"Nom\",\r\n    \"last_added\" => \"Última Afegida\",\r\n    \"price\" => \"Preu\",\r\n    \"next_payment\" => \"Pròxim Pagament\",\r\n    \"renewal_type\" => \"Tipus de Renovació\",\r\n    \"auto_renewal\" => \"Renovació Automàtica\",\r\n    \"automatically_renews\" => \"Renova Automàticament\",\r\n    \"manual_renewal\" => \"Renovació Manual\",\r\n    \"start_date\" => \"Data d'Inici\",\r\n    \"inactive\" => \"Desactivar Subscripció\",\r\n    \"replaced_with\" => \"Reemplaçada amb\",\r\n    \"none\" => \"Cap\",\r\n    \"member\" => \"Membre\",\r\n    \"category\" => \"Categoria\",\r\n    \"payment_method\" => \"Mètode de Pagament\",\r\n    \"Daily\" => \"Diari\",\r\n    \"Weekly\" => \"Setmanal\",\r\n    \"Monthly\" => \"Mensual\",\r\n    \"Yearly\" => \"Anual\",\r\n    \"daily\" => \"Dia / Dies\",\r\n    \"weekly\" => \"Setmana / Setmanes\",\r\n    \"monthly\" => \"Mes / Mesos\",\r\n    \"yearly\" => \"Any / Anys\",\r\n    \"days\" => \"dies\",\r\n    \"weeks\" => \"setmanes\",\r\n    \"months\" => \"mesos\",\r\n    \"years\" => \"anys\",\r\n    \"external_url\" => \"Visitar URL Externa\",\r\n    \"empty_page\" => \"Pàgina Buida\",\r\n    \"clear_filters\" => \"Netejar Filtres\",\r\n    \"no_matching_subscriptions\" => \"No s'han trobat subscripcions coincidents\",\r\n    \"clone\" => \"Clonar\",\r\n    \"renew\" => \"Renovar\",\r\n    \"calculate_next_payment_date\" => \"Calcular data del pròxim pagament\",\r\n    // Subscription form \r\n    \"add_subscription\" => \"Afegir subscripció\",\r\n    \"edit_subscription\" => \"Editar\",\r\n    \"subscription_name\" => \"Nom de la Subscripció\",\r\n    \"logo_preview\" => \"Vista Prèvia del Logotip\",\r\n    \"search_logo\" => \"Buscar logotip a la web\",\r\n    \"web_search\" => \"Cerca web\",\r\n    \"currency\" => \"Moneda\",\r\n    \"payment_every\" => \"Pagament cada\",\r\n    \"frequency\" => \"Freqüència\",\r\n    \"cycle\" => \"Cicle\",\r\n    \"no_category\" => \"Sense categoria\",\r\n    \"paid_by\" => \"Pagat per\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Notes\",\r\n    \"enable_notifications\" => \"Activar notificacions per aquesta subscripció\",\r\n    \"default_value_from_settings\" => \"Valor predeterminat al panell de Configuració\",\r\n    \"cancellation_notification\" => \"Notificació de cancel·lació\",\r\n    \"delete\" => \"Eliminar\",\r\n    \"cancel\" => \"Cancel·lar\",\r\n    \"upload_logo\" => \"Carregar Logotip\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"Tens subscripcions en diverses divises. Per obtenir estadístiques vàlides i precises, introdueix una clau API Fixer a la pàgina de configuració.\",\r\n    \"general_statistics\" => \"Estadístiques Generals\",\r\n    \"active_subscriptions\" => \"Subscripcions Actives\",\r\n    \"inactive_subscriptions\" => \"Subscripcions Inactives\",\r\n    \"monthly_cost\" => \"Cost Mensual\",\r\n    \"yearly_cost\" => \"Cost Anual\", \r\n    \"average_monthly\" => \"Cost mitjà mensual per subscripció\",\r\n    \"most_expensive\" => \"Cost de subscripció més car\",\r\n    \"amount_due\" => \"Import a pagar aquest mes\",\r\n    \"percentage_budget_used\" => \"Percentatge utilizat del pressupost\",\r\n    \"budget_remaining\" => \"Pressupost restant\",\r\n    \"amount_over_budget\" => \"Import per sobre del pressupost\",\r\n    \"monthly_savings\" => \"Estalvi Mensual (en subscripcions no actives)\",\r\n    \"yearly_savings\" => \"Estalvi Anual (en subscripcions no actives)\",\r\n    \"split_views\" => \"Vistes Dividides\",\r\n    \"category_split\" => \"Divisió per Categoria\",\r\n    \"household_split\" => \"Divisió per Membres Familiars\",\r\n    \"payment_method_split\" => \"Divisió per Mètode de Pagament\",\r\n    \"total_cost_trend\" => \"Tendència del Cost Total\",\r\n    \"cost_vs_budget\" => \"Cost vs Pressupost\",\r\n    // About page\r\n    \"about_and_credits\" => \"Quant a i Crèdits\",\r\n    \"credits\" => \"Crèdits\",\r\n    \"license\" => \"Llicència\",\r\n    \"release_notes\" => \"Notes de la Versió\",\r\n    \"update_available\" => \"Actualtizació Disponible\",\r\n    \"issues_and_requests\" => \"Problemes i Sol·licituds\",\r\n    \"the_author\" => \"L'autor\",\r\n    \"icons\" => \"Icones\",\r\n    \"payment_icons\" => \"Icones de Pagament\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"Penjar avatar\",\r\n    \"file_type_error\" => \"El fitxer ha de ser una imatge en format PNG, JPG, WEBP o SVG\",\r\n    \"user_details\" => \"Detalls de l'Usuari\",\r\n    \"two_factor_authentication\" => \"Autenticació de Dos Factors\",\r\n    \"two_factor_info\" => \"L'autenticació de dos factors afegeix una capa addicional de seguretat al teu compte, <br>  Necessitaràs una aplicació d’autenticació com Google Authenticator, Authy o Ente Auth per escanejar en codi QR.\",\r\n    \"two_factor_enabled_info\" => \"El teu compte està segur amb l'autenticació de dos factors. Pots desactivar-la fent clic al botó de dalt.\",\r\n    \"enable_two_factor_authentication\" => \"Activar Autenticació de Dos Factors\",\r\n    \"2fa_already_enabled\" => \"L'autenticació de dos factors ja està activada\",\r\n    \"totp_code_incorrect\" => \"El codi TOTP no és correcte\",\r\n    \"backup_codes\" => \"Codis de Seguretat\",\r\n    \"download_backup_codes\" => \"Descarregar codis de seguretat\",\r\n    \"copy_to_clipboard\" => \"Copiar al Porta-retalls\",\r\n    \"totp_backup_codes_info\" => \"Guarda aquests codis en un lloc segur. Pots utilitzar-los si perds l'accés a l'aplicació d'autenticació.\",\r\n    \"disable_two_factor_authentication\" => \"Desactivar l'Autenticació de Dos Factors \",\r\n    \"totp_code\" => \"Codi TOTP\",\r\n    \"api_key\" => \"Clau API\",\r\n    \"regenerate\" => \"Regenerar\",\r\n    \"api_key_info\" => \"La clau API s'utilitza per accedir a l'API de Wallos. No la comparteixis amb ningú.\",\r\n    // Settings page\r\n    \"monthly_budget\" => \"Pressupost Mensual\",\r\n    \"budget_info\" => \"El pressupost mensual s'utilitza per calcular les estadístiques. Si no vols utilitzar aquesta funció, deixa'l a 0.\",\r\n    \"household\" => \"Familia\",\r\n    \"save_member\" => \"Guardar Membre\",\r\n    \"delete_member\" => \"Eliminar Membre\",\r\n    \"cant_delete_member\" => \"No es pot eliminar el membre principal\",\r\n    \"cant_delete_member_in_use\" => \"No es pot eliminar a un membre que utilitza una subscripció\",\r\n    \"household_info\" => \"El correu electrònic permet notificar als membres de la llar les subscripcions que estan a punt de caducar.\",\r\n    \"notifications\" => \"Notificacions\",\r\n    \"enable_email_notifications\" => \"Activar notificacions per correu electrònic\",\r\n    \"notify_me\" => \"Notificar-me\",\r\n    \"day_before\" => \"dia abans\",\r\n    \"on_due_date\" => \"A la data de venciment\",\r\n    \"days_before\" => \"dies abans\",\r\n    \"smtp_address\" => \"Direcció SMTP\",\r\n    \"port\" => \"Port\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"Nom d'usuari SMTP\",\r\n    \"smtp_password\" => \"Contrasenya SMTP\",\r\n    \"from_email\" => \"Correu electrònic d'origen (Opcional)\",\r\n    \"send_to_other_emails\" => \"Enviar també notificacions als següents correus electrònics (utilitza ; per separar-los):\",\r\n    \"smtp_info\" => \"La contrasenya SMTP es desa i es transmet en text pla. Per seguretat, crea un compte només per això.\",\r\n    \"other_emails_placeholder\" => \"usuari@domini.com;test@usuari.com\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Token del Bot de Telegram\",\r\n    \"telegram_chat_id\" => \"ID del Xat de Telegram\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Token de Pushplus\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"URL del Webhook de Mattermost\",\r\n    \"mattermost_bot_username\" => \"Nom d'usuari del Bot de Mattermost\",\r\n    \"mattermost_bot_icon_emoji\" => \"Emoji de la Icona del Bot\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"URL del Webhook\",\r\n    \"request_method\" => \"Mètode de Petició\",\r\n    \"custom_headers\" => \"Capçaleres Personalitzades\",\r\n    \"webhook_payload\" => \"Càrrega Útil del Webhook\",\r\n    \"payment_notifications_payload\" => \"Càrrega Útil de la Notificació de Pagament\",\r\n    \"cancelation_notification_payload\" => \"Càrrega Útil de la Notificació de Cancel·lació\",\r\n    \"variables_available\" => \"Variables disponibles\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Nom d'usuari del bot\",\r\n    \"discord_bot_avatar_url\" => \"URL de l'avatar del bot\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Clau d'usuari\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Tema\",\r\n    \"ignore_ssl_errors\" => \"Ignorar errors SSL\",\r\n    \"categories\" => \"Categories\",\r\n    \"save_category\" => \"Desar Categoria\",\r\n    \"delete_category\" => \"Eliminar Categoria\",\r\n    \"cant_delete_category_in_use\" => \"No es pot eliminar una categoria utilitzada en una subscripció\",\r\n    \"currencies\" => \"Divises\",\r\n    \"save_currency\" => \"Desar Divisa\",\r\n    \"delete_currency\" => \"Eliminar Divisa\",\r\n    \"cant_delete_main_currency\" => \"No es pot eliminar la divisa principal\",\r\n    \"cant_delete_currency_in_use\" => \"No es pot eliminar una divisa utilitzada en una subscripció\",\r\n    \"exchange_update\" => \"Tipus de canvi actualitzats per última vegada el\",\r\n    \"currency_info\" => \"Troba les divises admeses i els codis de moneda correctes a\",\r\n    \"currency_performance\" => \"Per millorar el rendiment, elimina les divises que no utilitzis.\",\r\n    \"fixer_api_key\" => \"Clau API de Fixer\",\r\n    \"provider\" => \"Proveïdor\",\r\n    \"fixer_info\" => \"Si utilitzes varies divises i vols que les estadístiques siguin precises, és necessària una clau API gratuïta de Fixer.\",\r\n    \"get_key\" => \"Obtingues la teva clau a\",\r\n    \"get_free_fixer_api_key\" => \"Obtén una API Key de Fixer gratuita\",\r\n    \"get_key_alternative\" => \"També pots obtenir una clau gratuïta de Fixer a\",\r\n    \"ai_model\" => \"Model de IA\",\r\n    \"select_ai_model\" => \"Seleccionar Model de IA\",\r\n    \"run_schedule\" => \"Executar Tasca\",\r\n    \"manually\" => \"Manualment\",\r\n    \"coming_soon\" => \"Pròximament\",\r\n    \"invalid_host\" => \"Host Invàid\",\r\n    \"ai_recommendations_info\" => \"Les recomanacions per IA es generen en funció de les subscripcions i els membres de la llar.\",\r\n    \"may_take_time\" => \"En funció del proveïdor, model i número de subscripcions, la generació de recomanacions pot trigar un temps.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Les recomanacions seran visibles al taulell.\",\r\n    \"generate_recommendations\" => \"Generar Recomanacions\",\r\n    \"display_settings\" => \"Configuració de Pantalla\",\r\n    \"theme_settings\" => \"Configuració del Tema\",\r\n    \"colors\" => \"Colors\",\r\n    \"custom_colors\" => \"Colors Personalitzats\",\r\n    \"theme\" => \"Tema\",\r\n    \"dark_theme\" => \"Tema Fosc\",\r\n    \"light_theme\" => \"Tema Clar\",\r\n    \"automatic\" => \"Automàtic\",\r\n    \"main_color\" => \"Color Primari\",\r\n    \"accent_color\" => \"Color Accent\",\r\n    \"hover_color\" => \"Color de Hover\",\r\n    \"save_custom_colors\" => \"Desar Colors Personalitzats\",\r\n    \"reset_custom_colors\" => \"Restablir Colors Personalitzats\",\r\n    \"custom_css\" => \"CSS Personalitzat\",\r\n    \"save_custom_css\" => \"Desar CSS Personalitzat\",\r\n    \"calculate_monthly_price\" => \"Calcular i mostrar el preu mensual de totes les subscripcions\",\r\n    \"convert_prices\" => \"Convertir i mostrar sempre els preus en la meva divisa principal (més lent)\",\r\n    \"show_original_price\" => \"Mostrar també el preu original quan es realitzin conversions o càlculs\",\r\n    \"experience\" => \"Experiència\",\r\n    \"show_subscription_progress\" => \"Mostrar el progrés de la subscripció\",\r\n    \"disabled_subscriptions\" => \"Subscripcions Desactivades\",\r\n    \"hide_disabled_subscriptions\" => \"Amagar subscripcions desactivades\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Mostrar subscripcions desactivades al final\",\r\n    \"experimental_settings\" => \"Configuracions Experimentals\",\r\n    \"remove_background\" => \"Intentar eliminar el fons dels logotips quan es fa la cerca d'imatges\",\r\n    \"use_mobile_navigation_bar\" => \"Utilitzar la barra de navegació mòbil\",\r\n    \"experimental_info\" => \"És possible que les configuracions experimentals no funcionin perfectament.\",\r\n    \"payment_methods\" => \"Mètodes de Pagament\",\r\n    \"payment_methods_info\" => \"Fes clic en un mètode de pagament per desactivar-lo/activar-lo.\",\r\n    \"rename_payment_methods_info\" => \"Fes clic en el nom d'un mètode de pagament per canviar-lo.\",\r\n    \"cant_delete_payment_method_in_use\" => \"No es pot desactivar un mètode de pagament en ús\",\r\n    \"add_custom_payment\" => \"Afegir mètode de pagament personalitzat\",\r\n    \"payment_method_name\" => \"Nom del mètode de pagament\",\r\n    \"payment_method_added_successfuly\" => \"Mètode de pagament afegit correctament\",\r\n    \"payment_method_removed\" => \"Mètode de pagament eliminat\",\r\n    \"disable\" => \"Desactivar\",\r\n    \"enable\" => \"Activar\",\r\n    \"rename_payment_method\" => \"Reanomenar mètode de pagament\",\r\n    \"payment_renamed\" => \"Mètode de pagament reanomenat\",\r\n    \"payment_not_renamed\" => \"Error al reanomenar el mètode de pagament\",\r\n    \"test\" => \"Probar\",\r\n    \"add\" => \"Afegir\",\r\n    \"save\" => \"Desar\",\r\n    \"reset\" => \"Restablir\",\r\n    \"main_accent_color_error\" => \"El color primari i el color accent no poden ser el mateix\",\r\n    \"backup_and_restore\" => \"Còpia de Seguretat i Restauració\",\r\n    \"backup\" => \"Còpia de Seguretat\",\r\n    \"restore\" => \"Restaurar\",\r\n    \"restore_info\" => \"La restauració de la base de dades anul·larà totes les dades actuals. Un cop finalitzada la restauració es tancarà la sessió.\",\r\n    \"account\" => \"Compte\",\r\n    \"export_subscriptions\" => \"Exportar Subscripcions\",\r\n    \"export_as_json\" => \"Exportar com JSON\",\r\n    \"export_as_csv\" => \"Exportar com CSV\",\r\n    \"danger_zone\" => \"Zona de perill\",\r\n    \"delete_account\" => \"Eliminar compte\",\r\n    \"delete_account_info\" => \"Al eliminar el compte també s'eliminaran totes les subscripcions i configuracions.\",\r\n    // Filters menu\r\n    \"filter\" => \"Filtrar\",\r\n    \"clear\" => \"Netejar\",\r\n    // Toast\r\n    \"success\" => \"Èxit\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"La sessió ha expirat. Inicia sessió de nou\",\r\n    \"fields_missing\" => \"Falten alguns camps\",\r\n    \"fill_all_fields\" => \"Si us plau, completa tots els camps\",\r\n    \"fill_mandatory_fields\" => \"Si us plau, completa tots els camps obligatoris\",\r\n    \"error\" => \"Error\",\r\n    // Category\r\n    \"failed_add_category\" => \"Error al afegir la categoria\",\r\n    \"failed_edit_category\" => \"Error al editar la categoria\",\r\n    \"category_in_use\" => \"La categoria té subscripcions en ús i no es pot eliminar\",\r\n    \"failed_remove_category\" => \"Error al eliminar la categoria\",\r\n    \"category_saved\" => \"Categoria desada\",\r\n    \"category_removed\" => \"Categoria eliminada\",\r\n    \"sort_order_saved\" => \"Ordre de classificació desat\",\r\n    // Currency\r\n    \"currency_saved\" => \"s'ha desat.\",\r\n    \"error_adding_currency\" => \"S'ha produït un error en afegir la divisa.\",\r\n    \"failed_to_store_currency\" => \"No s'ha pogut emmagatzemar la divisa a la base de dades.\",\r\n    \"currency_in_use\" => \"La divisa té subscripcions en ús i no es pot eliminar\",\r\n    \"currency_is_main\" => \"La divisa està establerta com divisa principal i no es pot eliminar.\",\r\n    \"failed_to_remove_currency\" => \"Error al eliminar la divisa de la base de dades\",\r\n    \"failed_to_store_api_key\" => \"Error al desar la API KEY a la base de dades.\",\r\n    \"invalid_api_key\" => \"API KEY no vàlida.\",\r\n    \"api_key_saved\" => \"API KEY desada correctament\",\r\n    \"currency_removed\" => \"Divisa eliminada\",\r\n    // Household\r\n    \"failed_add_household\" => \"Error al afegir membre de la llar\",\r\n    \"failed_edit_household\" => \"Error al editar membre de la llar\",\r\n    \"failed_remove_household\" => \"Error al eliminar membre de la llar\",\r\n    \"household_in_use\" => \"El membre de la llar té subscripcions en ús i no es pot eliminar\",\r\n    \"member_saved\" => \"Membre desat\",\r\n    \"member_removed\" => \"Membre eliminat\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Error al desar les dades de notificacions.\",\r\n    \"wallos_notification\" => \"Notificació de Wallos\",\r\n    \"test_notification\" => \"Això és una notificació de prova. Si la veus, la configuració és correcta.\",\r\n    \"email_error\" => \"Error al enviar correu electrònic\",\r\n    \"notification_sent_successfuly\" => \"Notificació enviada correctament\",\r\n    \"notifications_settings_saved\" => \"Configuració de notificacions desada correctament.\",\r\n    \"notification_failed\" => \"Error al enviar la notificació\",\r\n    // Payments\r\n    \"payment_in_use\" => \"No es pot desactivar un mètodo de pagament en ús\",\r\n    \"failed_update_payment\" => \"Error al actualitzar el mètodo de pagament a la base de dades\",\r\n    \"enabled\" => \"activat\",\r\n    \"disabled\" => \"desactivat\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Error al obtenir la imatge \",\r\n    \"subscription_updated_successfuly\" => \"Subscripció actualitzada correctament\",\r\n    \"subscription_added_successfuly\" => \"Subscripció afegida correctament\",\r\n    \"error_deleting_subscription\" => \"Error al eliminar la subscripció.\",\r\n    \"invalid_request_method\" => \"Mètodo de petició invàlid.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Error al actualitzar les dades de l'usuari.\",\r\n    \"user_details_saved\" => \"Detalls de l'usuari desats\",\r\n    // Admin Page\r\n    \"registrations\" => \"Registre d'Usuaris\",\r\n    \"enable_user_registrations\" => \"Activar el registre d'usuaris\",\r\n    \"maximum_number_users\" => \"Nombre màxim d'usuaris\",\r\n    \"require_email_verification\" => \"Requerir verificació del correu electrònic\",\r\n    \"configure_smtp_settings_to_enable\" => \"Configura els paràmetres SMTP per habilitar\",\r\n    \"server_url\" => \"URL del Servidor\",\r\n    \"server_url_info\" => \"S'utilitza per verificar el correu electrònic i recuperar la contrasenya. Ha de ser una URL pública vàlida.\",\r\n    \"server_url_password_reset\" => \"Si es configura, també s'activarà la funció de restabliment de contrasenya.\",\r\n    \"disable_login\" => \"Desactivar l'Inici de Sessió\",\r\n    \"disable_login_info\" => \"Ometre l'inici de sessió. Si executes el teu servidor en una xarxa local, sense accés extern, pots desactivar l'inici de sessió. Això farà que s'iniciï automàticament la sessió de l'usuari administrador.\",\r\n    \"disable_login_info2\" => \"Només pots activar aquesta opció si el registre d'usuaris està desactivat i només hi ha el compte de l'usuari administrador.\",\r\n    \"max_users_info\" => \"0 per il·limitat\",\r\n    \"user_management\" => \"Gestió d'Usuaris\",\r\n    \"delete_user\" => \"Eliminar Usuari\",\r\n    \"delete_user_info\" => \"Al eliminar un usuari també s'eliminen totes les seves subscripcions i configuracions.\",\r\n    \"create_user\" => \"Crear Usuari\",\r\n    \"oidc_settings\" => \"Configuració OIDC\",\r\n    \"oidc_oauth_enabled\" => \"Activar OIDC/OAuth\",\r\n    \"create_user_automatically\" => \"Crear usuari automàticament\",\r\n    \"disable_password_login\" => \"Desactivar l'inici de sessió amb contrasenya\",\r\n    \"smtp_settings\" => \"Configuració SMTP\",\r\n    \"smtp_usage_info\" => \"S'utilitza per recuperar contrasenyes i enviar altres correus electrònics del sistema.\",\r\n    \"security_settings\" => \"Configuració de seguretat\",\r\n    \"ssrf_protection_info\" => \"Per evitar atacs de Server-Side Request Forgery (SSRF), Wallos bloqueja per defecte les notificacions webhook a adreces de xarxa privades o internes.\",\r\n    \"local_webhook_info\" => \"Si necessiteu enviar webhooks a serveis locals (com Home Assistant, Gotify o Node-RED), introduïu les seves adreces IP o noms d'amfitrió anteriorment com una llista separada per comes (p. ex., <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"maintenance_tasks\" => \"Tasques de Manteniment\",\r\n    \"orphaned_logos\" => \"Logotips orfes\",\r\n    \"update\" => \"Actualitzar\",\r\n    \"new_version_available\" => \"Hi ha disponible una nova versió de Wallos\",\r\n    \"current_version\" => \"Versió Actual\",\r\n    \"latest_version\" => \"Última Versió\",\r\n    \"on_current_version\" => \"Estàs utilitzant l'última versió de Wallos.\",\r\n    \"show_update_notification\" => \"Mostrar notificació d'actualitzacions al taulell\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"Correu electrònic verificat\",\r\n    \"email_verification_failed\" => \"Error al verificar el corru electrònic\",\r\n    // Calendar\r\n    \"calendar\" => \"Calendari\",\r\n    \"sun\" => \"Dg\",\r\n    \"mon\" => \"Dl\",\r\n    \"tue\" => \"Dt\",\r\n    \"wed\" => \"Dc\",\r\n    \"thu\" => \"Dj\",\r\n    \"fri\" => \"Dv\",\r\n    \"sat\" => \"Ds\",\r\n    \"month-01\" => \"Gener\",\r\n    \"month-02\" => \"Febrer\",\r\n    \"month-03\" => \"Març\",\r\n    \"month-04\" => \"Abril\",\r\n    \"month-05\" => \"Maig\",\r\n    \"month-06\" => \"Juny\",\r\n    \"month-07\" => \"Juliol\",\r\n    \"month-08\" => \"Agost\",\r\n    \"month-09\" => \"Setembre\",\r\n    \"month-10\" => \"Octubre\",\r\n    \"month-11\" => \"Novembre\",\r\n    \"month-12\" => \"Desembre\",\r\n    \"total_cost\" => \"Cost Total\",\r\n    \"export_icalendar\" => \"Exportar iCalendar\",\r\n    \"over_budget_warning\" => \"T'has passat del pressupost\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Introdueix el codi TOTP\",\r\n\r\n\r\n];\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/cs.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Než se budete moct přihlásit, musíte si vytvořit účet.\",\n    \"username\" => \"Uživatelské jméno\",\n    \"password\" => \"Heslo\",\n    \"email\" => \"E-mail\",\n    \"firstname\" => \"Křestní jméno\",\n    \"lastname\" => \"Příjmení\",\n    \"confirm_password\" => \"Potvrďte heslo\",\n    \"main_currency\" => \"Hlavní měna\",\n    \"language\" => \"Jazyk\",\n    \"passwords_dont_match\" => \"Hesla se neshodují\",\n    \"username_exists\" => \"Uživatelské jméno již existuje\",\n    \"email_exists\" => \"E-mail již existuje\",\n    \"registration_failed\" => \"Registrace se nezdařila, zkuste to prosím znovu.\",\n    \"register\" => \"Registrace\",\n    \"restore_database\" => \"Obnovit databázi\",\n    // Login Page\n    \"please_login\" => \"Přihlaste se, prosím\",\n    \"stay_logged_in\" => \"Zůstat přihlášený (30 dní)\",\n    \"login\" => \"Přihlásit\",\n    \"login_with\" => \"Přihlásit se pomocí\",\n    \"or\" => \"nebo\",\n    \"login_failed\" => \"Přihlašovací údaje jsou nesprávné\",\n    \"registration_successful\" => \"Úspěšná registrace\",\n    \"user_email_waiting_verification\" => \"Váš e-mail musí být ověřen. Zkontrolujte prosím svůj e-mail.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Zapomenuté heslo\",\n    \"reset_password\" => \"Obnovit heslo\",\n    \"reset_sent_check_email\" => \"E-mail pro obnovení odeslán. Zkontrolujte prosím svůj e-mail.\",\n    \"password_reset_successful\" => \"Úspěšné obnovení hesla\",\n    // Header\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Přehled\",\n    \"subscriptions\" => \"Předplatná\",\n    \"stats\" => \"Statistiky\",\n    \"settings\" => \"Nastavení\",\n    \"admin\" => \"Administrace\",\n    \"about\" => \"O aplikaci\",\n    \"logout\" => \"Odhlásit se\",\n    // Dashboard\n    \"hello\" => \"Ahoj\",\n    \"upcoming_payments\" => \"Plánované platby\",\n    \"no_upcoming_payments\" => \"Žádné plánované platby\",\n    \"overdue_renewals\" => \"Zpožděná obnovení\",\n    \"ai_recommendations\" => \"Doporučení AI\",\n    \"your_budget\" => \"Váš rozpočet\",\n    \"budget\" => \"Rozpočet\",\n    \"budget_used\" => \"Využití rozpočtu\",\n    \"over_budget\" => \"Překročení rozpočtu\",\n    \"your_subscriptions\" => \"Vaše předplatná\",\n    \"your_savings\" => \"Vaše úspory\",\n    // Subscriptions page\n    \"subscription\" => \"Předplatné\",\n    \"no_subscriptions_yet\" => \"Zatím nemáte žádná předplatná\",\n    \"add_first_subscription\" => \"Přidat první předplatné\",\n    \"new_subscription\" => \"Nové předplatné\",\n    \"search\" => \"Hledat\",\n    \"state\" => \"Stav\",\n    \"alphanumeric\" => \"Alfanumericky\",\n    \"sort\" => \"Seřadit\",\n    \"name\" => \"Název\",\n    \"last_added\" => \"Naposledy přidané\",\n    \"price\" => \"Cena\",\n    \"next_payment\" => \"Další platba\",\n    \"renewal_type\" => \"Typ obnovení\",\n    \"auto_renewal\" => \"Automatické obnovení\",\n    \"automatically_renews\" => \"Automaticky se obnovuje\",\n    \"manual_renewal\" => \"Ruční obnovení\",\n    \"start_date\" => \"Datum zahájení\",\n    \"inactive\" => \"Zakázat předplatné\",\n    \"replaced_with\" => \"Nahrazeno\",\n    \"none\" => \"Nic\",\n    \"member\" => \"Člen\",\n    \"category\" => \"Kategorie\",\n    \"payment_method\" => \"Platební metoda\",\n    \"Daily\" => \"Denně\",\n    \"Weekly\" => \"Týdně\",\n    \"Monthly\" => \"Měsíčně\",\n    \"Yearly\" => \"Ročně\",\n    \"daily\" => \"dní\",\n    \"weekly\" => \"týdnů\",\n    \"monthly\" => \"měsíců\",\n    \"yearly\" => \"roků\",\n    \"days\" => \"dní\",\n    \"weeks\" => \"týdnů\",\n    \"months\" => \"měsíců\",\n    \"years\" => \"roků\",\n    \"external_url\" => \"Navštívit externí adresu URL\",\n    \"empty_page\" => \"Prázdná stránka\",\n    \"clear_filters\" => \"Vymazat filtry\",\n    \"no_matching_subscriptions\" => \"Žádné odpovídající předplatné\",\n    \"clone\" => \"Klonovat\",\n    \"renew\" => \"Obnovit\",\n    \"calculate_next_payment_date\" => \"Vypočítat datum další platby\",\n    // Subscription form\n    \"add_subscription\" => \"Přidat předplatné\",\n    \"edit_subscription\" => \"Upravit předplatné\",\n    \"subscription_name\" => \"Název předplatného\",\n    \"logo_preview\" => \"Náhled loga\",\n    \"search_logo\" => \"Hledat logo na internetu\",\n    \"web_search\" => \"Hledání na internetu\",\n    \"currency\" => \"Měna\",\n    \"payment_every\" => \"Platba každých\",\n    \"frequency\" => \"Frekvence\",\n    \"cycle\" => \"Cyklus\",\n    \"no_category\" => \"Žádná kategorie\",\n    \"paid_by\" => \"Platí\",\n    \"url\" => \"Adresa URL\",\n    \"notes\" => \"Poznámky\",\n    \"enable_notifications\" => \"Povolit oznámení pro toto předplatné\",\n    \"default_value_from_settings\" => \"Výchozí hodnota z nastavení\",\n    \"cancellation_notification\" => \"Oznámení o zrušení\",\n    \"delete\" => \"Odstranit\",\n    \"cancel\" => \"Zrušit\",\n    \"upload_logo\" => \"Nahrát logo\",\n    // Statistics page\n    \"cant_convert_currency\" => \"U svých předplatných používáte více měn. Pokud chcete mít platné a přesné statistiky, nastavte na stránce nastavení klíč API služby Fixer.\",\n    \"general_statistics\" => \"Obecné statistiky\",\n    \"active_subscriptions\" => \"Aktivní předplatná\",\n    \"inactive_subscriptions\" => \"Neaktivní předplatná\",\n    \"monthly_cost\" => \"Měsíční náklady\",\n    \"yearly_cost\" => \"Roční náklady\",\n    \"average_monthly\" => \"Průměrné měsíční náklady na předplatné\",\n    \"most_expensive\" => \"Nejdražší náklady na předplatné\",\n    \"amount_due\" => \"Částka splatná tento měsíc\",\n    \"percentage_budget_used\" => \"Procento využití rozpočtu\",\n    \"budget_remaining\" => \"Zbývající rozpočet\",\n    \"amount_over_budget\" => \"Částka nad rozpočet\",\n    \"monthly_savings\" => \"Měsíční úspory (na neaktivních předplatných)\",\n    \"yearly_savings\" => \"Roční úspory (na neaktivních předplatných)\",\n    \"split_views\" => \"Zobrazení rozdělení\",\n    \"category_split\" => \"Rozdělení kategorií\",\n    \"household_split\" => \"Rozdělení domácnosti\",\n    \"payment_method_split\" => \"Rozdělení platebních metod\",\n    \"total_cost_trend\" => \"Trend celkových nákladů\",\n    \"cost_vs_budget\" => \"Celkové náklady vs. rozpočet\",\n    // About page\n    \"about_and_credits\" => \"O aplikaci a zásluhy\",\n    \"credits\" => \"Zásluhy\",\n    \"license\" => \"Licence\",\n    \"release_notes\" => \"Poznámky k vydání\",\n    \"update_available\" => \"Dostupná aktualizace\",\n    \"issues_and_requests\" => \"Problémy a požadavky\",\n    \"the_author\" => \"Autor\",\n    \"icons\" => \"Ikony\",\n    \"payment_icons\" => \"Ikony plateb\",\n    // Profile page\n    \"upload_avatar\" => \"Nahrát avatara\",\n    \"file_type_error\" => \"Zadaný typ souboru není podporován.\",\n    \"user_details\" => \"Podrobnosti o uživateli\",\n    \"two_factor_authentication\" => \"Dvoufaktorové ověřování\",\n    \"two_factor_info\" => \"Dvoufaktorové ověřování přidává k vašemu účtu další úroveň zabezpečení.<br>K naskenování QR kódu budete potřebovat ověřovací aplikaci, jako je Google Authenticator, Authy nebo Ente Auth.\",\n    \"two_factor_enabled_info\" => \"Váš účet je zabezpečen pomocí dvoufaktorového ověřování. Můžete jej vypnout kliknutím na výše uvedené tlačítko.\",\n    \"enable_two_factor_authentication\" => \"Povolit dvoufaktorové ověřování\",\n    \"2fa_already_enabled\" => \"Dvoufaktorové ověřování je již povoleno\",\n    \"totp_code_incorrect\" => \"Kód TOTP je nesprávný\",\n    \"backup_codes\" => \"Záložní kódy\",\n    \"download_backup_codes\" => \"Stáhnout záložní kódy\",\n    \"copy_to_clipboard\" => \"Kopírovat do schránky\",\n    \"totp_backup_codes_info\" => \"Tyto kódy lze použít k přihlášení, pokud ztratíte přístup k ověřovací aplikaci.\",\n    \"disable_two_factor_authentication\" => \"Zakázat dvoufaktorové ověřování\",\n    \"totp_code\" => \"Kód TOTP\",\n    \"api_key\" => \"Klíč API\",\n    \"regenerate\" => \"Znovu vygenerovat\",\n    \"api_key_info\" => \"Klíč API slouží k přístupu k rozhraní API. Udržujte jej v tajnosti.\",\n    // Settings page\n    \"monthly_budget\" => \"Měsíční rozpočet\",\n    \"budget_info\" => \"Měsíční rozpočet se používá pro výpočet statistik\",\n    \"household\" => \"Domácnost\",\n    \"save_member\" => \"Uložit člena\",\n    \"delete_member\" => \"Odstranit člena\",\n    \"cant_delete_member\" => \"Nelze odstranit hlavního člena\",\n    \"cant_delete_member_in_use\" => \"Nelze odstranit člena používaného v předplatném\",\n    \"household_info\" => \"Pole e-mailu umožňuje upozornit členy domácnosti na předplatné, kterému brzy vyprší platnost.\",\n    \"notifications\" => \"Oznámení\",\n    \"enable_email_notifications\" => \"Povolit e-mailová oznámení\",\n    \"notify_me\" => \"Upozorni mě\",\n    \"day_before\" => \"dní předem\",\n    \"on_due_date\" => \"V den splatnosti\",\n    \"days_before\" => \"dní předem\",\n    \"smtp_address\" => \"Adresa SMTP\",\n    \"port\" => \"Port\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Uživatelské jméno SMTP\",\n    \"smtp_password\" => \"Heslo SMTP\",\n    \"from_email\" => \"Z e-mailu (volitelné)\",\n    \"send_to_other_emails\" => \"Oznámení zasílat také na následující e-mailové adresy (oddělte je pomocí ;):\",\n    \"other_emails_placeholder\" => \"uzivatel@domena.cz;test@uzivatel.cz\",\n    \"smtp_info\" => \"Heslo SMTP se přenáší a ukládá v prostém textu. Z důvodu bezpečnosti si prosím vytvořte účet pouze pro tento účel.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Token služby Telegram Bot\",\n    \"telegram_chat_id\" => \"ID chatu v Telegramu\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus Token\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"WebHook URL\",\n    \"mattermost_bot_username\" => \"Uživatelské jméno služby Mattermost Bot\",\n    \"mattermost_bot_icon_emoji\" => \"Emoji ikony bota\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Adresa URL webhooku\",\n    \"request_method\" => \"Metoda požadavku\",\n    \"custom_headers\" => \"Vlastní hlavičky\",\n    \"webhook_payload\" => \"Zatížení webhooku\",\n    \"payment_notifications_payload\" => \"Zatížení oznámení o platbě\",\n    \"cancelation_notification_payload\" => \"Zatížení oznámení o zrušení\",\n    \"variables_available\" => \"Dostupné proměnné\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Uživatelské jméno služby Discord Bot\",\n    \"discord_bot_avatar_url\" => \"Adresa URL avatara služby Discord Bot\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Uživatelský klíč Pushover\",\n    \"host\" => \"Hostitel\",\n    \"topic\" => \"Téma\",\n    \"ignore_ssl_errors\" => \"Ignorovat chyby SSL\",\n    \"categories\" => \"Kategorie\",\n    \"save_category\" => \"Uložit kategorii\",\n    \"delete_category\" => \"Odstranit kategorii\",\n    \"cant_delete_category_in_use\" => \"Nelze odstranit kategorii používanou v předplatném\",\n    \"currencies\" => \"Měny\",\n    \"save_currency\" => \"Uložit měnu\",\n    \"delete_currency\" => \"Odstranit měnu\",\n    \"cant_delete_main_currency\" => \"Nelze odstranit hlavní měnu\",\n    \"cant_delete_currency_in_use\" => \"Nelze odstranit měnu používanou v předplatném\",\n    \"exchange_update\" => \"Směnné kurzy naposledy aktualizovány\",\n    \"currency_info\" => \"Podporované měny a správné kódy měn najdete na\",\n    \"currency_performance\" => \"Pro zlepšení výkonu si ponechte pouze měny, které používáte.\",\n    \"fixer_api_key\" => \"Klíč API služby Fixer\",\n    \"provider\" => \"Poskytovatel\",\n    \"fixer_info\" => \"Pokud používáte více měn a chcete mít přesné statistiky a řazení předplatných, je nutné mít BEZPLATNÝ klíč API od služby Fixer.\",\n    \"get_key\" => \"Získejte svůj klíč na\",\n    \"get_free_fixer_api_key\" => \"Získat bezplatný klíč API služby Fixer\",\n    \"get_key_alternative\" => \"Případně můžete získat bezplatný klíč API služby Fixer od\",\n    \"ai_model\" => \"AI Model\",\n    \"select_ai_model\" => \"Vybrat AI Model\",\n    \"run_schedule\" => \"Spustit plán\",\n    \"manually\" => \"Manuálně\",\n    \"coming_soon\" => \"Brzy bude k dispozici\",\n    \"invalid_host\" => \"Neplatný hostitel\",\n    \"ai_recommendations_info\" => \"AI Doporučení jsou generována na základě vašich předplatných a členů domácnosti.\",\n    \"may_take_time\" => \"V závislosti na poskytovateli, modelu a počtu předplatných může generování doporučení trvat nějakou dobu.\",\n    \"recommendations_visible_on_dashboard\" => \"Doporučení budou viditelná na řídicím panelu.\",\n    \"generate_recommendations\" => \"Generovat doporučení\",\n    \"display_settings\" => \"Nastavení zobrazení\",\n    \"theme_settings\" => \"Nastavení motivu\",\n    \"colors\" => \"Barvy\",\n    \"custom_colors\" => \"Vlastní barvy\",\n    \"theme\" => \"Motiv\",\n    \"dark_theme\" => \"Tmavý motiv\",\n    \"light_theme\" => \"Světlý motiv\",\n    \"automatic\"=> \"Automaticky\",\n    \"main_color\" => \"Hlavní barva\",\n    \"accent_color\" => \"Barva zdůraznění\",\n    \"hover_color\" => \"Barva najetí\",\n    \"save_custom_colors\" => \"Uložit vlastní barvy\",\n    \"reset_custom_colors\" => \"Obnovit vlastní barvy\",\n    \"custom_css\" => \"Vlastní CSS\",\n    \"save_custom_css\" => \"Uložit vlastní CSS\",\n    \"calculate_monthly_price\" => \"Vypočítávat a zobrazovat měsíční ceny pro všechna předplatná\",\n    \"convert_prices\" => \"Vždy převádět a zobrazovat ceny v mé hlavní měně (pomalejší)\",\n    \"show_original_price\" => \"Při převodech nebo výpočtech zobrazovat také původní cenu.\",\n    \"experience\" => \"Zážitek\",\n    \"show_subscription_progress\" => \"Zobrazovat průběh předplatného\",\n    \"disabled_subscriptions\" => \"Zakázaná předplatná\",\n    \"hide_disabled_subscriptions\" => \"Skrývat zakázaná předplatná\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Zobrazovat zakázaná předplatná v dolní části\",\n    \"experimental_settings\" => \"Experimentální nastavení\",\n    \"remove_background\" => \"Pokusit se odstranit pozadí log z vyhledávání obrázků\",\n    \"use_mobile_navigation_bar\" => \"Použít mobilní navigační panel\",\n    \"experimental_info\" => \"Experimentální nastavení pravděpodobně nebude fungovat dokonale.\",\n    \"payment_methods\" => \"Platební metody\",\n    \"payment_methods_info\" => \"Kliknutím na platební metodu ji zakážete nebo povolíte.\",\n    \"rename_payment_methods_info\" => \"Kliknutím na název platební metody ji přejmenujete.\",\n    \"cant_delete_payment_method_in_use\" => \"Nelze zakázat používanou platební metodu\",\n    \"add_custom_payment\" => \"Přidat vlastní platební metodu\",\n    \"payment_method_name\" => \"Název platební metody\",\n    \"payment_method_added_successfuly\" => \"Úspěšné přidání platební metody\",\n    \"payment_method_removed\" => \"Platební metoda odebrána\",\n    \"disable\" => \"Zakázat\",\n    \"enable\" => \"Povolit\",\n    \"rename_payment_method\" => \"Přejmenovat platební metodu\",\n    \"payment_renamed\" => \"Platební metoda přejmenována\",\n    \"payment_not_renamed\" => \"Platební metoda nebyla přejmenována\",\n    \"test\" => \"Testovat\",\n    \"add\" => \"Přidat\",\n    \"save\" => \"Uložit\",\n    \"reset\" => \"Obnovit\",\n    \"main_accent_color_error\" => \"Hlavní barva a barva zdůraznění nemohou být stejné.\",\n    \"backup_and_restore\" => \"Zálohování a obnovení\",\n    \"backup\" => \"Zálohovat\",\n    \"restore\" => \"Obnovit\",\n    \"restore_info\" => \"Obnovení databáze přepíše všechna aktuální data. Po obnovení budete odhlášeni.\",\n    \"account\" => \"Účet\",\n    \"export_subscriptions\" => \"Exportovat předplatná\",\n    \"export_as_json\" => \"Exportovat jako JSON\",\n    \"export_as_csv\" => \"Exportovat jako CSV\",\n    \"danger_zone\" => \"Nebezpečná zóna\",\n    \"delete_account\" => \"Odstranit účet\",\n    \"delete_account_info\" => \"Odstraněním účtu se odstraní také všechna vaše předplatná a nastavení.\",\n    // Filters menu\n    \"filter\" => \"Filtrovat\",\n    \"clear\" => \"Vymazat\",\n    // Toast\n    \"success\" => \"Úspěch\",\n    // Endpoint responses\n    \"session_expired\" => \"Vaše relace vypršela. Přihlaste se prosím znovu.\",\n    \"fields_missing\" => \"Některá pole chybí\",\n    \"fill_all_fields\" => \"Vyplňte prosím všechna pole\",\n    \"fill_mandatory_fields\" => \"Vyplňte prosím všechna povinná pole\",\n    \"error\" => \"Chyba\",\n    // Category\n    \"failed_add_category\" => \"Nepodařilo se přidat kategorii\",\n    \"failed_edit_category\" => \"Nepodařilo se upravit kategorii\",\n    \"category_in_use\" => \"Kategorie se používá v předplatných a nelze ji odebrat\",\n    \"failed_remove_category\" => \"Nepodařilo se odebrat kategorii\",\n    \"category_saved\" => \"Kategorie uložena\",\n    \"category_removed\" => \"Kategorie odebrána\",\n    \"sort_order_saved\" => \"Pořadí řazení uloženo\",\n    // Currency\n    \"currency_saved\" => \"byla uložena.\",\n    \"error_adding_currency\" => \"Chyba při přidávání položky měny.\",\n    \"failed_to_store_currency\" => \"Nepodařilo se uložit měnu do databáze.\",\n    \"currency_in_use\" => \"Měna se používá v předplatných a nelze ji odstranit.\",\n    \"currency_is_main\" => \"Měna je nastavena jako hlavní měna a nelze ji odstranit.\",\n    \"failed_to_remove_currency\" => \"Nepodařilo se odebrat měnu z databáze.\",\n    \"failed_to_store_api_key\" => \"Nepodařilo se uložit klíč API do databáze.\",\n    \"invalid_api_key\" => \"Neplatný klíč API.\",\n    \"api_key_saved\" => \"Klíč API úspěšně uložen\",\n    \"currency_removed\" => \"Měna odebrána\",\n    // Household\n    \"failed_add_household\" => \"Nepodařilo se přidat člena domácnosti\",\n    \"failed_edit_household\" => \"Nepodařilo se upravit člena domácnosti\",\n    \"failed_remove_household\" => \"Nepodařilo se odebrat člena domácnosti\",\n    \"household_in_use\" => \"Člen domácnosti se používá v předplatných a nelze ho odebrat.\",\n    \"member_saved\" => \"Člen uložen\",\n    \"member_removed\" => \"Člen odebrán\",\n    // Notifications\n    \"error_saving_notifications\" => \"Chyba při ukládání dat oznámení.\",\n    \"wallos_notification\" => \"Oznámení aplikace Wallos\",\n    \"test_notification\" => \"Toto je testovací oznámení. Pokud se zobrazuje, je konfigurace správná.\",\n    \"email_error\" => \"Chyba při odesílání e-mailu\",\n    \"notification_sent_successfuly\" => \"Oznámení úspěšně odesláno\",\n    \"notifications_settings_saved\" => \"Nastavení oznámení úspěšně uloženo.\",\n    \"notification_failed\" => \"Oznámení selhalo\",\n    // Payments\n    \"payment_in_use\" => \"Nelze zakázat používanou platební metodu\",\n    \"failed_update_payment\" => \"Nepodařilo se aktualizovat platební metodu v databázi\",\n    \"enabled\" => \"povoleno\",\n    \"disabled\" => \"zakázáno\",\n    // Subscription\n    \"error_fetching_image\" => \"Chyba při načítání obrázku\",\n    \"subscription_updated_successfuly\" => \"Předplatné úspěšně aktualizováno\",\n    \"subscription_added_successfuly\" => \"Předplatné úspěšně přidáno\",\n    \"error_deleting_subscription\" => \"Chyba při odstraňování předplatného.\",\n    \"invalid_request_method\" => \"Neplatná metoda požadavku.\",\n    // User\n    \"error_updating_user_data\" => \"Chyba při aktualizaci uživatelských dat.\",\n    \"user_details_saved\" => \"Podrobnosti o uživateli uloženy\",\n    // Admin Page\n    \"registrations\" => \"Registrace\",\n    \"enable_user_registrations\" => \"Povolit registraci uživatelů\",\n    \"maximum_number_users\" => \"Maximální počet uživatelů\",\n    \"require_email_verification\" => \"Vyžadovat ověření e-mailu\",\n    \"configure_smtp_settings_to_enable\" => \"Nakonfigurujte nastavení SMTP pro povolení\",\n    \"server_url\" => \"Adresa URL serveru\",\n    \"server_url_info\" => \"Slouží k ověření e-mailu a obnovení hesla. Musí to být platná veřejná adresa URL.\",\n    \"server_url_password_reset\" => \"Pokud je nastaveno, povolí také funkci obnovení hesla.\",\n    \"disable_login\" => \"Zakázat přihlašování\",\n    \"disable_login_info\" => \"Obejít přihlášení. Pokud provozujete server pouze v místní síti bez přístupu zvenčí, můžete přihlášení zakázat. Tím se automaticky přihlásí uživatel administrátor.\",\n    \"disable_login_info2\" => \"Toto nastavení můžete povolit pouze v případě, že je registrace uživatelů zakázána a že existuje pouze uživatelský účet administrátora.\",\n    \"max_users_info\" => \"0 znamená neomezeně\",\n    \"user_management\" => \"Správa uživatelů\",\n    \"delete_user\" => \"Odstranit uživatele\",\n    \"delete_user_info\" => \"Odstraněním uživatele se odstraní také všechna jeho předplatná a nastavení.\",\n    \"create_user\" => \"Vytvořit uživatele\",\n    \"oidc_settings\" => \"Nastavení OIDC\",\n    \"oidc_oauth_enabled\" => \"Povolit OIDC/OAuth\",\n    \"create_user_automatically\" => \"Automaticky vytvořit uživatele\",\n    \"disable_password_login\" => \"Zakázat přihlašování pomocí hesla\",\n    \"smtp_settings\" => \"Nastavení SMTP\",\n    \"smtp_usage_info\" => \"Bude použito pro obnovení hesla a další systémové e-maily.\",\n    \"security_settings\" => \"Nastavení zabezpečení\",\n    \"ssrf_protection_info\" => \"Aby se předešlo útokům Server-Side Request Forgery (SSRF), Wallos ve výchozím nastavení blokuje webhook notifikace směrem na soukromé nebo interní síťové adresy.\",\n    \"local_webhook_info\" => \"Pokud potřebujete posílat webhooks na lokální služby (např. Home Assistant, Gotify nebo Node-RED), zadejte jejich IP adresy nebo názvy hostitelů výše jako seznam oddělený čárkami (např. <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Úkoly údržby\",\n    \"orphaned_logos\" => \"Osiřelá loga\",\n    \"update\" => \"Aktualizace\",\n    \"new_version_available\" => \"K dispozici je nová verze aplikace Wallos\",\n    \"current_version\" => \"Aktuální verze\",\n    \"latest_version\" => \"Nejnovější verze\",\n    \"on_current_version\" => \"Používáte nejnovější verzi aplikace Wallos.\",\n    \"show_update_notification\" => \"Zobrazovat upozornění na aktualizace na nástěnce\",\n    \"cronjobs\" => \"Úlohy Cron\",\n    // Email Verification\n    \"email_verified\" => \"E-mail úspěšně ověřen\",\n    \"email_verification_failed\" => \"Ověření e-mailu selhalo\",\n    // Calendar\n    \"calendar\" => \"Kalendář\",\n    \"sun\" => \"Ne\",\n    \"mon\" => \"Po\",\n    \"tue\" => \"Út\",\n    \"wed\" => \"St\",\n    \"thu\" => \"Čt\",\n    \"fri\" => \"Pá\",\n    \"sat\" => \"So\",\n    \"month-01\" => \"Leden\",\n    \"month-02\" => \"Únor\",\n    \"month-03\" => \"Březen\",\n    \"month-04\" => \"Duben\",\n    \"month-05\" => \"Květen\",\n    \"month-06\" => \"Červen\",\n    \"month-07\" => \"Červenec\",\n    \"month-08\" => \"Srpen\",\n    \"month-09\" => \"Září\",\n    \"month-10\" => \"Říjen\",\n    \"month-11\" => \"Listopad\",\n    \"month-12\" => \"Prosinec\",\n    \"total_cost\" => \"Celkové náklady\",\n    \"export_icalendar\" => \"Exportovat iCalendar\",\n    \"over_budget_warning\" => \"Překročili jste rozpočet\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Vložte kód TOTP\",\n\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/da.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Du skal oprette en konto, før du kan logge ind\",\n    \"username\" => \"Brugernavn\",\n    \"password\" => \"Adgangskode\",\n    \"email\" => \"E-mail\",\n    \"firstname\" => \"Fornavn\",\n    \"lastname\" => \"Efternavn\",\n    \"confirm_password\" => \"Bekræft adgangskode\",\n    \"main_currency\" => \"Primær valuta\",\n    \"language\" => \"Sprog\",\n    \"passwords_dont_match\" => \"Adgangskoderne stemmer ikke overens\",\n    \"username_exists\" => \"Brugernavnet findes allerede\",\n    \"email_exists\" => \"E-mailen findes allerede\",\n    \"registration_failed\" => \"Registreringen mislykkedes, prøv igen.\",\n    \"register\" => \"Registrer\",\n    \"restore_database\" => \"Gendan database\",\n    // Login Page\n    \"please_login\" => \"Log venligst ind\",\n    \"stay_logged_in\" => \"Forbliv logget ind (30 dage)\",\n    \"login\" => \"Login\",\n    \"login_with\" => \"Log ind med\",\n    \"or\" => \"eller\",\n    \"login_failed\" => \"Loginoplysningerne er forkerte\",\n    \"registration_successful\" => \"Registreringen lykkedes\",\n    \"user_email_waiting_verification\" => \"Din e-mail skal bekræftes. Tjek venligst din indbakke.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Glemt adgangskode\",\n    \"reset_password\" => \"Nulstil adgangskode\",\n    \"reset_sent_check_email\" => \"Nulstillings-e-mail sendt. Tjek venligst din e-mail.\",\n    \"password_reset_successful\" => \"Adgangskoden blev nulstillet\",\n    // Header\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Dashboard\",\n    \"subscriptions\" => \"Abonnementer\",\n    \"stats\" => \"Statistik\",\n    \"settings\" => \"Indstillinger\",\n    \"admin\" => \"Admin\",\n    \"about\" => \"Om\",\n    \"logout\" => \"Log ud\",\n    // Dashboard\n    \"hello\" => \"Hej\",\n    \"upcoming_payments\" => \"Kommende betalinger\",\n    \"no_upcoming_payments\" => \"Du har ingen kommende betalinger\",\n    \"overdue_renewals\" => \"Forsinkede fornyelser\",\n    \"ai_recommendations\" => \"Denne AI anbefaling\",\n    \"your_budget\" => \"Dit budget\",\n    \"budget\" => \"Budget\",\n    \"budget_used\" => \"Budget brugt\",\n    \"over_budget\" => \"Over budget\",\n    \"your_subscriptions\" => \"Dine abonnementer\",\n    \"your_savings\" => \"Dine besparelser\",\n    // Subscriptions page\n    \"subscription\" => \"Abonnement\",\n    \"no_subscriptions_yet\" => \"Du har endnu ingen abonnementer\",\n    \"add_first_subscription\" => \"Tilføj første abonnement\",\n    \"new_subscription\" => \"Nyt abonnement\",\n    \"search\" => \"Søg\",\n    \"state\" => \"Status\",\n    \"alphanumeric\" => \"Alfanumerisk\",\n    \"sort\" => \"Sorter\",\n    \"name\" => \"Navn\",\n    \"last_added\" => \"Sidst tilføjet\",\n    \"price\" => \"Pris\",\n    \"next_payment\" => \"Næste betaling\",\n    \"renewal_type\" => \"Fornyelsestype\",\n    \"auto_renewal\" => \"Automatisk fornyelse\",\n    \"automatically_renews\" => \"Fornyes automatisk\",\n    \"manual_renewal\" => \"Manuel fornyelse\",\n    \"start_date\" => \"Startdato\",\n    \"inactive\" => \"Deaktiver abonnement\",\n    \"replaced_with\" => \"Erstattet med\",\n    \"none\" => \"Ingen\",\n    \"member\" => \"Medlem\",\n    \"category\" => \"Kategori\",\n    \"payment_method\" => \"Betalingsmetode\",\n    \"Daily\" => \"Dagligt\",\n    \"Weekly\" => \"Ugentligt\",\n    \"Monthly\" => \"Månedligt\",\n    \"Yearly\" => \"Årligt\",\n    \"daily\" => \"Dag(e)\",\n    \"weekly\" => \"Uge(r)\",\n    \"monthly\" => \"Måned(er)\",\n    \"yearly\" => \"År\",\n    \"days\" => \"dage\",\n    \"weeks\" => \"uger\",\n    \"months\" => \"måneder\",\n    \"years\" => \"år\",\n    \"external_url\" => \"Besøg ekstern URL\",\n    \"empty_page\" => \"Tom side\",\n    \"clear_filters\" => \"Ryd filtre\",\n    \"no_matching_subscriptions\" => \"Ingen matchende abonnementer\",\n    \"clone\" => \"Klon\",\n    \"renew\" => \"Forny\",\n    \"calculate_next_payment_date\" => \"Beregn næste betalingsdato\",\n    // Subscription form\n    \"add_subscription\" => \"Tilføj abonnement\",\n    \"edit_subscription\" => \"Rediger abonnement\",\n    \"subscription_name\" => \"Abonnementsnavn\",\n    \"logo_preview\" => \"Forhåndsvisning af logo\",\n    \"search_logo\" => \"Søg logo på nettet\",\n    \"web_search\" => \"Websøgning\",\n    \"currency\" => \"Valuta\",\n    \"payment_every\" => \"Betaling hver\",\n    \"frequency\" => \"Frekvens\",\n    \"cycle\" => \"Cyklus\",\n    \"no_category\" => \"Ingen kategori\",\n    \"paid_by\" => \"Betalt af\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Noter\",\n    \"enable_notifications\" => \"Aktivér notifikationer for dette abonnement\",\n    \"default_value_from_settings\" => \"Standardværdi fra indstillinger\",\n    \"cancellation_notification\" => \"Annulleringsnotifikation\",\n    \"delete\" => \"Slet\",\n    \"cancel\" => \"Annullér\",\n    \"upload_logo\" => \"Upload logo\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Du bruger flere valutaer i dine abonnementer. For at få valide og nøjagtige statistikker skal du sætte en Fixer API-nøgle på indstillingssiden.\",\n    \"general_statistics\" => \"Generelle statistikker\",\n    \"active_subscriptions\" => \"Aktive abonnementer\",\n    \"inactive_subscriptions\" => \"Inaktive abonnementer\",\n    \"monthly_cost\" => \"Månedlig udgift\",\n    \"yearly_cost\" => \"Årlig udgift\",\n    \"average_monthly\" => \"Gennemsnitlig månedlig abonnementsudgift\",\n    \"most_expensive\" => \"Dyreste abonnement\",\n    \"amount_due\" => \"Beløb forfalder denne måned\",\n    \"percentage_budget_used\" => \"Procentdel af budget brugt\",\n    \"budget_remaining\" => \"Resterende budget\",\n    \"amount_over_budget\" => \"Beløb over budget\",\n    \"monthly_savings\" => \"Månedlige besparelser (på inaktive abonnementer)\",\n    \"yearly_savings\" => \"Årlige besparelser (på inaktive abonnementer)\",\n    \"split_views\" => \"Opdelte visninger\",\n    \"category_split\" => \"Kategoriopdeling\",\n    \"household_split\" => \"Husstandsopdeling\",\n    \"payment_method_split\" => \"Betalingsmetodeopdeling\",\n    \"total_cost_trend\" => \"Samlet omkostningstendens\",\n    \"cost_vs_budget\" => \"Omkostning vs. budget\",\n    // About page\n    \"about_and_credits\" => \"Om og kreditering\",\n    \"credits\" => \"Kreditering\",\n    \"license\" => \"Licens\",\n    \"release_notes\" => \"Versionsnoter\",\n    \"update_available\" => \"Opdatering tilgængelig\",\n    \"issues_and_requests\" => \"Problemer og forespørgsler\",\n    \"the_author\" => \"Forfatteren\",\n    \"icons\" => \"Ikoner\",\n    \"payment_icons\" => \"Betalingsikoner\",\n    // Profile page\n    \"upload_avatar\" => \"Upload avatar\",\n    \"file_type_error\" => \"Den angivne filtype understøttes ikke.\",\n    \"user_details\" => \"Brugeroplysninger\",\n    \"two_factor_authentication\" => \"To-faktor-godkendelse\",\n    \"two_factor_info\" => \"To-faktor-godkendelse tilføjer et ekstra sikkerhedslag til din konto.<br>Du skal bruge en godkendelsesapp som Google Authenticator, Authy eller Ente Auth til at scanne QR-koden.\",\n    \"two_factor_enabled_info\" => \"Din konto er sikret med to-faktor-godkendelse. Du kan deaktivere det ved at klikke på knappen ovenfor.\",\n    \"enable_two_factor_authentication\" => \"Aktivér to-faktor-godkendelse\",\n    \"2fa_already_enabled\" => \"To-faktor-godkendelse er allerede aktiveret\",\n    \"totp_code_incorrect\" => \"TOTP-koden er forkert\",\n    \"backup_codes\" => \"Backupkoder\",\n    \"download_backup_codes\" => \"Download backupkoder\",\n    \"copy_to_clipboard\" => \"Kopiér til udklipsholder\",\n    \"totp_backup_codes_info\" => \"Disse koder kan bruges til at logge ind, hvis du mister adgangen til din godkendelsesapp.\",\n    \"disable_two_factor_authentication\" => \"Deaktivér to-faktor-godkendelse\",\n    \"totp_code\" => \"TOTP-kode\",\n    \"api_key\" => \"API-nøgle\",\n    \"regenerate\" => \"Generér igen\",\n    \"api_key_info\" => \"API-nøglen bruges til at få adgang til API'en. Hold den hemmelig.\",\n    // Settings page\n    \"monthly_budget\" => \"Månedligt budget\",\n    \"budget_info\" => \"Det månedlige budget bruges til at beregne statistikker\",\n    \"household\" => \"Husstand\",\n    \"save_member\" => \"Gem medlem\",\n    \"delete_member\" => \"Slet medlem\",\n    \"cant_delete_member\" => \"Kan ikke slette hovedmedlem\",\n    \"cant_delete_member_in_use\" => \"Kan ikke slette medlem i brug i abonnement\",\n    \"household_info\" => \"E-mail-feltet giver mulighed for at underrette husstandsmedlemmer om abonnementer, der er ved at udløbe.\",\n    \"notifications\" => \"Notifikationer\",\n    \"enable_email_notifications\" => \"Aktivér e-mail notifikationer\",\n    \"notify_me\" => \"Giv mig besked\",\n    \"day_before\" => \"dagen før\",\n    \"on_due_date\" => \"På forfaldsdatoen\",\n    \"days_before\" => \"dage før\",\n    \"smtp_address\" => \"SMTP-adresse\",\n    \"port\" => \"Port\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP-brugernavn\",\n    \"smtp_password\" => \"SMTP-adgangskode\",\n    \"from_email\" => \"Fra e-mail (valgfrit)\",\n    \"send_to_other_emails\" => \"Send også notifikationer til følgende e-mailadresser (brug ; til at adskille dem):\",\n    \"other_emails_placeholder\" => \"bruger@domæne.dk;test@bruger.dk\",\n    \"smtp_info\" => \"SMTP-adgangskode sendes og gemmes i klartekst. Opret af sikkerhedsmæssige årsager en separat konto til dette.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegram Bot Token\",\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus Token\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Brugernavn\",\n    \"mattermost_bot_icon_emoji\" => \"Emoji til bot-ikon\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"Request-metode\",\n    \"custom_headers\" => \"Brugerdefinerede headers\",\n    \"webhook_payload\" => \"Webhook Payload\",\n    \"payment_notifications_payload\" => \"Payload for betalingsnotifikationer\",\n    \"cancelation_notification_payload\" => \"Payload for annulleringsnotifikationer\",\n    \"variables_available\" => \"Tilgængelige variabler\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Discord Bot Brugernavn\",\n    \"discord_bot_avatar_url\" => \"Discord Bot Avatar URL\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover Bruger Nøgle\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Emne\",\n    \"ignore_ssl_errors\" => \"Ignorér SSL-fejl\",\n    \"categories\" => \"Kategorier\",\n    \"save_category\" => \"Gem kategori\",\n    \"delete_category\" => \"Slet kategori\",\n    \"cant_delete_category_in_use\" => \"Kan ikke slette kategori i brug i abonnement\",\n    \"currencies\" => \"Valutaer\",\n    \"save_currency\" => \"Gem valuta\",\n    \"delete_currency\" => \"Slet valuta\",\n    \"cant_delete_main_currency\" => \"Kan ikke slette hovedvaluta\",\n    \"cant_delete_currency_in_use\" => \"Kan ikke slette valuta i brug i abonnement\",\n    \"exchange_update\" => \"Valutakurser sidst opdateret den\",\n    \"currency_info\" => \"Find understøttede valutaer og korrekte valutakoder på\",\n    \"currency_performance\" => \"For bedre ydeevne, behold kun de valutaer du bruger.\",\n    \"fixer_api_key\" => \"Fixer API-nøgle\",\n    \"provider\" => \"Udbyder\",\n    \"fixer_info\" => \"Hvis du bruger flere valutaer og vil have nøjagtig statistik og sortering af abonnementer, kræves en GRATIS API-nøgle fra Fixer.\",\n    \"get_key\" => \"Få din nøgle på\",\n    \"get_free_fixer_api_key\" => \"Få gratis Fixer API-nøgle\",\n    \"get_key_alternative\" => \"Alternativt kan du få en gratis fixer API-nøgle fra\",\n    \"ai_model\" => \"AI Model\",\n    \"select_ai_model\" => \"Vælg AI Model\",\n    \"run_schedule\" => \"Kør tidsplan\",\n    \"manually\" => \"Manuelt\",\n    \"coming_soon\" => \"Kommer snart\",\n    \"invalid_host\" => \"Ugyldig vært\",\n    \"ai_recommendations_info\" => \"AI anbefalinger genereres baseret på dine abonnementer og husstandsmedlemmer.\",\n    \"may_take_time\" => \"Afhængigt af udbyderen, modellen og antallet af abonnementer kan genereringen af anbefalinger tage noget tid.\",\n    \"recommendations_visible_on_dashboard\" => \"Anbefalinger vil være synlige på instrumentbrættet.\",\n    \"generate_recommendations\" => \"Generer anbefalinger\",\n    \"display_settings\" => \"Visningsindstillinger\",\n    \"theme_settings\" => \"Temaindstillinger\",\n    \"colors\" => \"Farver\",\n    \"custom_colors\" => \"Brugerdefinerede farver\",\n    \"theme\" => \"Tema\",\n    \"dark_theme\" => \"Mørkt tema\",\n    \"light_theme\" => \"Lyst tema\",\n    \"automatic\"=> \"Automatisk\",\n    \"main_color\" => \"Hovedfarve\",\n    \"accent_color\" => \"Accentfarve\",\n    \"hover_color\" => \"Hover-farve\",\n    \"save_custom_colors\" => \"Gem brugerdefinerede farver\",\n    \"reset_custom_colors\" => \"Nulstil brugerdefinerede farver\",\n    \"custom_css\" => \"Brugerdefineret CSS\",\n    \"save_custom_css\" => \"Gem brugerdefineret CSS\",\n    \"calculate_monthly_price\" => \"Beregn og vis månedlig pris for alle abonnementer\",\n    \"convert_prices\" => \"Konverter og vis altid priser i min hovedvaluta (langsommere)\",\n    \"show_original_price\" => \"Vis også original pris ved konvertering eller beregning\",\n    \"experience\" => \"Oplevelse\",\n    \"show_subscription_progress\" => \"Vis abonnementsfremskridt\",\n    \"disabled_subscriptions\" => \"Deaktiverede abonnementer\",\n    \"hide_disabled_subscriptions\" => \"Skjul deaktiverede abonnementer\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Vis deaktiverede abonnementer nederst\",\n    \"experimental_settings\" => \"Eksperimentelle indstillinger\",\n    \"remove_background\" => \"Forsøg at fjerne baggrund på logoer ved billedsøgning\",\n    \"use_mobile_navigation_bar\" => \"Brug mobil navigationsbjælke\",\n    \"experimental_info\" => \"Eksperimentelle indstillinger fungerer sandsynligvis ikke perfekt.\",\n    \"payment_methods\" => \"Betalingsmetoder\",\n    \"payment_methods_info\" => \"Klik på en betalingsmetode for at deaktivere / aktivere den.\",\n    \"rename_payment_methods_info\" => \"Klik på navnet på en betalingsmetode for at omdøbe den.\",\n    \"cant_delete_payment_method_in_use\" => \"Kan ikke deaktivere betalingsmetode i brug\",\n    \"add_custom_payment\" => \"Tilføj brugerdefineret betalingsmetode\",\n    \"payment_method_name\" => \"Navn på betalingsmetode\",\n    \"payment_method_added_successfuly\" => \"Betalingsmetode tilføjet\",\n    \"payment_method_removed\" => \"Betalingsmetode fjernet\",\n    \"disable\" => \"Deaktiver\",\n    \"enable\" => \"Aktivér\",\n    \"rename_payment_method\" => \"Omdøb betalingsmetode\",\n    \"payment_renamed\" => \"Betalingsmetode omdøbt\",\n    \"payment_not_renamed\" => \"Betalingsmetode ikke omdøbt\",\n    \"test\" => \"Test\",\n    \"add\" => \"Tilføj\",\n    \"save\" => \"Gem\",\n    \"reset\" => \"Nulstil\",\n    \"main_accent_color_error\" => \"Hovedfarve og accentfarve må ikke være ens\",\n    \"backup_and_restore\" => \"Backup og gendannelse\",\n    \"backup\" => \"Backup\",\n    \"restore\" => \"Gendan\",\n    \"restore_info\" => \"Gendannelse af databasen vil overskrive alle nuværende data. Du vil blive logget ud efter gendannelsen.\",\n    \"account\" => \"Konto\",\n    \"export_subscriptions\" => \"Eksportér abonnementer\",\n    \"export_as_json\" => \"Eksportér som JSON\",\n    \"export_as_csv\" => \"Eksportér som CSV\",\n    \"danger_zone\" => \"Farezone\",\n    \"delete_account\" => \"Slet konto\",\n    \"delete_account_info\" => \"Sletning af din konto sletter også alle dine abonnementer og indstillinger.\",\n    // Filters menu\n    \"filter\" => \"Filter\",\n    \"clear\" => \"Ryd\",\n    // Toast\n    \"success\" => \"Succes\",\n    // Endpoint responses\n    \"session_expired\" => \"Din session er udløbet. Log venligst ind igen\",\n    \"fields_missing\" => \"Nogle felter mangler\",\n    \"fill_all_fields\" => \"Udfyld venligst alle felter\",\n    \"fill_mandatory_fields\" => \"Udfyld venligst alle obligatoriske felter\",\n    \"error\" => \"Fejl\",\n    // Category\n    \"failed_add_category\" => \"Kunne ikke tilføje kategori\",\n    \"failed_edit_category\" => \"Kunne ikke redigere kategori\",\n    \"category_in_use\" => \"Kategorien er i brug i abonnementer og kan ikke fjernes\",\n    \"failed_remove_category\" => \"Kunne ikke fjerne kategori\",\n    \"category_saved\" => \"Kategori gemt\",\n    \"category_removed\" => \"Kategori fjernet\",\n    \"sort_order_saved\" => \"Sorteringsrækkefølge gemt\",\n    // Currency\n    \"currency_saved\" => \"blev gemt.\",\n    \"error_adding_currency\" => \"Fejl ved tilføjelse af valuta.\",\n    \"failed_to_store_currency\" => \"Kunne ikke gemme valuta i databasen.\",\n    \"currency_in_use\" => \"Valutaen er i brug i abonnementer og kan ikke slettes.\",\n    \"currency_is_main\" => \"Valutaen er indstillet som hovedvaluta og kan ikke slettes.\",\n    \"failed_to_remove_currency\" => \"Kunne ikke fjerne valuta fra databasen.\",\n    \"failed_to_store_api_key\" => \"Kunne ikke gemme API-nøgle i databasen.\",\n    \"invalid_api_key\" => \"Ugyldig API-nøgle.\",\n    \"api_key_saved\" => \"API-nøgle gemt\",\n    \"currency_removed\" => \"Valuta fjernet\",\n    // Household\n    \"failed_add_household\" => \"Kunne ikke tilføje husstandsmedlem\",\n    \"failed_edit_household\" => \"Kunne ikke redigere husstandsmedlem\",\n    \"failed_remove_household\" => \"Kunne ikke fjerne husstandsmedlem\",\n    \"household_in_use\" => \"Husstandsmedlemmet er i brug i abonnementer og kan ikke fjernes\",\n    \"member_saved\" => \"Medlem gemt\",\n    \"member_removed\" => \"Medlem fjernet\",\n    // Notifications\n    \"error_saving_notifications\" => \"Fejl ved gemning af notifikationsdata.\",\n    \"wallos_notification\" => \"Wallos Notifikation\",\n    \"test_notification\" => \"Dette er en testnotifikation. Hvis du ser dette, er konfigurationen korrekt.\",\n    \"email_error\" => \"Fejl ved afsendelse af e-mail\",\n    \"notification_sent_successfuly\" => \"Notifikation sendt\",\n    \"notifications_settings_saved\" => \"Notifikationsindstillinger gemt\",\n    \"notification_failed\" => \"Notifikation mislykkedes\",\n    // Payments\n    \"payment_in_use\" => \"Kan ikke deaktivere en anvendt betalingsmetode\",\n    \"failed_update_payment\" => \"Kunne ikke opdatere betalingsmetode i databasen\",\n    \"enabled\" => \"aktiveret\",\n    \"disabled\" => \"deaktiveret\",\n    // Subscription\n    \"error_fetching_image\" => \"Fejl ved hentning af billede\",\n    \"subscription_updated_successfuly\" => \"Abonnement opdateret\",\n    \"subscription_added_successfuly\" => \"Abonnement tilføjet\",\n    \"error_deleting_subscription\" => \"Fejl ved sletning af abonnement.\",\n    \"invalid_request_method\" => \"Ugyldig anmodningsmetode.\",\n    // User\n    \"error_updating_user_data\" => \"Fejl ved opdatering af brugerdata.\",\n    \"user_details_saved\" => \"Brugeroplysninger gemt\",\n    // Admin Page\n    \"registrations\" => \"Registreringer\",\n    \"enable_user_registrations\" => \"Aktivér brugerregistrering\",\n    \"maximum_number_users\" => \"Maksimalt antal brugere\",\n    \"require_email_verification\" => \"Kræv e-mailverifikation\",\n    \"configure_smtp_settings_to_enable\" => \"Konfigurér SMTP-indstillinger for at aktivere\",\n    \"server_url\" => \"Server-URL\",\n    \"server_url_info\" => \"Bruges til e-mailverifikation og nulstilling af adgangskode. Skal være en gyldig offentlig URL.\",\n    \"server_url_password_reset\" => \"Hvis angivet, aktiveres nulstilling af adgangskode.\",\n    \"disable_login\" => \"Deaktivér login\",\n    \"disable_login_info\" => \"Omgå login. Hvis din server kun kører på et lokalt netværk uden ekstern adgang, kan du deaktivere login. Dette logger automatisk admin-brugeren ind.\",\n    \"disable_login_info2\" => \"Du kan kun aktivere denne indstilling, hvis brugerregistrering er deaktiveret, og der ikke er mere end én (admin) brugerkonto.\",\n    \"max_users_info\" => \"0 betyder ubegrænset\",\n    \"user_management\" => \"Brugeradministration\",\n    \"delete_user\" => \"Slet bruger\",\n    \"delete_user_info\" => \"Sletning af en bruger vil også slette alle deres abonnementer og indstillinger.\",\n    \"create_user\" => \"Opret bruger\",\n    \"oidc_settings\" => \"OIDC-indstillinger\",\n    \"oidc_oauth_enabled\" => \"Aktivér OIDC/OAuth\",\n    \"create_user_automatically\" => \"Opret bruger automatisk\",\n    \"disable_password_login\" => \"Deaktivér adgangskode-login\",\n    \"smtp_settings\" => \"SMTP-indstillinger\",\n    \"security_settings\" => \"Sikkerhedsindstillinger\",\n    \"ssrf_protection_info\" => \"For at forhindre Server-Side Request Forgery (SSRF) angreb blokerer Wallos som standard webhook-notifikationer til private eller interne netværksadresser.\",\n    \"local_webhook_info\" => \"Hvis du skal sende webhooks til lokale tjenester (som Home Assistant, Gotify eller Node-RED), indtast deres IP-adresser eller værtsnavne ovenfor som en kommasepareret liste (fx <code>192.168.1.100,192.168.1.101</code>).\",\n    \"smtp_usage_info\" => \"Vil blive brugt til adgangskodenulstilling og andre systemmails.\",\n    // Maintenance Tasks\n    \"maintenance_tasks\" => \"Vedligeholdelsesopgaver\",\n    \"orphaned_logos\" => \"Forladte logoer\",\n    \"update\" => \"Opdater\",\n    \"new_version_available\" => \"En ny version af Wallos er tilgængelig\",\n    \"current_version\" => \"Nuværende version\",\n    \"latest_version\" => \"Seneste version\",\n    \"on_current_version\" => \"Du kører den nyeste version af Wallos.\",\n    \"show_update_notification\" => \"Vis opdateringsnotifikation på dashboard\",\n    \"cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"E-mail bekræftet\",\n    \"email_verification_failed\" => \"E-mailverifikation mislykkedes\",\n    // Calendar\n    \"calendar\" => \"Kalender\",\n    \"sun\" => \"Søn\",\n    \"mon\" => \"Man\",\n    \"tue\" => \"Tir\",\n    \"wed\" => \"Ons\",\n    \"thu\" => \"Tor\",\n    \"fri\" => \"Fre\",\n    \"sat\" => \"Lør\",\n    \"month-01\" => \"Januar\",\n    \"month-02\" => \"Februar\",\n    \"month-03\" => \"Marts\",\n    \"month-04\" => \"April\",\n    \"month-05\" => \"Maj\",\n    \"month-06\" => \"Juni\",\n    \"month-07\" => \"Juli\",\n    \"month-08\" => \"August\",\n    \"month-09\" => \"September\",\n    \"month-10\" => \"Oktober\",\n    \"month-11\" => \"November\",\n    \"month-12\" => \"December\",\n    \"total_cost\" => \"Samlede omkostninger\",\n    \"export_icalendar\" => \"Eksportér iCalendar\",\n    \"over_budget_warning\" => \"Du er over budget\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Indtast TOTP-kode\",\n];\n?>\n"
  },
  {
    "path": "includes/i18n/de.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"Bitte erstelle zunächst einen Account, um dich einloggen zu können\",\r\n    \"username\" => \"Benutzername\",\r\n    \"password\" => \"Passwort\",\r\n    \"email\" => \"E-Mail\",\r\n    \"firstname\" => \"Vorname\",\r\n    \"lastname\" => \"Nachname\",\r\n    \"confirm_password\" => \"Passwort bestätigen\",\r\n    \"main_currency\" => \"Hauptwährung\",\r\n    \"language\" => \"Sprache\",\r\n    \"passwords_dont_match\" => \"Die Passwörter stimmen nicht überein\",\r\n    \"username_exists\" => \"Benutzername existiert bereits\",\r\n    \"email_exists\" => \"E-Mail existiert bereits\",\r\n    \"registration_failed\" => \"Registrierung fehlgeschlagen, bitte erneut versuchen.\",\r\n    \"register\" => \"Registrieren\",\r\n    \"restore_database\" => \"Datenbank wiederherstellen\",\r\n    // Login Page\r\n    \"please_login\" => \"Bitte einloggen\",\r\n    \"stay_logged_in\" => \"Angemeldet bleiben (30 Tage)\",\r\n    \"login\" => \"Login\",\r\n    \"login_with\" => \"Einloggen mit\",\r\n    \"or\" => \"oder\",\r\n    \"login_failed\" => \"Loginangaben sind nicht korrekt\",\r\n    \"registration_successful\" => \"Registrierung erfolgreich\",\r\n    \"user_email_waiting_verification\" => \"Ihre E-Mail muss noch verifiziert werden. Bitte überprüfen Sie Ihre E-Mail.\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Passwort vergessen?\",\r\n    \"reset_password\" => \"Passwort zurücksetzen\",\r\n    \"reset_sent_check_email\" => \"Passwort zurücksetzen E-Mail wurde gesendet. Bitte überprüfen Sie Ihr Postfach.\",\r\n    \"password_reset_successful\" => \"Passwort erfolgreich zurückgesetzt\",\r\n    // Header\r\n    \"profile\" => \"Profil\",\r\n    \"dashboard\" => \"Dashboard\",\r\n    \"subscriptions\" => \"Abonnements\",\r\n    \"stats\" => \"Statistiken\",\r\n    \"settings\" => \"Einstellungen\",\r\n    \"admin\" => \"Admin\",\r\n    \"about\" => \"Über\",\r\n    \"logout\" => \"Logout\",\r\n    // Dashboard\r\n    \"hello\" => \"Hallo\",\r\n    \"upcoming_payments\" => \"Bevorstehende Zahlungen\",\r\n    \"no_upcoming_payments\" => \"Sie haben keine bevorstehenden Zahlungen\",\r\n    \"overdue_renewals\" => \"Überfällige Verlängerungen\",\r\n    \"ai_recommendations\" => \"AI Empfehlungen\",\r\n    \"your_budget\" => \"Ihr Budget\",\r\n    \"budget\" => \"Budget\",\r\n    \"budget_used\" => \"Budget verwendet\",\r\n    \"over_budget\" => \"Über Budget\",\r\n    \"your_subscriptions\" => \"Ihre Abonnements\",\r\n    \"your_savings\" => \"Ihre Ersparnisse\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Abonnement\",\r\n    \"no_subscriptions_yet\" => \"Keine Abonnements hinzugefügt\",\r\n    \"add_first_subscription\" => \"Erstes Abonnement hinzufügen\",\r\n    \"new_subscription\" => \"Neues Abonnement\",\r\n    \"search\" => \"Suche\",\r\n    \"state\" => \"Status\",\r\n    \"alphanumeric\" => \"Alphanumerisch\",\r\n    \"sort\" => \"Sortieren\",\r\n    \"name\" => \"Bezeichnung\",\r\n    \"last_added\" => \"Zuletzt hinzugefügt\",\r\n    \"price\" => \"Preis\",\r\n    \"next_payment\" => \"Nächste Zahlung\",\r\n    \"renewal_type\" => \"Verlängerungstyp\",\r\n    \"auto_renewal\" => \"Automatische Verlängerung\",\r\n    \"automatically_renews\" => \"Automatisch verlängert\",\r\n    \"manual_renewal\" => \"Manuelle Verlängerung\",\r\n    \"start_date\" => \"Startdatum\",\r\n    \"inactive\" => \"Abonnement deaktivieren\",\r\n    \"replaced_with\" => \"Ersetzt durch\",\r\n    \"none\" => \"Keine\",\r\n    \"member\" => \"Mitglied\",\r\n    \"category\" => \"Kategorie\",\r\n    \"payment_method\" => \"Zahlungsmethode\",\r\n    \"Daily\" => \"Täglich\",\r\n    \"Weekly\" => \"Wöchentlich\",\r\n    \"Monthly\" => \"Monatlich\",\r\n    \"Yearly\" => \"Jährlich\",\r\n    \"dayly\" => \"Tag(e)\",\r\n    \"weekly\" => \"Woche(n)\",\r\n    \"monthly\" => \"Monat(e)\",\r\n    \"yearly\" => \"Jahr(e)\",\r\n    \"days\" => \"Tage\",\r\n    \"weeks\" => \"Wochen\",\r\n    \"months\" => \"Monate\",\r\n    \"years\" => \"Jahre\",\r\n    \"external_url\" => \"Externe URL besuchen\",\r\n    \"empty_page\" => \"Leere Seite\",\r\n    \"clear_filters\" => \"Filter zurücksetzen\",\r\n    \"no_matching_subscriptions\" => \"Keine passenden Abonnements gefunden\",\r\n    \"clone\" => \"Klonen\",\r\n    \"renew\" => \"Verlängern\",\r\n    \"calculate_next_payment_date\" => \"Berechne nächstes Zahlungsdatum\",\r\n    // Subscription form\r\n    \"add_subscription\" => \"Abonnement hinzufügen\",\r\n    \"edit_subscription\" => \"Abonnement editieren\",\r\n    \"subscription_name\" => \"Bezeichnung des Abonnements\",\r\n    \"logo_preview\" => \"Vorschau des Logos\",\r\n    \"search_logo\" => \"Logo im Web suchen\",\r\n    \"web_search\" => \"Websuche\",\r\n    \"currency\" => \"Währung\",\r\n    \"payment_every\" => \"Zahlung alle\",\r\n    \"frequency\" => \"Abrechnungsfrequenz\",\r\n    \"cycle\" => \"Zeitraum\",\r\n    \"no_category\" => \"Keine Kategorie\",\r\n    \"paid_by\" => \"Gezahlt durch\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Notizen\",\r\n    \"enable_notifications\" => \"Benachrichtigungen für dieses Abonnement aktivieren\",\r\n    \"default_value_from_settings\" => \"Standardwert aus den Einstellungen\",\r\n    \"cancellation_notification\" => \"Benachrichtigung bei Kündigung\",\r\n    \"delete\" => \"Löschen\",\r\n    \"cancel\" => \"Abbrechen\",\r\n    \"upload_logo\" => \"Logo hochladen\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"Sie verwenden mehrere Währungen für Ihre Abonnements. Um gültige und genaue Statistiken zu erhalten, legen Sie bitte einen Fixer-API-Schlüssel auf der Einstellungsseite fest.\",\r\n    \"general_statistics\" => \"Allgemeine Statistiken\",\r\n    \"active_subscriptions\" => \"Aktive Abonnements\",\r\n    \"inactive_subscriptions\" => \"Inaktive Abonnements\",\r\n    \"monthly_cost\" => \"Monatliche Kosten\",\r\n    \"yearly_cost\" => \"Jährliche Kosten\",\r\n    \"average_monthly\" => \"Durchschnittliche monatliche Kosten\",\r\n    \"most_expensive\" => \"Kosten des teuersten Abonnements\",\r\n    \"amount_due\" => \"Diesen Monat fällige Summe\",\r\n    \"percentage_budget_used\" => \"Prozentualer Anteil des Budgets genutzt\",\r\n    \"budget_remaining\" => \"Verbleibendes Budget\",\r\n    \"amount_over_budget\" => \"Überzogenes Budget\",\r\n    \"monthly_savings\" => \"Monatliche Ersparnisse (bei inaktiven Abonnements)\",\r\n    \"yearly_savings\" => \"Jährliche Ersparnisse (bei inaktiven Abonnements)\",\r\n    \"split_views\" => \"Aufgeteilte Ansichten\",\r\n    \"category_split\" => \"Kategorien\",\r\n    \"household_split\" => \"Haushalt\",\r\n    \"payment_method_split\" => \"Zahlungsmethode\",\r\n    \"total_cost_trend\" => \"Kostenentwicklung\",\r\n    \"cost_vs_budget\" => \"Kosten vs. Budget\",\r\n    // About page\r\n    \"about_and_credits\" => \"Informationen und Danksagungen\",\r\n    \"credits\" => \"Danksagungen\",\r\n    \"license\" => \"Lizenz\",\r\n    \"release_notes\" => \"Versionshinweise\",\r\n    \"update_available\" => \"Update verfügbar\",\r\n    \"issues_and_requests\" => \"Issues und Anfragen\",\r\n    \"the_author\" => \"Der Autor\",\r\n    \"icons\" => \"Icons\",\r\n    \"payment_icons\" => \"Zahlungsweisen Icons\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"Avatar hochladen\",\r\n    \"file_type_error\" => \"Dateityp nicht unterstützt\",\r\n    \"user_details\" => \"Benutzerdetails\",\r\n    \"two_factor_authentication\" => \"Zwei-Faktor-Authentifizierung\",\r\n    \"two_factor_info\" => \"Die Zwei-Faktor-Authentifizierung fügt Ihrem Konto eine zusätzliche Sicherheitsebene hinzu.<br>Sie benötigen eine Authentifizierungs-App wie Google Authenticator, Authy oder Ente Auth, um den QR-Code zu scannen.\",\r\n    \"two_factor_enabled_info\" => \"Ihr Konto ist mit der Zwei-Faktor-Authentifizierung gesichert. Sie können sie deaktivieren, indem Sie auf die Schaltfläche oben klicken.\",\r\n    \"enable_two_factor_authentication\" => \"Zwei-Faktor-Authentifizierung aktivieren\",\r\n    \"2fa_already_enabled\" => \"Zwei-Faktor-Authentifizierung ist bereits aktiviert\",\r\n    \"totp_code_incorrect\" => \"TOTP-Code ist falsch\",\r\n    \"backup_codes\" => \"Backup-Codes\",\r\n    \"download_backup_codes\" => \"Backup-Codes herunterladen\",\r\n    \"copy_to_clipboard\" => \"In die Zwischenablage kopieren\",\r\n    \"totp_backup_codes_info\" => \"Speichern Sie diese Codes an einem sicheren Ort. Sie können sie verwenden, wenn Sie keinen Zugriff auf Ihre Authentifizierungs-App haben.\",\r\n    \"disable_two_factor_authentication\" => \"Zwei-Faktor-Authentifizierung deaktivieren\",\r\n    \"totp_code\" => \"TOTP-Code\",\r\n    \"api_key\" => \"API Key\",\r\n    \"regenerate\" => \"Neu generieren\",\r\n    \"api_key_info\" => \"Der API-Schlüssel wird für die Integration von Drittanbieter-Apps verwendet. Wenn Sie Ihren Schlüssel neu generieren, müssen Sie ihn in allen Apps aktualisieren, die ihn verwenden.\",\r\n    // Settings page\r\n    \"monthly_budget\" => \"Monatliches Budget\",\r\n    \"budget_info\" => \"Das monatliche Budget wird für die Berechnung der Statistiken verwendet.\",\r\n    \"household\" => \"Haushalt\",\r\n    \"save_member\" => \"Mitglied speichern\",\r\n    \"delete_member\" => \"Mitglied löschen\",\r\n    \"cant_delete_member\" => \"Hauptmitglied kann nicht gelöscht werden\",\r\n    \"cant_delete_member_in_use\" => \"Mitglied mit Abonnement kann nicht gelöscht werden\",\r\n    \"household_info\" => \"Über das E-Mail-Feld können die Haushaltsmitglieder über auslaufende Abonnements benachrichtigt werden.\",\r\n    \"notifications\" => \"Benachrichtigungen\",\r\n    \"enable_email_notifications\" => \"E-Mail Benachrichtigung aktivieren\",\r\n    \"notify_me\" => \"Benachrichtige mich\",\r\n    \"day_before\" => \"Tag bevor\",\r\n    \"on_due_date\" => \"Am Fälligkeitsdatum\",\r\n    \"days_before\" => \"Tage bevor\",\r\n    \"smtp_address\" => \"SMTP Adresse\",\r\n    \"port\" => \"Port\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"SMTP Benutzername\",\r\n    \"smtp_password\" => \"SMTP Passwort\",\r\n    \"from_email\" => \"Absender E-Mail Adresse (optional)\",\r\n    \"send_to_other_emails\" => \"Benachrichtigungen auch an die folgenden E-Mail-Adressen senden (verwende ; um sie zu trennen):\",\r\n    \"smtp_info\" => \"Das SMTP Passwort wird in Klartext übermittelt und gespeichert. Aus Sicherheitsgründen erstelle bitte einen gesonderten Account nur zu diesem Zweck.\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Telegram Bot Token\",\r\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Pushplus Token\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\r\n    \"mattermost_bot_username\" => \"Mattermost Bot Benutzername\",\r\n    \"mattermost_bot_icon_emoji\" => \"Emoji zum Bot-Icon\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"Webhook URL\",\r\n    \"request_method\" => \"Request Methode\",\r\n    \"custom_headers\" => \"Benutzerdefinierte Kopfzeilen\",\r\n    \"webhook_payload\" => \"Webhook Payload\",\r\n    \"payment_notifications_payload\" => \"Zahlungsbenachrichtigung Payload\",\r\n    \"cancelation_notification_payload\" => \"Kündigungsbenachrichtigung Payload\",\r\n    \"variables_available\" => \"Verfügbare Variablen\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Bot Benutzername\",\r\n    \"discord_bot_avatar_url\" => \"Bot Avatar URL\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Pushover User Key\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Topic\",\r\n    \"ignore_ssl_errors\" => \"SSL Fehler ignorieren\",\r\n    \"categories\" => \"Kategorien\",\r\n    \"save_category\" => \"Kategorie speichern\",\r\n    \"delete_category\" => \"Kategorie löschen\",\r\n    \"cant_delete_category_in_use\" => \"Kategorie mit zugeordnetem Abonnement kann nicht gelöscht werden\",\r\n    \"currencies\" => \"Währungen\",\r\n    \"save_currency\" => \"Währung speichern\",\r\n    \"delete_currency\" => \"Währung löschen\",\r\n    \"cant_delete_main_currency\" => \"Hautwährung kann nicht gelöscht werden\",\r\n    \"cant_delete_currency_in_use\" => \"Währungen die in Abonnements genutzt werden können nicht gelöscht werden\",\r\n    \"exchange_update\" => \"Umrechnungskurs zuletzt aktualisiert am\",\r\n    \"currency_info\" => \"Finde die unterstützten Währungen und korrekten Währungscodes auf\",\r\n    \"currency_performance\" => \"Aus Gründen der Performance wähle bitte ausschließlich die genutzen Währungen.\",\r\n    \"fixer_api_key\" => \"Fixer API Key\",\r\n    \"provider\" => \"Anbieter\",\r\n    \"fixer_info\" => \"Falls du mehrere Währungen nutzt und genaue Statistiken und die Sortierungsfunktion nutzen möchtest, wird ein kostenfreier API Key von Fixer benötigt.\",\r\n    \"get_key\" => \"Erhalte deinen key bei\",\r\n    \"get_free_fixer_api_key\" => \"Erhalte deinen kostenfreien Fixer API Key\",\r\n    \"get_key_alternative\" => \"Alternativ können Sie einen kostenlosen Fixer-Api-Schlüssel erhalten von\",\r\n    \"ai_model\" => \"AI Modell\",\r\n    \"select_ai_model\" => \"Wählen Sie AI Modell\",\r\n    \"run_schedule\" => \"Zeitplan ausführen\",\r\n    \"manually\" => \"Manuell\",\r\n    \"coming_soon\" => \"Demnächst\",\r\n    \"invalid_host\" => \"Ungültiger Host\",\r\n    \"ai_recommendations_info\" => \"AI Empfehlungen werden basierend auf Ihren Abonnements und Haushaltsmitgliedern generiert.\",\r\n    \"may_take_time\" => \"Je nach Anbieter, Modell und Anzahl der Abonnements kann die Generierung von Empfehlungen einige Zeit in Anspruch nehmen.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Empfehlungen werden auf dem Dashboard sichtbar sein.\",\r\n    \"generate_recommendations\" => \"Empfehlungen generieren\",\r\n    \"display_settings\" => \"Anzeigeeinstellungen\",\r\n    \"theme_settings\" => \"Themen-Einstellungen\",\r\n    \"colors\" => \"Farben\",\r\n    \"custom_colors\" => \"Benutzerdefinierte Farben\",\r\n    \"theme\" => \"Thema\",\r\n    \"dark_theme\" => \"Dark Theme\",\r\n    \"light_theme\" => \"Light Theme\",\r\n    \"automatic\"=> \"Automatisch\",\r\n    \"main_color\" => \"Hauptfarbe\",\r\n    \"accent_color\" => \"Akzentfarbe\",\r\n    \"hover_color\" => \"Hover Farbe\",\r\n    \"custom_css\" => \"Benutzerdefiniertes CSS\",\r\n    \"save_custom_css\" => \"Benutzerdefiniertes CSS speichern\",\r\n    \"save_custom_colors\" => \"Benutzerdefinierte Farben speichern\",\r\n    \"reset_custom_colors\" => \"Benutzerdefinierte Farben zurücksetzen\",\r\n    \"calculate_monthly_price\" => \"Berechne und zeige monatlichen Preis für alle Abonnements an\",\r\n    \"convert_prices\" => \"Preise immer in meine Hauptwährung umrechnen und darin anzeigen (langsamer)\",\r\n    \"show_original_price\" => \"Originalpreis anzeigen, wenn Umrechnungen oder Berechnungen durchgeführt werden\",\r\n    \"experience\" => \"Erfahrung\",\r\n    \"show_subscription_progress\" => \"Abonnementfortschritt anzeigen\",\r\n    \"disabled_subscriptions\" => \"Deaktivierte Abonnements\",\r\n    \"hide_disabled_subscriptions\" => \"Deaktivierte Abonnements verstecken\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Deaktivierte Abonnements am Ende anzeigen\",\r\n    \"experimental_settings\" => \"Experimentelle Einstellungen\",\r\n    \"remove_background\" => \"Versuchen den Hintergrund von Logos aus der Bildersuche zu entfernen\",\r\n    \"use_mobile_navigation_bar\" => \"Mobile Navigationsleiste verwenden\",\r\n    \"experimental_info\" => \"Experimentelle Einstellungen funktionieren möglicherweise nicht perfekt.\",\r\n    \"payment_methods\" => \"Zahlungsmethoden\",\r\n    \"payment_methods_info\" => \"Zahlungsmethode zum (de-)aktivieren anklicken.\",\r\n    \"rename_payment_methods_info\" => \"Klicken Sie auf den Namen einer Zahlungsmethode, um sie umzubenennen\",\r\n    \"cant_delete_payment_method_in_use\" => \"Genutzte Zahlungsmethoden können nicht deaktiviert werden\",\r\n    \"add_custom_payment\" => \"Eigene Zahlungsmethode hinzufügen\",\r\n    \"payment_method_name\" => \"Name der Zahlungsmethode\",\r\n    \"payment_method_added_successfuly\" => \"Zahlungsmethode erfolgreich hinzugefügt\",\r\n    \"payment_method_removed\" => \"Zahlungsmethode gelöscht\",\r\n    \"disable\" => \"Deaktivieren\",\r\n    \"enable\" => \"Aktivieren\",\r\n    \"rename_payment_method\" => \"Zahlungsmethode umbenennen\",\r\n    \"payment_renamed\" => \"Zahlungsmethode umbenannt\",\r\n    \"payment_not_renamed\" => \"Zahlungsmethode konnte nicht umbenannt werden\",\r\n    \"test\" => \"Test\",\r\n    \"add\" => \"Hinzufügen\",\r\n    \"save\" => \"Speichern\",\r\n    \"reset\" => \"Zurücksetzen\",\r\n    \"main_accent_color_error\" => \"Haupt- und Akzentfarbe dürfen nicht identisch sein\",\r\n    \"backup_and_restore\" => \"Backup und Wiederherstellung\",\r\n    \"backup\" => \"Backup\",\r\n    \"restore\" => \"Wiederherstellen\",\r\n    \"restore_info\" => \"Durch die Wiederherstellung der Datenbank werden alle aktuellen Daten überschrieben. Nach der Wiederherstellung werden Sie abgemeldet.\",\r\n    \"account\" => \"Konto\",\r\n    \"export_subscriptions\" => \"Abonnements exportieren\",\r\n    \"export_as_json\" => \"Als JSON exportieren\",\r\n    \"export_as_csv\" => \"Als CSV exportieren\",\r\n    \"danger_zone\" => \"Gefahrenzone\",\r\n    \"delete_account\" => \"Konto löschen\",\r\n    \"delete_account_info\" => \"Mit dem Löschen Ihres Kontos werden auch alle Ihre Abonnements und Einstellungen gelöscht.\",\r\n    // Filters menu\r\n    \"filter\" => \"Filter\",\r\n    \"clear\" => \"Leeren\",\r\n    // Toast\r\n    \"success\" => \"Erfolgreich\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"Session abgelaufen. Bitte erneut einloggen\",\r\n    \"fields_missing\" => \"Einige Felder fehlen\",\r\n    \"fill_all_fields\" => \"Bitte alle Felder ausfüllen\",\r\n    \"fill_mandatory_fields\" => \"Bitte alle Pflichtfelder ausfüllen\",\r\n    \"error\" => \"Fehler\",\r\n    // Category\r\n    \"failed_add_category\" => \"Kategorie konnte nicht hinzugefügt werden\",\r\n    \"failed_edit_category\" => \"Kategorie konnte nicht editiert werden\",\r\n    \"category_in_use\" => \"Kategorie wird in Abonnements verwendet und kann nicht gelöscht werden\",\r\n    \"failed_remove_category\" => \"Kategorie konnte nicht gelöscht werden\",\r\n    \"category_saved\" => \"Kategorie gespeichert\",\r\n    \"category_removed\" => \"Kategorie gelöscht\",\r\n    \"sort_order_saved\" => \"Sortierung gespeichert\",\r\n    // Currency\r\n    \"currency_saved\" => \"wurde gespeichert.\",\r\n    \"error_adding_currency\" => \"Fehler beim hinzufügen der Währung.\",\r\n    \"failed_to_store_currency\" => \"Währung konnte nicht zur Datenbank hinzugefügt werden.\",\r\n    \"currency_in_use\" => \"Währung wird in Abonnements verwendet und kann nicht gelöscht werden.\",\r\n    \"currency_is_main\" => \"Währung ist als Hauptwährung konfiguriert und kann nicht gelöscht werden.\",\r\n    \"failed_to_remove_currency\" => \"Währung konnte nicht aus Datenbank gelöscht werden.\",\r\n    \"failed_to_store_api_key\" => \"API Key konnte nicht in Datenbank gespeichert werden.\",\r\n    \"invalid_api_key\" => \"Ungültiger API Key.\",\r\n    \"api_key_saved\" => \"API key erfolgreich gespeichert\",\r\n    \"currency_removed\" => \"Währung gelöscht\",\r\n    // Household\r\n    \"failed_add_household\" => \"Haushaltsmitglied konnte nicht hinzugefügt werden\",\r\n    \"failed_edit_household\" => \"Haushaltsmitglied konnte nicht editiert werden\",\r\n    \"failed_remove_household\" => \"Haushaltsmitglied konnte nicht gelöscht werden\",\r\n    \"household_in_use\" => \"Haushaltsmitglied wird in Abonnements verwendet und kann nicht gelöscht werden\",\r\n    \"member_saved\" => \"Mitglied gespeichert\",\r\n    \"member_removed\" => \"Mitglied gelöscht\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Benachrichtigungsangaben konnten nicht gespeichert werden.\",\r\n    \"wallos_notification\" => \"Wallos Benachrichtigung\",\r\n    \"test_notification\" => \"Dies ist eine Test-Benachrichtigung. Wenn du das hier siehst, sind deine Konfigurationen korrekt.\",\r\n    \"email_error\" => \"E-Mail konnte nicht gesendet werden\",\r\n    \"notification_sent_successfuly\" => \"Benachrichtigung erfolgreich gesendet\",\r\n    \"notifications_settings_saved\" => \"Benachrichtigungseinstellungen erfolgreich gespeichert.\",\r\n    \"notification_failed\" => \"Benachrichtigung fehlgeschlagen\",\r\n    // Payments\r\n    \"payment_in_use\" => \"Genutzte Zahlungsmethoden können nicht deaktiviert werden\",\r\n    \"failed_update_payment\" => \"Zahlungsmethode in Datenbank konnte nicht aktualisiert werden\",\r\n    \"enabled\" => \"aktiviert\",\r\n    \"disabled\" => \"deaktiviert\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Fehler beim Laden des Bildes\",\r\n    \"subscription_updated_successfuly\" => \"Abonnement erfolgreich aktualisiert\",\r\n    \"subscription_added_successfuly\" => \"Abonnement erfolgreich hinzugefügt\",\r\n    \"error_deleting_subscription\" => \"Abonnement konnte nicht gelöscht werden.\",\r\n    \"invalid_request_method\" => \"Ungültige Request Methode.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Benutzerangaben konnten nicht aktualisiert werden.\",\r\n    \"user_details_saved\" => \"Benutzerangaben gespeichert\",\r\n    // Admin Page\r\n    \"registrations\" => \"Registrierungen\",\r\n    \"enable_user_registrations\" => \"Benutzerregistrierungen aktivieren\",\r\n    \"maximum_number_users\" => \"Maximale Anzahl an Benutzern\",\r\n    \"require_email_verification\" => \"E-Mail Verifizierung erforderlich\",\r\n    \"configure_smtp_settings_to_enable\" => \"Konfiguriere SMTP Einstellungen um dies zu aktivieren\",\r\n    \"server_url\" => \"Server URL\",\r\n    \"server_url_info\" => \"Wird für die E-Mail-Überprüfung und die Passwortwiederherstellung verwendet. Muss eine gültige öffentliche URL sein.\",\r\n    \"server_url_password_reset\" => \"Wenn diese Option gesetzt ist, wird auch die Funktion zum Zurücksetzen des Passworts aktiviert.\",\r\n    \"disable_login\" => \"Login deaktivieren\",\r\n    \"disable_login_info\" => \"Anmeldung umgehen. Wenn Sie Ihren Server nur in einem lokalen Netzwerk betreiben, ohne Zugriff von außen, können Sie die Anmeldung deaktivieren. Dadurch wird automatisch der Admin-Benutzer angemeldet.\",\r\n    \"disable_login_info2\" => \"Sie können diese Einstellung nur aktivieren, wenn die Benutzerregistrierung ausgeschaltet ist und es nicht mehr als ein Admin-Benutzerkonto gibt.\",\r\n    \"max_users_info\" => \"0 für unbegrenzte Anzahl an Benutzern\",\r\n    \"user_management\" => \"Benutzerverwaltung\",\r\n    \"delete_user\" => \"Benutzer löschen\",\r\n    \"delete_user_info\" => \"Durch das Löschen eines Benutzers werden auch alle seine Abonnements und Einstellungen gelöscht.\",\r\n    \"create_user\" => \"Benutzer erstellen\",\r\n    \"oidc_settings\" => \"OIDC Einstellungen\",\r\n    \"oidc_oauth_enabled\" => \"OIDC/OAuth aktivieren\",\r\n    \"create_user_automatically\" => \"Benutzer automatisch erstellen\",\r\n    \"disable_password_login\" => \"Passwort-Login deaktivieren\",\r\n    \"smtp_settings\" => \"SMTP Einstellungen\",\r\n    \"smtp_usage_info\" => \"Wird für die Passwortwiederherstellung und andere System-E-Mails verwendet\",\r\n    \"security_settings\" => \"Sicherheitseinstellungen\",\r\n    \"ssrf_protection_info\" => \"Um Server-Side Request Forgery (SSRF)-Angriffe zu verhindern, blockiert Wallos standardmäßig Webhook-Benachrichtigungen an private oder interne Netzwerkadressen.\",\r\n    \"local_webhook_info\" => \"Wenn Sie Webhooks an lokale Dienste (z. B. Home Assistant, Gotify oder Node-RED) senden müssen, geben Sie deren IP-Adressen oder Hostnamen oben als durch Kommas getrennte Liste ein (z. B. <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"maintenance_tasks\" => \"Wartungsaufgaben\",\r\n    \"orphaned_logos\" => \"Verwaiste Logos\",\r\n    \"update\" => \"Update\",\r\n    \"new_version_available\" => \"Eine neue Version von Wallos ist verfügbar\",\r\n    \"current_version\" => \"Aktuelle Version\",\r\n    \"latest_version\" => \"Neueste Version\",\r\n    \"on_current_version\" => \"Sie verwenden die neueste Version von Wallos.\",\r\n    \"show_update_notification\" => \"Benachrichtigung über Updates auf dem Dashboard anzeigen\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"E-Mail verifiziert\",\r\n    \"email_verification_failed\" => \"E-Mail konnte nicht verifiziert werden\",\r\n    // Calendar\r\n    \"calendar\" => \"Kalender\",\r\n    \"sun\" => \"So\",\r\n    \"mon\" => \"Mo\",\r\n    \"tue\" => \"Di\",\r\n    \"wed\" => \"Mi\",\r\n    \"thu\" => \"Do\",\r\n    \"fri\" => \"Fr\",\r\n    \"sat\" => \"Sa\",\r\n    \"month-01\" => \"Januar\",\r\n    \"month-02\" => \"Februar\",\r\n    \"month-03\" => \"März\",\r\n    \"month-04\" => \"April\",\r\n    \"month-05\" => \"Mai\",\r\n    \"month-06\" => \"Juni\",\r\n    \"month-07\" => \"Juli\",\r\n    \"month-08\" => \"August\",\r\n    \"month-09\" => \"September\",\r\n    \"month-10\" => \"Oktober\",\r\n    \"month-11\" => \"November\",\r\n    \"month-12\" => \"Dezember\",\r\n    \"total_cost\" => \"Gesamtkosten\",\r\n    \"export_icalendar\" => \"iCalendar exportieren\",\r\n    \"over_budget_warning\" => \"Sie haben Ihr Budget überschritten\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Bitte geben Sie den TOTP-Code ein\",\r\n\r\n];\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/el.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"Πρέπει να δημιουργήσεις έναν λογαριασμό για να μπορέσεις να συνδεθείς.\",\r\n    \"username\" => \"Όνομα χρήστη\",\r\n    \"password\" => \"Κωδικός\",\r\n    \"email\" => \"Email\",\r\n    \"firstname\" => \"Ονομα\",\r\n    \"lastname\" => \"Επώνυμο\",\r\n    \"confirm_password\" => \"Επιβεβαίωση κωδικού\",\r\n    \"main_currency\" => \"Κύριο νόμισμα\",\r\n    \"language\" => \"Γλώσσα\",\r\n    \"passwords_dont_match\" => \"Οι κωδικοί πρόσβασης δεν ταιριάζουν\",\r\n    \"username_exists\" => \"Το όνομα χρήστη υπάρχει ήδη\",\r\n    \"email_exists\" => \"Το email υπάρχει ήδη\",\r\n    \"registration_failed\" => \"Η εγγραφή απέτυχε, παρακαλώ προσπάθησε ξανά.\",\r\n    \"register\" => \"Εγγραφή\",\r\n    \"restore_database\" => \"Επαναφορά βάσης δεδομένων\",\r\n    // Login Page\r\n    \"please_login\" => \"Παρακαλώ συνδέσου\",\r\n    \"stay_logged_in\" => \"Μείνε συνδεδεμένος (30 ημέρες)\",\r\n    \"login\" => \"Σύνδεση\",\r\n    \"login_with\" => \"Σύνδεση με\",\r\n    \"or\" => \"ή\",\r\n    \"login_failed\" => \"Τα στοιχεία σύνδεσης είναι λανθασμένα\",\r\n    \"registration_successful\" => \"Επιτυχής Εγγραφή\",\r\n    \"user_email_waiting_verification\" => \"Το email σας πρέπει να επαληθευτεί. Παρακαλούμε ελέγξτε το email σας\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Ξέχασες τον κωδικό σου; Κάνε κλικ\",\r\n    \"reset_password\" => \"Επαναφορά κωδικού πρόσβασης\",\r\n    \"reset_sent_check_email\" => \"Ένα email με οδηγίες για την επαναφορά του κωδικού πρόσβασης σας έχει σταλεί. Παρακαλώ ελέγξτε το email σας.\",\r\n    \"password_reset_successful\" => \"Επιτυχής επαναφορά κωδικού πρόσβασης\",\r\n    // Header\r\n    \"profile\" => \"Προφίλ\",\r\n    \"dashboard\" => \"Πίνακας\",\r\n    \"subscriptions\" => \"Συνδρομές\",\r\n    \"stats\" => \"Στατιστικές\",\r\n    \"settings\" => \"Ρυθμίσεις\",\r\n    \"admin\" => \"Διαχείριση\",\r\n    \"about\" => \"Για εμάς\",\r\n    \"logout\" => \"Αποσύνδεση\",\r\n    // Dashboard\r\n    \"hello\" => \"Γειά σου\",\r\n    \"upcoming_payments\" => \"Επερχόμενες Πληρωμές\",\r\n    \"no_upcoming_payments\" => \"Δεν έχετε καμία επερχόμενη πληρωμή\",\r\n    \"overdue_renewals\" => \"Καθυστερημένες Ανανεώσεις\",\r\n    \"ai_recommendations\" => \"Συστάσεις AI\",\r\n    \"your_budget\" => \"Ο Προϋπολογισμός σας\",\r\n    \"budget\" => \"Προϋπολογισμός\",\r\n    \"budget_used\" => \"Προϋπολογισμός Χρησιμοποιημένος\",\r\n    \"over_budget\" => \"Πάνω από τον Προϋπολογισμό\",\r\n    \"your_subscriptions\" => \"Οι Συνδρομές σας\",\r\n    \"your_savings\" => \"Οι Εξοικονομήσεις σας\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Συνδρομή\",\r\n    \"no_subscriptions_yet\" => \"Δεν υπάρχουν καταχωρημένες συνδρομές\",\r\n    \"add_first_subscription\" => \"Προσθήκη πρώτης συνδρομής\",\r\n    \"new_subscription\" => \"Νέα συνδρομή\",\r\n    \"search\" => \"Αναζήτηση\",\r\n    \"state\" => \"Κατάσταση\",\r\n    \"alphanumeric\" => \"Αλφαριθμητική\",\r\n    \"sort\" => \"Ταξινόμηση\",\r\n    \"name\" => \"Όνομα\",\r\n    \"last_added\" => \"Τελευταία προσθήκη\",\r\n    \"price\" => \"Τιμή\",\r\n    \"next_payment\" => \"Επόμενη πληρωμή\",\r\n    \"renewal_type\" => \"Τύπος ανανέωσης\",\r\n    \"auto_renewal\" => \"Αυτόματη ανανέωση\",\r\n    \"automatically_renews\" => \"Ανανεώνεται αυτόματα\",\r\n    \"manual_renewal\" => \"Χειροκίνητη ανανέωση\",\r\n    \"start_date\" => \"Ημερομηνία έναρξης\",\r\n    \"inactive\" => \"Απενεργοποίηση συνδρομής\",\r\n    \"replaced_with\" => \"Αντικαταστάθηκε με\",\r\n    \"none\" =>  \"Κανένα\",\r\n    \"member\" => \"Χρήστης\",\r\n    \"category\" => \"Κατηγορία\",\r\n    \"payment_method\" => \"Τρόπος πληρωμής\",\r\n    \"Daily\" => \"Καθημερινή\",\r\n    \"Weekly\" => \"Εβδομαδιαία\",\r\n    \"Monthly\" => \"Μηνιαία\",\r\n    \"Yearly\" => \"Ετήσια\",\r\n    \"dayly\" => \"Ημέρα(ες)\",\r\n    \"weekly\" => \"Εβδομάδα\",\r\n    \"monthly\" => \"Μήνας(ες)\",\r\n    \"yearly\" => \"Χρόνος(ια)\",\r\n    \"days\" => \"ημέρες\",\r\n    \"weeks\" => \"εβδομάδες\",\r\n    \"months\" => \"μήνες\",\r\n    \"years\" => \"χρόνια\",\r\n    \"external_url\" => \"Επίσκεψη εξωτερικού συνδέσμου\",\r\n    \"empty_page\" => \"Κενή σελίδα\",\r\n    \"clear_filters\" => \"Καθαρισμός φίλτρων\",\r\n    \"no_matching_subscriptions\" => \"Δεν υπάρχουν συνδρομές που ταιριάζουν με τα φίλτρα σου\",\r\n    \"clone\" => \"Κλώνος\",\r\n    \"renew\" => \"Ανανέωση\",\r\n    \"calculate_next_payment_date\" => \"Υπολογισμός ημερομηνίας επόμενης πληρωμής\",\r\n    // Subscription form\r\n    \"add_subscription\" => \"Προσθήκη συνδρομής\",\r\n    \"edit_subscription\" => \"Επεξεργασία συνδρομής\",\r\n    \"subscription_name\" => \"Όνομα συνδρομής\",\r\n    \"logo_preview\" => \"Προεπισκόπηση λογότυπου\",\r\n    \"search_logo\" => \"Αναζήτηση λογότυπου στο web\",\r\n    \"web_search\" => \"Αναζήτηση web\",\r\n    \"currency\" => \"Νόμισμα\",\r\n    \"payment_every\" => \"Πληρωμή κάθε\",\r\n    \"frequency\" => \"Συχνότητα\",\r\n    \"cycle\" => \"Κύκλος\",\r\n    \"no_category\" => \"Καμία κατηγορία\",\r\n    \"paid_by\" => \"Πληρώνεται από\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Σημειώσεις\",\r\n    \"enable_notifications\" => \"Ενεργοποίηση ειδοποιήσεων για αυτή τη συνδρομή\",\r\n    \"default_value_from_settings\" => \"Προεπιλεγμένη τιμή από τις ρυθμίσεις\",\r\n    \"cancellation_notification\" => \"Ειδοποίηση ακύρωσης\",\r\n    \"delete\" => \"Διαγραφή\",\r\n    \"cancel\" => \"Ακύρωση\",\r\n    \"upload_logo\" => \"Φόρτωση λογότυπου\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"Χρησιμοποιείτε πολλαπλά νομίσματα στις συνδρομές σας. Για να έχετε έγκυρα και ακριβή στατιστικά στοιχεία, παρακαλούμε ορίστε ένα κλειδί API Fixer στη σελίδα ρυθμίσεων.\",\r\n    \"general_statistics\" => \"Γενικές στατιστικές\",\r\n    \"active_subscriptions\" => \"Ενεργές συνδρομές\",\r\n    \"inactive_subscriptions\" => \"Ανενεργές συνδρομές\",\r\n    \"monthly_cost\" => \"Μηνιαίο κόστος\",\r\n    \"yearly_cost\" => \"Ετήσιο κόστος\",\r\n    \"average_monthly\" => \"Μέσο μηνιαίο κόστος συνδρομής\",\r\n    \"most_expensive\" => \"Πιο ακριβό κόστος συνδρομής\",\r\n    \"amount_due\" => \"Ποσό που οφείλεται αυτόν τον μήνα\",\r\n    \"percentage_budget_used\" => \"Ποσοστό προϋπολογισμού που χρησιμοποιείται\",\r\n    \"budget_remaining\" => \"Υπόλοιπο προϋπολογισμού\",\r\n    \"amount_over_budget\" => \"Ποσό πάνω από τον προϋπολογισμό\",\r\n    \"monthly_savings\" => \"Μηνιαίες εξοικονομήσεις (σε ανενεργές συνδρομές)\",\r\n    \"yearly_savings\" => \"Ετήσιες εξοικονομήσεις (σε ανενεργές συνδρομές)\",\r\n    \"split_views\" => \"Διαχωρισμένες προβολές\",\r\n    \"category_split\" => \"Διαχωρισμός κατηγορίας\",\r\n    \"household_split\" => \"Διαχωρισμός νοικοκυριού\",\r\n    \"payment_method_split\" => \"Διαχωρισμός τρόπου πληρωμής\",\r\n    \"total_cost_trend\" => \"Τάση συνολικού κόστους\",\r\n    \"cost_vs_budget\" => \"Κόστος έναντι προϋπολογισμού\",\r\n    // About page\r\n    \"about_and_credits\" => \"Σχετικά και Credits\",\r\n    \"credits\" => \"Credits\",\r\n    \"license\" => \"License\",\r\n    \"release_notes\" => \"Σημειώσεις έκδοσης\",\r\n    \"update_available\" => \"Διαθέσιμη ενημέρωση\",\r\n    \"issues_and_requests\" => \"Προβλήματα και αιτήσεις\",\r\n    \"the_author\" => \"Προγραμματιστής\",\r\n    \"icons\" => \"Εικονίδια\",\r\n    \"payment_icons\" => \"Εικονίδια Payment\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"μεταφόρτωση άβαταρ\",\r\n    \"file_type_error\" => \"Το αρχείο πρέπει να είναι τύπου jpg, jpeg, png, webp ή gif\",\r\n    \"user_details\" => \"Λεπτομέρειες χρήστη\",\r\n    \"two_factor_authentication\" => \"Διπλής πιστοποίησης\",\r\n    \"two_factor_info\" => \"Ο έλεγχος ταυτότητας δύο παραγόντων προσθέτει ένα επιπλέον επίπεδο ασφάλειας στο λογαριασμό σας.<br>Θα χρειαστείτε μια εφαρμογή ελέγχου ταυτότητας όπως το Google Authenticator, το Authy ή το Ente Auth για να σαρώσετε τον κωδικό QR.\",\r\n    \"two_factor_enabled_info\" => \"Ο λογαριασμός σας είναι ασφαλής με τον έλεγχο ταυτότητας δύο παραγόντων. Μπορείτε να τον απενεργοποιήσετε κάνοντας κλικ στο κουμπί παραπάνω.\",\r\n    \"enable_two_factor_authentication\" => \"Ενεργοποίηση διπλής πιστοποίησης\",\r\n    \"2fa_already_enabled\" => \"Ο έλεγχος ταυτότητας δύο παραγόντων είναι ήδη ενεργοποιημένος\",\r\n    \"totp_code_incorrect\" => \"Ο κωδικός TOTP είναι εσφαλμένος\",\r\n    \"backup_codes\" => \"Κωδικοί ανάκτησης\",\r\n    \"download_backup_codes\" => \"Κατέβασε τους κωδικούς ανάκτησης\",\r\n    \"copy_to_clipboard\" => \"Αντιγραφή στο πρόχειρο\",\r\n    \"totp_backup_codes_info\" => \"Αποθηκεύστε αυτούς τους κωδικούς ανάκτησης σε ένα ασφαλές μέρος. Θα χρειαστείτε έναν από αυτούς τους κωδικούς ανάκτησης για να αποκτήσετε πρόσβαση στο λογαριασμό σας σε περίπτωση που χάσετε τη συσκευή σας.\",\r\n    \"disable_two_factor_authentication\" => \"Απενεργοποίηση διπλής πιστοποίησης\",\r\n    \"totp_code\" => \"Κωδικός TOTP\",\r\n    \"api_key\" => \"API κλειδί\",\r\n    \"regenerate\" => \"Επαναδημιουργία\",\r\n    \"api_key_info\" => \"Το API κλειδί χρησιμοποιείται για την επικοινωνία με το API του Wallos. Μην αποκαλύπτετε το API κλειδί σας σε κανέναν.\",\r\n    // Settings page\r\n    \"monthly_budget\" => \"Μηνιαίος προϋπολογισμός\",\r\n    \"budget_info\" => \"Ο μηνιαίος προϋπολογισμός χρησιμοποιείται για τον υπολογισμό των στατιστικών\",\r\n    \"household\" => \"Νοικοκυριό\",\r\n    \"save_member\" => \"Αποθήκευση μέλους\",\r\n    \"delete_member\" => \"Διαγραφή μέλους\",\r\n    \"cant_delete_member\" => \"Δεν ειναι δυνατή η διαγραφή του βασικού μέλους\",\r\n    \"cant_delete_member_in_use\" => \"Δεν ειναι δυνατή η διαγραφή μέλους που χρησιμοποιείται\",\r\n    \"household_info\" => \"Το πεδίο ηλεκτρονικού ταχυδρομείου επιτρέπει στα μέλη του νοικοκυριού να ειδοποιούνται για συνδρομές που πρόκειται να λήξουν.\",\r\n    \"notifications\" => \"Ειδοποιήσεις\",\r\n    \"enable_email_notifications\" => \"Ενεργοποίηση ειδοποιήσεων με email\",\r\n    \"notify_me\" => \"Ειδοποίησε με\",\r\n    \"day_before\" => \"ημέρα πριν\",\r\n    \"on_due_date\" => \"την ημερομηνία λήξης\",\r\n    \"days_before\" => \"ημέρες πριν\",\r\n    \"smtp_address\" => \"SMTP Address\",\r\n    \"port\" => \"Θύρα\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"SMTP χρήστης\",\r\n    \"smtp_password\" => \"SMTP κωδικός\",\r\n    \"from_email\" => \"Από (Προαιρετικό)\",\r\n    \"send_to_other_emails\" => \"Επίσης στείλτε ειδοποιήσεις στις ακόλουθες διευθύνσεις email (χρησιμοποιήστε ; για να τις διαχωρίσετε):\",\r\n    \"smtp_info\" => \"Ο κωδικός πρόσβασης SMTP μεταδίδεται και αποθηκεύεται σε απλό κείμενο. Για λόγους ασφαλείας, παρακαλούμε δημιούργησε έναν λογαριασμό μόνο γι' αυτό το σκοπό.\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Τηλεγραφήματα Bot Token\",\r\n    \"telegram_chat_id\" => \"Τηλεγραφήματα Chat ID\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Pushplus Token\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"Mattermost WebHook URL\",\r\n    \"mattermost_bot_username\" => \"Mattermost Bot Όνομα χρήστη\",\r\n    \"mattermost_bot_icon_emoji\" => \"Emoji εικονιδίου bot\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"Webhook URL\",\r\n    \"request_method\" => \"Μέθοδος αίτησης\",\r\n    \"custom_headers\" => \"Προσαρμοσμένες κεφαλίδες\",\r\n    \"webhook_payload\" => \"Webhook Payload\",\r\n    \"payment_notifications_payload\" => \"Ειδοποίηση πληρωμής Payload\",\r\n    \"cancelation_notification_payload\" => \"Ακύρωση ειδοποίησης Payload\",\r\n    \"variables_available\" => \"Διαθέσιμες μεταβλητές\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Discord Bot Username\",\r\n    \"discord_bot_avatar_url\" => \"Discord Bot Avatar URL\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Pushover User Key\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Θέμα\",\r\n    \"ignore_ssl_errors\" => \"Αγνόηση σφαλμάτων SSL\",\r\n    \"categories\" => \"Κατηγορίες\",\r\n    \"save_category\" => \"Αποθήκευση κατηγορίας\",\r\n    \"delete_category\" => \"Διαγραφή κατηγορίας\",\r\n    \"cant_delete_category_in_use\" => \"Δεν ειναι δυνατή η διαγραφή κατηγορίας που χρησιμοποιείται\",\r\n    \"currencies\" => \"Νομίσματα\",\r\n    \"save_currency\" => \"Αποθήκευση νομίσματος\",\r\n    \"delete_currency\" => \"Διαγραφή νομίσματος\",\r\n    \"cant_delete_main_currency\" => \"Δεν ειναι δυνατή η διαγραφή βασικού νομίσματος\",\r\n    \"cant_delete_currency_in_use\" => \"Δεν ειναι δυνατή η διαγραφή νομίσματος που χρησιμοποιείται\",\r\n    \"exchange_update\" => \"Τελευταία ενημέρωση συναλλαγματικών ισοτιμίων\",\r\n    \"currency_info\" => \"Βρες τα υποστηριζόμενα νομίσματα και τους σωστούς κωδικούς νομίσματος στο\",\r\n    \"currency_performance\" => \"Για βελτιωμένη απόδοση κράτησε μόνο τα νομίσματα που χρησιμοποιείς.\",\r\n    \"fixer_api_key\" => \"Fixer API κλειδί\",\r\n    \"provider\" => \"Πάροχος\",\r\n    \"fixer_info\" => \"Εάν χρησιμοποιείς πολλαπλά νομίσματα και θέλεις ακριβή στατιστικά στοιχεία και ταξινόμηση των συνδρομών, είναι απαραίτητο ένα ΔΩΡΕΑΝ κλειδί API από το Fixer.\",\r\n    \"get_key\" => \"Απόκτησε το κλειδί στο\",\r\n    \"get_free_fixer_api_key\" => \"Απόκτησε ΔΩΡΕΑΝ Fixer API κλειδί\",\r\n    \"get_key_alternative\" => \"Εναλλακτικά, μπορείτε να λάβετε ένα δωρεάν κλειδί api fixer από το\",\r\n    \"ai_model\" => \"AI Μοντέλο\",\r\n    \"select_ai_model\" => \"Επιλέξτε AI Μοντέλο\",\r\n    \"run_schedule\" => \"Εκτέλεση προγράμματος\",\r\n    \"manually\" => \"Χειροκίνητα\",\r\n    \"coming_soon\" => \"Έρχεται σύντομα\",\r\n    \"invalid_host\" => \"Μη έγκυρος διακομιστής\",\r\n    \"ai_recommendations_info\" => \"AI προτάσεις δημιουργούνται με βάση τα συνδρομητικά σας και τα μέλη του νοικοκυριού σας.\",\r\n    \"may_take_time\" => \"Ανάλογα με τον πάροχο, το μοντέλο και τον αριθμό των συνδρομών, η δημιουργία προτάσεων μπορεί να διαρκέσει κάποιο χρόνο.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Οι προτάσεις θα είναι ορατές στον πίνακα ελέγχου.\",\r\n    \"generate_recommendations\" => \"Δημιουργία προτάσεων\",\r\n    \"display_settings\" => \"Ρυθμίσεις εμφάνισης\",\r\n    \"theme_settings\" => \"Ρυθμίσεις θέματος\",\r\n    \"colors\" => \"Χρώματα\",\r\n    \"custom_colors\" => \"Προσαρμοσμένα χρώματα\",\r\n    \"theme\" => \"Θέμα\",\r\n    \"dark_theme\" => \"Dark Theme\",\r\n    \"light_theme\" => \"Light Theme\",\r\n    \"automatic\" => \"Αυτόματο\",\r\n    \"main_color\" => \"Κύριο χρώμα\",\r\n    \"accent_color\" => \"Χρώμα επισήμανσης\",\r\n    \"hover_color\" => \"Χρώμα πάνω από\",\r\n    \"save_custom_colors\" => \"Αποθήκευση προσαρμοσμένων χρωμάτων\",\r\n    \"reset_custom_colors\" => \"Επαναφορά προεπιλεγμένων χρωμάτων\",\r\n    \"custom_css\" => \"Προσαρμοσμένο CSS\",\r\n    \"save_custom_css\" => \"Αποθήκευση προσαρμοσμένου CSS\",\r\n    \"calculate_monthly_price\" => \"Υπολογισμός και εμφάνιση της μηνιαίας τιμής για όλες τις συνδρομές\",\r\n    \"convert_prices\" => \"Πάντα να μετατρέπει και να εμφανίζει τις τιμές στο κύριο νόμισμά μου (πιο αργό)\",\r\n    \"show_original_price\" => \"Εμφάνιση της αρχικής τιμής όταν γίνονται μετατροπές ή υπολογισμοί\",\r\n    \"experience\" => \"Εμπειρία\",\r\n    \"show_subscription_progress\" => \"Εμφάνιση προόδου συνδρομής\",\r\n    \"disabled_subscriptions\" => \"Απενεργοποιημένες συνδρομές\",\r\n    \"hide_disabled_subscriptions\" => \"Απόκρυψη απενεργοποιημένων συνδρομών\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Εμφάνιση απενεργοποιημένων συνδρομών στο τέλος\",\r\n    \"experimental_settings\" => \"Πειραματικές ρυθμίσεις\",\r\n    \"remove_background\" => \"Προσπάθεια αφαίρεσης του φόντου των λογότυπων από την αναζήτηση εικόνας\",\r\n    \"use_mobile_navigation_bar\" => \"Χρήση μπάρας πλοήγησης για κινητά\",\r\n    \"experimental_info\" => \"Οι πειραματικές ρυθμίσεις πιθανότατα δεν θα λειτουργούν τέλεια.\",\r\n    \"payment_methods\" => \"Τρόποι πληρωμής\",\r\n    \"payment_methods_info\" => \"Κάνε κλικ σε μια μέθοδο πληρωμής για να την απενεργοποιήσεις/ενεργοποιήσεις.\",\r\n    \"rename_payment_methods_info\" => \"Κάντε κλικ στο όνομα μιας μεθόδου πληρωμής για να τη μετονομάσετε.\",\r\n    \"cant_delete_payment_method_in_use\" => \"Δεν είναι εφικτό να απενεργοποιηθεί η χρησιμοποιούμενη μέθοδο πληρωμής\",\r\n    \"add_custom_payment\" => \"Προσθήκη προσαρμοσμένης μεθόδου πληρωμής\",\r\n    \"payment_method_name\" => \"Όνομα μεθόδου πληρωμής\",\r\n    \"payment_method_added_successfuly\" => \"Η μέθοδος πληρωμής προστέθηκε με επιτυχία\",\r\n    \"payment_method_removed\" => \"Η μέθοδος πληρωμής αφαιρέθηκε\",\r\n    \"disable\" => \"Ανενεργό\",\r\n    \"enable\" => \"Ενεργό\",\r\n    \"rename_payment_method\" => \"Μετονομασία μεθόδου πληρωμής\",\r\n    \"payment_renamed\" => \"Η μέθοδος πληρωμής μετονομάστηκε\",\r\n    \"payment_not_renamed\" => \"Η μέθοδος πληρωμής δεν μετονομάστηκε\",\r\n    \"test\" => \"Δοκιμή\",\r\n    \"add\" => \"Προσθήκη\",\r\n    \"save\" => \"Αποθήκευση\",\r\n    \"reset\" => \"Επαναφορά\",\r\n    \"main_accent_color_error\" => \"Το κύριο χρώμα δεν μπορεί να είναι το ίδιο με το χρώμα επισήμανσης\",\r\n    \"backup_and_restore\" => \"Αντίγραφο ασφαλείας και επαναφορά\",\r\n    \"backup\" => \"Αντίγραφο ασφαλείας\",\r\n    \"restore\" => \"Επαναφορά\",\r\n    \"restore_info\" => \"Η επαναφορά της βάσης δεδομένων θα ακυρώσει όλα τα τρέχοντα δεδομένα. Μετά την επαναφορά θα αποσυνδεθείτε.\",\r\n    \"account\" => \"Λογαριασμός\",\r\n    \"export_subscriptions\" => \"Εξαγωγή συνδρομών\",\r\n    \"export_as_json\" => \"Εξαγωγή ως JSON\",\r\n    \"export_as_csv\" => \"Εξαγωγή ως CSV\",\r\n    \"danger_zone\" => \"Ζώνη κινδύνου\",\r\n    \"delete_account\" => \"Διαγραφή λογαριασμού\",\r\n    \"delete_account_info\" => \"Η διαγραφή του λογαριασμού θα ακυρώσει όλα τα δεδομένα και θα αποσυνδεθείτε. Αυτή η ενέργεια είναι μη αναστρέψιμη.\",\r\n    // Filters menu\r\n    \"filter\" => \"Φίλτρο\",\r\n    \"clear\" => \"Καθαρισμός\",\r\n    // Toast\r\n    \"success\" => \"Επιτυχία\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"Η συνεδρία σου έληξε. Παρακαλώ συνδέσου ξανά\",\r\n    \"fields_missing\" => \"Some fields are missing\",\r\n    \"fill_all_fields\" => \"Παρακαλώ συμπλήρωσε όλα τα πεδία\",\r\n    \"fill_mandatory_fields\" => \"Παρακαλώ συμπλήρωσε όλα τα  υποχρεωτικά πεδία\",\r\n    \"error\" => \"Σφάλμα\",\r\n    // Category\r\n    \"failed_add_category\" => \"Απέτυχε η προσθήκη κατηγορίας\",\r\n    \"failed_edit_category\" => \"Απέτυχε η επεξεργασία κατηγορίας\",\r\n    \"category_in_use\" => \"Η κατηγορία χρησιμοποιείται στις συνδρομές και δεν μπορεί να αφαιρεθεί\",\r\n    \"failed_remove_category\" => \"Απέτυχε η διαγραφή κατηγορίας\",\r\n    \"category_saved\" => \"Αποθήκευση κατηγορίας\",\r\n    \"category_removed\" => \"Διαγραφή κατηγορίας\",\r\n    \"sort_order_saved\" => \"Η ταξινόμηση αποθηκεύτηκε\",\r\n    // Currency\r\n    \"currency_saved\" => \"αποθηκεύτηκε.\",\r\n    \"error_adding_currency\" => \"Error adding currency entry.\",\r\n    \"failed_to_store_currency\" => \"Failed to store Currency on the Database.\",\r\n    \"currency_in_use\" => \"Currency is in use in subscriptions and can't be deleted.\",\r\n    \"currency_is_main\" => \"Currency is set as main currency and can't be deleted.\",\r\n    \"failed_to_remove_currency\" => \"Failed to remove currency from the Database.\",\r\n    \"failed_to_store_api_key\" => \"Failed to store API Key on the Database.\",\r\n    \"invalid_api_key\" => \"Invalid API Key.\",\r\n    \"api_key_saved\" => \"API key saved successfully\",\r\n    \"currency_removed\" => \"Currency removed\",\r\n    // Household\r\n    \"failed_add_household\" => \"Η πρόσθεση μέλους απέτυχε\",\r\n    \"failed_edit_household\" => \"Η επεξεργασία μέλους απέτυχε\",\r\n    \"failed_remove_household\" => \"Η διαγραφή μέλους απέτυχε\",\r\n    \"household_in_use\" => \"Το μέλος χρησιμοποιείται σε συνδρομές και δεν μπορεί να αφαιρεθεί\",\r\n    \"member_saved\" => \"Αποθήκευση μέλους\",\r\n    \"member_removed\" => \"Διαγραφή μέλους\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Σφάλμα αποθήκευσης δεδομένων ειδοποιήσεων.\",\r\n    \"wallos_notification\" => \"Ειδοποίηση Wallos\",\r\n    \"test_notification\" => \"Πρόκειται για δοκιμαστική ειδοποίηση. Αν το βλέπεις αυτό, η ρύθμιση είναι σωστή.\",\r\n    \"email_error\" => \"Σφάλμα αποστολής email\",\r\n    \"notification_sent_successfuly\" => \"Η ειδοποίηση εστάλη επιτυχώς\",\r\n    \"notifications_settings_saved\" => \"Οι ρυθμίσεις ειδοποίησης αποθηκεύτηκαν με επιτυχία.\",\r\n    \"notification_failed\" => \"Η ειδοποίηση απέτυχε\",\r\n    // Payments\r\n    \"payment_in_use\" => \"Δεν είναι εφικτό να απενεργοποιηθεί η χρησιμοποιούμενη μέθοδο πληρωμής\",\r\n    \"failed_update_payment\" => \"Απέτυχε η ενημέρωση της μεθόδου πληρωμής στη βάση δεδομένων\",\r\n    \"enabled\" => \"ενεργοποιημένο\",\r\n    \"disabled\" => \"απενεργοποιημένο\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Σφάλμα λήψης εικόνας\",\r\n    \"subscription_updated_successfuly\" => \"Η συνδρομή ενημερώθηκε επιτυχώς\",\r\n    \"subscription_added_successfuly\" => \"Η συνδρομή προστέθηκε με επιτυχία\",\r\n    \"error_deleting_subscription\" => \"Σφάλμα διαγραφής συνδρομής.\",\r\n    \"invalid_request_method\" => \"Μη έγκυρη μέθοδος αιτήματος.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Σφάλμα ενημέρωσης δεδομένων χρήστη.\",\r\n    \"user_details_saved\" => \"Αποθήκευση στοιχείων χρήστη\",\r\n    // Admin Page\r\n    \"registrations\" => \"Εγγραφές\",\r\n    \"enable_user_registrations\" => \"Ενεργοποίηση εγγραφών χρηστών\",\r\n    \"maximum_number_users\" => \"Μέγιστος αριθμός χρηστών\",\r\n    \"require_email_verification\" => \"Απαιτείται επιβεβαίωση email\",\r\n    \"configure_smtp_settings_to_enable\" => \"Διαμόρφωσε τις ρυθμίσεις SMTP για να ενεργοποιήσεις αυτή την επιλογή\",\r\n    \"server_url\" => \"Διεύθυνση URL διακομιστή\",\r\n    \"server_url_info\" => \"Χρησιμοποιείται για επαλήθευση email και ανάκτηση κωδικού πρόσβασης. Πρέπει να είναι ένα έγκυρο δημόσιο URL.\",\r\n    \"server_url_password_reset\" => \"Εάν οριστεί, θα ενεργοποιήσει επίσης τη λειτουργία επαναφοράς κωδικού πρόσβασης.\",\r\n    \"disable_login\" => \"Απενεργοποίηση σύνδεσης\",\r\n    \"disable_login_info\" => \"Παράκαμψη σύνδεσης. Εάν εκτελείτε το διακομιστή σας μόνο σε τοπικό δίκτυο, χωρίς εξωτερική πρόσβαση, μπορείτε να απενεργοποιήσετε τη σύνδεση. Αυτό θα πραγματοποιήσει αυτόματα την είσοδο του χρήστη διαχειριστή.\",\r\n    \"disable_login_info2\" => \"Μπορείτε να ενεργοποιήσετε αυτή τη ρύθμιση μόνο εάν η εγγραφή χρηστών είναι απενεργοποιημένη και δεν υπάρχουν περισσότεροι από το λογαριασμό χρήστη διαχειριστή.\",\r\n    \"max_users_info\" => \"Ο μέγιστος αριθμός χρηστών που μπορούν να εγγραφούν. Αν η τιμή είναι 0, δεν υπάρχει όριο.\",\r\n    \"user_management\" => \"Διαχείριση χρηστών\",\r\n    \"delete_user\" => \"Διαγραφή χρήστη\",\r\n    \"delete_user_info\" => \"Η διαγραφή ενός χρήστη θα διαγράψει επίσης όλες τις συνδρομές και τις ρυθμίσεις του.\",\r\n    \"create_user\" => \"Δημιουργία χρήστη\",\r\n    \"oidc_settings\" => \"Ρυθμίσεις OIDC\",\r\n    \"oidc_oauth_enabled\" => \"Ενεργοποίηση OIDC/OAuth\",\r\n    \"create_user_automatically\" => \"Δημιουργία χρήστη αυτόματα\",\r\n    \"disable_password_login\" => \"Απενεργοποίηση σύνδεσης με κωδικό πρόσβασης\",\r\n    \"smtp_settings\" => \"SMTP ρυθμίσεις\",\r\n    \"security_settings\" => \"Ρυθμίσεις ασφάλειας\",\r\n    \"ssrf_protection_info\" => \"Για να αποτρέψετε επιθέσεις Server-Side Request Forgery (SSRF), το Wallos αποκλείει από προεπιλογή τις ειδοποιήσεις webhook σε ιδιωτικές ή εσωτερικές διευθύνσεις δικτύου.\",\r\n    \"local_webhook_info\" => \"Εάν πρέπει να στείλετε webhooks σε τοπικές υπηρεσίες (όπως Home Assistant, Gotify ή Node-RED), εισάγετε τις διευθύνσεις IP ή τα ονόματα κεντρικού υπολογιστή τους παραπάνω ως λίστα χωρισμένη με κόμματα (π.χ. <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"smtp_usage_info\" => \"Θα χρησιμοποιηθεί για ανάκτηση κωδικού πρόσβασης και άλλα μηνύματα ηλεκτρονικού ταχυδρομείου συστήματος.\",\r\n    \"maintenance_tasks\" => \"Εργασίες συντήρησης\",\r\n    \"orphaned_logos\" => \"Ορφανά λογότυπα\",\r\n    \"update\" => \"Ενημέρωση\",\r\n    \"new_version_available\" => \"Μια νέα έκδοση του Wallos είναι διαθέσιμη\",\r\n    \"current_version\" => \"Τρέχουσα Έκδοση\",\r\n    \"latest_version\" => \"Τελευταία Έκδοση\",\r\n    \"on_current_version\" => \"Χρησιμοποιείτε την τελευταία έκδοση του Wallos.\",\r\n    \"show_update_notification\" => \"Εμφάνιση ειδοποίησης για ενημερώσεις στο dashboard\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"Το email επιβεβαιώθηκε\",\r\n    \"email_verification_failed\" => \"Η επαλήθευση email απέτυχε\",\r\n    // Calendar\r\n    \"calendar\" => \"Ημερολόγιο\",\r\n    \"sun\" => \"Κυριακή\",\r\n    \"mon\" => \"Δευτέρα\",\r\n    \"tue\" => \"Τρίτη\",\r\n    \"wed\" => \"Τετάρτη\",\r\n    \"thu\" => \"Πέμπτη\",\r\n    \"fri\" => \"Παρασκευή\",\r\n    \"sat\" => \"Σάββατο\",\r\n    \"month-01\" => \"Ιανουάριος\",\r\n    \"month-02\" => \"Φεβρουάριος\",\r\n    \"month-03\" => \"Μάρτιος\",\r\n    \"month-04\" => \"Απρίλιος\",\r\n    \"month-05\" => \"Μάιος\",\r\n    \"month-06\" => \"Ιούνιος\",\r\n    \"month-07\" => \"Ιούλιος\",\r\n    \"month-08\" => \"Αύγουστος\",\r\n    \"month-09\" => \"Σεπτέμβριος\",\r\n    \"month-10\" => \"Οκτώβριος\",\r\n    \"month-11\" => \"Νοέμβριος\",\r\n    \"month-12\" => \"Δεκέμβριος\",\r\n    \"total_cost\" => \"Συνολικό κόστος\",\r\n    \"export_icalendar\" => \"Εξαγωγή iCalendar\",\r\n    \"over_budget_warning\" => \"Έχετε ξεπεράσει τον προϋπολογισμό\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Εισάγετε τον κωδικό TOTP\",\r\n\r\n];\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/en.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"You need to create an account before you're able to login\",\r\n    \"username\" => \"Username\",\r\n    \"password\" => \"Password\",\r\n    \"firstname\" => \"First name\",\r\n    \"lastname\" => \"Last name\",\r\n    \"email\" => \"Email\",\r\n    \"confirm_password\" => \"Confirm Password\",\r\n    \"main_currency\" => \"Main Currency\",\r\n    \"language\" => \"Language\",\r\n    \"passwords_dont_match\" => \"Passwords do not match\",\r\n    \"username_exists\" => \"Username already exists\",\r\n    \"email_exists\" => \"Email already exists\",\r\n    \"registration_failed\" => \"Registration failed, please try again.\",\r\n    \"register\" => \"Register\",\r\n    \"restore_database\" => \"Restore Database\",\r\n    // Login Page\r\n    \"please_login\" => \"Please login\",\r\n    \"stay_logged_in\" => \"Stay logged in (30 days)\",\r\n    \"login\" => \"Login\",\r\n    \"login_with\" => \"Login with\",\r\n    \"or\" => \"or\",\r\n    \"login_failed\" => \"Login details are incorrect\",\r\n    \"registration_successful\" => \"Registration successful\",\r\n    \"user_email_waiting_verification\" => \"Your email needs to be verified. Please check your email.\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Forgot Password\",\r\n    \"reset_password\" => \"Reset Password\",\r\n    \"reset_sent_check_email\" => \"Reset email sent. Please check your email.\",\r\n    \"password_reset_successful\" => \"Password reset successful\",\r\n    // Header\r\n    \"profile\" => \"Profile\",\r\n    \"dashboard\" => \"Dashboard\",\r\n    \"subscriptions\" => \"Subscriptions\",\r\n    \"stats\" => \"Statistics\",\r\n    \"settings\" => \"Settings\",\r\n    \"admin\" => \"Admin\",\r\n    \"about\" => \"About\",\r\n    \"logout\" => \"Logout\",\r\n    // Dashboard\r\n    \"hello\" => \"Hello\",\r\n    \"upcoming_payments\" => \"Upcoming Payments\",\r\n    \"no_upcoming_payments\" => \"You don't have any upcoming payments\",\r\n    \"overdue_renewals\" => \"Overdue Renewals\",\r\n    \"ai_recommendations\" => \"AI Recommendations\",\r\n    \"your_budget\" => \"Your Budget\",\r\n    \"budget\" => \"Budget\",\r\n    \"budget_used\" => \"Budget Used\",\r\n    \"over_budget\" => \"Over Budget\",\r\n    \"your_subscriptions\" => \"Your Subscriptions\",\r\n    \"your_savings\" => \"Your Savings\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Subscription\",\r\n    \"no_subscriptions_yet\" => \"You don't have any subscriptions yet\",\r\n    \"add_first_subscription\" => \"Add first subscription\",\r\n    \"new_subscription\" => \"New Subscription\",\r\n    \"search\" => \"Search\",\r\n    \"state\" => \"State\",\r\n    \"alphanumeric\" => \"Alphanumeric\",\r\n    \"sort\" => \"Sort\",\r\n    \"name\" => \"Name\",\r\n    \"last_added\" => \"Last Added\",\r\n    \"price\" => \"Price\",\r\n    \"next_payment\" => \"Next Payment\",\r\n    \"renewal_type\" => \"Renewal Type\",\r\n    \"auto_renewal\" => \"Auto Renewal\",\r\n    \"automatically_renews\" => \"Automatically renews\",\r\n    \"manual_renewal\" => \"Manual Renewal\",\r\n    \"start_date\" => \"Start Date\",\r\n    \"inactive\" => \"Disable Subscription\",\r\n    \"replaced_with\" => \"Replaced with\",\r\n    \"none\" => \"None\",\r\n    \"member\" => \"Member\",\r\n    \"category\" => \"Category\",\r\n    \"payment_method\" => \"Payment Method\",\r\n    \"Daily\" => \"Daily\",\r\n    \"Weekly\" => \"Weekly\",\r\n    \"Monthly\" => \"Monthly\",\r\n    \"Yearly\" => \"Yearly\",\r\n    \"daily\" => \"Day(s)\",\r\n    \"weekly\" => \"Week(s)\",\r\n    \"monthly\" => \"Month(s)\",\r\n    \"yearly\" => \"Year(s)\",\r\n    \"days\" => \"days\",\r\n    \"weeks\" => \"weeks\",\r\n    \"months\" => \"months\",\r\n    \"years\" => \"years\",\r\n    \"external_url\" => \"Visit External URL\",\r\n    \"empty_page\" => \"Empty Page\",\r\n    \"clear_filters\" => \"Clear Filters\",\r\n    \"no_matching_subscriptions\" => \"No matching subscriptions\",\r\n    \"clone\" => \"Clone\",\r\n    \"renew\" => \"Renew\",\r\n    \"calculate_next_payment_date\" => \"Calculate Next Payment Date\",\r\n    // Subscription form\r\n    \"add_subscription\" => \"Add subscription\",\r\n    \"edit_subscription\" => \"Edit subscription\",\r\n    \"subscription_name\" => \"Subscription name\",\r\n    \"logo_preview\" => \"Logo Preview\",\r\n    \"search_logo\" => \"Search logo on the web\",\r\n    \"web_search\" => \"Web search\",\r\n    \"currency\" => \"Currency\",\r\n    \"payment_every\" => \"Payment every\",\r\n    \"frequency\" => \"Frequency\",\r\n    \"cycle\" => \"Cycle\",\r\n    \"no_category\" => \"No category\",\r\n    \"paid_by\" => \"Paid by\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Notes\",\r\n    \"enable_notifications\" => \"Enable Notifications for this subscription\",\r\n    \"default_value_from_settings\" => \"Default value from settings\",\r\n    \"cancellation_notification\" => \"Cancellation Notification\",\r\n    \"delete\" => \"Delete\",\r\n    \"cancel\" => \"Cancel\",\r\n    \"upload_logo\" => \"Upload Logo\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"You are using multiple currencies on your subscriptions. To have valid and accurate statistics, please set a Fixer API Key on the settings page.\",\r\n    \"general_statistics\" => \"General Statistics\",\r\n    \"active_subscriptions\" => \"Active Subscriptions\",\r\n    \"inactive_subscriptions\" => \"Inactive Subscriptions\",\r\n    \"monthly_cost\" => \"Monthly Cost\",\r\n    \"yearly_cost\" => \"Yearly Cost\",\r\n    \"average_monthly\" => \"Average Monthly Subscription Cost\",\r\n    \"most_expensive\" => \"Most Expensive Subscription Cost\",\r\n    \"amount_due\" => \"Amount due this month\",\r\n    \"percentage_budget_used\" => \"Percentage of budget used\",\r\n    \"budget_remaining\" => \"Budget Remaining\",\r\n    \"amount_over_budget\" => \"Amount over budget\",\r\n    \"monthly_savings\" => \"Monthly Savings (on inactive subscriptions)\",\r\n    \"yearly_savings\" => \"Yearly Savings (on inactive subscriptions)\",\r\n    \"split_views\" => \"Split Views\",\r\n    \"category_split\" => \"Category Split\",\r\n    \"household_split\" => \"Household Split\",\r\n    \"payment_method_split\" => \"Payment Method Split\",\r\n    \"total_cost_trend\" => \"Total Cost Trend\",\r\n    \"cost_vs_budget\" => \"Cost vs Budget\",\r\n    // About page\r\n    \"about_and_credits\" => \"About and Credits\",\r\n    \"credits\" => \"Credits\",\r\n    \"license\" => \"License\",\r\n    \"release_notes\" => \"Release Notes\",\r\n    \"update_available\" => \"Update Available\",\r\n    \"issues_and_requests\" => \"Issues and Requests\",\r\n    \"the_author\" => \"The author\",\r\n    \"icons\" => \"Icons\",\r\n    \"payment_icons\" => \"Payment Icons\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"Upload Avatar\",\r\n    \"file_type_error\" => \"The file type supplied is not supported.\",\r\n    \"user_details\" => \"User Details\",\r\n    \"two_factor_authentication\" => \"Two Factor Authentication\",\r\n    \"two_factor_info\" => \"Two Factor Authentication adds an extra layer of security to your account.<br>You will need an authenticator app like Google Authenticator, Authy or Ente Auth to scan the QR code.\",\r\n    \"two_factor_enabled_info\" => \"Your account is secure with Two Factor Authentication. You can disable it by clicking the button above.\",\r\n    \"enable_two_factor_authentication\" => \"Enable Two Factor Authentication\",\r\n    \"2fa_already_enabled\" => \"Two Factor Authentication is already enabled\",\r\n    \"totp_code_incorrect\" => \"TOTP code is incorrect\",\r\n    \"backup_codes\" => \"Backup Codes\",\r\n    \"download_backup_codes\" => \"Download Backup Codes\",\r\n    \"copy_to_clipboard\" => \"Copy to clipboard\",\r\n    \"totp_backup_codes_info\" => \"These codes can be used to login if you lose access to your authenticator app.\",\r\n    \"disable_two_factor_authentication\" => \"Disable Two Factor Authentication\",\r\n    \"totp_code\" => \"TOTP Code\",\r\n    \"api_key\" => \"API Key\",\r\n    \"regenerate\" => \"Regenerate\",\r\n    \"api_key_info\" => \"The API key is used to access the API. Keep it secret.\",\r\n    // Settings page\r\n    \"monthly_budget\" => \"Monthly Budget\",\r\n    \"budget_info\" => \"Monthly budget is used to calculate statistics\",\r\n    \"household\" => \"Household\",\r\n    \"save_member\" => \"Save Member\",\r\n    \"delete_member\" => \"Delete Member\",\r\n    \"cant_delete_member\" => \"Can't delete main member\",\r\n    \"cant_delete_member_in_use\" => \"Can't delete member in use in subscription\",\r\n    \"household_info\" => \"Email field allows for household members to be notified of subscriptions about to expire.\",\r\n    \"notifications\" => \"Notifications\",\r\n    \"enable_email_notifications\" => \"Enable email notifications\",\r\n    \"notify_me\" => \"Notify me\",\r\n    \"day_before\" => \"day before\",\r\n    \"on_due_date\" => \"On due date\",\r\n    \"days_before\" => \"days before\",\r\n    \"smtp_address\" => \"SMTP Address\",\r\n    \"port\" => \"Port\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"SMTP Username\",\r\n    \"smtp_password\" => \"SMTP Password\",\r\n    \"from_email\" => \"From email (Optional)\",\r\n    \"send_to_other_emails\" => \"Also send notifications to the following email addresses (use ; to separate them):\",\r\n    \"other_emails_placeholder\" => \"user@domain.com;test@user.com\",\r\n    \"smtp_info\" => \"SMTP Password is transmitted and stored in plaintext. For security, please create an account just for this.\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Telegram Bot Token\",\r\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Pushplus Token\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"WebHook URL\",\r\n    \"mattermost_bot_username\" => \"Bot Username\",\r\n    \"mattermost_bot_icon_emoji\" => \"Bot Icon Emoji\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"Webhook URL\",\r\n    \"request_method\" => \"Request Method\",\r\n    \"custom_headers\" => \"Custom Headers\",\r\n    \"webhook_payload\" => \"Webhook Payload\",\r\n    \"payment_notifications_payload\" => \"Payment Notification Payload\",\r\n    \"cancelation_notification_payload\" => \"Cancelation Notification Payload\",\r\n    \"variables_available\" => \"Variables available\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Discord Bot Username\",\r\n    \"discord_bot_avatar_url\" => \"Discord Bot Avatar URL\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Pushover User Key\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Topic\",\r\n    \"ignore_ssl_errors\" => \"Ignore SSL Errors\",\r\n    \"categories\" => \"Categories\",\r\n    \"save_category\" => \"Save Category\",\r\n    \"delete_category\" => \"Delete Category\",\r\n    \"cant_delete_category_in_use\" => \"Can't delete category in use in subscription\",\r\n    \"currencies\" => \"Currencies\",\r\n    \"save_currency\" => \"Save currency\",\r\n    \"delete_currency\" => \"Delete currency\",\r\n    \"cant_delete_main_currency\" => \"Can't delete main currency\",\r\n    \"cant_delete_currency_in_use\" => \"Can't delete currency in use in subscription\",\r\n    \"exchange_update\" => \"Exchange rates last updated on\",\r\n    \"currency_info\" => \"Find the supported currencies and correct currency codes on\",\r\n    \"currency_performance\" => \"For improved performance keep only the currencies you use.\",\r\n    \"fixer_api_key\" => \"Fixer API Key\",\r\n    \"provider\" => \"Provider\",\r\n    \"fixer_info\" => \"If you use multiple currencies, and want accurate statistics and sorting on the subscriptions, a FREE API Key from Fixer is necessary.\",\r\n    \"get_key\" => \"Get your key at\",\r\n    \"get_free_fixer_api_key\" => \"Get free Fixer API Key\",\r\n    \"get_key_alternative\" => \"Alternatively, you can get a free fixer api key from\",\r\n    \"ai_model\" => \"AI Model\",\r\n    \"select_ai_model\" => \"Select AI Model\",\r\n    \"run_schedule\" => \"Run Schedule\",\r\n    \"manually\" => \"Manually\",\r\n    \"coming_soon\" => \"Coming Soon\",\r\n    \"invalid_host\" => \"Invalid Host\",\r\n    \"ai_recommendations_info\" => \"AI Recommendations are generated based on your subscriptions and household members.\",\r\n    \"may_take_time\" => \"Depending on the provider, model and number of subscriptions, recommendations generation may take some time.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Recommendations will be visible on the dashboard.\",\r\n    \"generate_recommendations\" => \"Generate Recommendations\",\r\n    \"display_settings\" => \"Display Settings\",\r\n    \"theme_settings\" => \"Theme Settings\",\r\n    \"colors\" => \"Colors\",\r\n    \"custom_colors\" => \"Custom Colors\",\r\n    \"theme\" => \"Theme\",\r\n    \"dark_theme\" => \"Dark Theme\",\r\n    \"light_theme\" => \"Light Theme\",\r\n    \"automatic\"=> \"Automatic\",\r\n    \"main_color\" => \"Main Color\",\r\n    \"accent_color\" => \"Accent Color\",\r\n    \"hover_color\" => \"Hover Color\",\r\n    \"save_custom_colors\" => \"Save Custom Colors\",\r\n    \"reset_custom_colors\" => \"Reset Custom Colors\",\r\n    \"custom_css\" => \"Custom CSS\",\r\n    \"save_custom_css\" => \"Save Custom CSS\",\r\n    \"calculate_monthly_price\" => \"Calculate and show monthly price for all subscriptions\",\r\n    \"convert_prices\" => \"Always convert and show prices on my main currency (slower)\",\r\n    \"show_original_price\" => \"Also show original price when conversions or calculations are made\",\r\n    \"experience\" => \"Experience\",\r\n    \"show_subscription_progress\" => \"Show subscription progress\",\r\n    \"disabled_subscriptions\" => \"Disabled Subscriptions\",\r\n    \"hide_disabled_subscriptions\" => \"Hide disabled subscriptions\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Show disabled subscriptions at the bottom\",\r\n    \"experimental_settings\" => \"Experimental Settings\",\r\n    \"remove_background\" => \"Attempt to remove background of logos from image search\",\r\n    \"use_mobile_navigation_bar\" => \"Use mobile navigation bar\",\r\n    \"experimental_info\" => \"Experimental settings will probably not work perfectly.\",\r\n    \"payment_methods\" => \"Payment Methods\",\r\n    \"payment_methods_info\" => \"Click a payment method to disable / enable it.\",\r\n    \"rename_payment_methods_info\" => \"Click the name on a payment method to rename it.\",\r\n    \"cant_delete_payment_method_in_use\" => \"Can't disable used payment method\",\r\n    \"add_custom_payment\" => \"Add Custom Payment Method\",\r\n    \"payment_method_name\" => \"Payment Method Name\",\r\n    \"payment_method_added_successfuly\" => \"Payment method added successfully\",\r\n    \"payment_method_removed\" => \"Payment method removed\",\r\n    \"disable\" => \"Disable\",\r\n    \"enable\" => \"Enable\",\r\n    \"rename_payment_method\" => \"Rename Payment Method\",\r\n    \"payment_renamed\" => \"Payment method renamed\",\r\n    \"payment_not_renamed\" => \"Payment method not renamed\",\r\n    \"test\" => \"Test\",\r\n    \"add\" => \"Add\",\r\n    \"save\" => \"Save\",\r\n    \"reset\" => \"Reset\",\r\n    \"main_accent_color_error\" => \"Main and accent color can't be the same\",\r\n    \"backup_and_restore\" => \"Backup and Restore\",\r\n    \"backup\" => \"Backup\",\r\n    \"restore\" => \"Restore\",\r\n    \"restore_info\" => \"Restoring the database will override all current data. You will be signed out after the restore.\",\r\n    \"account\" => \"Account\",\r\n    \"export_subscriptions\" => \"Export Subscriptions\",\r\n    \"export_as_json\" => \"Export as JSON\",\r\n    \"export_as_csv\" => \"Export as CSV\",\r\n    \"danger_zone\" => \"Danger Zone\",\r\n    \"delete_account\" => \"Delete Account\",\r\n    \"delete_account_info\" => \"Deleting your account will also delete all your subscriptions and settings.\",\r\n    // Filters menu\r\n    \"filter\" => \"Filter\",\r\n    \"clear\" => \"Clear\",\r\n    // Toast\r\n    \"success\" => \"Success\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"Your session expired. Please login again\",\r\n    \"fields_missing\" => \"Some fields are missing\",\r\n    \"fill_all_fields\" => \"Please fill all fields\",\r\n    \"fill_mandatory_fields\" => \"Please fill all mandatory fields\",\r\n    \"error\" => \"Error\",\r\n    // Category\r\n    \"failed_add_category\" => \"Failed to add category\",\r\n    \"failed_edit_category\" => \"Failed to edit category\",\r\n    \"category_in_use\" => \"Category is in use in subscriptions and can't be removed\",\r\n    \"failed_remove_category\" => \"Failed to remove category\",\r\n    \"category_saved\" => \"Category saved\",\r\n    \"category_removed\" => \"Category removed\",\r\n    \"sort_order_saved\" => \"Sort order saved\",\r\n    // Currency\r\n    \"currency_saved\" => \"was saved.\",\r\n    \"error_adding_currency\" => \"Error adding currency entry.\",\r\n    \"failed_to_store_currency\" => \"Failed to store Currency on the Database.\",\r\n    \"currency_in_use\" => \"Currency is in use in subscriptions and can't be deleted.\",\r\n    \"currency_is_main\" => \"Currency is set as main currency and can't be deleted.\",\r\n    \"failed_to_remove_currency\" => \"Failed to remove currency from the Database.\",\r\n    \"failed_to_store_api_key\" => \"Failed to store API Key on the Database.\",\r\n    \"invalid_api_key\" => \"Invalid API Key.\",\r\n    \"api_key_saved\" => \"API key saved successfully\",\r\n    \"currency_removed\" => \"Currency removed\",\r\n    // Household\r\n    \"failed_add_household\" => \"Failed to add household member\",\r\n    \"failed_edit_household\" => \"Failed to edit household member\",\r\n    \"failed_remove_household\" => \"Failed to remove household member\",\r\n    \"household_in_use\" => \"Household member is in use in subscriptions and can't be removed\",\r\n    \"member_saved\" => \"Member saved\",\r\n    \"member_removed\" => \"Member removed\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Error saving notifications data.\",\r\n    \"wallos_notification\" => \"Wallos Notification\",\r\n    \"test_notification\" => \"This is a test notification. If you're seeing this, the configuration is correct.\",\r\n    \"email_error\" => \"Error sending email\",\r\n    \"notification_sent_successfuly\" => \"Notification sent successfully\",\r\n    \"notifications_settings_saved\" => \"Notification settings saved successfully.\",\r\n    \"notification_failed\" => \"Notification failed\",\r\n    // Payments\r\n    \"payment_in_use\" => \"Can't disable used payment method\",\r\n    \"failed_update_payment\" => \"Failed to update payment method in the database\",\r\n    \"enabled\" => \"enabled\",\r\n    \"disabled\" => \"disabled\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Error fetching image\",\r\n    \"subscription_updated_successfuly\" => \"Subscription updated successfully\",\r\n    \"subscription_added_successfuly\" => \"Subscription added successfully\",\r\n    \"error_deleting_subscription\" => \"Error deleting subscription.\",\r\n    \"invalid_request_method\" => \"Invalid request method.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Error updating user data.\",\r\n    \"user_details_saved\" => \"User details saved\",\r\n    // Admin Page\r\n    \"registrations\" => \"Registrations\",\r\n    \"enable_user_registrations\" => \"Enable user registrations\",\r\n    \"maximum_number_users\" => \"Maximum number of users\",\r\n    \"require_email_verification\" => \"Require email verification\",\r\n    \"configure_smtp_settings_to_enable\" => \"Configure SMTP settings to enable\",\r\n    \"server_url\" => \"Server URL\",\r\n    \"server_url_info\" => \"Used for email verification and password recovery. Must be a valid public URL.\",\r\n    \"server_url_password_reset\" => \"If set will also enable password reset functionality.\",\r\n    \"disable_login\" => \"Disable login\",\r\n    \"disable_login_info\" => \"Bypass login. If you run your server on a local network only, without external access you can disable the login. This will automatically login the admin user.\",\r\n    \"disable_login_info2\" => \"You can only enable this setting if user registration is disabled and there are no more than the admin user account.\",\r\n    \"max_users_info\" => \"0 means unlimited\",\r\n    \"user_management\" => \"User Management\",\r\n    \"delete_user\" => \"Delete User\",\r\n    \"delete_user_info\" => \"Deleting a user will also delete all their subscriptions and settings.\",\r\n    \"create_user\" => \"Create User\",\r\n    \"oidc_settings\" => \"OIDC Settings\",\r\n    \"oidc_oauth_enabled\" => \"Enable OIDC/OAuth\",\r\n    \"create_user_automatically\" => \"Create user automatically\",\r\n    \"disable_password_login\" => \"Disable password login\",\r\n    \"smtp_settings\" => \"SMTP Settings\",\r\n    \"smtp_usage_info\" => \"Will be used for password recovery and other system emails.\",\r\n    \"security_settings\" => \"Security Settings\",\r\n    \"ssrf_protection_info\" => \"To prevent Server-Side Request Forgery (SSRF) attacks, Wallos blocks webhook notifications to private or internal network addresses by default.\",\r\n    \"local_webhook_info\" => \"If you need to send webhooks to local services (like Home Assistant, Gotify, or Node-RED), enter their IP addresses or hostnames above as a comma-separated list (e.g., <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"maintenance_tasks\" => \"Maintenance Tasks\",\r\n    \"orphaned_logos\" => \"Orphaned Logos\",\r\n    \"update\" => \"Update\",\r\n    \"new_version_available\" => \"A new version of Wallos is available\",\r\n    \"current_version\" => \"Current Version\",\r\n    \"latest_version\" => \"Latest Version\",\r\n    \"on_current_version\" => \"You're running the latest version of Wallos.\",\r\n    \"show_update_notification\" => \"Show notification for updates on the dashboard\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"Email verified successfully\",\r\n    \"email_verification_failed\" => \"Email verification failed\",\r\n    // Calendar\r\n    \"calendar\" => \"Calendar\",\r\n    \"sun\" => \"Sun\",\r\n    \"mon\" => \"Mon\",\r\n    \"tue\" => \"Tue\",\r\n    \"wed\" => \"Wed\",\r\n    \"thu\" => \"Thu\",\r\n    \"fri\" => \"Fri\",\r\n    \"sat\" => \"Sat\",\r\n    \"month-01\" => \"January\",\r\n    \"month-02\" => \"February\",\r\n    \"month-03\" => \"March\",\r\n    \"month-04\" => \"April\",\r\n    \"month-05\" => \"May\",\r\n    \"month-06\" => \"June\",\r\n    \"month-07\" => \"July\",\r\n    \"month-08\" => \"August\",\r\n    \"month-09\" => \"September\",\r\n    \"month-10\" => \"October\",\r\n    \"month-11\" => \"November\",\r\n    \"month-12\" => \"December\",\r\n    \"total_cost\" => \"Total Cost\",\r\n    \"export_icalendar\" => \"Export iCalendar\",\r\n    \"over_budget_warning\" => \"You're over budget\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Insert TOTP code\",\r\n\r\n\r\n];\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/es.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"Necesitas crear una cuenta antes de poder iniciar sesión\",\r\n    \"username\" => \"Nombre de Usuario\",\r\n    \"password\" => \"Contraseña\",\r\n    \"email\" => \"Correo Electrónico\",\r\n    \"firstname\" => \"Nombre\",\r\n    \"lastname\" => \"Apellido\",\r\n    \"confirm_password\" => \"Confirmar Contraseña\",\r\n    \"main_currency\" => \"Moneda Principal\",\r\n    \"language\" => \"Idioma\",\r\n    \"passwords_dont_match\" => \"Las contraseñas no coinciden\",\r\n    \"username_exists\" => \"El nombre de usuario ya existe\",\r\n    \"email_exists\" => \"El correo electrónico ya está registrado\",\r\n    \"registration_failed\" => \"Error en el registro, por favor inténtalo de nuevo.\",\r\n    \"register\" => \"Registrar\",\r\n    \"restore_database\" => \"Restaurar Base de Datos\",\r\n    // Login Page\r\n    \"please_login\" => \"Por favor, inicia sesión\",\r\n    \"stay_logged_in\" => \"Mantener sesión iniciada (30 días)\",\r\n    \"login\" => \"Iniciar Sesión\",\r\n    \"login_with\" => \"Iniciar sesión con\",\r\n    \"or\" => \"o\",\r\n    \"login_failed\" => \"Los detalles de inicio de sesión son incorrectos\",\r\n    \"registration_successful\" => \"Registro efectuado con éxito\",\r\n    \"user_email_waiting_verification\" => \"Tu correo electrónico necesita ser verificado. Por favor, compruebe su correo electrónico\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"¿Olvidaste tu contraseña?\",\r\n    \"reset_password\" => \"Restablecer Contraseña\",\r\n    \"reset_sent_check_email\" => \"Se ha enviado un correo electrónico con instrucciones para restablecer la contraseña. Por favor, compruebe su correo electrónico.\",\r\n    \"password_reset_successful\" => \"Contraseña restablecida con éxito\",\r\n    // Header\r\n    \"profile\" => \"Perfil\",\r\n    \"dashboard\" => \"Tablero\",\r\n    \"subscriptions\" => \"Suscripciones\",\r\n    \"stats\" => \"Estadísticas\",\r\n    \"settings\" => \"Configuración\",\r\n    \"admin\" => \"Admin\",\r\n    \"about\" => \"Acerca de\",\r\n    \"logout\" => \"Cerrar Sesión\",\r\n    // Dashboard\r\n    \"hello\" => \"Hola\",\r\n    \"upcoming_payments\" => \"Próximos Pagos\",\r\n    \"no_upcoming_payments\" => \"No tienes pagos próximos\",\r\n    \"overdue_renewals\" => \"Renovaciones Atrasadas\",\r\n    \"ai_recommendations\" => \"Recomendaciones de IA\",\r\n    \"your_budget\" => \"Tu Presupuesto\",\r\n    \"budget\" => \"Presupuesto\",\r\n    \"budget_used\" => \"Presupuesto Utilizado\",\r\n    \"over_budget\" => \"Sobre Presupuesto\",\r\n    \"your_subscriptions\" => \"Tus Suscripciones\",\r\n    \"your_savings\" => \"Tus Ahorros\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Suscripción\",\r\n    \"no_subscriptions_yet\" => \"Aún no tienes ninguna suscripción\",\r\n    \"add_first_subscription\" => \"Añadir primera suscripción\",\r\n    \"new_subscription\" => \"Nueva Suscripción\",\r\n    \"search\" => \"Buscar\",\r\n    \"state\" => \"Estado\",\r\n    \"alphanumeric\" => \"Alfanumérico\",\r\n    \"sort\" => \"Ordenar\",\r\n    \"name\" => \"Nombre\",\r\n    \"last_added\" => \"Última Añadida\",\r\n    \"price\" => \"Precio\",\r\n    \"next_payment\" => \"Próximo Pago\",\r\n    \"renewal_type\" => \"Tipo de Renovación\",\r\n    \"auto_renewal\" => \"Renovación Automática\",\r\n    \"automatically_renews\" => \"Renovación Automática\",\r\n    \"manual_renewal\" => \"Renovación Manual\",\r\n    \"start_date\" => \"Fecha de Inicio\",\r\n    \"inactive\" => \"Desactivar Suscripción\",\r\n    \"replaced_with\" => \"Reemplazada con\",\r\n    \"none\" => \"Ninguna\",\r\n    \"member\" => \"Miembro\",\r\n    \"category\" => \"Categoría\",\r\n    \"payment_method\" => \"Método de Pago\",\r\n    \"Daily\" => \"Diario\",\r\n    \"Weekly\" => \"Semanal\",\r\n    \"Monthly\" => \"Mensual\",\r\n    \"Yearly\" => \"Anual\",\r\n    \"daily\" => \"Día(s)\",\r\n    \"weekly\" => \"Semana(s)\",\r\n    \"monthly\" => \"Mes(es)\",\r\n    \"yearly\" => \"Año(s)\",\r\n    \"days\" => \"días\",\r\n    \"weeks\" => \"semanas\",\r\n    \"months\" => \"meses\",\r\n    \"years\" => \"años\",\r\n    \"external_url\" => \"Visitar URL Externa\",\r\n    \"empty_page\" => \"Página Vacía\",\r\n    \"clear_filters\" => \"Limpiar Filtros\",\r\n    \"no_matching_subscriptions\" => \"No hay suscripciones que coincidan con los filtros\",\r\n    \"clone\" => \"Clonar\",\r\n    \"renew\" => \"Renovar\",\r\n    \"calculate_next_payment_date\" => \"Calcular Fecha del Próximo Pago\",\r\n    // Subscription form\r\n    \"add_subscription\" => \"Añadir suscripción\",\r\n    \"edit_subscription\" => \"Editar suscripción\",\r\n    \"subscription_name\" => \"Nombre de la Suscripción\",\r\n    \"logo_preview\" => \"Vista Previa del Logotipo\",\r\n    \"search_logo\" => \"Buscar logotipo en la web\",\r\n    \"web_search\" => \"Búsqueda web\",\r\n    \"currency\" => \"Moneda\",\r\n    \"payment_every\" => \"Pago cada\",\r\n    \"frequency\" => \"Frecuencia\",\r\n    \"cycle\" => \"Ciclo\",\r\n    \"no_category\" => \"Sin categoría\",\r\n    \"paid_by\" => \"Pagado por\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Notas\",\r\n    \"enable_notifications\" => \"Habilitar notificaciones para esta suscripción\",\r\n    \"default_value_from_settings\" => \"Valor predeterminado de la configuración\",\r\n    \"cancellation_notification\" => \"Notificación de cancelación\",\r\n    \"delete\" => \"Eliminar\",\r\n    \"cancel\" => \"Cancelar\",\r\n    \"upload_logo\" => \"Cargar Logotipo\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"Estás utilizando varias monedas en tus suscripciones. Para disponer de estadísticas válidas y precisas, establece una clave API Fixer en la página de configuración.\",\r\n    \"general_statistics\" => \"Estadísticas Generales\",\r\n    \"active_subscriptions\" => \"Suscripciones Activas\",\r\n    \"inactive_subscriptions\" => \"Suscripciones inactivas\",\r\n    \"monthly_cost\" => \"Costo Mensual\",\r\n    \"yearly_cost\" => \"Costo Anual\",\r\n    \"average_monthly\" => \"Costo Promedio Mensual de Suscripción\",\r\n    \"most_expensive\" => \"Costo de Suscripción Más Caro\",\r\n    \"amount_due\" => \"Monto a pagar este mes\",\r\n    \"percentage_budget_used\" => \"Porcentaje del presupuesto utilizado\",\r\n    \"budget_remaining\" => \"Presupuesto Restante\",\r\n    \"amount_over_budget\" => \"Monto sobre el presupuesto\",\r\n    \"monthly_savings\" => \"Ahorro Mensual (en suscripciones inactivas)\",\r\n    \"yearly_savings\" => \"Ahorro Anual (en suscripciones inactivas)\",\r\n    \"split_views\" => \"Vistas Divididas\",\r\n    \"category_split\" => \"División por Categoría\",\r\n    \"household_split\" => \"División por Hogar\",\r\n    \"payment_method_split\" => \"División por Método de Pago\",\r\n    \"total_cost_trend\" => \"Tendencia del Costo Total\",\r\n    \"cost_vs_budget\" => \"Costo vs Presupuesto\",\r\n    // About page\r\n    \"about_and_credits\" => \"Acerca de y Créditos\",\r\n    \"credits\" => \"Créditos\",\r\n    \"license\" => \"Licencia\",\r\n    \"release_notes\" => \"Notas de la Versión\",\r\n    \"update_available\" => \"Actualización Disponible\",\r\n    \"issues_and_requests\" => \"Problemas y Solicitudes\",\r\n    \"the_author\" => \"El autor\",\r\n    \"icons\" => \"Iconos\",\r\n    \"payment_icons\" => \"Iconos de Pago\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"Subir avatar\",\r\n    \"file_type_error\" => \"El archivo debe ser una imagen en formato PNG, JPG, WEBP o SVG\",\r\n    \"user_details\" => \"Detalles del Usuario\",\r\n    \"two_factor_authentication\" => \"Autenticación de Dos Factores\",\r\n    \"two_factor_info\" => \"La autenticación de dos factores añade una capa adicional de seguridad a tu cuenta.<br>Necesitarás una aplicación de autenticación como Google Authenticator, Authy o Ente Auth para escanear el código QR.\",\r\n    \"two_factor_enabled_info\" => \"Tu cuenta está segura con la autenticación de dos factores. Puedes desactivarla haciendo clic en el botón de arriba.\",\r\n    \"enable_two_factor_authentication\" => \"Habilitar Autenticación de Dos Factores\",\r\n    \"2fa_already_enabled\" => \"La autenticación de dos factores ya está habilitada\",\r\n    \"totp_code_incorrect\" => \"El código TOTP es incorrecto\",\r\n    \"backup_codes\" => \"Códigos de Respaldo\",\r\n    \"download_backup_codes\" => \"Descargar Códigos de Respaldo\",\r\n    \"copy_to_clipboard\" => \"Copiar al Portapapeles\",\r\n    \"totp_backup_codes_info\" => \"Guarda estos códigos en un lugar seguro. Puedes usarlos si pierdes acceso a tu aplicación de autenticación.\",\r\n    \"disable_two_factor_authentication\" => \"Desactivar Autenticación de Dos Factores\",\r\n    \"totp_code\" => \"Código TOTP\",\r\n    \"api_key\" => \"Clave API\",\r\n    \"regenerate\" => \"Regenerar\",\r\n    \"api_key_info\" => \"La clave API se utiliza para acceder a la API de Wallos. No compartas esta clave con nadie.\",\r\n    // Settings page\r\n    \"monthly_budget\" => \"Presupuesto Mensual\",\r\n    \"budget_info\" => \"El presupuesto mensual se utiliza para calcular las estadísticas. Si no deseas utilizar esta función, déjalo en 0.\",\r\n    \"household\" => \"Hogar\",\r\n    \"save_member\" => \"Guardar Miembro\",\r\n    \"delete_member\" => \"Eliminar Miembro\",\r\n    \"cant_delete_member\" => \"No se puede eliminar el miembro principal\",\r\n    \"cant_delete_member_in_use\" => \"No se puede eliminar el miembro en uso en la suscripción\",\r\n    \"household_info\" => \"El campo de correo electrónico permite notificar a los miembros del hogar las suscripciones que están a punto de caducar.\",\r\n    \"notifications\" => \"Notificaciones\",\r\n    \"enable_email_notifications\" => \"Habilitar notificaciones por correo electrónico\",\r\n    \"notify_me\" => \"Notificarme\",\r\n    \"day_before\" => \"día antes\",\r\n    \"on_due_date\" => \"En la fecha de vencimiento\",\r\n    \"days_before\" => \"días antes\",\r\n    \"smtp_address\" => \"Dirección SMTP\",\r\n    \"port\" => \"Puerto\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"Nombre de usuario SMTP\",\r\n    \"smtp_password\" => \"Contraseña SMTP\",\r\n    \"from_email\" => \"Correo electrónico de origen (Opcional)\",\r\n    \"send_to_other_emails\" => \"También enviar notificaciones a las siguientes direcciones de correo electrónico (use ; para separarlas):\",\r\n    \"smtp_info\" => \"La contraseña SMTP se transmite y almacena en texto plano. Por seguridad, crea una cuenta solo para esto.\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Token del Bot de Telegram\",\r\n    \"telegram_chat_id\" => \"ID del Chat de Telegram\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Token de Pushplus\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"URL del Webhook de Mattermost\",\r\n    \"mattermost_bot_username\" => \"Nombre de usuario del Bot de Mattermost\",\r\n    \"mattermost_bot_icon_emoji\" => \"Emoji del Icono del Bot\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"URL del Webhook\",\r\n    \"request_method\" => \"Método de Solicitud\",\r\n    \"custom_headers\" => \"Cabeceras Personalizadas\",\r\n    \"webhook_payload\" => \"Carga del Webhook\",\r\n    \"payment_notifications_payload\" => \"Carga de la Notificación de Pago\",\r\n    \"cancelation_notification_payload\" => \"Carga de la Notificación de Cancelación\",\r\n    \"variables_available\" => \"Variables disponibles\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Nombre de usuario del bot\",\r\n    \"discord_bot_avatar_url\" => \"URL del avatar del bot\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Clave de usuario\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Topico\",\r\n    \"ignore_ssl_errors\" => \"Ignorar errores SSL\",\r\n    \"categories\" => \"Categorías\",\r\n    \"save_category\" => \"Guardar Categoría\",\r\n    \"delete_category\" => \"Eliminar Categoría\",\r\n    \"cant_delete_category_in_use\" => \"No se puede eliminar la categoría en uso en la suscripción\",\r\n    \"currencies\" => \"Monedas\",\r\n    \"save_currency\" => \"Guardar Moneda\",\r\n    \"delete_currency\" => \"Eliminar Moneda\",\r\n    \"cant_delete_main_currency\" => \"No se puede eliminar la moneda principal\",\r\n    \"cant_delete_currency_in_use\" => \"No se puede eliminar la moneda en uso en la suscripción\",\r\n    \"exchange_update\" => \"Tasas de cambio actualizadas por última vez en\",\r\n    \"currency_info\" => \"Encuentra las monedas admitidas y los códigos de moneda correctos en\",\r\n    \"currency_performance\" => \"Para un rendimiento mejorado, guarda solo las monedas que uses.\",\r\n    \"fixer_api_key\" => \"API Key de Fixer\",\r\n    \"provider\" => \"Proveedor\",\r\n    \"fixer_info\" => \"Si usas varias monedas y deseas estadísticas y orden precisos en las suscripciones, es necesaria una API KEY gratuita de Fixer.\",\r\n    \"get_key\" => \"Obtén tu clave en\",\r\n    \"get_free_fixer_api_key\" => \"Obtén una API Key de Fixer gratuita\",\r\n    \"get_key_alternative\" => \"También puede obtener una clave api gratuita de Fixer en\",\r\n    \"ai_model\" => \"Modelo de IA\",\r\n    \"select_ai_model\" => \"Seleccionar Modelo de IA\",\r\n    \"run_schedule\" => \"Ejecutar Programa\",\r\n    \"manually\" => \"Manualmente\",\r\n    \"coming_soon\" => \"Próximamente\",\r\n    \"invalid_host\" => \"Host Inválido\",\r\n    \"ai_recommendations_info\" => \"Las recomendaciones de IA se generan en función de sus suscripciones y miembros del hogar.\",\r\n    \"may_take_time\" => \"Dependiendo del proveedor, modelo y número de suscripciones, la generación de recomendaciones puede tardar algún tiempo.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Las recomendaciones serán visibles en el panel.\",\r\n    \"generate_recommendations\" => \"Generar Recomendaciones\",\r\n    \"display_settings\" => \"Configuración de Pantalla\",\r\n    \"theme_settings\" => \"Configuración de Tema\",\r\n    \"colors\" => \"Colores\",\r\n    \"custom_colors\" => \"Colores Personalizados\",\r\n    \"theme\" => \"Tema\",\r\n    \"dark_theme\" => \"Tema Oscuro\",\r\n    \"light_theme\" => \"Tema Claro\",\r\n    \"automatic\" => \"Automático\",\r\n    \"main_color\" => \"Color Principal\",\r\n    \"accent_color\" => \"Color de Acento\",\r\n    \"hover_color\" => \"Color de Hover\",\r\n    \"save_custom_colors\" => \"Guardar Colores Personalizados\",\r\n    \"reset_custom_colors\" => \"Restablecer Colores Personalizados\",\r\n    \"custom_css\" => \"CSS Personalizado\",\r\n    \"save_custom_css\" => \"Guardar CSS Personalizado\",\r\n    \"calculate_monthly_price\" => \"Calcular y mostrar el precio mensual de todas las suscripciones\",\r\n    \"convert_prices\" => \"Convertir y mostrar siempre los precios en mi moneda principal (más lento)\",\r\n    \"show_original_price\" => \"Mostrar también el precio original cuando se realicen conversiones o cálculos\",\r\n    \"experience\" => \"Experiencia\",\r\n    \"show_subscription_progress\" => \"Mostrar el progreso de la suscripción\",\r\n    \"disabled_subscriptions\" => \"Suscripciones Desactivadas\",\r\n    \"hide_disabled_subscriptions\" => \"Ocultar suscripciones desactivadas\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Mostrar suscripciones desactivadas al final\",\r\n    \"experimental_settings\" => \"Configuraciones Experimentales\",\r\n    \"remove_background\" => \"Intentar quitar el fondo de los logotipos de la búsqueda de imágenes\",\r\n    \"use_mobile_navigation_bar\" => \"Usar barra de navegación móvil\",\r\n    \"experimental_info\" => \"Las configuraciones experimentales probablemente no funcionarán perfectamente.\",\r\n    \"payment_methods\" => \"Métodos de Pago\",\r\n    \"payment_methods_info\" => \"Haz clic en un método de pago para deshabilitarlo/habilitarlo.\",\r\n    \"rename_payment_methods_info\" => \"Haz clic en el nombre de un método de pago para cambiarle el nombre.\",\r\n    \"cant_delete_payment_method_in_use\" => \"No se puede desactivar el método de pago utilizado\",\r\n    \"add_custom_payment\" => \"Añadir método de pago personalizado\",\r\n    \"payment_method_name\" => \"Nombre del método de pago\",\r\n    \"payment_method_added_successfuly\" => \"Método de pago añadido con éxito\",\r\n    \"payment_method_removed\" => \"Método de pago eliminado\",\r\n    \"disable\" => \"Desactivar\",\r\n    \"enable\" => \"Activar\",\r\n    \"rename_payment_method\" => \"Renombrar método de pago\",\r\n    \"payment_renamed\" => \"Método de pago renombrado\",\r\n    \"payment_not_renamed\" => \"Error al renombrar el método de pago\",\r\n    \"test\" => \"Probar\",\r\n    \"add\" => \"Agregar\",\r\n    \"save\" => \"Guardar\",\r\n    \"reset\" => \"Restablecer\",\r\n    \"main_accent_color_error\" => \"El color principal y el color de acento no pueden ser iguales\",\r\n    \"backup_and_restore\" => \"Copia de Seguridad y Restauración\",\r\n    \"backup\" => \"Copia de Seguridad\",\r\n    \"restore\" => \"Restaurar\",\r\n    \"restore_info\" => \"La restauración de la base de datos anulará todos los datos actuales. Se cerrará la sesión después de la restauración.\",\r\n    \"account\" => \"Cuenta\",\r\n    \"export_subscriptions\" => \"Exportar suscripciones\",\r\n    \"export_as_json\" => \"Exportar como JSON\",\r\n    \"export_as_csv\" => \"Exportar como CSV\",\r\n    \"danger_zone\" => \"Zona de peligro\",\r\n    \"delete_account\" => \"Eliminar cuenta\",\r\n    \"delete_account_info\" => \"Al eliminar tu cuenta también se eliminarán todas tus suscripciones y configuraciones.\",\r\n    // Filters menu\r\n    \"filter\" => \"Filtrar\",\r\n    \"clear\" => \"Limpiar\",\r\n    // Toast\r\n    \"success\" => \"Éxito\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"Tu sesión ha expirado. Por favor, inicia sesión nuevamente\",\r\n    \"fields_missing\" => \"Faltan algunos campos\",\r\n    \"fill_all_fields\" => \"Por favor, completa todos los campos\",\r\n    \"fill_mandatory_fields\" => \"Por favor, completa todos los campos obligatorios\",\r\n    \"error\" => \"Error\",\r\n    // Category\r\n    \"failed_add_category\" => \"Error al agregar la categoría\",\r\n    \"failed_edit_category\" => \"Error al editar la categoría\",\r\n    \"category_in_use\" => \"La categoría está en uso en suscripciones y no se puede eliminar\",\r\n    \"failed_remove_category\" => \"Error al eliminar la categoría\",\r\n    \"category_saved\" => \"Categoría guardada\",\r\n    \"category_removed\" => \"Categoría eliminada\",\r\n    \"sort_order_saved\" => \"Orden de clasificación guardado\",\r\n    // Currency\r\n    \"currency_saved\" => \"fue guardada.\",\r\n    \"error_adding_currency\" => \"Error al añadir la entrada de la moneda.\",\r\n    \"failed_to_store_currency\" => \"Error al almacenar la moneda en la base de datos.\",\r\n    \"currency_in_use\" => \"La moneda está en uso en suscripciones y no se puede eliminar.\",\r\n    \"currency_is_main\" => \"La moneda está establecida como moneda principal y no se puede eliminar.\",\r\n    \"failed_to_remove_currency\" => \"Error al eliminar la moneda de la base de datos.\",\r\n    \"failed_to_store_api_key\" => \"Error al almacenar la API KEY en la base de datos.\",\r\n    \"invalid_api_key\" => \"API KEY no válida.\",\r\n    \"api_key_saved\" => \"API KEY guardada con éxito\",\r\n    \"currency_removed\" => \"Moneda eliminada\",\r\n    // Household\r\n    \"failed_add_household\" => \"Error al añadir miembro del hogar\",\r\n    \"failed_edit_household\" => \"Error al editar miembro del hogar\",\r\n    \"failed_remove_household\" => \"Error al eliminar miembro del hogar\",\r\n    \"household_in_use\" => \"El miembro del hogar está en uso en suscripciones y no se puede eliminar\",\r\n    \"member_saved\" => \"Miembro guardado\",\r\n    \"member_removed\" => \"Miembro eliminado\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Error al guardar los datos de notificaciones.\",\r\n    \"wallos_notification\" => \"Notificación de Wallos\",\r\n    \"test_notification\" => \"Esta es una notificación de prueba. Si estás viendo esto, la configuración es correcta.\",\r\n    \"email_error\" => \"Error al enviar correo electrónico\",\r\n    \"notification_sent_successfuly\" => \"Notificación enviada con éxito\",\r\n    \"notifications_settings_saved\" => \"Configuración de notificaciones guardada con éxito.\",\r\n    \"notification_failed\" => \"Error al enviar la notificación\",\r\n    // Payments\r\n    \"payment_in_use\" => \"No se puede desactivar el método de pago utilizado\",\r\n    \"failed_update_payment\" => \"Error al actualizar el método de pago en la base de datos\",\r\n    \"enabled\" => \"habilitado\",\r\n    \"disabled\" => \"desactivado\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Error al obtener la imagen\",\r\n    \"subscription_updated_successfuly\" => \"Suscripción actualizada con éxito\",\r\n    \"subscription_added_successfuly\" => \"Suscripción añadida con éxito\",\r\n    \"error_deleting_subscription\" => \"Error al eliminar la suscripción.\",\r\n    \"invalid_request_method\" => \"Método de solicitud no válido.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Error al actualizar los datos del usuario.\",\r\n    \"user_details_saved\" => \"Detalles del usuario guardados\",\r\n    // Admin Page\r\n    \"registrations\" => \"Registro de Usuarios\",\r\n    \"enable_user_registrations\" => \"Habilitar registro de usuarios\",\r\n    \"maximum_number_users\" => \"Número máximo de usuarios\",\r\n    \"require_email_verification\" => \"Requerir verificación de correo electrónico\",\r\n    \"configure_smtp_settings_to_enable\" => \"Configura la configuración SMTP para habilitar\",\r\n    \"server_url\" => \"URL del Servidor\",\r\n    \"server_url_info\" => \"Se utiliza para verificar el correo electrónico y recuperar la contraseña. Debe ser una URL pública válida.\",\r\n    \"server_url_password_reset\" => \"Si se configura, también se habilitará la función de restablecimiento de contraseña.\",\r\n    \"disable_login\" => \"Deshabilitar Inicio de Sesión\",\r\n    \"disable_login_info\" => \"Omitir el inicio de sesión. Si ejecuta su servidor sólo en una red local, sin acceso externo, puede desactivar el inicio de sesión. Esto iniciará automáticamente la sesión del usuario administrador.\",\r\n    \"disable_login_info2\" => \"Sólo puede activar esta configuración si el registro de usuarios está desactivado y no hay más que la cuenta de usuario admin.\",\r\n    \"max_users_info\" => \"0 para ilimitado\",\r\n    \"user_management\" => \"Gestión de Usuarios\",\r\n    \"delete_user\" => \"Eliminar Usuario\",\r\n    \"delete_user_info\" => \"Al eliminar un usuario, también se eliminarán todas sus suscripciones y configuraciones.\",\r\n    \"create_user\" => \"Crear Usuario\",\r\n    \"oidc_settings\" => \"Configuración OIDC\",\r\n    \"oidc_oauth_enabled\" => \"Habilitar OIDC/OAuth\",\r\n    \"create_user_automatically\" => \"Crear usuario automáticamente\",\r\n    \"disable_password_login\" => \"Deshabilitar inicio de sesión con contraseña\",\r\n    \"smtp_settings\" => \"Configuración SMTP\",\r\n    \"smtp_usage_info\" => \"Se utilizará para recuperar contraseñas y otros correos electrónicos del sistema.\",\r\n    \"security_settings\" => \"Configuración de seguridad\",\r\n    \"ssrf_protection_info\" => \"Para evitar ataques de falsificación de solicitudes del lado del servidor (SSRF), Wallos bloquea por defecto las notificaciones webhook a direcciones de red privadas o internas.\",\r\n    \"local_webhook_info\" => \"Si necesita enviar webhooks a servicios locales (como Home Assistant, Gotify o Node-RED), introduzca sus direcciones IP o nombres de host arriba como una lista separada por comas (p. ej., <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"maintenance_tasks\" => \"Tareas de Mantenimiento\",\r\n    \"orphaned_logos\" => \"Logotipos huérfanos\",\r\n    \"update\" => \"Actualizar\",\r\n    \"new_version_available\" => \"Una nueva versión de Wallos está disponible\",\r\n    \"current_version\" => \"Versión Actual\",\r\n    \"latest_version\" => \"Última Versión\",\r\n    \"on_current_version\" => \"Está utilizando la última versión de Wallos.\",\r\n    \"show_update_notification\" => \"Mostrar notificación de actualizaciones en el dashboard\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"Correo electrónico verificado\",\r\n    \"email_verification_failed\" => \"Error al verificar el correo electrónico\",\r\n    // Calendar\r\n    \"calendar\" => \"Calendario\",\r\n    \"sun\" => \"Dom\",\r\n    \"mon\" => \"Lun\",\r\n    \"tue\" => \"Mar\",\r\n    \"wed\" => \"Mié\",\r\n    \"thu\" => \"Jue\",\r\n    \"fri\" => \"Vie\",\r\n    \"sat\" => \"Sáb\",\r\n    \"month-01\" => \"Enero\",\r\n    \"month-02\" => \"Febrero\",\r\n    \"month-03\" => \"Marzo\",\r\n    \"month-04\" => \"Abril\",\r\n    \"month-05\" => \"Mayo\",\r\n    \"month-06\" => \"Junio\",\r\n    \"month-07\" => \"Julio\",\r\n    \"month-08\" => \"Agosto\",\r\n    \"month-09\" => \"Septiembre\",\r\n    \"month-10\" => \"Octubre\",\r\n    \"month-11\" => \"Noviembre\",\r\n    \"month-12\" => \"Diciembre\",\r\n    \"total_cost\" => \"Costo Total\",\r\n    \"export_icalendar\" => \"Exportar iCalendar\",\r\n    \"over_budget_warning\" => \"Te has pasado del presupuesto\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Introduce el código TOTP\",\r\n\r\n];\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/fr.php",
    "content": "<?php\n\n$i18n = [\n    // Page d'inscription\n    \"create_account\" => \"Vous devez créer un compte avant de pouvoir vous connecter\",\n    \"username\" => \"Nom d'utilisateur\",\n    \"password\" => \"Mot de passe\",\n    \"email\" => \"Courriel\",\n    \"firstname\" => \"Prénom\",\n    \"lastname\" => \"Nom de famille\",\n    \"confirm_password\" => \"Confirmer le mot de passe\",\n    \"main_currency\" => \"Devise principale\",\n    \"language\" => \"Langue\",\n    \"passwords_dont_match\" => \"Les mots de passe ne correspondent pas\",\n    \"username_exists\" => \"Le nom d'utilisateur existe déjà\",\n    \"email_exists\" => \"L'adresse courriel existe déjà\",\n    \"registration_failed\" => \"L'inscription a échoué, veuillez réessayer.\",\n    \"register\" => \"S'inscrire\",\n    \"restore_database\" => \"Restaurer la base de données\",\n    // Page de connexion\n    \"please_login\" => \"Veuillez vous connecter\",\n    \"stay_logged_in\" => \"Rester connecté (30 jours)\",\n    \"login\" => \"Se connecter\",\n    \"login_with\" => \"Se connecter avec\",\n    \"or\" => \"ou\",\n    \"login_failed\" => \"Les détails de connexion sont incorrects\",\n    \"registration_successful\" => \"Inscription réussie\",\n    \"user_email_waiting_verification\" => \"Votre email doit être vérifié. Veuillez vérifier votre email\",\n    // Password Reset Page\n    \"forgot_password\" => \"Mot de passe oublié\",\n    \"reset_password\" => \"Réinitialiser le mot de passe\",\n    \"reset_sent_check_email\" => \"Un courriel a été envoyé à l'adresse fournie. Vérifiez votre boîte de réception.\",\n    \"password_reset_successful\" => \"Réinitialisation du mot de passe réussie\",\n    // En-tête\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Accueil\",\n    \"subscriptions\" => \"Abonnements\",\n    \"stats\" => \"Statistiques\",\n    \"settings\" => \"Paramètres\",\n    \"admin\" => \"Admin\",\n    \"about\" => \"À propos\",\n    \"logout\" => \"Déconnexion\",\n    // Dashboard\n    \"hello\" => \"Bonjour\",\n    \"upcoming_payments\" => \"Paiements à venir\",\n    \"no_upcoming_payments\" => \"Vous n'avez aucun paiement à venir\",\n    \"overdue_renewals\" => \"Renouvellements en retard\",\n    \"ai_recommendations\" => \"Recommandations AI\",\n    \"your_budget\" => \"Votre budget\",\n    \"budget\" => \"Budget\",\n    \"budget_used\" => \"Budget Utilisé\",\n    \"over_budget\" => \"Au-dessus du Budget\",\n    \"your_subscriptions\" => \"Vos Abonnements\",\n    \"your_savings\" => \"Vos Économies\",\n    // Page d'abonnements\n    \"subscription\" => \"Abonnement\",\n    \"no_subscriptions_yet\" => \"Vous n'avez pas encore d'abonnement\",\n    \"add_first_subscription\" => \"Ajoutez le premier abonnement\",\n    \"new_subscription\" => \"Nouvel abonnement\",\n    \"search\" => \"Rechercher\",\n    \"state\" => \"État\",\n    \"alphanumeric\" => \"Alphanumérique\",\n    \"sort\" => \"Trier\",\n    \"name\" => \"Nom\",\n    \"last_added\" => \"Dernier ajouté\",\n    \"price\" => \"Prix\",\n    \"next_payment\" => \"Prochain paiement\",\n    \"renewal_type\" => \"Type de renouvellement\",\n    \"auto_renewal\" => \"Renouvellement automatique\",\n    \"automatically_renews\" => \"Renouvellement automatique\",\n    \"manual_renewal\" => \"Renouvellement manuel\",\n    \"start_date\" => \"Date de début\",\n    \"inactive\" => \"Désactiver l'abonnement\",\n    \"replaced_with\" => \"Remplacé par\",\n    \"none\" => \"Aucun\",\n    \"member\" => \"Membre\",\n    \"category\" => \"Catégorie\",\n    \"payment_method\" => \"Méthode de paiement\",\n    \"Daily\" => \"Quotidien\",\n    \"Weekly\" => \"Hebdomadaire\",\n    \"Monthly\" => \"Mensuel\",\n    \"Yearly\" => \"Annuel\",\n    \"daily\" => \"Jour(s)\",\n    \"weekly\" => \"Semaine(s)\",\n    \"monthly\" => \"Mois\",\n    \"yearly\" => \"Année(s)\",\n    \"days\" => \"jours\",\n    \"weeks\" => \"semaines\",\n    \"months\" => \"mois\",\n    \"years\" => \"années\",\n    \"external_url\" => \"Visiter l'URL externe\",\n    \"empty_page\" => \"Page vide\",\n    \"clear_filters\" => \"Effacer les filtres\",\n    \"no_matching_subscriptions\" => \"Aucun abonnement ne correspond à vos critères de recherche\",\n    \"clone\" => \"Cloner\",\n    \"renew\" => \"Renouveler\",\n    \"calculate_next_payment_date\" => \"Calculer la date du prochain paiement\",\n    // Formulaire d'abonnement\n    \"add_subscription\" => \"Ajouter un abonnement\",\n    \"edit_subscription\" => \"Modifier l'abonnement\",\n    \"subscription_name\" => \"Nom de l'abonnement\",\n    \"logo_preview\" => \"Aperçu du logo\",\n    \"search_logo\" => \"Rechercher un logo sur le web\",\n    \"web_search\" => \"Recherche web\",\n    \"currency\" => \"Devise\",\n    \"payment_every\" => \"Paiement tous les\",\n    \"frequency\" => \"Fréquence\",\n    \"cycle\" => \"Cycle\",\n    \"no_category\" => \"Pas de catégorie\",\n    \"paid_by\" => \"Payé par\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Notes\",\n    \"enable_notifications\" => \"Activer les notifications pour cet abonnement\",\n    \"default_value_from_settings\" => \"Valeur par défaut des paramètres\",\n    \"cancellation_notification\" => \"Notification d'annulation\",\n    \"delete\" => \"Supprimer\",\n    \"cancel\" => \"Annuler\",\n    \"upload_logo\" => \"Télécharger le logo\",\n    // Page de statistiques\n    \"cant_convert_currency\" => \"Vous utilisez plusieurs devises dans vos abonnements. Pour obtenir des statistiques valides et précises, veuillez définir une clé API Fixer sur la page des paramètres.\",\n    \"general_statistics\" => \"Statistiques générales\",\n    \"active_subscriptions\" => \"Abonnements actifs\",\n    \"inactive_subscriptions\" => \"Abonnements inactifs\",\n    \"monthly_cost\" => \"Coût mensuel\",\n    \"yearly_cost\" => \"Coût annuel\",\n    \"average_monthly\" => \"Coût moyen mensuel de l'abonnement\",\n    \"most_expensive\" => \"Coût d'abonnement le plus élevé\",\n    \"amount_due\" => \"Montant dû ce mois-ci\",\n    \"percentage_budget_used\" => \"Pourcentage du budget utilisé\",\n    \"budget_remaining\" => \"Budget restant\",\n    \"amount_over_budget\" => \"Montant dépassant le budget\",\n    \"monthly_savings\" => \"Économies mensuelles (sur les abonnements inactifs)\",\n    \"yearly_savings\" => \"Économies annuelles (sur les abonnements inactifs)\",\n    \"split_views\" => \"Vues partagées\",\n    \"category_split\" => \"Répartition par catégorie\",\n    \"household_split\" => \"Répartition du ménage\",\n    \"payment_method_split\" => \"Répartition par méthode de paiement\",\n    \"total_cost_trend\" => \"Tendance du coût total\",\n    \"cost_vs_budget\" => \"Coût par rapport au budget\",\n    // Page À propos\n    \"about_and_credits\" => \"À propos et crédits\",\n    \"credits\" => \"Crédits\",\n    \"license\" => \"Licence\",\n    \"release_notes\" => \"Notes de version\",\n    \"update_available\" => \"Mise à jour disponible\",\n    \"issues_and_requests\" => \"Problèmes et demandes\",\n    \"the_author\" => \"L'auteur\",\n    \"icons\" => \"Icônes\",\n    \"payment_icons\" => \"Icônes de paiement\",\n    // Page de profil\n    \"upload_avatar\" => \"Télécharger un Avatar\",\n    \"file_type_error\" => \"Le type de fichier n'est pas pris en charge\",\n    \"user_details\" => \"Détails de l'utilisateur\",\n    \"two_factor_authentication\" => \"Authentification à deux facteurs\",\n    \"two_factor_info\" => \"L'authentification à deux facteurs ajoute une couche supplémentaire de sécurité à votre compte. <br>Vous aurez besoin d'une application d'authentification comme Google Authenticator, Authy ou Ente Auth pour scanner le code QR.\",\n    \"two_factor_enabled_info\" => \"Votre compte est sécurisé grâce à l'authentification à deux facteurs. Vous pouvez la désactiver en cliquant sur le bouton ci-dessus.\",\n    \"enable_two_factor_authentication\" => \"Activer l'authentification à deux facteurs\",\n    \"2fa_already_enabled\" => \"L'authentification à deux facteurs est déjà activée\",\n    \"totp_code_incorrect\" => \"Le code TOTP est incorrect\",\n    \"backup_codes\" => \"Codes de sauvegarde\",\n    \"download_backup_codes\" => \"Télécharger les codes de sauvegarde\",\n    \"copy_to_clipboard\" => \"Copier dans le presse-papiers\",\n    \"totp_backup_codes_info\" => \"Conservez ces codes en lieu sûr. Vous ne pourrez pas les récupérer plus tard.\",\n    \"disable_two_factor_authentication\" => \"Désactiver l'authentification à deux facteurs\",\n    \"totp_code\" => \"Code TOTP\",\n    \"api_key\" => \"Clé API\",\n    \"regenerate\" => \"Régénérer\",\n    \"api_key_info\" => \"La clé API est utilisée pour les applications tierces et les intégrations. Ne la partagez pas.\",\n    // Page de paramètres\n    \"monthly_budget\" => \"Budget mensuel\",\n    \"budget_info\" => \"Le budget mensuel est utilisé pour calculer les statistiques. Laissez vide pour désactiver.\",\n    \"household\" => \"Ménage\",\n    \"save_member\" => \"Enregistrer le membre\",\n    \"delete_member\" => \"Supprimer le membre\",\n    \"cant_delete_member\" => \"Impossible de supprimer le membre principal\",\n    \"cant_delete_member_in_use\" => \"Impossible de supprimer le membre utilisé dans l'abonnement\",\n    \"household_info\" => \"Le champ Courriel permet aux membres du ménage d'être informés des abonnements arrivant à expiration.\",\n    \"notifications\" => \"Notifications\",\n    \"enable_email_notifications\" => \"Activer les notifications par courriel\",\n    \"notify_me\" => \"Me prevenir\",\n    \"day_before\" => \"jour avant\",\n    \"on_due_date\" => \"Le jour de l'échéance\",\n    \"days_before\" => \"jours avant\",\n    \"smtp_address\" => \"Adresse SMTP\",\n    \"port\" => \"Port\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Nom d'utilisateur SMTP\",\n    \"smtp_password\" => \"Mot de passe SMTP\",\n    \"from_email\" => \"De l'adresse courriel (facultatif)\",\n    \"send_to_other_emails\" => \"Envoyer également des notifications aux adresses courriel suivantes (utilisez ; pour les séparer):\",\n    \"smtp_info\" => \"Le mot de passe SMTP est transmis et stocké en texte brut. Pour des raisons de sécurité, veuillez créer un compte uniquement à cette fin.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Jeton du bot Telegram\",\n    \"telegram_chat_id\" => \"ID de chat Telegram\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Jeton Pushplus\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"URL du webhook Mattermost\",\n    \"mattermost_bot_username\" => \"Nom d'utilisateur du bot Mattermost\",\n    \"mattermost_bot_icon_emoji\" => \"Emoji de l'icône du bot Mattermost\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"URL du webhook\",\n    \"request_method\" => \"Méthode de requête\",\n    \"custom_headers\" => \"En-têtes personnalisés\",\n    \"webhook_payload\" => \"Charge utile du webhook\",\n    \"payment_notifications_payload\" => \"Charge utile de la notification de paiement\",\n    \"cancelation_notification_payload\" => \"Charge utile de la notification d'annulation\",\n    \"variables_available\" => \"Variables disponibles\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Jeton\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Nom d'utilisateur du bot Discord\",\n    \"discord_bot_avatar_url\" => \"URL de l'avatar du bot Discord\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Clé utilisateur Pushover\",\n    \"host\" => \"Hôte\",\n    \"topic\" => \"Sujet\",\n    \"ignore_ssl_errors\" => \"Ignorer les erreurs SSL\",\n    \"categories\" => \"Catégories\",\n    \"save_category\" => \"Enregistrer la catégorie\",\n    \"delete_category\" => \"Supprimer la catégorie\",\n    \"cant_delete_category_in_use\" => \"Impossible de supprimer la catégorie utilisée dans l'abonnement\",\n    \"currencies\" => \"Devises\",\n    \"save_currency\" => \"Enregistrer la devise\",\n    \"delete_currency\" => \"Supprimer la devise\",\n    \"cant_delete_main_currency\" => \"Impossible de supprimer la devise principale\",\n    \"cant_delete_currency_in_use\" => \"Impossible de supprimer la devise utilisée dans l'abonnement\",\n    \"exchange_update\" => \"Les taux de change ont été mis à jour pour la dernière fois le\",\n    \"currency_info\" => \"Trouvez les devises prises en charge et les codes de devise corrects sur\",\n    \"currency_performance\" => \"Pour des performances améliorées, ne conservez que les devises que vous utilisez.\",\n    \"fixer_api_key\" => \"Clé API de Fixer\",\n    \"provider\" => \"Fournisseur\",\n    \"fixer_info\" => \"Si vous utilisez plusieurs devises et souhaitez des statistiques et un tri précis sur les abonnements, une clé API GRATUITE de Fixer est nécessaire.\",\n    \"get_key\" => \"Obtenez votre clé sur\",\n    \"get_free_fixer_api_key\" => \"Obtenez une clé API Fixer gratuite\",\n    \"get_key_alternative\" => \"Vous pouvez également obtenir une clé api de fixation gratuite auprès de\",\n    \"ai_model\" => \"Modèle AI\",\n    \"select_ai_model\" => \"Sélectionner le modèle AI\",\n    \"run_schedule\" => \"Exécuter le programme\",\n    \"manually\" => \"Manuellement\",\n    \"coming_soon\" => \"À venir\",\n    \"invalid_host\" => \"Hôte invalide\",\n    \"ai_recommendations_info\" => \"Les recommandations de l'IA sont générées en fonction de vos abonnements et des membres de votre foyer.\",\n    \"may_take_time\" => \"En fonction du fournisseur, du modèle et du nombre d'abonnements, la génération de recommandations peut prendre un certain temps.\",\n    \"recommendations_visible_on_dashboard\" => \"Les recommandations seront visibles sur le tableau de bord.\",\n    \"generate_recommendations\" => \"Générer des recommandations\",\n    \"display_settings\" => \"Paramètres d'affichage\",\n    \"theme_settings\" => \"Paramètres de thème\",\n    \"colors\" => \"Couleurs\",\n    \"custom_colors\" => \"Couleurs personnalisées\",\n    \"theme\" => \"Thème\",\n    \"dark_theme\" => \"Thème sombre\",\n    \"light_theme\" => \"Thème clair\",\n    \"automatic\" => \"Automatique\",\n    \"main_color\" => \"Couleur principale\",\n    \"accent_color\" => \"Couleur d'accent\",\n    \"hover_color\" => \"Couleur de survol\",\n    \"save_custom_colors\" => \"Enregistrer les couleurs personnalisées\",\n    \"reset_custom_colors\" => \"Réinitialiser les couleurs personnalisées\",\n    \"custom_css\" => \"CSS personnalisé\",\n    \"save_custom_css\" => \"Enregistrer le CSS personnalisé\",\n    \"calculate_monthly_price\" => \"Calculer et afficher le prix mensuel pour tous les abonnements\",\n    \"convert_prices\" => \"Convertir toujours et afficher les prix dans ma devise principale (plus lent)\",\n    \"show_original_price\" => \"Afficher également le prix original lorsque des conversions ou des calculs sont effectués\",\n    \"experience\" => \"Expérience\",\n    \"show_subscription_progress\" => \"Afficher la progression de l'abonnement\",\n    \"disabled_subscriptions\" => \"Abonnements désactivés\",\n    \"hide_disabled_subscriptions\" => \"Masquer les abonnements désactivés\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Afficher les abonnements désactivés en bas\",\n    \"experimental_settings\" => \"Paramètres expérimentaux\",\n    \"remove_background\" => \"Tenter de supprimer l'arrière-plan des logos de la recherche d'images\",\n    \"use_mobile_navigation_bar\" => \"Utiliser la barre de navigation mobile\",\n    \"experimental_info\" => \"Les paramètres expérimentaux ne fonctionneront probablement pas parfaitement.\",\n    \"payment_methods\" => \"Méthodes de paiement\",\n    \"payment_methods_info\" => \"Cliquez sur une méthode de paiement pour la désactiver / l'activer.\",\n    \"rename_payment_methods_info\" => \"Cliquez sur le nom d'un mode de paiement pour le renommer.\",\n    \"cant_delete_payment_method_in_use\" => \"Impossible de désactiver la méthode de paiement utilisée\",\n    \"add_custom_payment\" => \"Ajouter un paiement personnalisé\",\n    \"payment_method_name\" => \"Nom de la méthode de paiement\",\n    \"payment_method_added_successfuly\" => \"Méthode de paiement ajoutée avec succès\",\n    \"payment_method_removed\" => \"Méthode de paiement supprimée\",\n    \"disable\" => \"Désactiver\",\n    \"enable\" => \"Activer\",\n    \"rename_payment_method\" => \"Renommer la méthode de paiement\",\n    \"payment_renamed\" => \"Méthode de paiement renommée\",\n    \"payment_not_renamed\" => \"La méthode de paiement n'a pas été renommée\",\n    \"test\" => \"Test\",\n    \"add\" => \"Ajouter\",\n    \"save\" => \"Enregistrer\",\n    \"reset\" => \"Réinitialiser\",\n    \"main_accent_color_error\" => \"La couleur principale et la couleur d'accent ne peuvent pas être identiques\",\n    \"backup_and_restore\" => \"Sauvegarde et restauration\",\n    \"backup\" => \"Sauvegarde\",\n    \"restore\" => \"Restauration\",\n    \"restore_info\" => \"La restauration de la base de données annulera toutes les données actuelles. Vous serez déconnecté après la restauration.\",\n    \"account\" => \"Compte\",\n    \"export_subscriptions\" => \"Exporter les abonnements\",\n    \"export_as_json\" => \"Exporter en JSON\",\n    \"export_as_csv\" => \"Exporter en CSV\",\n    \"danger_zone\" => \"Zone de danger\",\n    \"delete_account\" => \"Supprimer le compte\",\n    \"delete_account_info\" => \"La suppression de votre compte entraînera également la suppression de tous vos abonnements et paramètres.\",\n    // Menu des filtes\n    \"filter\" => \"Filtre\",\n    \"clear\" => \"Effacer\",\n    // Toast\n    \"success\" => \"Succès\",\n    // Réponses de l'API\n    \"session_expired\" => \"Votre session a expiré. Veuillez vous reconnecter\",\n    \"fields_missing\" => \"Certains champs manquent\",\n    \"fill_all_fields\" => \"Veuillez remplir tous les champs\",\n    \"fill_mandatory_fields\" => \"Veuillez remplir tous les champs obligatoires\",\n    \"error\" => \"Erreur\",\n    // Catégorie\n    \"failed_add_category\" => \"Échec de l'ajout de la catégorie\",\n    \"failed_edit_category\" => \"Échec de la modification de la catégorie\",\n    \"category_in_use\" => \"La catégorie est utilisée dans des abonnements et ne peut pas être supprimée\",\n    \"failed_remove_category\" => \"Échec de la suppression de la catégorie\",\n    \"category_saved\" => \"Catégorie enregistrée\",\n    \"category_removed\" => \"Catégorie supprimée\",\n    \"sort_order_saved\" => \"L'ordre de tri a été enregistré\",\n    // Devise\n    \"currency_saved\" => \"a été enregistrée.\",\n    \"error_adding_currency\" => \"Erreur lors de l'ajout de l'entrée de devise.\",\n    \"failed_to_store_currency\" => \"Échec de l'enregistrement de la devise dans la base de données.\",\n    \"currency_in_use\" => \"La devise est utilisée dans des abonnements et ne peut pas être supprimée.\",\n    \"currency_is_main\" => \"La devise est définie comme devise principale et ne peut pas être supprimée.\",\n    \"failed_to_remove_currency\" => \"Échec de la suppression de la devise de la base de données.\",\n    \"failed_to_store_api_key\" => \"Échec de l'enregistrement de la clé API dans la base de données.\",\n    \"invalid_api_key\" => \"Clé API invalide.\",\n    \"api_key_saved\" => \"Clé API enregistrée avec succès\",\n    \"currency_removed\" => \"Devise supprimée\",\n    // Ménage\n    \"failed_add_household\" => \"Échec de l'ajout de membre du ménage\",\n    \"failed_edit_household\" => \"Échec de la modification du membre du ménage\",\n    \"failed_remove_household\" => \"Échec de la suppression du membre du ménage\",\n    \"household_in_use\" => \"Le membre du ménage est utilisé dans des abonnements et ne peut pas être supprimé\",\n    \"member_saved\" => \"Membre enregistré\",\n    \"member_removed\" => \"Membre supprimé\",\n    // Notifications\n    \"error_saving_notifications\" => \"Erreur lors de l'enregistrement des données de notifications.\",\n    \"wallos_notification\" => \"Notification de Wallos\",\n    \"test_notification\" => \"Il s'agit d'une notification de test. Si vous la voyez, la configuration est correcte.\",\n    \"email_error\" => \"Erreur dlors de l'envoi de courriel\",\n    \"notification_sent_successfuly\" => \"Notification envoyée avec succès\",\n    \"notifications_settings_saved\" => \"Paramètres de notifications enregistrés avec succès.\",\n    \"notification_failed\" => \"Échec de la notification\",\n    // Paiements\n    \"payment_in_use\" => \"Impossible de désactiver la méthode de paiement utilisée\",\n    \"failed_update_payment\" => \"Échec de la mise à jour de la méthode de paiement dans la base de données\",\n    \"enabled\" => \"activé\",\n    \"disabled\" => \"désactivé\",\n    // Abonnement\n    \"error_fetching_image\" => \"Erreur lors de la récupération de l'image\",\n    \"subscription_updated_successfuly\" => \"Abonnement mis à jour avec succès\",\n    \"subscription_added_successfuly\" => \"Abonnement ajouté avec succès\",\n    \"error_deleting_subscription\" => \"Erreur de suppression de l'abonnement.\",\n    \"invalid_request_method\" => \"Méthode de demande invalide.\",\n    // Utilisateur\n    \"error_updating_user_data\" => \"Erreur lors de la mise à jour des données utilisateur.\",\n    \"user_details_saved\" => \"Détails de l'utilisateur enregistrés\",\n    // Admin Page\n    \"registrations\" => \"Inscriptions\",\n    \"enable_user_registrations\" => \"Activer les inscriptions d'utilisateurs\",\n    \"maximum_number_users\" => \"Nombre maximum d'utilisateurs\",\n    \"require_email_verification\" => \"Exiger la vérification de l'adresse courriel\",\n    \"configure_smtp_settings_to_enable\" => \"Configurer les paramètres SMTP pour activer\",\n    \"server_url\" => \"URL du serveur\",\n    \"server_url_info\" => \"Utilisé pour la vérification du courrier électronique et la récupération du mot de passe. Il doit s'agir d'une URL publique valide.\",\n    \"server_url_password_reset\" => \"Si cette option est activée, la fonction de réinitialisation du mot de passe sera également activée.\",\n    \"disable_login\" => \"Désactiver la connexion\",\n    \"disable_login_info\" => \"Contourner le login. Si vous utilisez votre serveur sur un réseau local uniquement, sans accès externe, vous pouvez désactiver le login. L'utilisateur admin se connectera automatiquement.\",\n    \"disable_login_info2\" => \"Vous ne pouvez activer ce paramètre que si l'enregistrement des utilisateurs est désactivé et qu'il n'y a pas d'autre compte utilisateur que celui de l'administrateur.\",\n    \"max_users_info\" => \"0 signifie un nombre illimité d'utilisateurs\",\n    \"user_management\" => \"Gestion des utilisateurs\",\n    \"delete_user\" => \"Supprimer l'utilisateur\",\n    \"delete_user_info\" => \"La suppression d'un utilisateur supprimera également tous ses abonnements et paramètres.\",\n    \"create_user\" => \"Créer un utilisateur\",\n    \"oidc_settings\" => \"Paramètres OIDC\",\n    \"oidc_auth_enabled\" => \"Authentification OIDC activée\",\n    \"create_user_automatically\" => \"Créer un utilisateur automatiquement\",\n    \"disable_password_login\" => \"Désactiver la connexion par mot de passe\",\n    \"smtp_settings\" => \"Paramètres SMTP\",\n    \"smtp_usage_info\" => \"Sera utilisé pour la récupération du mot de passe et d'autres e-mails système.\",\n    \"security_settings\" => \"Paramètres de sécurité\",\n    \"ssrf_protection_info\" => \"Pour prévenir les attaques Server-Side Request Forgery (SSRF), Wallos bloque par défaut les notifications webhook vers des adresses réseau privées ou internes.\",\n    \"local_webhook_info\" => \"Si vous devez envoyer des webhooks vers des services locaux (comme Home Assistant, Gotify ou Node-RED), saisissez leurs adresses IP ou noms d'hôte ci-dessus sous forme d'une liste séparée par des virgules (par ex. <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Tâches de maintenance\",\n    \"orphaned_logos\" => \"Logos orphelins\",\n    \"update\" => \"Mise à jour\",\n    \"new_version_available\" => \"Une nouvelle version de Wallos est disponible\",\n    \"current_version\" => \"Version actuelle\",\n    \"latest_version\" => \"Dernière version\",\n    \"on_current_version\" => \"Vous utilisez la dernière version de Wallos.\",\n    \"show_update_notification\" => \"Afficher la notification de mise à jour sur le tableau de bord\",\n    \"cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"Votre adresse courriel a été vérifiée avec succès\",\n    \"email_verification_failed\" => \"La vérification de l'adresse courriel a échoué\",\n    // Calendar\n    \"calendar\" => \"Calendrier\",\n    \"sun\" => \"Dim\",\n    \"mon\" => \"Lun\",\n    \"tue\" => \"Mar\",\n    \"wed\" => \"Mer\",\n    \"thu\" => \"Jeu\",\n    \"fri\" => \"Ven\",\n    \"sat\" => \"Sam\",\n    \"month-01\" => \"Janvier\",\n    \"month-02\" => \"Février\",\n    \"month-03\" => \"Mars\",\n    \"month-04\" => \"Avril\",\n    \"month-05\" => \"Mai\",\n    \"month-06\" => \"Juin\",\n    \"month-07\" => \"Juillet\",\n    \"month-08\" => \"Août\",\n    \"month-09\" => \"Septembre\",\n    \"month-10\" => \"Octobre\",\n    \"month-11\" => \"Novembre\",\n    \"month-12\" => \"Décembre\",\n    \"total_cost\" => \"Coût total\",\n    \"export_icalendar\" => \"Exporter en iCalendar\",\n    \"over_budget_warning\" => \"Vous avez dépassé votre budget\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Veuillez insérer le code TOTP\",\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/getlang.php",
    "content": "<?php\r\n\r\n$lang = \"en\";\r\nif (isset($_COOKIE['language'])) {\r\n    $selectedLanguage = $_COOKIE['language'];\r\n\r\n    if (array_key_exists($selectedLanguage, $languages)) {\r\n        $lang = $selectedLanguage;\r\n    }\r\n}\r\n\r\nfunction translate($text, $translations)\r\n{\r\n    if (array_key_exists($text, $translations)) {\r\n        return $translations[$text];\r\n    } else {\r\n        require 'en.php';\r\n        if (array_key_exists($text, $i18n)) {\r\n            return $i18n[$text];\r\n        } else {\r\n            return \"[i18n String Missing]\";\r\n        }\r\n    }\r\n}\r\n\r\n?>"
  },
  {
    "path": "includes/i18n/id.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Anda perlu membuat akun sebelum dapat masuk\",\n    \"username\" => \"Nama Pengguna\",\n    \"password\" => \"Kata Sandi\",\n    \"email\" => \"Email\",\n    \"firstname\" => \"Nama depan\",\n    \"lastname\" => \"Nama belakang\",\n    \"confirm_password\" => \"Konfirmasi Kata Sandi\",\n    \"main_currency\" => \"Mata Uang Utama\",\n    \"language\" => \"Bahasa\",\n    \"passwords_dont_match\" => \"Kata sandi tidak cocok\",\n    \"username_exists\" => \"Nama pengguna sudah ada\",\n    \"email_exists\" => \"Email sudah ada\",\n    \"registration_failed\" => \"Pendaftaran gagal, silakan coba lagi.\",\n    \"register\" => \"Daftar\",\n    \"restore_database\" => \"Pulihkan Database\",\n    // Login Page\n    \"please_login\" => \"Silakan masuk\",\n    \"stay_logged_in\" => \"Tetap masuk (30 hari)\",\n    \"login\" => \"Masuk\",\n    \"login_with\" => \"Masuk dengan\",\n    \"or\" => \"atau\",\n    \"login_failed\" => \"Detail masuk salah\",\n    \"registration_successful\" => \"Pendaftaran berhasil\",\n    \"user_email_waiting_verification\" => \"Email Anda perlu diverifikasi. Silakan periksa email Anda.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Lupa Kata Sandi\",\n    \"reset_password\" => \"Atur Ulang Kata Sandi\",\n    \"reset_sent_check_email\" => \"Email pengaturan ulang telah dikirim. Silakan periksa email Anda.\",\n    \"password_reset_successful\" => \"Pengaturan ulang kata sandi berhasil\",\n    // Header\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Dasbor\",\n    \"subscriptions\" => \"Langganan\",\n    \"stats\" => \"Statistik\",\n    \"settings\" => \"Pengaturan\",\n    \"admin\" => \"Admin\",\n    \"about\" => \"Tentang\",\n    \"logout\" => \"Keluar\",\n    // Dashboard\n    \"hello\" => \"Halo\",\n    \"upcoming_payments\" => \"Pembayaran Mendatang\",\n    \"no_upcoming_payments\" => \"Anda tidak memiliki pembayaran mendatang\",\n    \"overdue_renewals\" => \"Perpanjangan Terlambat\",\n    \"ai_recommendations\" => \"Rekomendasi AI\",\n    \"your_budget\" => \"Anggaran Anda\",\n    \"budget\" => \"Anggaran\",\n    \"budget_used\" => \"Anggaran Digunakan\",\n    \"over_budget\" => \"Di Atas Anggaran\",\n    \"your_subscriptions\" => \"Langganan Anda\",\n    \"your_savings\" => \"Tabungan Anda\",\n    // Subscriptions page\n    \"subscription\" => \"Langganan\",\n    \"no_subscriptions_yet\" => \"Anda belum memiliki langganan\",\n    \"add_first_subscription\" => \"Tambahkan langganan pertama\",\n    \"new_subscription\" => \"Langganan Baru\",\n    \"search\" => \"Cari\",\n    \"state\" => \"Status\",\n    \"alphanumeric\" => \"Alfanumerik\",\n    \"sort\" => \"Urutkan\",\n    \"name\" => \"Nama\",\n    \"last_added\" => \"Terakhir Ditambahkan\",\n    \"price\" => \"Harga\",\n    \"next_payment\" => \"Pembayaran Berikutnya\",\n    \"renewal_type\" => \"Jenis Perpanjangan\",\n    \"auto_renewal\" => \"Perpanjangan Otomatis\",\n    \"automatically_renews\" => \"Diperpanjang secara otomatis\",\n    \"manual_renewal\" => \"Perpanjangan Manual\",\n    \"start_date\" => \"Tanggal Mulai\",\n    \"inactive\" => \"Nonaktifkan Langganan\",\n    \"replaced_with\" => \"Diganti dengan\",\n    \"none\" => \"Tidak ada\",\n    \"member\" => \"Anggota\",\n    \"category\" => \"Kategori\",\n    \"payment_method\" => \"Metode Pembayaran\",\n    \"Daily\" => \"Harian\",\n    \"Weekly\" => \"Mingguan\",\n    \"Monthly\" => \"Bulanan\",\n    \"Yearly\" => \"Tahunan\",\n    \"daily\" => \"Hari\",\n    \"weekly\" => \"Minggu\",\n    \"monthly\" => \"Bulan\",\n    \"yearly\" => \"Tahun\",\n    \"days\" => \"hari\",\n    \"weeks\" => \"minggu\",\n    \"months\" => \"bulan\",\n    \"years\" => \"tahun\",\n    \"external_url\" => \"Kunjungi URL Eksternal\",\n    \"empty_page\" => \"Halaman Kosong\",\n    \"clear_filters\" => \"Hapus Filter\",\n    \"no_matching_subscriptions\" => \"Tidak ada langganan yang cocok\",\n    \"clone\" => \"Duplikat\",\n    \"renew\" => \"Perpanjang\",\n    \"calculate_next_payment_date\" => \"Hitung Tanggal Pembayaran Berikutnya\",\n    // Subscription form\n    \"add_subscription\" => \"Tambahkan langganan\",\n    \"edit_subscription\" => \"Edit langganan\",\n    \"subscription_name\" => \"Nama langganan\",\n    \"logo_preview\" => \"Pratinjau Logo\",\n    \"search_logo\" => \"Cari logo di web\",\n    \"web_search\" => \"Pencarian Web\",\n    \"currency\" => \"Mata Uang\",\n    \"payment_every\" => \"Pembayaran setiap\",\n    \"frequency\" => \"Frekuensi\",\n    \"cycle\" => \"Siklus\",\n    \"no_category\" => \"Tidak ada kategori\",\n    \"paid_by\" => \"Dibayar oleh\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Catatan\",\n    \"enable_notifications\" => \"Aktifkan Notifikasi untuk langganan ini\",\n    \"default_value_from_settings\" => \"Nilai default dari pengaturan\",\n    \"cancellation_notification\" => \"Notifikasi Pembatalan\",\n    \"delete\" => \"Hapus\",\n    \"cancel\" => \"Batal\",\n    \"upload_logo\" => \"Unggah Logo\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Anda menggunakan beberapa mata uang pada langganan Anda. Untuk mendapatkan statistik yang valid dan akurat, silakan atur Kunci API Fixer di halaman pengaturan.\",\n    \"general_statistics\" => \"Statistik Umum\",\n    \"active_subscriptions\" => \"Langganan Aktif\",\n    \"inactive_subscriptions\" => \"Langganan Tidak Aktif\",\n    \"monthly_cost\" => \"Biaya Bulanan\",\n    \"yearly_cost\" => \"Biaya Tahunan\",\n    \"average_monthly\" => \"Rata-rata Biaya Langganan Bulanan\",\n    \"most_expensive\" => \"Biaya Langganan Termahal\",\n    \"amount_due\" => \"Jumlah yang jatuh tempo bulan ini\",\n    \"percentage_budget_used\" => \"Persentase anggaran yang digunakan\",\n    \"budget_remaining\" => \"Sisa Anggaran\",\n    \"amount_over_budget\" => \"Jumlah melebihi anggaran\",\n    \"monthly_savings\" => \"Penghematan Bulanan (pada langganan tidak aktif)\",\n    \"yearly_savings\" => \"Penghematan Tahunan (pada langganan tidak aktif)\",\n    \"split_views\" => \"Tampilan Terpisah\",\n    \"category_split\" => \"Pemisahan Kategori\",\n    \"household_split\" => \"Pemisahan Rumah Tangga\",\n    \"payment_method_split\" => \"Pemisahan Metode Pembayaran\",\n    \"total_cost_trend\" => \"Tren Biaya Total\",\n    \"cost_vs_budget\" => \"Biaya vs Anggaran\",\n    // About page\n    \"about_and_credits\" => \"Tentang dan Kredit\",\n    \"credits\" => \"Kredit\",\n    \"license\" => \"Lisensi\",\n    \"release_notes\" => \"Catatan Rilis\",\n    \"update_available\" => \"Pembaruan Tersedia\",\n    \"issues_and_requests\" => \"Masalah dan Permintaan\",\n    \"the_author\" => \"Penulis\",\n    \"icons\" => \"Ikon\",\n    \"payment_icons\" => \"Ikon Pembayaran\",\n    // Profile page\n    \"upload_avatar\" => \"Unggah Avatar\",\n    \"file_type_error\" => \"Jenis file yang disediakan tidak didukung.\",\n    \"user_details\" => \"Detail Pengguna\",\n    \"two_factor_authentication\" => \"Autentikasi Dua Faktor\",\n    \"two_factor_info\" => \"Autentikasi Dua Faktor menambahkan lapisan keamanan ekstra ke akun Anda.<br>Anda memerlukan aplikasi autentikator seperti Google Authenticator, Authy, atau Ente Auth untuk memindai kode QR.\",\n    \"two_factor_enabled_info\" => \"Akun Anda aman dengan Autentikasi Dua Faktor. Anda dapat menonaktifkannya dengan mengklik tombol di atas.\",\n    \"enable_two_factor_authentication\" => \"Aktifkan Autentikasi Dua Faktor\",\n    \"2fa_already_enabled\" => \"Autentikasi Dua Faktor sudah diaktifkan\",\n    \"totp_code_incorrect\" => \"Kode TOTP salah\",\n    \"backup_codes\" => \"Kode Cadangan\",\n    \"download_backup_codes\" => \"Unduh Kode Cadangan\",\n    \"copy_to_clipboard\" => \"Salin ke clipboard\",\n    \"totp_backup_codes_info\" => \"Kode-kode ini dapat digunakan untuk masuk jika Anda kehilangan akses ke aplikasi autentikator Anda.\",\n    \"disable_two_factor_authentication\" => \"Nonaktifkan Autentikasi Dua Faktor\",\n    \"totp_code\" => \"Kode TOTP\",\n    \"api_key\" => \"Kunci API\",\n    \"regenerate\" => \"Buat Ulang\",\n    \"api_key_info\" => \"Kunci API digunakan untuk mengakses API. Jaga kerahasiaannya.\",\n    // Settings page\n    \"monthly_budget\" => \"Anggaran Bulanan\",\n    \"budget_info\" => \"Anggaran bulanan digunakan untuk menghitung statistik\",\n    \"household\" => \"Rumah Tangga\",\n    \"save_member\" => \"Simpan Anggota\",\n    \"delete_member\" => \"Hapus Anggota\",\n    \"cant_delete_member\" => \"Tidak dapat menghapus anggota utama\",\n    \"cant_delete_member_in_use\" => \"Tidak dapat menghapus anggota yang digunakan dalam langganan\",\n    \"household_info\" => \"Bidang email memungkinkan anggota rumah tangga diberitahu tentang langganan yang akan kedaluwarsa.\",\n    \"notifications\" => \"Notifikasi\",\n    \"enable_email_notifications\" => \"Aktifkan notifikasi email\",\n    \"notify_me\" => \"Beri tahu saya\",\n    \"day_before\" => \"hari sebelum\",\n    \"on_due_date\" => \"Pada tanggal jatuh tempo\",\n    \"days_before\" => \"hari sebelum\",\n    \"smtp_address\" => \"Alamat SMTP\",\n    \"port\" => \"Port\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Nama Pengguna SMTP\",\n    \"smtp_password\" => \"Kata Sandi SMTP\",\n    \"from_email\" => \"Dari email (Opsional)\",\n    \"send_to_other_emails\" => \"Juga kirim notifikasi ke alamat email berikut (gunakan ; untuk memisahkannya):\",\n    \"other_emails_placeholder\" => \"pengguna@domain.com;tes@pengguna.com\",\n    \"smtp_info\" => \"Kata Sandi SMTP dikirim dan disimpan dalam bentuk teks biasa. Untuk keamanan, silakan buat akun khusus untuk ini.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Token Bot Telegram\",\n    \"telegram_chat_id\" => \"ID Obrolan Telegram\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Token Pushplus\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"URL Webhook Mattermost\",\n    \"mattermost_bot_username\" => \"Nama Pengguna Bot Mattermost\",\n    \"mattermost_bot_icon_emoji\" => \"Emoji Ikon Bot Mattermost\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"URL Webhook\",\n    \"request_method\" => \"Metode Permintaan\",\n    \"custom_headers\" => \"Header Kustom\",\n    \"webhook_payload\" => \"Payload Webhook\",\n    \"payment_notifications_payload\" => \"Payload Notifikasi Pembayaran\",\n    \"cancelation_notification_payload\" => \"Payload Notifikasi Pembatalan\",\n    \"variables_available\" => \"Variabel yang tersedia\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Nama Pengguna Bot Discord\",\n    \"discord_bot_avatar_url\" => \"URL Avatar Bot Discord\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Kunci Pengguna Pushover\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Topik\",\n    \"ignore_ssl_errors\" => \"Abaikan Kesalahan SSL\",\n    \"categories\" => \"Kategori\",\n    \"save_category\" => \"Simpan Kategori\",\n    \"delete_category\" => \"Hapus Kategori\",\n    \"cant_delete_category_in_use\" => \"Tidak dapat menghapus kategori yang digunakan dalam langganan\",\n    \"currencies\" => \"Mata Uang\",\n    \"save_currency\" => \"Simpan mata uang\",\n    \"delete_currency\" => \"Hapus mata uang\",\n    \"cant_delete_main_currency\" => \"Tidak dapat menghapus mata uang utama\",\n    \"cant_delete_currency_in_use\" => \"Tidak dapat menghapus mata uang yang digunakan dalam langganan\",\n    \"exchange_update\" => \"Nilai tukar terakhir diperbarui pada\",\n    \"currency_info\" => \"Temukan mata uang yang didukung dan kode mata uang yang benar di\",\n    \"currency_performance\" => \"Untuk kinerja yang lebih baik, simpan hanya mata uang yang Anda gunakan.\",\n    \"fixer_api_key\" => \"Kunci API Fixer\",\n    \"provider\" => \"Penyedia\",\n    \"fixer_info\" => \"Jika Anda menggunakan beberapa mata uang, dan menginginkan statistik dan pengurutan yang akurat pada langganan, Kunci API GRATIS dari Fixer diperlukan.\",\n    \"get_key\" => \"Dapatkan kunci Anda di\",\n    \"get_free_fixer_api_key\" => \"Dapatkan Kunci API Fixer gratis\",\n    \"get_key_alternative\" => \"Sebagai alternatif, Anda bisa mendapatkan kunci api fixer gratis dari\",\n    \"ai_model\" => \"Model AI\",\n    \"select_ai_model\" => \"Pilih Model AI\",\n    \"run_schedule\" => \"Jadwalkan Eksekusi\",\n    \"manually\" => \"Secara Manual\",\n    \"coming_soon\" => \"Segera Hadir\",\n    \"invalid_host\" => \"Host Tidak Valid\",\n    \"ai_recommendations_info\" => \"Rekomendasi AI dihasilkan berdasarkan langganan dan anggota rumah tangga Anda.\",\n    \"may_take_time\" => \"Bergantung pada penyedia, model, dan jumlah langganan, pembuatan rekomendasi mungkin memerlukan waktu.\",\n    \"recommendations_visible_on_dashboard\" => \"Rekomendasi akan terlihat di dasbor.\",\n    \"generate_recommendations\" => \"Hasilkan Rekomendasi\",\n    \"display_settings\" => \"Pengaturan Tampilan\",\n    \"theme_settings\" => \"Pengaturan Tema\",\n    \"colors\" => \"Warna\",\n    \"custom_colors\" => \"Warna Kustom\",\n    \"theme\" => \"Tema\",\n    \"dark_theme\" => \"Tema Gelap\",\n    \"light_theme\" => \"Tema Terang\",\n    \"automatic\"=> \"Otomatis\",\n    \"main_color\" => \"Warna Utama\",\n    \"accent_color\" => \"Warna Aksen\",\n    \"hover_color\" => \"Warna Hover\",\n    \"save_custom_colors\" => \"Simpan Warna Kustom\",\n    \"reset_custom_colors\" => \"Atur Ulang Warna Kustom\",\n    \"custom_css\" => \"CSS Kustom\",\n    \"save_custom_css\" => \"Simpan CSS Kustom\",\n    \"calculate_monthly_price\" => \"Hitung dan tampilkan harga bulanan untuk semua langganan\",\n    \"convert_prices\" => \"Selalu konversi dan tampilkan harga dalam mata uang utama saya (lebih lambat)\",\n    \"show_original_price\" => \"Juga tampilkan harga asli saat konversi atau perhitungan dilakukan\",\n    \"experience\" => \"Pengalaman\",\n    \"show_subscription_progress\" => \"Tampilkan progres langganan\",\n    \"disabled_subscriptions\" => \"Langganan yang Dinonaktifkan\",\n    \"hide_disabled_subscriptions\" => \"Sembunyikan langganan yang dinonaktifkan\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Tampilkan langganan yang dinonaktifkan di bagian bawah\",\n    \"experimental_settings\" => \"Pengaturan Eksperimental\",\n    \"remove_background\" => \"Coba hapus latar belakang logo dari pencarian gambar\",\n    \"use_mobile_navigation_bar\" => \"Gunakan bilah navigasi seluler\",\n    \"experimental_info\" => \"Pengaturan eksperimental mungkin tidak akan berfungsi dengan sempurna.\",\n    \"payment_methods\" => \"Metode Pembayaran\",\n    \"payment_methods_info\" => \"Klik metode pembayaran untuk menonaktifkan / mengaktifkannya.\",\n    \"rename_payment_methods_info\" => \"Klik nama pada metode pembayaran untuk mengganti namanya.\",\n    \"cant_delete_payment_method_in_use\" => \"Tidak dapat menonaktifkan metode pembayaran yang digunakan\",\n    \"add_custom_payment\" => \"Tambahkan Metode Pembayaran Kustom\",\n    \"payment_method_name\" => \"Nama Metode Pembayaran\",\n    \"payment_method_added_successfuly\" => \"Metode pembayaran berhasil ditambahkan\",\n    \"payment_method_removed\" => \"Metode pembayaran dihapus\",\n    \"disable\" => \"Nonaktifkan\",\n    \"enable\" => \"Aktifkan\",\n    \"rename_payment_method\" => \"Ganti Nama Metode Pembayaran\",\n    \"payment_renamed\" => \"Metode pembayaran diganti namanya\",\n    \"payment_not_renamed\" => \"Metode pembayaran tidak diganti namanya\",\n    \"test\" => \"Tes\",\n    \"add\" => \"Tambah\",\n    \"save\" => \"Simpan\",\n    \"reset\" => \"Atur Ulang\",\n    \"main_accent_color_error\" => \"Warna utama dan warna aksen tidak boleh sama\",\n    \"backup_and_restore\" => \"Cadangkan dan Pulihkan\",\n    \"backup\" => \"Cadangkan\",\n    \"restore\" => \"Pulihkan\",\n    \"restore_info\" => \"Memulihkan database akan menimpa semua data saat ini. Anda akan keluar setelah pemulihan.\",\n    \"account\" => \"Akun\",\n    \"export_subscriptions\" => \"Ekspor Langganan\",\n    \"export_as_json\" => \"Ekspor sebagai JSON\",\n    \"export_as_csv\" => \"Ekspor sebagai CSV\",\n    \"danger_zone\" => \"Zona Berbahaya\",\n    \"delete_account\" => \"Hapus Akun\",\n    \"delete_account_info\" => \"Menghapus akun Anda juga akan menghapus semua langganan dan pengaturan Anda.\",\n    // Filters menu\n    \"filter\" => \"Filter\",\n    \"clear\" => \"Hapus\",\n    // Toast\n    \"success\" => \"Sukses\",\n    // Endpoint responses\n    \"session_expired\" => \"Sesi Anda telah berakhir. Silakan masuk lagi\",\n    \"fields_missing\" => \"Beberapa bidang kosong\",\n    \"fill_all_fields\" => \"Silakan isi semua bidang\",\n    \"fill_mandatory_fields\" => \"Silakan isi semua bidang wajib\",\n    \"error\" => \"Kesalahan\",\n    // Category\n    \"failed_add_category\" => \"Gagal menambahkan kategori\",\n    \"failed_edit_category\" => \"Gagal mengedit kategori\",\n    \"category_in_use\" => \"Kategori sedang digunakan dalam langganan dan tidak dapat dihapus\",\n    \"failed_remove_category\" => \"Gagal menghapus kategori\",\n    \"category_saved\" => \"Kategori disimpan\",\n    \"category_removed\" => \"Kategori dihapus\",\n    \"sort_order_saved\" => \"Urutan sortir disimpan\",\n    // Currency\n    \"currency_saved\" => \"telah disimpan.\",\n    \"error_adding_currency\" => \"Kesalahan saat menambahkan entri mata uang.\",\n    \"failed_to_store_currency\" => \"Gagal menyimpan Mata Uang di Database.\",\n    \"currency_in_use\" => \"Mata uang sedang digunakan dalam langganan dan tidak dapat dihapus.\",\n    \"currency_is_main\" => \"Mata uang ditetapkan sebagai mata uang utama dan tidak dapat dihapus.\",\n    \"failed_to_remove_currency\" => \"Gagal menghapus mata uang dari Database.\",\n    \"failed_to_store_api_key\" => \"Gagal menyimpan Kunci API di Database.\",\n    \"invalid_api_key\" => \"Kunci API tidak valid.\",\n    \"api_key_saved\" => \"Kunci API berhasil disimpan\",\n    \"currency_removed\" => \"Mata uang dihapus\",\n    // Household\n    \"failed_add_household\" => \"Gagal menambahkan anggota rumah tangga\",\n    \"failed_edit_household\" => \"Gagal mengedit anggota rumah tangga\",\n    \"failed_remove_household\" => \"Gagal menghapus anggota rumah tangga\",\n    \"household_in_use\" => \"Anggota rumah tangga sedang digunakan dalam langganan dan tidak dapat dihapus\",\n    \"member_saved\" => \"Anggota disimpan\",\n    \"member_removed\" => \"Anggota dihapus\",\n    // Notifications\n    \"error_saving_notifications\" => \"Kesalahan saat menyimpan data notifikasi.\",\n    \"wallos_notification\" => \"Notifikasi Wallos\",\n    \"test_notification\" => \"Ini adalah notifikasi tes. Jika Anda melihat ini, konfigurasinya benar.\",\n    \"email_error\" => \"Kesalahan saat mengirim email\",\n    \"notification_sent_successfuly\" => \"Notifikasi berhasil dikirim\",\n    \"notifications_settings_saved\" => \"Pengaturan notifikasi berhasil disimpan.\",\n    \"notification_failed\" => \"Notifikasi gagal\",\n    // Payments\n    \"payment_in_use\" => \"Tidak dapat menonaktifkan metode pembayaran yang digunakan\",\n    \"failed_update_payment\" => \"Gagal memperbarui metode pembayaran di database\",\n    \"enabled\" => \"diaktifkan\",\n    \"disabled\" => \"dinonaktifkan\",\n    // Subscription\n    \"error_fetching_image\" => \"Kesalahan saat mengambil gambar\",\n    \"subscription_updated_successfuly\" => \"Langganan berhasil diperbarui\",\n    \"subscription_added_successfuly\" => \"Langganan berhasil ditambahkan\",\n    \"error_deleting_subscription\" => \"Kesalahan saat menghapus langganan.\",\n    \"invalid_request_method\" => \"Metode permintaan tidak valid.\",\n    // User\n    \"error_updating_user_data\" => \"Kesalahan saat memperbarui data pengguna.\",\n    \"user_details_saved\" => \"Detail pengguna disimpan\",\n    // Admin Page\n    \"registrations\" => \"Pendaftaran\",\n    \"enable_user_registrations\" => \"Aktifkan pendaftaran pengguna\",\n    \"maximum_number_users\" => \"Jumlah maksimum pengguna\",\n    \"require_email_verification\" => \"Perlu verifikasi email\",\n    \"configure_smtp_settings_to_enable\" => \"Konfigurasikan pengaturan SMTP untuk mengaktifkan\",\n    \"server_url\" => \"URL Server\",\n    \"server_url_info\" => \"Digunakan untuk verifikasi email dan pemulihan kata sandi. Harus berupa URL publik yang valid.\",\n    \"server_url_password_reset\" => \"Jika diatur juga akan mengaktifkan fungsionalitas pengaturan ulang kata sandi.\",\n    \"disable_login\" => \"Nonaktifkan masuk\",\n    \"disable_login_info\" => \"Lewati masuk. Jika Anda menjalankan server Anda di jaringan lokal saja, tanpa akses eksternal Anda dapat menonaktifkan proses masuk. Ini akan secara otomatis memasukkan pengguna admin.\",\n    \"disable_login_info2\" => \"Anda hanya dapat mengaktifkan pengaturan ini jika pendaftaran pengguna dinonaktifkan dan tidak ada lebih dari akun pengguna admin.\",\n    \"max_users_info\" => \"0 berarti tidak terbatas\",\n    \"user_management\" => \"Manajemen Pengguna\",\n    \"delete_user\" => \"Hapus Pengguna\",\n    \"delete_user_info\" => \"Menghapus pengguna juga akan menghapus semua langganan dan pengaturan mereka.\",\n    \"create_user\" => \"Buat Pengguna\",\n    \"oidc_settings\" => \"Pengaturan OIDC\",\n    \"oidc_oauth_enabled\" => \"Aktifkan OIDC/OAuth\",\n    \"create_user_automatically\" => \"Buat pengguna secara otomatis\",\n    \"disable_password_login\" => \"Nonaktifkan masuk dengan kata sandi\",\n    \"smtp_settings\" => \"Pengaturan SMTP\",\n    \"smtp_usage_info\" => \"Akan digunakan untuk pemulihan kata sandi dan email sistem lainnya.\",\n    \"security_settings\" => \"Pengaturan Keamanan\",\n    \"ssrf_protection_info\" => \"Untuk mencegah serangan Server-Side Request Forgery (SSRF), Wallos secara default memblokir notifikasi webhook ke alamat jaringan pribadi atau internal.\",\n    \"local_webhook_info\" => \"Jika Anda perlu mengirim webhook ke layanan lokal (seperti Home Assistant, Gotify, atau Node-RED), masukkan alamat IP atau nama host mereka di atas sebagai daftar yang dipisahkan koma (mis., <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Tugas Pemeliharaan\",\n    \"orphaned_logos\" => \"Logo Yatim Piatu\",\n    \"update\" => \"Perbarui\",\n    \"new_version_available\" => \"Versi baru Wallos tersedia\",\n    \"current_version\" => \"Versi Saat Ini\",\n    \"latest_version\" => \"Versi Terbaru\",\n    \"on_current_version\" => \"Anda menjalankan versi terbaru Wallos.\",\n    \"show_update_notification\" => \"Tampilkan notifikasi untuk pembaruan di dasbor\",\n    \"cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"Email berhasil diverifikasi\",\n    \"email_verification_failed\" => \"Verifikasi email gagal\",\n    // Calendar\n    \"calendar\" => \"Kalender\",\n    \"sun\" => \"Min\",\n    \"mon\" => \"Sen\",\n    \"tue\" => \"Sel\",\n    \"wed\" => \"Rab\",\n    \"thu\" => \"Kam\",\n    \"fri\" => \"Jum\",\n    \"sat\" => \"Sab\",\n    \"month-01\" => \"Januari\",\n    \"month-02\" => \"Februari\",\n    \"month-03\" => \"Maret\",\n    \"month-04\" => \"April\",\n    \"month-05\" => \"Mei\",\n    \"month-06\" => \"Juni\",\n    \"month-07\" => \"Juli\",\n    \"month-08\" => \"Agustus\",\n    \"month-09\" => \"September\",\n    \"month-10\" => \"Oktober\",\n    \"month-11\" => \"November\",\n    \"month-12\" => \"Desember\",\n    \"total_cost\" => \"Total Biaya\",\n    \"export_icalendar\" => \"Ekspor iCalendar\",\n    \"over_budget_warning\" => \"Anda melebihi anggaran\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Masukkan kode TOTP\",\n];\n\n?>\n"
  },
  {
    "path": "includes/i18n/it.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration\r\n    \"create_account\" => 'Devi creare un account prima di poter accedere',\r\n    \"username\" => 'Nome utente',\r\n    \"password\" => 'Password',\r\n    \"email\" => 'Email',\r\n    \"firstname\" => 'Nome di battesimo',\r\n    \"lastname\" => 'Cognome',\r\n    \"confirm_password\" => 'Conferma password',\r\n    \"main_currency\" => 'Valuta principale',\r\n    \"language\" => 'Lingua',\r\n    \"passwords_dont_match\" => 'Le password non corrispondono',\r\n    \"username_exists\" => 'Il nome utente esiste già',\r\n    \"email_exists\" => 'L\\'indirizzo email esiste già',\r\n    \"registration_failed\" => 'Registrazione fallita, riprova.',\r\n    \"register\" => 'Registrati',\r\n    \"restore_database\" => 'Ripristina database',\r\n\r\n    // Login\r\n    \"please_login\" => 'Per favore, accedi',\r\n    \"stay_logged_in\" => 'Rimani connesso (30 giorni)',\r\n    \"login\" => 'Accedi',\r\n    \"login_with\" => 'Accedi con',\r\n    \"or\" => 'o',\r\n    \"login_failed\" => 'Le credenziali non sono corrette',\r\n    \"registration_successful\" => \"L'account è stato creato con successo\",\r\n    \"user_email_waiting_verification\" => \"L'e-mail deve essere verificata. Controlla la tua email\",\r\n\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Hai dimenticato la password?\",\r\n    \"reset_password\" => \"Reimposta password\",\r\n    \"reset_sent_check_email\" => \"Un'email è stata inviata. Controlla la tua casella di posta\",\r\n    \"password_reset_successful\" => \"La password è stata reimpostata con successo\",\r\n\r\n    // Header\r\n    \"profile\" => 'Profilo',\r\n    \"dashboard\" => 'Dashboard',\r\n    \"subscriptions\" => 'Abbonamenti',\r\n    \"stats\" => 'Statistiche',\r\n    \"settings\" => 'Impostazioni',\r\n    \"admin\" => 'Amministrazione',\r\n    \"about\" => 'Informazioni',\r\n    \"logout\" => 'Esci',\r\n\r\n    // Dashboard\r\n    \"hello\" => \"Ciao\",\r\n    \"upcoming_payments\" => \"Pagamenti in arrivo\",\r\n    \"no_upcoming_payments\" => \"Non hai pagamenti in arrivo\",\r\n    \"overdue_renewals\" => \"Rinnovi scaduti\",\r\n    \"ai_recommendations\" => \"Raccomandazioni AI\",\r\n    \"your_budget\" => \"Il tuo budget\",\r\n    \"budget\" => \"Budget\",\r\n    \"budget_used\" => \"Budget Utilizzato\",\r\n    \"over_budget\" => \"Sopra il Budget\",\r\n    \"your_subscriptions\" => \"I tuoi Abbonamenti\",\r\n    \"your_savings\" => \"I tuoi Risparmi\",\r\n\r\n    // Subscriptions\r\n    \"subscription\" => 'Abbonamento',\r\n    \"no_subscriptions_yet\" => 'Non hai ancora nessun abbonamento',\r\n    \"add_first_subscription\" => 'Aggiungi il tuo primo abbonamento',\r\n    \"new_subscription\" => 'Nuovo abbonamento',\r\n    \"search\" => 'Cerca',\r\n    \"state\" => \"Stato\",\r\n    \"alphanumeric\" => 'Alfanumerico',\r\n    \"sort\" => 'Ordina',\r\n    \"name\" => 'Nome',\r\n    \"last_added\" => 'Ultimo aggiunto',\r\n    \"price\" => 'Prezzo',\r\n    \"next_payment\" => 'Prossimo pagamento',\r\n    \"renewal_type\" => 'Tipo di rinnovo',\r\n    \"auto_renewal\" => 'Rinnovo automatico',\r\n    \"automatically_renews\" => 'Si rinnova automaticamente',\r\n    \"manual_renewal\" => \"Rinnovo manuale\",\r\n    \"start_date\" => 'Data di inizio',\r\n    \"inactive\" => 'Disattiva abbonamento',\r\n    \"replaced_with\" => 'Sostituito con',\r\n    \"none\" => 'Nessuno',\r\n    \"member\" => 'Membro',\r\n    \"category\" => 'Categoria',\r\n    \"payment_method\" => 'Metodo di pagamento',\r\n    \"Daily\" => 'Quotidiano',\r\n    \"Weekly\" => 'Settimanale',\r\n    \"Monthly\" => 'Mensile',\r\n    \"Yearly\" => 'Annuale',\r\n    \"daily\" => 'Giorno/i',\r\n    \"weekly\" => 'Settimana/e',\r\n    \"monthly\" => 'Mese/i',\r\n    \"yearly\" => 'Anno/i',\r\n    \"days\" => 'giorni',\r\n    \"weeks\" => 'settimane',\r\n    \"months\" => 'mesi',\r\n    \"years\" => 'anni',\r\n    \"external_url\" => 'Apri URL esterno',\r\n    \"empty_page\" => 'Pagina vuota',\r\n    \"clear_filters\" => 'Pulisci filtri',\r\n    \"no_matching_subscriptions\" => 'Nessun abbonamento corrispondente',\r\n    \"clone\" => \"Clona\",\r\n    \"renew\" => \"Rinnova\",\r\n    \"calculate_next_payment_date\" => \"Calcola la prossima data di pagamento\",\r\n\r\n    // Add/Edit Subscription\r\n    \"add_subscription\" => 'Aggiungi abbonamento',\r\n    \"edit_subscription\" => 'Modifica abbonamento',\r\n    \"subscription_name\" => 'Nome abbonamento',\r\n    \"logo_preview\" => 'Anteprima del logo',\r\n    \"search_logo\" => 'Cerca il logo sul web',\r\n    \"web_search\" => 'Ricerca web',\r\n    \"currency\" => 'Valuta',\r\n    \"payment_every\" => 'Pagamento ogni',\r\n    \"frequency\" => 'Frequenza',\r\n    \"cycle\" => 'Ciclo',\r\n    \"no_category\" => 'Nessuna categoria',\r\n    \"paid_by\" => 'Pagato da',\r\n    \"url\" => 'URL',\r\n    \"notes\" => 'Note',\r\n    \"enable_notifications\" => 'Abilita notifiche per questo abbonamento',\r\n    \"default_value_from_settings\" => 'Valore predefinito dalle impostazioni',\r\n    \"cancellation_notification\" => \"Notifica di cancellazione\",\r\n    \"delete\" => 'Cancella',\r\n    \"cancel\" => 'Annulla',\r\n    \"upload_logo\" => 'Carica logo',\r\n\r\n    // Statistics\r\n    \"cant_convert_currency\" => \"Si stanno utilizzando più valute per gli abbonamenti. Per avere statistiche valide e precise, impostare una chiave API Fixer nella pagina delle impostazioni.\",\r\n    \"general_statistics\" => 'Statistiche generali',\r\n    \"active_subscriptions\" => 'Abbonamenti attivi',\r\n    \"inactive_subscriptions\" => 'Abbonamenti inattivi',\r\n    \"monthly_cost\" => 'Costo mensile',\r\n    \"yearly_cost\" => 'Costo annuale',\r\n    \"average_monthly\" => \"Costo medio mensile dell'abbonamento\",\r\n    \"most_expensive\" => \"Costo dell'abbonamento più elevato\",\r\n    \"amount_due\" => 'Importo dovuto questo mese',\r\n    \"percentage_budget_used\" => 'Percentuale del budget utilizzata',\r\n    \"budget_remaining\" => 'Budget rimanente',\r\n    \"amount_over_budget\" => 'Importo oltre il budget',\r\n    \"monthly_savings\" => 'Risparmi mensili (su abbonamenti inattivi)',\r\n    \"yearly_savings\" => 'Risparmi annuali (su abbonamenti inattivi)',\r\n    \"split_views\" => 'Visualizzazioni con grafici',\r\n    \"category_split\" => 'Suddivisione per categoria',\r\n    \"household_split\" => 'Suddivisione per nucleo familiare',\r\n    \"payment_method_split\" => 'Suddivisione per metodo di pagamento',\r\n    \"total_cost_trend\" => 'Trend del costo totale',\r\n    \"cost_vs_budget\" => 'Costo vs Budget',\r\n\r\n    // About\r\n    \"about_and_credits\" => 'Informazioni e crediti',\r\n    \"credits\" => \"Crediti\",\r\n    \"license\" => 'Licenza',\r\n    \"release_notes\" => \"Note sulla versione\",\r\n    \"update_available\" => \"Aggiornamento disponibile\",\r\n    \"issues_and_requests\" => 'Problemi e richieste',\r\n    \"the_author\" => \"L'autore\",\r\n    \"icons\" => 'Icone',\r\n    \"payment_icons\" => 'Icone di pagamento',\r\n\r\n    // Profile\r\n    \"upload_avatar\" => 'Carica avatar',\r\n    \"file_type_error\" => 'Il tipo di file fornito non è supportato.',\r\n    \"user_details\" => 'Dettagli utente',\r\n    \"two_factor_authentication\" => 'Autenticazione a due fattori',\r\n    \"two_factor_info\" => \"L'Autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al vostro account.<br>Per scansionare il codice QR è necessaria un'app di autenticazione come Google Authenticator, Authy o Ente Auth.\",\r\n    \"two_factor_enabled_info\" => \"Il vostro account è sicuro con l'Autenticazione a due fattori. È possibile disattivarla facendo clic sul pulsante in alto.\",\r\n    \"enable_two_factor_authentication\" => \"Abilita l'autenticazione a due fattori\",\r\n    \"2fa_already_enabled\" => \"L'autenticazione a due fattori è già abilitata\",\r\n    \"totp_code_incorrect\" => \"Il codice TOTP è incorretto\",\r\n    \"backup_codes\" => \"Codici di backup\",\r\n    \"download_backup_codes\" => \"Scarica i codici di backup\",\r\n    \"copy_to_clipboard\" => \"Copia negli appunti\",\r\n    \"totp_backup_codes_info\" => \"I codici di backup possono essere utilizzati per accedere al tuo account se non hai accesso al tuo dispositivo di autenticazione a due fattori.\",\r\n    \"disable_two_factor_authentication\" => \"Disabilita l'autenticazione a due fattori\",\r\n    \"totp_code\" => \"Codice TOTP\",\r\n    \"api_key\" => \"Chiave API\",\r\n    \"regenerate\" => \"Rigenera\",\r\n    \"api_key_info\" => \"La chiave API viene utilizzata per accedere ai dati tramite l'API di Wallos. Non condividere la tua chiave API con nessuno.\",\r\n    // Settings\r\n    \"monthly_budget\" => \"Budget mensile\",\r\n    \"budget_info\" => \"Il budget mensile viene utilizzato per calcolare le statistiche. Se non si desidera utilizzare questa funzionalità, impostare il budget su 0.\",\r\n    \"household\" => 'Nucleo familiare',\r\n    \"save_member\" => 'Salva membro',\r\n    \"delete_member\" => 'Elimina membro',\r\n    \"cant_delete_member\" => 'Non è possibile eliminare il membro principale',\r\n    \"cant_delete_member_in_use\" => 'Non è possibile eliminare un membro che utilizza almeno un abbonamento',\r\n    \"household_info\" => 'Il campo e-mail consente ai membri del nucleo familiare di essere avvisati degli abbonamenti in procinto di scadere.',\r\n    \"notifications\" => 'Notifiche',\r\n    \"enable_email_notifications\" => 'Abilita le notifiche via e-mail',\r\n    \"notify_me\" => 'Avvisami',\r\n    \"day_before\" => 'giorno prima',\r\n    \"on_due_date\" => 'Alla data di scadenza',\r\n    \"days_before\" => 'giorni prima',\r\n    \"smtp_address\" => 'Indirizzo SMTP',\r\n    \"port\" => 'Porta',\r\n    \"tls\" => 'TLS',\r\n    \"ssl\" => 'SSL',\r\n    \"smtp_username\" => 'Nome utente SMTP',\r\n    \"smtp_password\" => 'Password SMTP',\r\n    \"from_email\" => 'Da quale e-mail (Opzionale)',\r\n    \"smtp_info\" => 'La password SMTP viene memorizzata e trasmessa in chiaro. Per motivi di sicurezza, si prega di creare un account da utilizzare solo per questo.',\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Telegram Bot Token\",\r\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Pushplus Token\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\r\n    \"mattermost_bot_username\" => \"Mattermost Bot Nome utente\",\r\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Icon Emoji\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"Webhook URL\",\r\n    \"request_method\" => \"Metodo di richiesta\",\r\n    \"custom_headers\" => \"Intestazioni personalizzate\",\r\n    \"webhook_payload\" => \"Webhook Payload\",\r\n    \"payment_notifications_payload\" => \"Carica il payload della notifica di pagamento\",\r\n    \"cancelation_notification_payload\" => \"Carica il payload della notifica di cancellazione\",\r\n    \"variables_available\" => \"Variabili disponibili\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Nome utente del bot\",\r\n    \"discord_bot_avatar_url\" => \"URL dell'avatar del bot\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Chiave utente\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Topic\",\r\n    \"ignore_ssl_errors\" => \"Ignora errori SSL\",\r\n    \"categories\" => 'Categorie',\r\n    \"save_category\" => 'Salva categoria',\r\n    \"delete_category\" => 'Elimina categoria',\r\n    \"cant_delete_category_in_use\" => 'Non è possibile eliminare una categoria in uso da almeno un abbonamento',\r\n    \"currencies\" => 'Valute',\r\n    \"save_currency\" => 'Salva valuta',\r\n    \"delete_currency\" => 'Elimina valuta',\r\n    \"cant_delete_main_currency\" => 'Impossibile eliminare la valuta principale',\r\n    \"cant_delete_currency_in_use\" => 'Non è possibile eliminare la valuta in uso da almeno un abbonamento',\r\n    \"exchange_update\" => \"Tassi di cambio aggiornati l'ultima volta il\",\r\n    \"currency_info\" => 'Trova le valute supportate e i codici valuta corretti su',\r\n    \"currency_performance\" => 'Per garantire prestazioni migliori, tieni solo le valute che utilizzi.',\r\n    \"fixer_api_key\" => 'Chiave API di Fixer',\r\n    \"provider\" => 'Fornitore',\r\n    \"fixer_info\" => 'Se utilizzi più valute e desideri visualizzare statistiche e ordinamenti accurati sugli abbonamenti, è necessaria una chiave API (Gratuita) da Fixer.',\r\n    \"get_key\" => 'Ottieni la tua chiave su',\r\n    \"get_free_fixer_api_key\" => 'Ottieni gratuitamente la chiave API di Fixer',\r\n    \"get_key_alternative\" => 'In alternativa, puoi ottenere gratuitamente una chiave API di Fixer da',\r\n    \"ai_model\" => \"Modello AI\",\r\n    \"select_ai_model\" => \"Seleziona Modello AI\",\r\n    \"run_schedule\" => \"Esegui Programma\",\r\n    \"manually\" => \"Manuale\",\r\n    \"coming_soon\" => \"In Arrivo\",\r\n    \"invalid_host\" => \"Host Non Valido\",\r\n    \"ai_recommendations_info\" => \"Le raccomandazioni dell'IA sono generate in base ai tuoi abbonamenti e ai membri del tuo nucleo familiare.\",\r\n    \"may_take_time\" => \"A seconda del fornitore, del modello e del numero di abbonamenti, la generazione delle raccomandazioni potrebbe richiedere del tempo.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Le raccomandazioni saranno visibili sul dashboard.\",\r\n    \"generate_recommendations\" => \"Genera Raccomandazioni\",\r\n    \"display_settings\" => 'Impostazioni di visualizzazione',\r\n    \"theme_settings\" => 'Impostazioni del tema',\r\n    \"colors\" => 'Colori',\r\n    \"custom_colors\" => 'Colori personalizzati',\r\n    \"theme\" => 'Tema',\r\n    \"dark_theme\" => 'Tema scuro',\r\n    \"light_theme\" => 'Tema chiaro',\r\n    \"automatic\" => \"Automatico\",\r\n    \"main_color\" => \"Colore principale\",\r\n    \"accent_color\" => \"Colore di accento\",\r\n    \"hover_color\" => \"Colore al passaggio del mouse\",\r\n    \"save_custom_colors\" => \"Salva colori personalizzati\",\r\n    \"reset_custom_colors\" => \"Ripristina colori personalizzati\",\r\n    \"custom_css\" => \"CSS personalizzato\",\r\n    \"save_custom_css\" => \"Salva CSS personalizzato\",\r\n    \"calculate_monthly_price\" => 'Calcola e mostra il prezzo mensile per tutti gli abbonamenti',\r\n    \"convert_prices\" => 'Converti sempre e mostra i prezzi nella mia valuta principale (più lento)',\r\n    \"show_original_price\" => \"Mostra anche il prezzo originale quando vengono effettuate conversioni o calcoli\",\r\n    \"experience\" => 'Esperienza',\r\n    \"show_subscription_progress\" => 'Mostra il progresso dell\\'abbonamento',\r\n    \"disabled_subscriptions\" => 'Abbonamenti disattivati',\r\n    \"hide_disabled_subscriptions\" => 'Nascondi gli abbonamenti disattivati',\r\n    \"show_disabled_subscriptions_at_the_bottom\" => 'Mostra gli abbonamenti disattivati in fondo',\r\n    \"experimental_settings\" => 'Impostazioni sperimentali',\r\n    \"remove_background\" => 'Prova a rimuovere lo sfondo dei loghi dalla ricerca delle immagini',\r\n    \"use_mobile_navigation_bar\" => \"Utilizza la barra di navigazione mobile\",\r\n    \"experimental_info\" => 'Le impostazioni sperimentali potrebbero non funzioneranno perfettamente.',\r\n    \"payment_methods\" => 'Metodi di pagamento',\r\n    \"payment_methods_info\" => 'Fai clic su un metodo di pagamento per abilitarlo/disabilitarlo.',\r\n    \"rename_payment_methods_info\" => 'Fai clic sul nome di un metodo di pagamento per rinominarlo.',\r\n    \"cant_delete_payment_method_in_use\" => 'Non è possibile disabilitare un metodo di pagamento in uso',\r\n    \"add_custom_payment\" => 'Aggiungi metodo di pagamento personalizzato',\r\n    \"payment_method_name\" => 'Nome del metodo di pagamento',\r\n    \"payment_method_added_successfuly\" => 'Metodo di pagamento aggiunto con successo',\r\n    \"payment_method_removed\" => 'Metodo di pagamento rimosso',\r\n    \"disable\" => 'Disabilita',\r\n    \"enable\" => 'Abilita',\r\n    \"rename_payment_method\" => 'Rinomina metodo di pagamento',\r\n    \"payment_renamed\" => 'Metodo di pagamento rinominato',\r\n    \"payment_not_renamed\" => 'Metodo di pagamento non rinominato',\r\n    \"test\" => 'Test',\r\n    \"add\" => 'Aggiungi',\r\n    \"save\" => 'Salva',\r\n    \"reset\" => 'Ripristina',\r\n    \"main_accent_color_error\" => 'Il colore principale e il colore di accento non possono essere uguali',\r\n    \"backup_and_restore\" => 'Backup e ripristino',\r\n    \"backup\" => 'Backup',\r\n    \"restore\" => 'Ripristina',\r\n    \"restore_info\" => \"Il ripristino del database annullerà tutti i dati correnti. Al termine del ripristino, l'utente verrà disconnesso.\",\r\n    \"account\" => \"Account\",\r\n    \"export_subscriptions\" => \"Esporta abbonamenti\",\r\n    \"export_as_json\" => \"Esporta come JSON\",\r\n    \"export_as_csv\" => \"Esporta come CSV\",\r\n    \"danger_zone\" => \"Zona di pericolo\",\r\n    \"delete_account\" => \"Elimina account\",\r\n    \"delete_account_info\" => \"L'eliminazione del vostro account cancellerà anche tutte le vostre sottoscrizioni e impostazioni.\",\r\n\r\n    // Filters\r\n    \"filter\" => 'Filtra',\r\n    \"clear\" => 'Pulisci',\r\n\r\n    // Toast\r\n    \"success\" => 'Successo',\r\n\r\n    // Endpoint responses\r\n    \"session_expired\" => 'La tua sessione è scaduta. Effettua nuovamente il login',\r\n    \"fields_missing\" => 'Mancano alcuni campi',\r\n    \"fill_all_fields\" => 'Si prega di compilare tutti i campi',\r\n    \"fill_mandatory_fields\" => 'Si prega di compilare tutti i campi obbligatori',\r\n    \"error\" => 'Errore',\r\n\r\n    // Category\r\n    \"failed_add_category\" => 'Impossibile aggiungere la categoria',\r\n    \"failed_edit_category\" => 'Impossibile modificare la categoria',\r\n    \"category_in_use\" => \"La categoria è attualmente in uso da almeno un abbonamento\",\r\n    \"failed_remove_category\" => \"Impossibile rimuovere la categoria\",\r\n    \"category_saved\" => \"Categoria salvata\",\r\n    \"category_removed\" => \"Categoria rimossa\",\r\n    \"sort_order_saved\" => \"Ordine di visualizzazione salvato\",\r\n\r\n    // Currency\r\n    \"currency_saved\" => \"Valuta salvata con successo\",\r\n    \"error_adding_currency\" => \"Errore nell'aggiunta della valuta\",\r\n    \"failed_to_store_currency\" => \"Impossibile salvare la valuta nel Database\",\r\n    \"currency_in_use\" => \"La valuta è attualmente in uso da almeno un abbonamento\",\r\n    \"currency_is_main\" => \"Impossibile rimuovere la valuta principale\",\r\n    \"failed_to_remove_currency\" => \"Impossibile rimuovere la valuta\",\r\n    \"failed_to_store_api_key\" => \"Impossibile salvare la chiave API\",\r\n    \"invalid_api_key\" => \"Chiave API non valida\",\r\n    \"api_key_saved\" => \"Chiave API salvata\",\r\n    \"currency_removed\" => \"Valuta rimossa\",\r\n\r\n    // Household\r\n    \"failed_add_household\" => \"Impossibile aggiungere un membro del nucleo familiare\",\r\n    \"failed_edit_household\" => \"Impossibile modificare un membro del nucleo familiare\",\r\n    \"failed_remove_household\" => \"Impossibile rimuovere un membro del nucleo familiare\",\r\n    \"household_in_use\" => \"Il membro del nucleo familiare è attualmente in uso da almeno un abbonamento\",\r\n    \"member_saved\" => \"Membro salvato\",\r\n    \"member_removed\" => \"Membro rimosso\",\r\n\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Errore nel salvataggio delle notifiche\",\r\n    \"wallos_notification\" => \"Notifica Wallos\",\r\n    \"test_notification\" => \"Questa è una notifica di prova\",\r\n    \"email_error\" => \"Errore nell'invio dell'e-mail\",\r\n    \"notification_sent_successfuly\" => \"Notifica inviata con successo\",\r\n    \"notifications_settings_saved\" => \"Impostazioni delle notifiche salvate\",\r\n    \"notification_failed\" => \"Invio della notifica fallito\",\r\n\r\n    // Payments\r\n    \"payment_in_use\" => \"Questo metodo di pagamento è attualmente in uso da almeno un abbonamento\",\r\n    \"failed_update_payment\" => \"Aggiornamento del metodo di pagamento fallito\",\r\n    \"enabled\" => \"abilitato\",\r\n    \"disabled\" => \"disabilitato\",\r\n\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Errore nel recupero dell'immagine\",\r\n    \"subscription_updated_successfuly\" => \"Abbonamento aggiornato con successo\",\r\n    \"subscription_added_successfuly\" => \"Abbonamento aggiunto con successo\",\r\n    \"error_deleting_subscription\" => \"Errore nell'eliminazione dell'abbonamento\",\r\n    \"invalid_request_method\" => \"Metodo di richiesta non valido\",\r\n\r\n    // User\r\n    \"error_updating_user_data\" => \"Errore nell'aggiornamento dei dati utente\",\r\n    \"user_details_saved\" => \"Dettagli utente salvati\",\r\n\r\n    // Admin Page\r\n    \"registrations\" => \"Registrazioni\",\r\n    \"enable_user_registrations\" => \"Abilita le registrazioni utente\",\r\n    \"maximum_number_users\" => \"Numero massimo di utenti\",\r\n    \"require_email_verification\" => \"Richiedi la verifica dell'e-mail\",\r\n    \"configure_smtp_settings_to_enable\" => \"Configura le impostazioni SMTP per abilitare\",\r\n    \"server_url\" => \"URL del server\",\r\n    \"server_url_info\" => \"Utilizzato per la verifica dell'e-mail e il recupero della password. Deve essere un URL pubblico valido.\",\r\n    \"server_url_password_reset\" => \"Se impostato, abilita anche la funzionalità di reimpostazione della password.\",\r\n    \"disable_login\" => \"Disabilita il login\",\r\n    \"disable_login_info\" => \"Bypassare il login. Se si gestisce il server solo su una rete locale, senza accesso esterno, è possibile disabilitare il login. In questo modo, l'utente amministratore effettuerà automaticamente il login.\",\r\n    \"disable_login_info2\" => \"Questa impostazione può essere attivata solo se la registrazione degli utenti è disattivata e non ci sono più account utente oltre a quello dell'amministratore.\",\r\n    \"max_users_info\" => \"Impostare a 0 per un numero illimitato di utenti\",\r\n    \"user_management\" => \"Gestione utenti\",\r\n    \"delete_user\" => \"Elimina utente\",\r\n    \"delete_user_info\" => \"L'eliminazione di un utente eliminerà anche tutte le sue iscrizioni e impostazioni.\",\r\n    \"create_user\" => \"Crea utente\",\r\n    \"oidc_settings\" => \"Impostazioni OIDC\",\r\n    \"oidc_auth_enabled\" => \"Autenticazione OIDC abilitata\",\r\n    \"create_user_automatically\" => \"Crea utente automaticamente\",\r\n    \"disable_password_login\" => \"Disabilita la connessione con password\",\r\n    \"smtp_settings\" => \"Impostazioni SMTP\",\r\n    \"smtp_usage_info\" => \"Verrà utilizzato per il recupero della password e altre e-mail di sistema.\",\r\n    \"security_settings\" => \"Impostazioni di sicurezza\",\r\n    \"ssrf_protection_info\" => \"Per prevenire attacchi Server-Side Request Forgery (SSRF), Wallos blocca per impostazione predefinita le notifiche webhook verso indirizzi di rete privati o interni.\",\r\n    \"local_webhook_info\" => \"Se è necessario inviare webhook a servizi locali (come Home Assistant, Gotify o Node-RED), inserisci i loro indirizzi IP o nomi host sopra come elenco separato da virgole (ad es. <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"maintenance_tasks\" => \"Compiti di manutenzione\",\r\n    \"orphaned_logos\" => \"Loghi orfani\",\r\n    \"update\" => \"Aggiorna\",\r\n    \"new_version_available\" => \"È disponibile una nuova versione di Wallos\",\r\n    \"current_version\" => \"Versione attuale\",\r\n    \"latest_version\" => \"Ultima versione\",\r\n    \"on_current_version\" => \"Stai utilizzando l'ultima versione di Wallos.\",\r\n    \"show_update_notification\" => \"Mostra notifica di aggiornamento sulla dashboard\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n\r\n    // Email Verification\r\n    \"email_verified\" => \"L'indirizzo email è stato verificato con successo\",\r\n    \"email_verification_failed\" => \"La verifica dell'email è fallita\",\r\n\r\n    // Calendar\r\n    \"calendar\" => \"Calendario\",\r\n    \"sun\" => \"Dom\",\r\n    \"mon\" => \"Lun\",\r\n    \"tue\" => \"Mar\",\r\n    \"wed\" => \"Mer\",\r\n    \"thu\" => \"Gio\",\r\n    \"fri\" => \"Ven\",\r\n    \"sat\" => \"Sab\",\r\n    \"month-01\" => \"Gennaio\",\r\n    \"month-02\" => \"Febbraio\",\r\n    \"month-03\" => \"Marzo\",\r\n    \"month-04\" => \"Aprile\",\r\n    \"month-05\" => \"Maggio\",\r\n    \"month-06\" => \"Giugno\",\r\n    \"month-07\" => \"Luglio\",\r\n    \"month-08\" => \"Agosto\",\r\n    \"month-09\" => \"Settembre\",\r\n    \"month-10\" => \"Ottobre\",\r\n    \"month-11\" => \"Novembre\",\r\n    \"month-12\" => \"Dicembre\",\r\n    \"total_cost\" => \"Costo totale\",\r\n    \"export_icalendar\" => \"Esporta iCal\",\r\n    \"over_budget_warning\" => \"Avete superato il budget\",\r\n\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Inserisci il codice TOTP\",\r\n];\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/jp.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"ログインする前にアカウントを作成する必要があります\",\n    \"username\" => \"ユーザー名\",\n    \"password\" => \"パスワード\",\n    \"email\" => \"メール\",\n    \"firstname\" => \"名\",\n    \"lastname\" => \"苗字\",\n    \"confirm_password\" => \"パスワードの確認\",\n    \"main_currency\" => \"主要通貨\",\n    \"language\" => \"言語\",\n    \"passwords_dont_match\" => \"パスワードが違います\",\n    \"username_exists\" => \"ユーザー名が既に存在します\",\n    \"email_exists\" => \"メールアドレスが既に存在します\",\n    \"registration_failed\" => \"登録に失敗しました。もう一度お試しください。\",\n    \"register\" => \"登録する\",\n    \"restore_database\" => \"データベースをリストア\",\n    // Login Page\n    \"please_login\" => \"ログインしてください\",\n    \"stay_logged_in\" => \"ログインしたままにする (30日)\",\n    \"login\" => \"ログイン\",\n    \"login_with\" => \"ログインする\",\n    \"or\" => \"または\",\n    \"login_failed\" => \"ログイン情報が間違っています\",\n    \"registration_successful\" => \"登録に成功\",\n    \"user_email_waiting_verification\" => \"Eメールの確認が必要です。メールを確認してください。\",\n    // Password Reset Page\n    \"forgot_password\" => \"パスワードを忘れた場合\",\n    \"reset_password\" => \"パスワードをリセット\",\n    \"reset_sent_check_email\" => \"パスワードリセットリンクが送信されました。メールを確認してください。\",\n    \"password_reset_successful\" => \"パスワードリセットに成功\",\n    // Header\n    \"profile\" => \"プロフィール\",\n    \"dashboard\" => \"ダッシュボード\",\n    \"subscriptions\" => \"定期購入\",\n    \"stats\" => \"統計\",\n    \"settings\" => \"設定\",\n    \"admin\" => \"管理者\",\n    \"about\" => \"About\",\n    \"logout\" => \"ログアウト\",\n    // Dashboard\n    \"hello\" => \"こんにちは\",\n    \"upcoming_payments\" => \"今後の支払い\",\n    \"no_upcoming_payments\" => \"今後の支払いはありません\",\n    \"overdue_renewals\" => \"期限切れの更新\",\n    \"ai_recommendations\" => \"AIによる推奨\",\n    \"your_budget\" => \"あなたの予算\",\n    \"budget\" => \"予算\",\n    \"budget_used\" => \"予算の使用\",\n    \"over_budget\" => \"予算オーバー\",\n    \"your_subscriptions\" => \"あなたの定期購入\",\n    \"your_savings\" => \"あなたの貯蓄\",\n    // Subscriptions page\n    \"subscription\" => \"定期購入\",\n    \"no_subscriptions_yet\" => \"まだ定期購入がありません\",\n    \"add_first_subscription\" => \"最初の定期購入を追加する\",\n    \"new_subscription\" => \"新しい定期購入\",\n    \"search\" => \"検索\",\n    \"state\" => \"状態\",\n    \"alphanumeric\" => \"アルファベット順\",\n    \"sort\" => \"並べ替え\",\n    \"name\" => \"名前\",\n    \"last_added\" => \"最終追加日\",\n    \"price\" => \"金額\",\n    \"next_payment\" => \"次回支払い\",\n    \"renewal_type\" => \"更新タイプ\",\n    \"auto_renewal\" => \"自動更新\",\n    \"automatically_renews\" => \"自動更新\",\n    \"manual_renewal\" => \"手動更新\",\n    \"start_date\" => \"開始日\",\n    \"inactive\" => \"サブスクリプションを無効にする\",\n    \"replaced_with\" => \"置き換えられた\",\n    \"none\" => \"なし\",\n    \"member\" => \"メンバー\",\n    \"category\" => \"カテゴリ\",\n    \"payment_method\" => \"支払い方法\",\n    \"Daily\" => \"毎日\",\n    \"Weekly\" => \"毎週\",\n    \"Monthly\" => \"毎月\",\n    \"Yearly\" => \"毎年\",\n    \"daily\" => \"日\",\n    \"weekly\" => \"週\",\n    \"monthly\" => \"月\",\n    \"yearly\" => \"年\",\n    \"days\" => \"日毎\",\n    \"weeks\" => \"週毎\",\n    \"months\" => \"月毎\",\n    \"years\" => \"年毎\",\n    \"external_url\" => \"外部URLにアクセス\",\n    \"empty_page\" => \"空のページ\",\n    \"clear_filters\" => \"フィルタをクリア\",\n    \"no_matching_subscriptions\" => \"一致する定期購入がありません\",\n    \"clone\" => \"複製\",\n    \"renew\" => \"更新\",\n    \"calculate_next_payment_date\" => \"次回支払い日を計算\",\n    // Subscription form\n    \"add_subscription\" => \"定期購入の追加\",\n    \"edit_subscription\" => \"定期購入の編集\",\n    \"subscription_name\" => \"定期購入名\",\n    \"logo_preview\" => \"ロゴのプレビュー\",\n    \"search_logo\" => \"ウェブ上でロゴを検索する\",\n    \"web_search\" => \"ウェブ検索\",\n    \"currency\" => \"通貨\",\n    \"payment_every\" => \"支払い頻度\",\n    \"frequency\" => \"頻度\",\n    \"cycle\" => \"サイクル\",\n    \"no_category\" => \"カテゴリなし\",\n    \"paid_by\" => \"支払い元\",\n    \"url\" => \"URL\",\n    \"notes\" => \"注釈\",\n    \"enable_notifications\" => \"この定期購入の通知を有効にする\",\n    \"default_value_from_settings\" => \"設定からデフォルト値を使用\",\n    \"cancellation_notification\" => \"キャンセル通知\",\n    \"delete\" => \"削除\",\n    \"cancel\" => \"キャンセル\",\n    \"upload_logo\" => \"ロゴのアップロード\",\n    // Statistics page\n    \"cant_convert_currency\" => \"購読に複数の通貨を使用しています。有効で正確な統計を取るには、設定ページでFixer API Keyを設定してください。\",\n    \"general_statistics\" => \"一般統計\",\n    \"active_subscriptions\" => \"アクティブな定期購入\",\n    \"inactive_subscriptions\" => \"非アクティブなサブスクリプション\",\n    \"monthly_cost\" => \"月間費用\",\n    \"yearly_cost\" => \"年間費用\",\n    \"average_monthly\" => \"月額平均費用\",\n    \"most_expensive\" => \"最も高額な定期購入費用\",\n    \"amount_due\" => \"今月の支払額\",\n    \"percentage_budget_used\" => \"予算使用率\",\n    \"budget_remaining\" => \"予算残高\",\n    \"amount_over_budget\" => \"予算オーバー\",\n    \"monthly_savings\" => \"月間節約 (非アクティブな定期購入)\",\n    \"yearly_savings\" => \"年間節約 (非アクティブな定期購入)\",\n    \"split_views\" => \"分割表示\",\n    \"category_split\" => \"カテゴリ別\",\n    \"household_split\" => \"世帯別\",\n    \"payment_method_split\" => \"支払い方法別\",\n    \"total_cost_trend\" => \"合計費用のトレンド\",\n    \"cost_vs_budget\" => \"費用対予算\",\n    // About page\n    \"about_and_credits\" => \"概要とクレジット\",\n    \"credits\" => \"クレジット\",\n    \"license\" => \"ライセンス\",\n    \"release_notes\" => \"リリースノート\",\n    \"update_available\" => \"利用可能な更新\",\n    \"issues_and_requests\" => \"問題と要望\",\n    \"the_author\" => \"著者\",\n    \"icons\" => \"アイコン\",\n    \"payment_icons\" => \"支払いアイコン\",\n    // Profile page\n    \"upload_avatar\" => \"アバターをアップロードする\",\n    \"file_type_error\" => \"ファイルタイプが許可されていません\",\n    \"user_details\" => \"ユーザー詳細\",\n    \"two_factor_authentication\" => \"2要素認証\",\n    \"two_factor_info\" => \"二要素認証は、アカウントに追加のセキュリティレイヤーを追加します。QR コードをスキャンするには、Google Authenticator、Authy、Ente Auth などの認証アプリが必要です。\",\n    \"two_factor_enabled_info\" => \"お客様のアカウントは二要素認証で保護されています。上のボタンをクリックして無効にすることができます。\",\n    \"enable_two_factor_authentication\" => \"二要素認証を有効にする\",\n    \"2fa_already_enabled\" => \"2要素認証は既に有効です\",\n    \"totp_code_incorrect\" => \"TOTPコードが正しくありません\",\n    \"backup_codes\" => \"バックアップコード\",\n    \"download_backup_codes\" => \"バックアップコードをダウンロード\",\n    \"copy_to_clipboard\" => \"クリップボードにコピー\",\n    \"totp_backup_codes_info\" => \"これらのコードは、2要素認証アプリが利用できない場合に使用します。コードは一度しか表示されません。\",\n    \"disable_two_factor_authentication\" => \"二要素認証を無効にする\",\n    \"totp_code\" => \"TOTPコード\",\n    \"api_key\" => \"APIキー\",\n    \"regenerate\" => \"再生成\",\n    \"api_key_info\" => \"APIキーは、WallosのAPIを使用するために必要です。APIキーを再生成すると、以前のキーは無効になります。\",\n    // Settings page\n    \"monthly_budget\" => \"月間予算\",\n    \"budget_info\" => \"予算を設定すると、統計ページで予算と実際の支出を比較できます。\",\n    \"household\" => \"世帯\",\n    \"save_member\" => \"世帯員を保存\",\n    \"delete_member\" => \"世帯員を削除\",\n    \"cant_delete_member\" => \"世帯主は削除出ません\",\n    \"cant_delete_member_in_use\" => \"定期購入を使用中の世帯員は削除できません\",\n    \"household_info\" => \"Eメールフィールドでは、世帯のメンバーに購読期限が近づいたことを通知することができます。\",\n    \"notifications\" => \"通知\",\n    \"enable_email_notifications\" => \"電子メール通知を有効にする\",\n    \"notify_me\" => \"通知\",\n    \"day_before\" => \"日前\",\n    \"on_due_date\" => \"支払い日\",\n    \"days_before\" => \"日前\",\n    \"smtp_address\" => \"SMTPアドレス\",\n    \"port\" => \"ポート番号\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTPユーザー名\",\n    \"smtp_password\" => \"SMTPパスワード\",\n    \"from_email\" => \"送信元アドレス (オプション)\",\n    \"send_to_other_emails\" => \"通知を以下のメールアドレスにも送信する（区切りには ; を使用）:\",\n    \"other_emails_placeholder\" => \"user@domain.com;test@user.com\",\n    \"smtp_info\" => \"SMTPパスワードは平文で送信および保存されます。セキュリティのため専用のアカウントを作成してください。\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegramボットトークン\",\n    \"telegram_chat_id\" => \"TelegramチャットID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplusトークン\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot ユーザー名\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot アイコン絵文字\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"リクエストメソッド\",\n    \"custom_headers\" => \"カスタムヘッダー\",\n    \"webhook_payload\" => \"Webhookペイロード\",\n    \"payment_notifications_payload\" => \"支払い通知ペイロード\",\n    \"cancelation_notification_payload\" => \"キャンセル通知ペイロード\",\n    \"variables_available\" => \"利用可能な変数\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"トークン\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Discordボットユーザー名\",\n    \"discord_bot_avatar_url\" => \"DiscordボットアバターURL\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushoverユーザーキー\",\n    \"host\" => \"ホスト\",\n    \"topic\" => \"トピック\",\n    \"ignore_ssl_errors\" => \"SSLエラーを無視\",\n    \"categories\" => \"カテゴリ\",\n    \"save_category\" => \"カテゴリを保存\",\n    \"delete_category\" => \"カテゴリを削除\",\n    \"cant_delete_category_in_use\" => \"定期購入で使用中のカテゴリは削除できません\",\n    \"currencies\" => \"通貨\",\n    \"save_currency\" => \"通貨を保存\",\n    \"delete_currency\" => \"通貨を削除\",\n    \"cant_delete_main_currency\" => \"メイン通貨は削除できません\",\n    \"cant_delete_currency_in_use\" => \"定期購入で使用中の通貨は削除できません\",\n    \"exchange_update\" => \"為替レートの最終更新日\",\n    \"currency_info\" => \"サポートされている通貨と正しい通貨コードを見つける\",\n    \"currency_performance\" => \"Fパフォーマンスを向上させるには、使用する通貨のみを保持してください。\",\n    \"fixer_api_key\" => \"FixerのAPIキー\",\n    \"provider\" => \"プロバイダ\",\n    \"fixer_info\" => \"複数の通貨を使用し、定期購入に関する正確な統計と並べ替えが必要な場合は、Fixerからの無料APIキーが必要です。\",\n    \"get_key\" => \"キーを入手する\",\n    \"get_free_fixer_api_key\" => \"無料のFixer APIキーを取得\",\n    \"get_key_alternative\" => \"または、以下のサイトから無料のフィクサーapiキーを入手することもできます。\",\n    \"ai_model\" => \"AIモデル\",\n    \"select_ai_model\" => \"AIモデルを選択\",\n    \"run_schedule\" => \"スケジュールを実行\",\n    \"manually\" => \"手動\",\n    \"coming_soon\" => \"近日公開\",\n    \"invalid_host\" => \"無効なホスト\",\n    \"ai_recommendations_info\" => \"AIによる推奨は、あなたの定期購入と世帯メンバーに基づいて生成されます。\",\n    \"may_take_time\" => \"プロバイダ、モデル、定期購入数によっては、推奨の生成に時間がかかる場合があります。\",\n    \"recommendations_visible_on_dashboard\" => \"推奨はダッシュボードに表示されます。\",\n    \"generate_recommendations\" => \"推奨\",\n    \"display_settings\" => \"表示設定\",\n    \"theme_settings\" => \"テーマ設定\",\n    \"colors\" => \"色\",\n    \"custom_colors\" => \"カスタムカラー\",\n    \"theme\" => \"テーマ\",\n    \"dark_theme\" => \"ダークテーマ\",\n    \"light_theme\" => \"ライトテーマ\",\n    \"automatic\" => \"自動\",\n    \"main_color\" => \"メインカラー\",\n    \"accent_color\" => \"アクセントカラー\",\n    \"hover_color\" => \"ホバーカラー\",\n    \"save_custom_colors\" => \"カスタムカラーを保存\",\n    \"reset_custom_colors\" => \"カスタムカラーをリセット\",\n    \"custom_css\" => \"カスタムCSS\",\n    \"save_custom_css\" => \"カスタムCSSを保存\",\n    \"calculate_monthly_price\" => \"すべての定期購入の月額料金を計算して表示する\",\n    \"convert_prices\" => \"常にメイン通貨で価格を換算して表示する (遅い)\",\n    \"show_original_price\" => \"変換や計算が行われるときに元の価格も表示する\",\n    \"experience\" => \"体験\",\n    \"show_subscription_progress\" => \"定期購入の進捗を表示する\",\n    \"disabled_subscriptions\" => \"無効な定期購入\",\n    \"hide_disabled_subscriptions\" => \"無効な定期購入を非表示にする\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"無効な定期購入を一番下に表示する\",\n    \"experimental_settings\" => \"実験的な設定\",\n    \"remove_background\" => \"画像検索からロゴの背景を削除する\",\n    \"use_mobile_navigation_bar\" => \"モバイルナビゲーションバーを使用する\",\n    \"experimental_info\" => \"実験的な設定は、おそらく完全には機能しません。\",\n    \"payment_methods\" => \"支払い方法\",\n    \"payment_methods_info\" => \"支払い方法をクリックして無効/有効を切り替えます。\",\n    \"rename_payment_methods_info\" => \"支払い方法の名前をクリックして、名前を変更します。\",\n    \"cant_delete_payment_method_in_use\" => \"支払い方法が使用中のため無効にできません。\",\n    \"add_custom_payment\" => \"カスタム支払い方法を追加\",\n    \"payment_method_name\" => \"支払い方法名\",\n    \"payment_method_added_successfuly\" => \"支払い方法が追加されました\",\n    \"payment_method_removed\" => \"支払い方法が削除されました\",\n    \"disable\" => \"無効\",\n    \"enable\" => \"有効\",\n    \"rename_payment_method\" => \"支払い方法の名前を変更\",\n    \"payment_renamed\" => \"支払い方法が変更されました\",\n    \"payment_not_renamed\" => \"支払い方法が変更されませんでした\",\n    \"test\" => \"テスト\",\n    \"add\" => \"追加\",\n    \"save\" => \"保存\",\n    \"reset\" => \"リセット\",\n    \"main_accent_color_error\" => \"メインカラーとアクセントカラーは同じにすることはできません\",\n    \"backup_and_restore\" => \"バックアップとリストア\",\n    \"backup\" => \"バックアップ\",\n    \"restore\" => \"リストア\",\n    \"restore_info\" => \"データベースをリストアすると、現在のデータがすべて上書きされます。リストア後はサインアウトされます。\",\n    \"account\" => \"アカウント\",\n    \"export_subscriptions\" => \"定期購入をエクスポート\",\n    \"export_as_json\" => \"JSONとしてエクスポート\",\n    \"export_as_csv\" => \"CSVとしてエクスポート\",\n    \"danger_zone\" => \"危険地帯\",\n    \"delete_account\" => \"アカウントを削除\",\n    \"delete_account_info\" => \"アカウントを削除するとすべてのサブスクリプションと設定も削除されます。\",\n    // Filters menu\n    \"filter\" => \"フィルタ\",\n    \"clear\" => \"クリア\",\n    // Toast\n    \"success\" => \"成功\",\n    // Endpoint responses\n    \"session_expired\" => \"セッションの有効期限が切れました。再度ログインしてください。\",\n    \"fields_missing\" => \"いくつかの項目が抜けています\",\n    \"fill_all_fields\" => \"すべての項目を記入してください\",\n    \"fill_mandatory_fields\" => \"必須項目をすべて記入してください\",\n    \"error\" => \"エラー\",\n    // Category\n    \"failed_add_category\" => \"カテゴリの追加に失敗\",\n    \"failed_edit_category\" => \"カテゴリの編集に失敗\",\n    \"category_in_use\" => \"定期購入で使用中のカテゴリは削除できません\",\n    \"failed_remove_category\" => \"カテゴリの削除に失敗\",\n    \"category_saved\" => \"カテゴリの保存\",\n    \"category_removed\" => \"カテゴリの削除\",\n    \"sort_order_saved\" => \"並べ替え順が保存されました\",\n    // Currency\n    \"currency_saved\" => \"通貨を保存\",\n    \"error_adding_currency\" => \"通貨エントリの追加エラー.\",\n    \"failed_to_store_currency\" => \"データベースに通貨を保存できませんでした\",\n    \"currency_in_use\" => \"定期購入で使用中の通貨は削除できません\",\n    \"currency_is_main\" => \"メイン通貨に設定中の通貨は削除できません\",\n    \"failed_to_remove_currency\" => \"データベースから通貨を削除できませんでした\",\n    \"failed_to_store_api_key\" => \"データベースにAPIキーを保存できませんでした\",\n    \"invalid_api_key\" => \"無効なAPIキーです\",\n    \"api_key_saved\" => \"APIキーの保存に成功\",\n    \"currency_removed\" => \"通貨を削除\",\n    // Household\n    \"failed_add_household\" => \"世帯員の追加に失敗\",\n    \"failed_edit_household\" => \"世帯員の編集に失敗\",\n    \"failed_remove_household\" => \"世帯員の削除に失敗\",\n    \"household_in_use\" => \"定期購入を使用中の世帯員は削除できません\",\n    \"member_saved\" => \"世帯員を保存\",\n    \"member_removed\" => \"世帯員を削除\",\n    // Notifications\n    \"error_saving_notifications\" => \"通知データの保存エラー\",\n    \"wallos_notification\" => \"Wallosからの通知\",\n    \"test_notification\" => \"これは通知テストです。これが見られるなら成功です。\",\n    \"email_error\" => \"電子メールの送信エラー\",\n    \"notification_sent_successfuly\" => \"通知の送信に成功しました\",\n    \"notifications_settings_saved\" => \"通知設定の保存に成功\",\n    \"notification_failed\" => \"通知の送信に失敗\",\n    // Payments\n    \"payment_in_use\" => \"使用中の支払い方法は削除できません\",\n    \"failed_update_payment\" => \"データーベースの支払い方法の更新に失敗しました\",\n    \"enabled\" => \"有効\",\n    \"disabled\" => \"無効\",\n    // Subscription\n    \"error_fetching_image\" => \"画像の取得エラー\",\n    \"subscription_updated_successfuly\" => \"定期購入の更新成功\",\n    \"subscription_added_successfuly\" => \"定期購入の追加成功\",\n    \"error_deleting_subscription\" => \"定期購入の削除エラー\",\n    \"invalid_request_method\" => \"無効なリクエスト方法\",\n    // User\n    \"error_updating_user_data\" => \"ユーザデータの更新エラー\",\n    \"user_details_saved\" => \"ユーザー詳細の保存\",\n    // Admin Page\n    \"registrations\" => \"登録\",\n    \"enable_user_registrations\" => \"ユーザー登録を有効にする\",\n    \"maximum_number_users\" => \"最大ユーザ数\",\n    \"require_email_verification\" => \"メール確認を必要とする\",\n    \"configure_smtp_settings_to_enable\" => \"SMTP設定を構成して有効にする\",\n    \"server_url\" => \"サーバーURL\",\n    \"server_url_info\" => \"電子メール認証とパスワード回復に使用される。有効な公開URLでなければなりません。\",\n    \"server_url_password_reset\" => \"設定すると、パスワードリセット機能も有効になる。\",\n    \"disable_login\" => \"ログインを無効にする\",\n    \"disable_login_info\" => \"ログインをバイパスします。サーバーをローカルネットワークのみで運用し、外部からのアクセスがない場合、ログインを無効にすることができます。これにより、管理者ユーザが自動的にログインします。\",\n    \"disable_login_info2\" => \"この設定を有効にできるのは、ユーザー登録がオフで、管理者以上のユーザーアカウントが存在しない場合のみです。\",\n    \"max_users_info\" => \"0に設定すると無制限になります\",\n    \"user_management\" => \"ユーザー管理\",\n    \"delete_user\" => \"ユーザーを削除\",\n    \"delete_user_info\" => \"ユーザーを削除すると、そのユーザーのサブスクリプションと設定もすべて削除されます。\",\n    \"create_user\" => \"ユーザーを作成\",\n    \"oidc_settings\" => \"OIDC設定\",\n    \"oidc_auth_enabled\" => \"OIDC認証を有効にする\",\n    \"create_user_automatically\" => \"OIDCユーザーを自動的に作成する\",\n    \"disable_password_login\" => \"パスワードログインを無効にする\",\n    \"smtp_settings\" => \"SMTP設定\",\n    \"smtp_usage_info\" => \"パスワードの回復やその他のシステム電子メールに使用されます。\",\n    \"security_settings\" => \"セキュリティ設定\",\n    \"ssrf_protection_info\" => \"Server-Side Request Forgery（SSRF）攻撃を防ぐために、Wallosはデフォルトでプライベートまたは内部ネットワークアドレスへのWebhook通知をブロックします。\",\n    \"local_webhook_info\" => \"Home Assistant、Gotify、Node-REDなどのローカルサービスにWebhookを送信する必要がある場合は、それらのIPアドレスまたはホスト名を上記にカンマ区切りで入力してください（例：<code>192.168.1.100,192.168.1.101</code>）。\",\n    \"maintenance_tasks\" => \"メンテナンスタスク\",\n    \"orphaned_logos\" => \"孤立したロゴ\",\n    \"update\" => \"更新\",\n    \"new_version_available\" => \"新しいバージョンのWallosが利用可能です\",\n    \"current_version\" => \"現在のバージョン\",\n    \"latest_version\" => \"最新バージョン\",\n    \"on_current_version\" => \"最新バージョンのWallosを使用しています。\",\n    \"show_update_notification\" => \"ダッシュボードに更新通知を表示する\",\n    \"cronjobs\" => \"クロンジョブズ\",\n    // Email Verification\n    \"email_verified\" => \"メールアドレスが確認されました\",\n    \"email_verification_failed\" => \"メールアドレスの確認に失敗しました\",\n    // Calendar\n    \"calendar\" => \"カレンダー\",\n    \"sun\" => \"日\",\n    \"mon\" => \"月\",\n    \"tue\" => \"火\",\n    \"wed\" => \"水\",\n    \"thu\" => \"木\",\n    \"fri\" => \"金\",\n    \"sat\" => \"土\",\n    \"month-01\" => \"1月\",\n    \"month-02\" => \"2月\",\n    \"month-03\" => \"3月\",\n    \"month-04\" => \"4月\",\n    \"month-05\" => \"5月\",\n    \"month-06\" => \"6月\",\n    \"month-07\" => \"7月\",\n    \"month-08\" => \"8月\",\n    \"month-09\" => \"9月\",\n    \"month-10\" => \"10月\",\n    \"month-11\" => \"11月\",\n    \"month-12\" => \"12月\",\n    \"total_cost\" => \"合計費用\",\n    \"export_icalendar\" => \"iCalendarをエクスポート\",\n    \"over_budget_warning\" => \"予算オーバーだ\",\n    // TOTP Page\n    \"insert_totp_code\" => \"TOTPコードを入力してください\",\n    \n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/ko.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"로그인 하기 전에 회원가입을 진행해야 합니다.\",\n    \"username\" => \"유저명\",\n    \"password\" => \"비밀번호\",\n    \"email\" => \"이메일\",\n    \"firstname\" => \"이름\",\n    \"lastname\" => \"성\",\n    \"confirm_password\" => \"비밀번호 확인\",\n    \"main_currency\" => \"기본 통화\",\n    \"language\" => \"언어\",\n    \"passwords_dont_match\" => \"비밀번호가 일치하지 않습니다.\",\n    \"username_exists\" => \"이미 존재하는 유저명입니다.\",\n    \"email_exists\" => \"이미 존재하는 이메일입니다.\",\n    \"registration_failed\" => \"회원가입 실패. 다시 시도해 주세요.\",\n    \"register\" => \"회원가입\",\n    \"restore_database\" => \"데이터베이스 복구\",\n    // Login Page\n    \"please_login\" => \"로그인 해 주세요.\",\n    \"stay_logged_in\" => \"로그인 유지 (30일)\",\n    \"login\" => \"로그인\",\n    \"login_with\" => \"다음으로 로그인\",\n    \"or\" => \"또는\",\n    \"login_failed\" => \"로그인 정보가 부정확합니다.\",\n    \"registration_successful\" => \"등록 성공\",\n    \"user_email_waiting_verification\" => \"이메일을 인증해야 합니다. 이메일을 확인해 주세요.\",\n    // Password Reset Page\n    \"forgot_password\" => \"비밀번호를 잊으셨나요?\",\n    \"reset_password\" => \"비밀번호 재설정\",\n    \"reset_sent_check_email\" => \"비밀번호 재설정 이메일이 전송되었습니다. 이메일을 확인해 주세요.\",\n    \"password_reset_successful\" => \"비밀번호 재설정 성공\",\n    // Header\n    \"profile\" => \"프로필\",\n    \"dashboard\" => \"대시보드\",\n    \"subscriptions\" => \"구독\",\n    \"stats\" => \"통계\",\n    \"settings\" => \"설정\",\n    \"admin\" => \"관리자\",\n    \"about\" => \"정보\",\n    \"logout\" => \"로그아웃\",\n    // Dashboard\n    \"hello\" => \"안녕하세요\",\n    \"upcoming_payments\" => \"예정된 결제\",\n    \"no_upcoming_payments\" => \"예정된 결제가 없습니다.\",\n    \"overdue_renewals\" => \"연체 갱신\",\n    \"ai_recommendations\" => \"AI 추천\",\n    \"your_budget\" => \"당신의 예산\",\n    \"budget\" => \"예산\",\n    \"budget_used\" => \"예산 사용\",\n    \"over_budget\" => \"예산 초과\",\n    \"your_subscriptions\" => \"당신의 구독\",\n    \"your_savings\" => \"당신의 저축\",\n    // Subscriptions page\n    \"subscription\" => \"구독\",\n    \"no_subscriptions_yet\" => \"아직 구독을 등록하지 않았습니다.\",\n    \"add_first_subscription\" => \"첫번째 구독을 추가하세요.\",\n    \"new_subscription\" => \"새 구독\",\n    \"search\" => \"검색\",\n    \"state\" => \"상태\",\n    \"alphanumeric\" => \"알파벳순\",\n    \"sort\" => \"정렬\",\n    \"name\" => \"이름\",\n    \"last_added\" => \"최근 등록\",\n    \"price\" => \"가격\",\n    \"next_payment\" => \"다음 결제일\",\n    \"renewal_type\" => \"갱신 방법\",\n    \"auto_renewal\" => \"자동 갱신\",\n    \"automatically_renews\" => \"자동 갱신\",\n    \"manual_renewal\" => \"수동 갱신\",\n    \"start_date\" => \"시작일\",\n    \"inactive\" => \"구독 비활성화\",\n    \"replaced_with\" => \"다음 구독으로 대체됨\",\n    \"none\" => \"없음\",\n    \"member\" => \"구성원\",\n    \"category\" => \"카테고리\",\n    \"payment_method\" => \"지불 수단\",\n    \"Daily\" => \"일간 결제\",\n    \"Weekly\" => \"주간 결제\",\n    \"Monthly\" => \"월간 결제\",\n    \"Yearly\" => \"연간 결제\",\n    \"daily\" => \"일\",\n    \"weekly\" => \"주\",\n    \"monthly\" => \"월\",\n    \"yearly\" => \"년\",\n    \"days\" => \"매일\",\n    \"weeks\" => \"매주\",\n    \"months\" => \"매월\",\n    \"years\" => \"매년\",\n    \"external_url\" => \"외부 URL 방문\",\n    \"empty_page\" => \"빈 페이지\",\n    \"clear_filters\" => \"필터 제거\",\n    \"no_matching_subscriptions\" => \"해당하는 구독이 없습니다.\",\n    \"clone\" => \"복제\",\n    \"renew\" => \"갱신\",\n    \"calculate_next_payment_date\" => \"다음 결제일 계산\",\n    // Subscription form\n    \"add_subscription\" => \"구독 추가\",\n    \"edit_subscription\" => \"구독 편집\",\n    \"subscription_name\" => \"구독 이름\",\n    \"logo_preview\" => \"로고 미리보기\",\n    \"search_logo\" => \"웹에서 로고 검색하기\",\n    \"web_search\" => \"웹 검색\",\n    \"currency\" => \"통화\",\n    \"payment_every\" => \"지불 빈도\",\n    \"frequency\" => \"빈도\",\n    \"cycle\" => \"주기\",\n    \"no_category\" => \"카테고리 없음\",\n    \"paid_by\" => \"결제하는 사람\",\n    \"url\" => \"URL\",\n    \"notes\" => \"메모\",\n    \"enable_notifications\" => \"이 구독에 대한 알림을 활성화합니다.\",\n    \"default_value_from_settings\" => \"설정에서 기본값 사용\",\n    \"cancellation_notification\" => \"구독 취소 알림\",\n    \"delete\" => \"삭제\",\n    \"cancel\" => \"취소\",\n    \"upload_logo\" => \"로고 업로드\",\n    // Statistics page\n    \"cant_convert_currency\" => \"구독에서 여러 통화를 사용하고 있습니다. 유효하고 정확한 통계를 얻으려면 설정 페이지에서 Fixer API 키를 설정하세요.\",\n    \"general_statistics\" => \"일반 통계\",\n    \"active_subscriptions\" => \"활성 구독\",\n    \"inactive_subscriptions\" => \"비활성 구독\",\n    \"monthly_cost\" => \"월간 지출\",\n    \"yearly_cost\" => \"연간 지출\",\n    \"average_monthly\" => \"월별 평균 구독 비용\",\n    \"most_expensive\" => \"최고가 구독 비용\",\n    \"amount_due\" => \"이달의 결제 비용\",\n    \"percentage_budget_used\" => \"예산 사용률\",\n    \"budget_remaining\" => \"남은 예산\",\n    \"amount_over_budget\" => \"예산 초과\",\n    \"monthly_savings\" => \"월간 절약 (비활성 구독)\",\n    \"yearly_savings\" => \"연간 절약 (비활성 구독)\",\n    \"split_views\" => \"분할 표시\",\n    \"category_split\" => \"카테고리별\",\n    \"household_split\" => \"가구별\",\n    \"payment_method_split\" => \"지불방법별\",\n    \"total_cost_trend\" => \"총 비용 추이\",\n    \"cost_vs_budget\" => \"비용 vs 예산\",\n    // About page\n    \"about_and_credits\" => \"개요 및 크레딧\",\n    \"credits\" => \"크레딧\",\n    \"license\" => \"라이선스\",\n    \"release_notes\" => \"릴리즈 노트\",\n    \"update_available\" => \"업데이트 가능\",\n    \"issues_and_requests\" => \"이슈 및 요청\",\n    \"the_author\" => \"제작자\",\n    \"icons\" => \"아이콘\",\n    \"payment_icons\" => \"지불 방식 아이콘\",\n    // Profile page\n    \"upload_avatar\" => \"아바타 업로드\",\n    \"file_type_error\" => \"제공된 파일이 지원하지 않는 타입입니다.\",\n    \"user_details\" => \"유저 상세\",\n    \"two_factor_authentication\" => \"이중 인증\",\n    \"two_factor_info\" => \"2단계 인증은 계정에 보안을 한층 더 강화합니다. QR 코드를 스캔하려면 Google Authenticator, Authy 또는 Ente Auth와 같은 인증 앱이 필요합니다.\",\n    \"two_factor_enabled_info\" => \"계정은 2단계 인증으로 안전하게 보호됩니다. 위의 버튼을 클릭하여 비활성화할 수 있습니다.\",\n    \"enable_two_factor_authentication\" => \"2단계 인증 활성화\",\n    \"2fa_already_enabled\" => \"2단계 인증이 이미 활성화되어 있습니다.\",\n    \"totp_code_incorrect\" => \"TOTP 코드가 올바르지 않습니다.\",\n    \"backup_codes\" => \"백업 코드\",\n    \"download_backup_codes\" => \"백업 코드 다운로드\",\n    \"copy_to_clipboard\" => \"클립보드로 복사\",\n    \"totp_backup_codes_info\" => \"이 코드는 계정에 대한 백업 코드입니다. 이 코드를 안전한 곳에 보관하세요. 이 코드는 한 번만 사용할 수 있습니다.\",\n    \"disable_two_factor_authentication\" => \"2단계 인증 비활성화\",\n    \"totp_code\" => \"TOTP 코드\",\n    \"api_key\" => \"API 키\",\n    \"regenerate\" => \"재생성\",\n    \"api_key_info\" => \"API 키는 외부 애플리케이션과 통신할 때 사용됩니다. API 키를 재생성하면 이전 키는 더 이상 유효하지 않습니다.\",\n    // Settings page\n    \"monthly_budget\" => \"월간 예산\",\n    \"budget_info\" => \"예산을 설정하면 통계 페이지에서 예산과 실제 지출을 비교할 수 있습니다.\",\n    \"household\" => \"가구\",\n    \"save_member\" => \"구성원 저장\",\n    \"delete_member\" => \"구성원 삭제\",\n    \"cant_delete_member\" => \"메인 구성원은 삭제할 수 없습니다\",\n    \"cant_delete_member_in_use\" => \"구독에서 사용 중인 구성원은 삭제할 수 없습니다\",\n    \"household_info\" => \"이메일 항목은 가구 구성원이 구독 만료에 대한 안내를 받기 위해 필요합니다.\",\n    \"notifications\" => \"알림\",\n    \"enable_email_notifications\" => \"이메일 알림 활성화\",\n    \"notify_me\" => \"알림 받기\",\n    \"day_before\" => \"일 전\",\n    \"on_due_date\" => \"만기일\",\n    \"days_before\" => \"일 전\",\n    \"smtp_address\" => \"SMTP 주소\",\n    \"port\" => \"포트\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP 유저명\",\n    \"smtp_password\" => \"SMTP 비밀번호\",\n    \"from_email\" => \"발송 주소 (선택사항)\",\n    \"send_to_other_emails\" => \"알림을 다음 이메일 주소로도 보내기 (구분자는 ; 사용):\",\n    \"smtp_info\" => \"SMTP 비밀번호는 평문으로 저장되고 발송됩니다. 보안을 위해, 이 서비스를 위해서만 사용하는 계정을 생성해 주세요.\",\n    \"telegram\" => \"텔레그램\",\n    \"telegram_bot_token\" => \"텔레그램 봇 토큰\",\n    \"telegram_chat_id\" => \"텔레그램 채팅 ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus 토큰\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost 웹훅 URL\",\n    \"mattermost_bot_username\" => \"Mattermost 봇 유저명\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost 봇 아이콘 이모지\",\n    \"webhook\" => \"웹훅\",\n    \"webhook_url\" => \"웹훅 URL\",\n    \"request_method\" => \"요청 메서드\",\n    \"custom_headers\" => \"커스텀 헤더\",\n    \"webhook_payload\" => \"웹훅 페이로드\",\n    \"payment_notifications_payload\" => \"결제 알림 페이로드\",\n    \"cancelation_notification_payload\" => \"구독 취소 알림 페이로드\",\n    \"variables_available\" => \"사용 가능한 변수\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"토큰\",\n    \"discord\" => \"디스코드\",\n    \"discord_bot_username\" => \"디스코드 봇 유저명\",\n    \"discord_bot_avatar_url\" => \"디스코드 봇 아바타 URL\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover User Key\",\n    \"host\" => \"호스트\",\n    \"topic\" => \"토픽\",\n    \"ignore_ssl_errors\" => \"SSL 에러 무시\",\n    \"categories\" => \"카테고리\",\n    \"save_category\" => \"카테고리 저장\",\n    \"delete_category\" => \"카테고리 삭제\",\n    \"cant_delete_category_in_use\" => \"구독에서 사용 중인 카테고리는 삭제할 수 없습니다.\",\n    \"currencies\" => \"통화\",\n    \"save_currency\" => \"통화 저장\",\n    \"delete_currency\" => \"통화 삭제\",\n    \"cant_delete_main_currency\" => \"기본 통화는 삭제할 수 없습니다.\",\n    \"cant_delete_currency_in_use\" => \"구독에서 사용 중인 통화는 삭제할 수 없습니다.\",\n    \"exchange_update\" => \"환율 최종 갱신일\",\n    \"currency_info\" => \"지원하는 통화와 정확한 통화 코드 찾기\",\n    \"currency_performance\" => \"성능을 향상시키기 위해서는 사용할 통화들만 유지하세요.\",\n    \"fixer_api_key\" => \"Fixer API 키\",\n    \"provider\" => \"제공자\",\n    \"fixer_info\" => \"여러 통화를 사용하고, 정확한 통계와 구독별 정렬을 원하시느 경우에는, Fixer에서 발급받은 무료 API 키가 필요합니다.\",\n    \"get_key\" => \"키 얻기\",\n    \"get_free_fixer_api_key\" => \"무료 Fixer API 키 얻기\",\n    \"get_key_alternative\" => \"또는 다음 사이트에서 무료 Fixer api 키를 얻을 수 있습니다.\",\n    \"ai_model\" => \"AI 모델\",\n    \"select_ai_model\" => \"AI 모델 선택\",\n    \"run_schedule\" => \"일정 실행\",\n    \"manually\" => \"수동으로\",\n    \"coming_soon\" => \"곧 출시 예정\",\n    \"invalid_host\" => \"유효하지 않은 호스트\",\n    \"ai_recommendations_info\" => \"AI 추천은 사용자의 구독과 가구 구성원을 기반으로 생성됩니다.\",\n    \"may_take_time\" => \"제공자, 모델, 구독 수에 따라 추천 생성에 시간이 걸릴 수 있습니다.\",\n    \"recommendations_visible_on_dashboard\" => \"추천은 대시보드에서 확인할 수 있습니다.\",\n    \"generate_recommendations\" => \"추천 생성\",\n    \"display_settings\" => \"디스플레이 설정\",\n    \"theme_settings\" => \"테마 설정\",\n    \"colors\" => \"색상\",\n    \"custom_colors\" => \"커스텀 색상\",\n    \"theme\" => \"테마\",\n    \"dark_theme\" => \"다크 테마\",\n    \"light_theme\" => \"라이트 테마\",\n    \"automatic\" => \"자동\",\n    \"main_color\" => \"메인 색상\",\n    \"accent_color\" => \"강조 색상\",\n    \"hover_color\" => \"마우스 호버 색상\",\n    \"save_custom_colors\" => \"커스텀 색상 저장\",\n    \"reset_custom_colors\" => \"커스텀 색상 리셋\",\n    \"custom_css\" => \"커스텀 CSS\",\n    \"save_custom_css\" => \"커스텀 CSS 저장\",\n    \"calculate_monthly_price\" => \"모든 구독에 대한 월별 요금을 계산하고 표시\",\n    \"convert_prices\" => \"항상 기본 통화로 가격을 환산하고 표시 (느림)\",\n    \"show_original_price\" => \"변환이나 계산이 이루어질 때 원래 가격도 표시\",\n    \"experience\" => \"경험\",\n    \"show_subscription_progress\" => \"구독 진행률 표시\",\n    \"disabled_subscriptions\" => \"비활성화된 구독\",\n    \"hide_disabled_subscriptions\" => \"비활성화된 구독 숨기기\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"비활성화된 구독을 하단에 표시\",\n    \"experimental_settings\" => \"실험적 설정\",\n    \"remove_background\" => \"로고 이미지 검색에서 배경 삭제\",\n    \"use_mobile_navigation_bar\" => \"모바일 네비게이션 바 사용\",\n    \"experimental_info\" => \"실험적 설정은 제대로 작동하지 않을 수 있습니다.\",\n    \"payment_methods\" => \"결제 수단\",\n    \"payment_methods_info\" => \"결제 수단을 클릭하여 활성화/비활성화 할 수 있습니다.\",\n    \"rename_payment_methods_info\" => \"결제 수단의 이름을 클릭해 이름을 변경합니다.\",\n    \"cant_delete_payment_method_in_use\" => \"사용중인 결제 수단을 비활성화 할 수 없습니다\",\n    \"add_custom_payment\" => \"커스텀 결제 수단 추가\",\n    \"payment_method_name\" => \"결제 수단 이름\",\n    \"payment_method_added_successfuly\" => \"결제 수단이 성공적으로 추가되었습니다\",\n    \"payment_method_removed\" => \"결제 수단이 제거되었습니다.\",\n    \"disable\" => \"비활성화\",\n    \"enable\" => \"활성화\",\n    \"rename_payment_method\" => \"결제 수단 이름 변경\",\n    \"payment_renamed\" => \"결제 수단 이름이 변경되었습니다.\",\n    \"payment_not_renamed\" => \"결제 수단 이름이 변경되지 않았습니다.\",\n    \"test\" => \"테스트\",\n    \"add\" => \"추가\",\n    \"save\" => \"저장\",\n    \"reset\" => \"리셋\",\n    \"main_accent_color_error\" => \"메인 색상과 강조 색상은 같을 수 없습니다.\",\n    \"backup_and_restore\" => \"백업 및 복구\",\n    \"backup\" => \"백업\",\n    \"restore\" => \"복구\",\n    \"restore_info\" => \"데이터베이스를 복구하면 현재 데이터를 모두 덮어씁니다. 복구 후 로그아웃됩니다.\",\n    \"account\" => \"계정\",\n    \"export_subscriptions\" => \"구독 내보내기\",\n    \"export_as_json\" => \"JSON으로 내보내기\",\n    \"export_as_csv\" => \"CSV로 내보내기\",\n    \"danger_zone\" => \"위험 구역\",\n    \"delete_account\" => \"계정 삭제\",\n    \"delete_account_info\" => \"계정을 삭제하면 모든 구독과 설정도 함께 삭제됩니다.\",\n    // Filters menu\n    \"filter\" => \"필터\",\n    \"clear\" => \"초기화\",\n    // Toast\n    \"success\" => \"성공\",\n    // Endpoint responses\n    \"session_expired\" => \"세션이 만료되었습니다. 다시 로그인 해 주세요\",\n    \"fields_missing\" => \"일부 항목이 누락되었습니다\",\n    \"fill_all_fields\" => \"모든 항목을 채워 주세요\",\n    \"fill_mandatory_fields\" => \"모든 필수 항목을 채워 주세요\",\n    \"error\" => \"에러\",\n    // Category\n    \"failed_add_category\" => \"통화를 추가하는 데 실패했습니다\",\n    \"failed_edit_category\" => \"카테고리를 수정하는데 실패했습니다\",\n    \"category_in_use\" => \"카테고리가 구독에서 사용중이므로 제거할 수 없습니다\",\n    \"failed_remove_category\" => \"카테고리를 제거하는데 실패했습니다\",\n    \"category_saved\" => \"카테고리가 저장되었습니다\",\n    \"category_removed\" => \"카테고리가 삭제되었습니다\",\n    \"sort_order_saved\" => \"정렬 순서가 저장되었습니다\",\n    // Currency\n    \"currency_saved\" => \"통화 저장\",\n    \"error_adding_currency\" => \"통화 항목 추가 오류.\",\n    \"failed_to_store_currency\" => \"데이터베이스에 통화를 저장할 수 없습니다.\",\n    \"currency_in_use\" => \"구독에 사용 중인 통화는 삭제할 수 없습니다.\",\n    \"currency_is_main\" => \"기본 통화로 설정된 통화는 삭제할 수 없습니다.\",\n    \"failed_to_remove_currency\" => \"데이터베이스에서 통화를 삭제할 수 없습니다.\",\n    \"failed_to_store_api_key\" => \"데이터베이스에 API 키를 저장할 수 없습니다.\",\n    \"invalid_api_key\" => \"유효하지 않은 API 키.\",\n    \"api_key_saved\" => \"API 성공적으로 저장했습니다\",\n    \"currency_removed\" => \"통화 삭제\",\n    // Household\n    \"failed_add_household\" => \"가구 구성원을 추가하는데 실패했습니다\",\n    \"failed_edit_household\" => \"가구 구성원을 수정하는데 실패했습니다\",\n    \"failed_remove_household\" => \"가구 구성원을 삭제하는데 실패했습니다\",\n    \"household_in_use\" => \"구독에서 사용 중인 가구 구성원은 삭제할 수 없습니다\",\n    \"member_saved\" => \"구성원 저장\",\n    \"member_removed\" => \"구성원 삭제\",\n    // Notifications\n    \"error_saving_notifications\" => \"알림 데이터 저장 오류.\",\n    \"wallos_notification\" => \"Wallos 알림\",\n    \"test_notification\" => \"이 메세지는 테스트 알림입니다. 이 메세지를 보고 계시다면, 올바르게 설정된 상태입니다.\",\n    \"email_error\" => \"이메일 전송 오류\",\n    \"notification_sent_successfuly\" => \"알림 전송에 성공했습니다\",\n    \"notifications_settings_saved\" => \"알림 설정 저장 성공.\",\n    \"notification_failed\" => \"알림 전송 실패\",\n    // Payments\n    \"payment_in_use\" => \"사용 중인 결제 수단은 비활성화 할 수 없습니다\",\n    \"failed_update_payment\" => \"결제 수단을 데이터베이스에 업데이트 하지 못 했습니다\",\n    \"enabled\" => \"활성\",\n    \"disabled\" => \"비활성\",\n    // Subscription\n    \"error_fetching_image\" => \"이미지 가져오기 오류\",\n    \"subscription_updated_successfuly\" => \"구독이 성공적으로 수정되었습니다\",\n    \"subscription_added_successfuly\" => \"구독이 성공적으로 추가되었습니다\",\n    \"error_deleting_subscription\" => \"구독 삭제 에러.\",\n    \"invalid_request_method\" => \"잘못된 요청 메서드.\",\n    // User\n    \"error_updating_user_data\" => \"유저 데이터 갱신 실패.\",\n    \"user_details_saved\" => \"유저 세부정보 저장 성공\",\n    // Admin Page\n    \"registrations\" => \"회원가입\",\n    \"enable_user_registrations\" => \"유저 회원가입 활성화\",\n    \"maximum_number_users\" => \"최대 유저 수\",\n    \"require_email_verification\" => \"이메일 인증 필요\",\n    \"configure_smtp_settings_to_enable\" => \"SMTP 설정을 구성하여 이메일 인증을 활성화합니다.\",\n    \"server_url\" => \"서버 URL\",\n    \"server_url_info\" => \"이메일 인증 및 비밀번호 복구에 사용됩니다. 유효한 공개 URL이어야 합니다.\",\n    \"server_url_password_reset\" => \"설정하면 비밀번호 재설정 기능도 활성화됩니다.\",\n    \"disable_login\" => \"로그인 비활성화\",\n    \"disable_login_info\" => \"로그인 우회. 외부 액세스 없이 로컬 네트워크에서만 서버를 실행하는 경우 로그인을 비활성화할 수 있습니다. 그러면 관리자 사용자가 자동으로 로그인됩니다.\",\n    \"disable_login_info2\" => \"이 설정은 사용자 등록이 해제되어 있고 관리자 사용자 계정이 없는 경우에만 활성화할 수 있습니다.\",\n    \"max_users_info\" => \"0으로 설정하면 무제한으로 설정됩니다.\",\n    \"user_management\" => \"유저 관리\",\n    \"delete_user\" => \"유저 삭제\",\n    \"delete_user_info\" => \"사용자를 삭제하면 모든 구독 및 설정도 삭제됩니다.\",\n    \"create_user\" => \"유저 생성\",\n    \"oidc_settings\" => \"OIDC 설정\",\n    \"oidc_auth_enabled\" => \"OIDC 인증 활성화\",\n    \"create_user_automatically\" => \"사용자 자동 생성\",\n    \"disable_password_login\" => \"비밀번호 로그인 비활성화\",\n    \"smtp_settings\" => \"SMTP 설정\",\n    \"smtp_usage_info\" => \"비밀번호 복구 및 기타 시스템 이메일에 사용됩니다.\",\n    \"security_settings\" => \"보안 설정\",\n    \"ssrf_protection_info\" => \"서버 측 요청 위조(SSRF) 공격을 방지하기 위해 Wallos는 기본적으로 개인 또는 내부 네트워크 주소로의 webhook 알림을 차단합니다.\",\n    \"local_webhook_info\" => \"Home Assistant, Gotify 또는 Node-RED와 같은 로컬 서비스로 webhook을 보내야 하는 경우 해당 IP 주소 또는 호스트 이름을 위에 쉼표로 구분된 목록으로 입력하세요(예: <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"유지보수 작업\",\n    \"orphaned_logos\" => \"고아 로고\",\n    \"update\" => \"업데이트\",\n    \"new_version_available\" => \"새로운 버전의 Wallos가 이용 가능합니다\",\n    \"current_version\" => \"현재 버전\",\n    \"latest_version\" => \"최신 버전\",\n    \"on_current_version\" => \"최신 버전의 Wallos를 사용 중입니다.\",\n    \"show_update_notification\" => \"대시보드에 업데이트 알림 표시\",\n    \"cronjobs\" => \"크론잡\",\n    // Email Verification\n    \"email_verified\" => \"이메일 인증 완료\",\n    \"email_verification_failed\" => \"이메일 인증 실패\",\n    // Calendar\n    \"calendar\" => \"달력\",\n    \"sun\" => \"일\",\n    \"mon\" => \"월\",\n    \"tue\" => \"화\",\n    \"wed\" => \"수\",\n    \"thu\" => \"목\",\n    \"fri\" => \"금\",\n    \"sat\" => \"토\",\n    \"month-01\" => \"1월\",\n    \"month-02\" => \"2월\",\n    \"month-03\" => \"3월\",\n    \"month-04\" => \"4월\",\n    \"month-05\" => \"5월\",\n    \"month-06\" => \"6월\",\n    \"month-07\" => \"7월\",\n    \"month-08\" => \"8월\",\n    \"month-09\" => \"9월\",\n    \"month-10\" => \"10월\",\n    \"month-11\" => \"11월\",\n    \"month-12\" => \"12월\",\n    \"total_cost\" => \"총 비용\",\n    \"export_icalendar\" => \"iCalendar 내보내기\",\n    \"over_budget_warning\" => \"예산이 초과되었습니다\",\n    // TOTP Page\n    \"insert_totp_code\" => \"2단계 인증 코드를 입력하세요\",\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/languages.php",
    "content": "<?php\r\n// File Name => Language Name\r\n$languages = [\r\n    // English first\r\n    \"en\" => [\"name\" => \"English\", \"dir\" => \"ltr\"],\r\n    // Remaining sorted alphabetically by language code\r\n    \"ca\" => [\"name\" => \"Català\", \"dir\" => \"ltr\"],\r\n    \"cs\" => [\"name\" => \"Čeština\", \"dir\" => \"ltr\"],\r\n    \"da\" => [\"name\" => \"Dansk\", \"dir\" => \"ltr\"],\r\n    \"de\" => [\"name\" => \"Deutsch\", \"dir\" => \"ltr\"],\r\n    \"el\" => [\"name\" => \"Ελληνικά\", \"dir\" => \"ltr\"],\r\n    \"es\" => [\"name\" => \"Español\", \"dir\" => \"ltr\"],\r\n    \"fr\" => [\"name\" => \"Français\", \"dir\" => \"ltr\"],\r\n    \"id\" => [\"name\" => \"bahasa indonesia\", \"dir\" => \"ltr\"],\r\n    \"it\" => [\"name\" => \"Italiano\", \"dir\" => \"ltr\"],\r\n    \"jp\" => [\"name\" => \"日本語\", \"dir\" => \"ltr\"],\r\n    \"ko\" => [\"name\" => \"한국어\", \"dir\" => \"ltr\"],\r\n    \"nl\" => [\"name\" => \"Nederlands\", \"dir\" => \"ltr\"], \r\n    \"pl\" => [\"name\" => \"Polski\", \"dir\" => \"ltr\"],\r\n    \"pt\" => [\"name\" => \"Português\", \"dir\" => \"ltr\"],\r\n    \"pt_br\" => [\"name\" => \"Português Brasileiro\", \"dir\" => \"ltr\"],\r\n    \"ro\" => [\"name\" => \"Română\", \"dir\" => \"ltr\"],\r\n    \"ru\" => [\"name\" => \"Русский\", \"dir\" => \"ltr\"],\r\n    \"sl\" => [\"name\" => \"Slovenščina\", \"dir\" => \"ltr\"],\r\n    \"sr_lat\" => [\"name\" => \"Srpski\", \"dir\" => \"ltr\"],\r\n    \"sr\" => [\"name\" => \"Српски\", \"dir\" => \"ltr\"],\r\n    \"tr\" => [\"name\" => \"Türkçe\", \"dir\" => \"ltr\"],\r\n    \"uk\" => [\"name\" => \"Українська\", \"dir\" => \"ltr\"],\r\n    \"vi\" => [\"name\" => \"Tiếng Việt\", \"dir\" => \"ltr\"],\r\n    \"zh_cn\" => [\"name\" => \"简体中文\", \"dir\" => \"ltr\"],\r\n    \"zh_tw\" => [\"name\" => \"繁體中文\", \"dir\" => \"ltr\"],\r\n];"
  },
  {
    "path": "includes/i18n/nl.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Maak een account aan om te kunnen inloggen\",\n    \"username\" => \"Gebruikersnaam\",\n    \"password\" => \"Wachtwoord\",\n    \"email\" => \"E-mail\",\n    \"firstname\" => \"Voornaam\",\n    \"lastname\" => \"Achternaam\",\n    \"confirm_password\" => \"Bevestig wachtwoord\",\n    \"main_currency\" => \"Basisvaluta\",\n    \"language\" => \"Taal\",\n    \"passwords_dont_match\" => \"Wachtwoorden komen niet overeen\",\n    \"username_exists\" => \"Gebruikersnaam bestaat al\",\n    \"email_exists\" => \"E-mailadres bestaat al\",\n    \"registration_failed\" => \"Registratie mislukt, probeer het opnieuw\",\n    \"register\" => \"Registreren\",\n    \"restore_database\" => \"Database herstellen\",\n    // Login Page\n    \"please_login\" => \"Login\",\n    \"stay_logged_in\" => \"Ingelogd blijven (30 dagen)\",\n    \"login\" => \"Inloggen\",\n    \"login_with\" => \"Inloggen met\",\n    \"or\" => \"of\",\n    \"login_failed\" => \"Inloggegevens zijn onjuist\",\n    \"registration_successful\" => \"Registratie succesvol\",\n    \"user_email_waiting_verification\" => \"Je e-mail moet worden geverifieerd. Controleer het e-mail bericht.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Wachtwoord vergeten\",\n    \"reset_password\" => \"Wachtwoord resetten\",\n    \"reset_sent_check_email\" => \"Reset e-mail verzonden. Controleer het e-mail bericht.\",\n    \"password_reset_successful\" => \"Wachtwoord reset succesvol\",\n    // Header\n    \"profile\" => \"Profiel\",\n    \"dashboard\" => \"Dashboard\",\n    \"subscriptions\" => \"Abonnementen\",\n    \"stats\" => \"Statistieken\",\n    \"settings\" => \"Instellingen\",\n    \"admin\" => \"Beheer\",\n    \"about\" => \"Over\",\n    \"logout\" => \"Uitloggen\",\n    // Dashboard\n    \"hello\" => \"Hallo\",\n    \"upcoming_payments\" => \"Aankomende Betalingen\",\n    \"no_upcoming_payments\" => \"Je hebt geen aankomende betalingen\",\n    \"overdue_renewals\" => \"Verlopen Verlengen\",\n    \"ai_recommendations\" => \"AI Aanbevelingen\",\n    \"your_budget\" => \"Je Budget\",\n    \"budget\" => \"Budget\",\n    \"budget_used\" => \"Budget Gebruikt\",\n    \"over_budget\" => \"Over Budget\",\n    \"your_subscriptions\" => \"Je Abonnementen\",\n    \"your_savings\" => \"Je Besparingen\",\n    // Subscriptions page\n    \"subscription\" => \"Abonnement\",\n    \"no_subscriptions_yet\" => \"Je hebt nog geen abonnementen\",\n    \"add_first_subscription\" => \"Voeg eerste abonnement toe\",\n    \"new_subscription\" => \"Nieuw Abonnement\",\n    \"search\" => \"Zoeken\",\n    \"state\" => \"Status\",\n    \"alphanumeric\" => \"Alfanumeriek\",\n    \"sort\" => \"Sorteren\",\n    \"name\" => \"Naam\",\n    \"last_added\" => \"Laatst toegevoegd\",\n    \"price\" => \"Prijs\",\n    \"next_payment\" => \"Volgende betaling\",\n    \"renewal_type\" => \"Verlengingstype\",\n    \"auto_renewal\" => \"Automatische verlenging\",\n    \"automatically_renews\" => \"Verlengt automatisch\",\n    \"manual_renewal\" => \"Handmatige verlenging\",\n    \"start_date\" => \"Startdatum\",\n    \"inactive\" => \"Abonnement uitschakelen\",\n    \"replaced_with\" => \"Vervangen door\",\n    \"none\" => \"Geen\",\n    \"member\" => \"Lid\",\n    \"category\" => \"Categorie\",\n    \"payment_method\" => \"Betaalmethode\",\n    \"Daily\" => \"Dagelijks\",\n    \"Weekly\" => \"Wekelijks\",\n    \"Monthly\" => \"Maandelijks\",\n    \"Yearly\" => \"Jaarlijks\",\n    \"daily\" => \"Dagen\",\n    \"weekly\" => \"Weken\",\n    \"monthly\" => \"Maanden\",\n    \"yearly\" => \"Jaren\",\n    \"days\" => \"dagen\",\n    \"weeks\" => \"weken\",\n    \"months\" => \"maanden\",\n    \"years\" => \"jaren\",\n    \"external_url\" => \"Externe URL bezoeken\",\n    \"empty_page\" => \"Lege pagina\",\n    \"clear_filters\" => \"Filters wissen\",\n    \"no_matching_subscriptions\" => \"Geen overeenkomende abonnementen\",\n    \"clone\" => \"Klonen\",\n    \"renew\" => \"Verlengen\",\n    \"calculate_next_payment_date\" => \"Bereken volgende betalingsdatum\",\n    // Subscription form\n    \"add_subscription\" => \"Abonnement toevoegen\",\n    \"edit_subscription\" => \"Abonnement bewerken\",\n    \"subscription_name\" => \"Abonnementsnaam\",\n    \"logo_preview\" => \"Logo voorbeeld\",\n    \"search_logo\" => \"Zoek logo op het web\",\n    \"web_search\" => \"Web zoeken\",\n    \"currency\" => \"Valuta\",\n    \"payment_every\" => \"Betaling elke\",\n    \"frequency\" => \"Frequentie\",\n    \"cycle\" => \"Cyclus\",\n    \"no_category\" => \"Geen categorie\",\n    \"paid_by\" => \"Betaald door\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Notities\",\n    \"enable_notifications\" => \"Notificaties inschakelen voor dit abonnement\",\n    \"default_value_from_settings\" => \"Standaardwaarde uit instellingen\",\n    \"cancellation_notification\" => \"Opzegnotificatie\",\n    \"delete\" => \"Verwijderen\",\n    \"cancel\" => \"Annuleren\",\n    \"upload_logo\" => \"Logo uploaden\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Je gebruikt meerdere valuta voor je abonnementen. Voor geldige en nauwkeurige statistieken, stel een Fixer API-sleutel in op de instellingenpagina.\",\n    \"general_statistics\" => \"Algemene statistieken\",\n    \"active_subscriptions\" => \"Actieve abonnementen\",\n    \"inactive_subscriptions\" => \"Inactieve abonnementen\",\n    \"monthly_cost\" => \"Maandelijkse kosten\",\n    \"yearly_cost\" => \"Jaarlijkse kosten\",\n    \"average_monthly\" => \"Gemiddelde maandelijkse abonnementskosten\",\n    \"most_expensive\" => \"Duurste abonnementskosten\",\n    \"amount_due\" => \"Verschuldigd bedrag deze maand\",\n    \"percentage_budget_used\" => \"Percentage van budget gebruikt\",\n    \"budget_remaining\" => \"Resterend budget\",\n    \"amount_over_budget\" => \"Bedrag over budget\",\n    \"monthly_savings\" => \"Maandelijkse besparingen (op inactieve abonnementen)\",\n    \"yearly_savings\" => \"Jaarlijkse besparingen (op inactieve abonnementen)\",\n    \"split_views\" => \"Verdelingen\",\n    \"category_split\" => \"Categorieverdeling\",\n    \"household_split\" => \"Huishoudenverdeling\",\n    \"payment_method_split\" => \"Betaalmethodeverdeling\",\n    \"total_cost_trend\" => \"Totale kosten trend\",\n    \"cost_vs_budget\" => \"Kosten vs Budget\",\n    // About page\n    \"about_and_credits\" => \"Over en credits\",\n    \"credits\" => \"Credits\",\n    \"license\" => \"Licentie\",\n    \"release_notes\" => \"Release notes\",\n    \"update_available\" => \"Update beschikbaar\",\n    \"issues_and_requests\" => \"Problemen en verzoeken\",\n    \"the_author\" => \"De auteur\",\n    \"icons\" => \"Iconen\",\n    \"payment_icons\" => \"Betaaliconen\",\n    // Profielpagina\n    \"upload_avatar\" => \"Avatar uploaden\",\n    \"file_type_error\" => \"Het bestandstype wordt niet ondersteund.\",\n    \"user_details\" => \"Gebruikersgegevens\",\n    \"two_factor_authentication\" => \"Twee-factor-authenticatie\",\n    \"two_factor_info\" => \"Twee-factor-authenticatie voegt een extra beveiligingslaag toe aan je account.<br>Je hebt een authenticatie-app nodig zoals Google Authenticator, Authy of Ente Auth om de QR-code te scannen.\",\n    \"two_factor_enabled_info\" => \"Je account is beveiligd met twee-factor-authenticatie. Je kunt dit uitschakelen door op de knop hierboven te klikken.\",\n    \"enable_two_factor_authentication\" => \"Twee-factor-authenticatie inschakelen\",\n    \"2fa_already_enabled\" => \"Twee-factor-authenticatie is al ingeschakeld\",\n    \"totp_code_incorrect\" => \"TOTP-code is onjuist\",\n    \"backup_codes\" => \"Backup codes\",\n    \"download_backup_codes\" => \"Backup codes downloaden\",\n    \"copy_to_clipboard\" => \"Kopiëren naar klembord\",\n    \"totp_backup_codes_info\" => \"Deze codes kunnen worden gebruikt om in te loggen als je geen toegang meer hebt tot je authenticatie-app.\",\n    \"disable_two_factor_authentication\" => \"Twee-factor-authenticatie uitschakelen\",\n    \"totp_code\" => \"TOTP-code\",\n    \"api_key\" => \"API-sleutel\",\n    \"regenerate\" => \"Opnieuw genereren\",\n    \"api_key_info\" => \"De API-sleutel wordt gebruikt om toegang te krijgen tot de API. Houd deze geheim.\",\n    // Instellingen pagina\n    \"monthly_budget\" => \"Maandelijks budget\",\n    \"budget_info\" => \"Maandelijks budget wordt gebruikt om statistieken te berekenen\",\n    \"household\" => \"Huishouden\",\n    \"save_member\" => \"Lid opslaan\",\n    \"delete_member\" => \"Lid verwijderen\",\n    \"cant_delete_member\" => \"Kan hoofdlid niet verwijderen\",\n    \"cant_delete_member_in_use\" => \"Kan lid dat in gebruik is bij abonnement niet verwijderen\",\n    \"household_info\" => \"E-mailveld maakt het mogelijk om huishoudleden te informeren over abonnementen die bijna verlopen.\",\n    \"notifications\" => \"Notificaties\",\n    \"enable_email_notifications\" => \"E-mail notificaties inschakelen\",\n    \"notify_me\" => \"Informeer mij\",\n    \"day_before\" => \"dag voor\",\n    \"on_due_date\" => \"Op vervaldatum\",\n    \"days_before\" => \"dagen voor\",\n    \"smtp_address\" => \"SMTP-adres\",\n    \"port\" => \"Poort\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP-gebruikersnaam\",\n    \"smtp_password\" => \"SMTP-wachtwoord\",\n    \"from_email\" => \"Afzender e-mail (Optioneel)\",\n    \"send_to_other_emails\" => \"Stuur notificaties ook naar de volgende e-mailadressen (gebruik ; om ze te scheiden):\",\n    \"other_emails_placeholder\" => \"gebruiker@domein.nl;test@gebruiker.nl\",\n    \"smtp_info\" => \"SMTP-wachtwoord wordt verzonden en opgeslagen in platte tekst. Maak voor de veiligheid een apart account hiervoor aan.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegram Bot Token\",\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus Token\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Gebruikersnaam\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Icon Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"Request Methode\",\n    \"custom_headers\" => \"Aangepaste Headers\",\n    \"webhook_payload\" => \"Webhook Payload\",\n    \"payment_notifications_payload\" => \"Betalingsnotificatie Payload\",\n    \"cancelation_notification_payload\" => \"Opzegnotificatie Payload\",\n    \"variables_available\" => \"Beschikbare variabelen\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Discord Bot Gebruikersnaam\",\n    \"discord_bot_avatar_url\" => \"Discord Bot Avatar URL\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover Gebruikerssleutel\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Onderwerp\",\n    \"ignore_ssl_errors\" => \"SSL-fouten negeren\",\n    \"categories\" => \"Categorieën\",\n    \"save_category\" => \"Categorie opslaan\",\n    \"delete_category\" => \"Categorie verwijderen\",\n    \"cant_delete_category_in_use\" => \"Kan categorie in gebruik bij abonnement niet verwijderen\",\n    \"currencies\" => \"Valuta\",\n    \"save_currency\" => \"Valuta opslaan\",\n    \"delete_currency\" => \"Valuta verwijderen\",\n    \"cant_delete_main_currency\" => \"Kan basisvaluta niet verwijderen\",\n    \"cant_delete_currency_in_use\" => \"Kan valuta in gebruik bij abonnement niet verwijderen\",\n    \"exchange_update\" => \"Wisselkoersen laatst bijgewerkt op\",\n    \"currency_info\" => \"Vind de ondersteunde valuta en juiste valutacodes op\",\n    \"currency_performance\" => \"Voor betere prestaties, behoud alleen de valuta die je gebruikt.\",\n    \"fixer_api_key\" => \"Fixer API-sleutel\",\n    \"provider\" => \"Aanbieder\",\n    \"fixer_info\" => \"Als je meerdere valuta gebruikt en nauwkeurige statistieken en sortering wilt, is een GRATIS API-sleutel van Fixer noodzakelijk.\",\n    \"get_key\" => \"Haal je sleutel op bij\",\n    \"get_free_fixer_api_key\" => \"Krijg gratis Fixer API-sleutel\",\n    \"get_key_alternative\" => \"Als alternatief kun je een gratis Fixer API-sleutel krijgen van\",\n    \"ai_model\" => \"AI-model\",\n    \"select_ai_model\" => \"Selecteer AI-model\",\n    \"run_schedule\" => \"Uitvoerschema\",\n    \"manually\" => \"Handmatig\",\n    \"coming_soon\" => \"Binnenkort beschikbaar\",\n    \"invalid_host\" => \"Ongeldige host\",\n    \"ai_recommendations_info\" => \"AI-aanbevelingen worden gegenereerd op basis van je abonnementen en huishoudleden.\",\n    \"may_take_time\" => \"Afhankelijk van de aanbieder, het model en het aantal abonnementen kan het genereren van aanbevelingen enige tijd duren.\",\n    \"recommendations_visible_on_dashboard\" => \"Aanbevelingen zijn zichtbaar op het dashboard.\",\n    \"generate_recommendations\" => \"Genereer aanbevelingen\",\n    \"display_settings\" => \"Weergave-instellingen\",\n    \"theme_settings\" => \"Thema-instellingen\",\n    \"colors\" => \"Kleuren\",\n    \"custom_colors\" => \"Aangepaste kleuren\",\n    \"theme\" => \"Thema\",\n    \"dark_theme\" => \"Donker thema\",\n    \"light_theme\" => \"Licht thema\",\n    \"automatic\" => \"Automatisch\",\n    \"main_color\" => \"Hoofdkleur\",\n    \"accent_color\" => \"Accentkleur\",\n    \"hover_color\" => \"Hover-kleur\",\n    \"save_custom_colors\" => \"Aangepaste kleuren opslaan\",\n    \"reset_custom_colors\" => \"Aangepaste kleuren resetten\",\n    \"custom_css\" => \"Aangepaste CSS\",\n    \"save_custom_css\" => \"Aangepaste CSS opslaan\",\n    \"calculate_monthly_price\" => \"Bereken en toon maandelijkse prijs voor alle abonnementen\",\n    \"convert_prices\" => \"Converteer en toon prijzen altijd in mijn basisvaluta (langzamer)\",\n    \"show_original_price\" => \"Toon ook originele prijs bij conversies of berekeningen\",\n    \"experience\" => \"Ervaring\",\n    \"show_subscription_progress\" => \"Toon abonnementsvoortgang\",\n    \"disabled_subscriptions\" => \"Uitgeschakelde abonnementen\",\n    \"hide_disabled_subscriptions\" => \"Verberg uitgeschakelde abonnementen\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Toon uitgeschakelde abonnementen onderaan\",\n    \"experimental_settings\" => \"Experimentele instellingen\",\n    \"remove_background\" => \"Probeer achtergrond van logo's uit afbeelding zoekresultaten te verwijderen\",\n    \"use_mobile_navigation_bar\" => \"Gebruik mobiele navigatiebalk\",\n    \"experimental_info\" => \"Experimentele instellingen werken waarschijnlijk niet perfect.\",\n    \"payment_methods\" => \"Betaalmethoden\",\n    \"payment_methods_info\" => \"Klik op een betaalmethode om deze uit/in te schakelen.\",\n    \"rename_payment_methods_info\" => \"Klik op de naam van een betaalmethode om deze te hernoemen.\",\n    \"cant_delete_payment_method_in_use\" => \"Kan gebruikte betaalmethode niet uitschakelen\",\n    \"add_custom_payment\" => \"Aangepaste betaalmethode toevoegen\",\n    \"payment_method_name\" => \"Naam betaalmethode\",\n    \"payment_method_added_successfuly\" => \"Betaalmethode succesvol toegevoegd\",\n    \"payment_method_removed\" => \"Betaalmethode verwijderd\",\n    \"disable\" => \"Uitschakelen\",\n    \"enable\" => \"Inschakelen\",\n    \"rename_payment_method\" => \"Betaalmethode hernoemen\",\n    \"payment_renamed\" => \"Betaalmethode hernoemd\",\n    \"payment_not_renamed\" => \"Betaalmethode niet hernoemd\",\n    \"test\" => \"Test\",\n    \"add\" => \"Toevoegen\",\n    \"save\" => \"Opslaan\",\n    \"reset\" => \"Resetten\",\n    \"main_accent_color_error\" => \"Hoofdkleur en accentkleur kunnen niet hetzelfde zijn\",\n    \"backup_and_restore\" => \"Back-up en herstel\",\n    \"backup\" => \"Back-up\",\n    \"restore\" => \"Herstellen\",\n    \"restore_info\" => \"Het herstellen van de database zal alle huidige gegevens overschrijven. Je wordt uitgelogd na het herstel.\",\n    \"account\" => \"Account\",\n    \"export_subscriptions\" => \"Exporteer abonnementen\",\n    \"export_as_json\" => \"Exporteer als JSON\",\n    \"export_as_csv\" => \"Exporteer als CSV\",\n    \"danger_zone\" => \"Gevarenzone\",\n    \"delete_account\" => \"Account verwijderen\",\n    \"delete_account_info\" => \"Het verwijderen van je account zal ook al je abonnementen en instellingen verwijderen.\",\n    // Filters menu\n    \"filter\" => \"Filter\",\n    \"clear\" => \"Wissen\",\n    // Toast\n    \"success\" => \"Geslaagd\",\n    // Endpoint responses\n    \"session_expired\" => \"Je sessie is verlopen. Log opnieuw in\",\n    \"fields_missing\" => \"Sommige velden ontbreken\",\n    \"fill_all_fields\" => \"Vul alle velden in\",\n    \"fill_mandatory_fields\" => \"Vul alle verplichte velden in\",\n    \"error\" => \"Fout\",\n    // Categorie\n    \"failed_add_category\" => \"Categorie toevoegen mislukt\",\n    \"failed_edit_category\" => \"Categorie bewerken mislukt\",\n    \"category_in_use\" => \"Categorie is in gebruik bij abonnementen en kan niet worden verwijderd\",\n    \"failed_remove_category\" => \"Categorie verwijderen mislukt\",\n    \"category_saved\" => \"Categorie opgeslagen\",\n    \"category_removed\" => \"Categorie verwijderd\",\n    \"sort_order_saved\" => \"Sorteervolgorde opgeslagen\",\n    // Valuta\n    \"currency_saved\" => \"is opgeslagen.\",\n    \"error_adding_currency\" => \"Fout bij toevoegen van valuta.\",\n    \"failed_to_store_currency\" => \"Opslaan van valuta in de database mislukt.\",\n    \"currency_in_use\" => \"Valuta is in gebruik bij abonnementen en kan niet worden verwijderd.\",\n    \"currency_is_main\" => \"Valuta is ingesteld als basisvaluta en kan niet worden verwijderd.\",\n    \"failed_to_remove_currency\" => \"Verwijderen van valuta uit de database mislukt.\",\n    \"failed_to_store_api_key\" => \"Opslaan van API-sleutel in de database mislukt.\",\n    \"invalid_api_key\" => \"Ongeldige API-sleutel.\",\n    \"api_key_saved\" => \"API-sleutel succesvol opgeslagen\",\n    \"currency_removed\" => \"Valuta verwijderd\",\n    // Huishouden\n    \"failed_add_household\" => \"Huishoud lid toevoegen mislukt\",\n    \"failed_edit_household\" => \"Huishoud lid bewerken mislukt\",\n    \"failed_remove_household\" => \"Huishoud lid verwijderen mislukt\",\n    \"household_in_use\" => \"Huishoud lid is in gebruik bij abonnementen en kan niet worden verwijderd\",\n    \"member_saved\" => \"Lid opgeslagen\",\n    \"member_removed\" => \"Lid verwijderd\",\n    // Notificaties\n    \"error_saving_notifications\" => \"Fout bij het opslaan van notificatiegegevens.\",\n    \"wallos_notification\" => \"Wallos Notificatie\",\n    \"test_notification\" => \"Dit is een testnotificatie. Als je dit ziet, is de configuratie correct.\",\n    \"email_error\" => \"Fout bij het verzenden van e-mail\",\n    \"notification_sent_successfuly\" => \"Notificatie succesvol verzonden\",\n    \"notifications_settings_saved\" => \"Notificatie-instellingen succesvol opgeslagen.\",\n    \"notification_failed\" => \"Notificatie mislukt\",\n    // Betalingen\n    \"payment_in_use\" => \"Kan gebruikte betaalmethode niet uitschakelen\",\n    \"failed_update_payment\" => \"Betaalmethode bijwerken in database mislukt\",\n    \"enabled\" => \"ingeschakeld\", \n    \"disabled\" => \"uitgeschakeld\",\n    // Subscription\n    \"error_fetching_image\" => \"Fout bij ophalen afbeelding\",\n    \"subscription_updated_successfuly\" => \"Abonnement succesvol bijgewerkt\",\n    \"subscription_added_successfuly\" => \"Abonnement succesvol toegevoegd\",\n    \"error_deleting_subscription\" => \"Fout bij verwijderen abonnement.\",\n    \"invalid_request_method\" => \"Ongeldige aanvraagmethode.\",\n    // Gebruiker\n    \"error_updating_user_data\" => \"Fout bij bijwerken gebruikersgegevens.\",\n    \"user_details_saved\" => \"Gebruikersgegevens opgeslagen\",\n    // Beheerderspagina\n    \"registrations\" => \"Registraties\",\n    \"enable_user_registrations\" => \"Gebruikersregistraties inschakelen\",\n    \"maximum_number_users\" => \"Maximaal aantal gebruikers\",\n    \"require_email_verification\" => \"E-mailverificatie vereisen\",\n    \"configure_smtp_settings_to_enable\" => \"Configureer SMTP-instellingen om in te schakelen\",\n    \"server_url\" => \"Server URL\",\n    \"server_url_info\" => \"Gebruikt voor e-mailverificatie en wachtwoordherstel. Moet een geldige openbare URL zijn.\",\n    \"server_url_password_reset\" => \"Indien ingesteld zal ook wachtwoordherstel functionaliteit inschakelen.\",\n    \"disable_login\" => \"Inloggen uitschakelen\",\n    \"disable_login_info\" => \"Login overslaan. Als je server alleen in een lokaal netwerk draait, zonder externe toegang, kun je het inloggen uitschakelen. Dit zal automatisch de beheerder inloggen.\",\n    \"disable_login_info2\" => \"Je kunt deze instelling alleen inschakelen als gebruikersregistratie is uitgeschakeld en er niet meer dan het beheerdersaccount is.\",\n    \"max_users_info\" => \"0 betekent onbeperkt\",\n    \"user_management\" => \"Gebruikersbeheer\",\n    \"delete_user\" => \"Gebruiker verwijderen\",\n    \"delete_user_info\" => \"Het verwijderen van een gebruiker zal ook al hun abonnementen en instellingen verwijderen.\",\n    \"create_user\" => \"Gebruiker aanmaken\",\n    \"oidc_settings\" => \"OIDC-instellingen\",\n    \"oidc_oauth_enabled\" => \"OIDC/OAuth inschakelen\",\n    \"create_user_automatically\" => \"Gebruiker automatisch aanmaken\",\n    \"disable_password_login\" => \"Wachtwoordlogin uitschakelen\",\n    \"smtp_settings\" => \"SMTP-instellingen\",\n    \"smtp_usage_info\" => \"Wordt gebruikt voor wachtwoordherstel en andere systeem e-mails.\",\n    \"security_settings\" => \"Beveiligingsinstellingen\",\n    \"ssrf_protection_info\" => \"Om Server-Side Request Forgery (SSRF) aanvallen te voorkomen, blokkeert Wallos standaard webhookmeldingen naar privé- of interne netwerkadressen.\",\n    \"local_webhook_info\" => \"Als u webhooks naar lokale services moet sturen (zoals Home Assistant, Gotify of Node-RED), voer dan hun IP-adressen of hostnamen hierboven in als een door komma's gescheiden lijst (bijv. <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Onderhoudstaken\",\n    \"orphaned_logos\" => \"Ongebruikte logo's\",\n    \"update\" => \"Bijwerken\",\n    \"new_version_available\" => \"Er is een nieuwe versie van Wallos beschikbaar\",\n    \"current_version\" => \"Huidige versie\",\n    \"latest_version\" => \"Nieuwste versie\",\n    \"on_current_version\" => \"Je gebruikt de nieuwste versie van Wallos.\",\n    \"show_update_notification\" => \"Toon melding voor updates op het dashboard\",\n    \"cronjobs\" => \"Cronjobs\",\n    // E-mailverificatie\n    \"email_verified\" => \"E-mail succesvol geverifieerd\",\n    \"email_verification_failed\" => \"E-mailverificatie mislukt\",\n    // Kalender\n    \"calendar\" => \"Kalender\",\n    \"sun\" => \"Zo\",\n    \"mon\" => \"Ma\",\n    \"tue\" => \"Di\", \n    \"wed\" => \"Wo\",\n    \"thu\" => \"Do\",\n    \"fri\" => \"Vr\",\n    \"sat\" => \"Za\",\n    \"month-01\" => \"Januari\",\n    \"month-02\" => \"Februari\",\n    \"month-03\" => \"Maart\",\n    \"month-04\" => \"April\",\n    \"month-05\" => \"Mei\",\n    \"month-06\" => \"Juni\",\n    \"month-07\" => \"Juli\",\n    \"month-08\" => \"Augustus\",\n    \"month-09\" => \"September\",\n    \"month-10\" => \"Oktober\",\n    \"month-11\" => \"November\",\n    \"month-12\" => \"December\",\n    \"total_cost\" => \"Totale kosten\",\n    \"export_icalendar\" => \"Exporteer iCalendar\",\n    \"over_budget_warning\" => \"U bent over uw budget\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Voer TOTP code in\",\n\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/pl.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"Musisz utworzyć konto, zanim będziesz mógł się zalogować\",\r\n    \"username\" => \"Nazwa użytkownika\",\r\n    \"password\" => \"Hasło\",\r\n    \"email\" => \"E-mail\",\r\n    \"firstname\" => \"Imię\",\r\n    \"lastname\" => \"Nazwisko\",\r\n    \"confirm_password\" => \"Potwierdź hasło\",\r\n    \"main_currency\" => \"Główna waluta\",\r\n    \"language\" => \"Język\",\r\n    \"passwords_dont_match\" => \"Hasła nie pasują\",\r\n    \"username_exists\" => \"Nazwa użytkownika już istnieje\",\r\n    \"email_exists\" => \"E-mail już istnieje\",\r\n    \"registration_failed\" => \"Rejestracja nie powiodła się, spróbuj ponownie.\",\r\n    \"register\" => \"Rejestracja\",\r\n    \"restore_database\" => \"Przywróć bazę danych\",\r\n    // Login Page\r\n    \"please_login\" => \"Proszę się zalogować\",\r\n    \"stay_logged_in\" => \"Pozostań zalogowany (30 dni)\",\r\n    \"login\" => \"Zaloguj się\",\r\n    \"login_with\" => \"Zaloguj się przez\",\r\n    \"or\" => \"lub\",\r\n    \"login_failed\" => \"Dane logowania są nieprawidłowe\",\r\n    \"registration_successful\" => \"Pomyślnie zarejestrowano\",\r\n    \"user_email_waiting_verification\" => \"Twój adres e-mail musi zostać zweryfikowany. Sprawdź swój adres e-mail\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Zapomniałeś hasła? Kliknij tutaj\",\r\n    \"reset_password\" => \"Zresetuj hasło\",\r\n    \"reset_sent_check_email\" => \"Link do zresetowania hasła został wysłany na Twój adres e-mail\",\r\n    \"password_reset_successful\" => \"Hasło zostało zresetowane pomyślnie\",\r\n    // Header\r\n    \"profile\" => \"Profil\",\r\n    \"dashboard\" => \"Panel\",\r\n    \"subscriptions\" => \"Subskrypcje\",\r\n    \"stats\" => \"Statystyki\",\r\n    \"settings\" => \"Ustawienia\",\r\n    \"admin\" => \"Admin\",\r\n    \"about\" => \"O aplikacji\",\r\n    \"logout\" => \"Wyloguj się\",\r\n    // Dashboard\r\n    \"hello\" => \"Cześć\",\r\n    \"upcoming_payments\" => \"Nadchodzące płatności\",\r\n    \"no_upcoming_payments\" => \"Nie masz żadnych nadchodzących płatności\",\r\n    \"overdue_renewals\" => \"Zaległe odnowienia\",\r\n    \"ai_recommendations\" => \"Rekomendacje AI\",\r\n    \"your_budget\" => \"Twój budżet\",\r\n    \"budget\" => \"Budżet\",\r\n    \"budget_used\" => \"Budżet użyty\",\r\n    \"over_budget\" => \"Przekroczony budżet\",\r\n    \"your_subscriptions\" => \"Twoje subskrypcje\",\r\n    \"your_savings\" => \"Twoje oszczędności\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Subskrypcja\",\r\n    \"no_subscriptions_yet\" => \"Nie masz jeszcze żadnych subskrypcji\",\r\n    \"add_first_subscription\" => \"Dodaj pierwszą subskrypcję\",\r\n    \"new_subscription\" => \"Nowa subskrypcja\",\r\n    \"search\" => \"Szukaj\",\r\n    \"state\" => \"Stan\",\r\n    \"alphanumeric\" => \"Alfanumeryczny\",\r\n    \"sort\" => \"Sortuj\",\r\n    \"name\" => \"Nazwa\",\r\n    \"last_added\" => \"Ostatnio dodane\",\r\n    \"price\" => \"Cena\",\r\n    \"next_payment\" => \"Następna płatność\",\r\n    \"renewal_type\" => \"Typ odnowienia\",\r\n    \"auto_renewal\" => \"Automatyczne odnawianie\",\r\n    \"automatically_renews\" => \"Automatycznie odnawia się\",\r\n    \"manual_renewal\" => \"Ręczne odnawianie\",\r\n    \"start_date\" => \"Data rozpoczęcia\",\r\n    \"inactive\" => \"Wyłącz subskrypcję\",\r\n    \"replaced_with\" => \"Zastąpione przez\",\r\n    \"none\" => \"Brak\",\r\n    \"member\" => \"Użytkownik\",\r\n    \"category\" => \"Kategoria\",\r\n    \"payment_method\" => \"Metoda płatności\",\r\n    \"Daily\" => \"Codziennie\",\r\n    \"Weekly\" => \"Co tydzień\",\r\n    \"Monthly\" => \"Miesięcznie\",\r\n    \"Yearly\" => \"Rocznie\",\r\n    \"daily\" => \"Dzień/Dni\",\r\n    \"weekly\" => \"Tydzień/Tygodni\",\r\n    \"monthly\" => \"Miesiąc/Miesięcy\",\r\n    \"yearly\" => \"Rok/Lat\",\r\n    \"days\" => \"dni\",\r\n    \"weeks\" => \"tygodnie\",\r\n    \"months\" => \"miesiące\",\r\n    \"years\" => \"lata\",\r\n    \"external_url\" => \"Odwiedź zewnętrzny adres URL\",\r\n    \"empty_page\" => \"Pusta strona\",\r\n    \"clear_filters\" => \"Wyczyść filtry\",\r\n    \"no_matching_subscriptions\" => \"Brak pasujących subskrypcji\",\r\n    \"clone\" => \"Klonuj\",\r\n    \"renew\" => \"Odnów\",\r\n    \"calculate_next_payment_date\" => \"Oblicz datę następnej płatności\",\r\n    // Subscription form\r\n    \"add_subscription\" => \"Dodaj subskrypcję\",\r\n    \"edit_subscription\" => \"Edytuj subskrypcję\",\r\n    \"subscription_name\" => \"Nazwa subskrypcji\",\r\n    \"logo_preview\" => \"Podgląd logo\",\r\n    \"search_logo\" => \"Wyszukaj logo w sieci\",\r\n    \"web_search\" => \"Wyszukiwanie w Internecie\",\r\n    \"currency\" => \"Waluta\",\r\n    \"payment_every\" => \"Płatność co\",\r\n    \"frequency\" => \"Częstotliwość\",\r\n    \"cycle\" => \"Cykl\",\r\n    \"no_category\" => \"Brak kategorii\",\r\n    \"paid_by\" => \"Zapłacone przez\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Notatki\",\r\n    \"enable_notifications\" => \"Włącz powiadomienia dla tej subskrypcji\",\r\n    \"default_value_from_settings\" => \"Wartość domyślna z ustawień\",\r\n    \"cancellation_notification\" => \"Powiadomienie o anulowaniu\",\r\n    \"delete\" => \"Usuń\",\r\n    \"cancel\" => \"Anuluj\",\r\n    \"upload_logo\" => \"Prześlij logo\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"Używasz wielu walut w swoich subskrypcjach. Aby uzyskać prawidłowe i dokładne statystyki, należy ustawić klucz API Fixer na stronie ustawień.\",\r\n    \"general_statistics\" => \"Statystyki ogólne\",\r\n    \"active_subscriptions\" => \"Aktywne subskrypcje\",\r\n    \"inactive_subscriptions\" => \"Nieaktywne subskrypcje\",\r\n    \"monthly_cost\" => \"Koszt miesięczny\",\r\n    \"yearly_cost\" => \"Koszt roczny\",\r\n    \"average_monthly\" => \"Średni miesięczny koszt subskrypcji\",\r\n    \"most_expensive\" => \"Najdroższy koszt subskrypcji\",\r\n    \"amount_due\" => \"Kwota należna w tym miesiącu\",\r\n    \"percentage_budget_used\" => \"Procent wykorzystania budżetu\",\r\n    \"budget_remaining\" => \"Pozostały budżet\",\r\n    \"amount_over_budget\" => \"Kwota przekraczająca budżet\",\r\n    \"monthly_savings\" => \"Miesięczne oszczędności (w przypadku nieaktywnych subskrypcji)\",\r\n    \"yearly_savings\" => \"Roczne oszczędności (w przypadku nieaktywnych subskrypcji)\",\r\n    \"split_views\" => \"Podziel widoki\",\r\n    \"category_split\" => \"Podział kategorii\",\r\n    \"household_split\" => \"Podział gospodarstwa domowego\",\r\n    \"payment_method_split\" => \"Podział metod płatności\",\r\n    \"total_cost_trend\" => \"Trend całkowitego kosztu\",\r\n    \"cost_vs_budget\" => \"Koszt w porównaniu do budżetu\",\r\n    // About page\r\n    \"about_and_credits\" => \"Informacje i podziękowania\",\r\n    \"credits\" => \"Podziękowania\",\r\n    \"license\" => \"Licencja\",\r\n    \"release_notes\" => \"Notatki wydania\",\r\n    \"update_available\" => \"Dostępna aktualizacja\",\r\n    \"issues_and_requests\" => \"Problemy i prośby\",\r\n    \"the_author\" => \"Autor\",\r\n    \"icons\" => \"Ikony\",\r\n    \"payment_icons\" => \"Ikony płatności\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"Prześlij awatar\",\r\n    \"file_type_error\" => \"Podany typ pliku nie jest obsługiwany.\",\r\n    \"user_details\" => \"Szczegóły użytkownika\",\r\n    \"two_factor_authentication\" => \"Uwierzytelnianie dwuskładnikowe\",\r\n    \"two_factor_info\" => \"Uwierzytelnianie dwuskładnikowe dodaje dodatkową warstwę zabezpieczeń do konta.<br>Do zeskanowania kodu QR potrzebna będzie aplikacja uwierzytelniająca, taka jak Google Authenticator, Authy lub Ente Auth.\",\r\n    \"two_factor_enabled_info\" => \"Twoje konto jest bezpieczne dzięki uwierzytelnianiu dwuetapowemu. Możesz ją wyłączyć, klikając przycisk powyżej.\",\r\n    \"enable_two_factor_authentication\" => \"Włącz uwierzytelnianie dwuskładnikowe\",\r\n    \"2fa_already_enabled\" => \"Uwierzytelnianie dwuskładnikowe jest już włączone\",\r\n    \"totp_code_incorrect\" => \"Kod TOTP jest nieprawidłowy\",\r\n    \"backup_codes\" => \"Kody zapasowe\",\r\n    \"download_backup_codes\" => \"Pobierz kody zapasowe\",\r\n    \"copy_to_clipboard\" => \"Skopiuj do schowka\",\r\n    \"totp_backup_codes_info\" => \"Kody zapasowe są jednorazowe i można je użyć do zalogowania się, jeśli nie masz dostępu do aplikacji uwierzytelniającej.\",\r\n    \"disable_two_factor_authentication\" => \"Wyłącz uwierzytelnianie dwuskładnikowe\",\r\n    \"totp_code\" => \"Kod TOTP\",\r\n    \"api_key\" => \"Klucz API\",\r\n    \"regenerate\" => \"Wygeneruj ponownie\",\r\n    \"api_key_info\" => \"Klucz API jest używany do integracji z innymi aplikacjami. Nie udostępniaj go nikomu.\",\r\n    // Settings page\r\n    \"monthly_budget\" => \"Miesięczny budżet\",\r\n    \"budget_info\" => \"Jeśli ustawisz budżet, zobaczysz pasek postępu na stronie głównej.\",\r\n    \"household\" => \"Gospodarstwo domowe\",\r\n    \"save_member\" => \"Zapisz użytkownika\",\r\n    \"delete_member\" => \"Usuń użytkownika\",\r\n    \"cant_delete_member\" => \"Nie można usunąć głównego użytkownika\",\r\n    \"cant_delete_member_in_use\" => \"Nie można usunąć tego użytkownika w ramach subskrypcji\",\r\n    \"household_info\" => \"Pole e-mail umożliwia powiadamianie domowników o zbliżającym się wygaśnięciu subskrypcji.\",\r\n    \"notifications\" => \"Powiadomienia\",\r\n    \"enable_email_notifications\" => \"Włącz powiadomienia e-mail\",\r\n    \"notify_me\" => \"Powiadom mnie\",\r\n    \"day_before\" => \"dzień wcześniej\",\r\n    \"on_due_date\" => \"W dniu wymagalności\",\r\n    \"days_before\" => \"dni wcześniej\",\r\n    \"smtp_address\" => \"Adres SMTP\",\r\n    \"port\" => \"Port\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"Nazwa użytkownika SMTP\",\r\n    \"smtp_password\" => \"Hasło SMTP\",\r\n    \"from_email\" => \"Z adresu e-mail (opcjonalnie)\",\r\n    \"send_to_other_emails\" => \"Wyślij powiadomienia również na następujące adresy e-mail (użyj ; aby je rozdzielić):\",\r\n    \"smtp_info\" => \"Hasło SMTP jest przesyłane i przechowywane w postaci zwykłego tekstu. Ze względów bezpieczeństwa utwórz konto tylko w tym celu.\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Token bota\",\r\n    \"telegram_chat_id\" => \"ID czatu\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Token Pushplus\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"URL webhooka Mattermost\",\r\n    \"mattermost_bot_username\" => \"Nazwa użytkownika bota Mattermost\",\r\n    \"mattermost_bot_icon_emoji\" => \"Emoji ikony bota Mattermost\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"URL webhooka\",\r\n    \"request_method\" => \"Metoda żądania\",\r\n    \"custom_headers\" => \"Niestandardowe nagłówki\",\r\n    \"webhook_payload\" => \"Dane webhooka\",\r\n    \"payment_notifications_payload\" => \"Dane powiadomienia o płatności\",\r\n    \"cancelation_notification_payload\" => \"Dane powiadomienia o anulowaniu subskrypcji\",\r\n    \"variables_available\" => \"Dostępne zmienne\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Nazwa użytkownika bota\",\r\n    \"discord_bot_avatar_url\" => \"URL awatara bota\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Klucz użytkownika\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Temat\",\r\n    \"ignore_ssl_errors\" => \"Ignoruj błędy SSL\",\r\n    \"categories\" => \"Kategorie\",\r\n    \"save_category\" => \"Zapisz kategorię\",\r\n    \"delete_category\" => \"Usuń kategorię\",\r\n    \"cant_delete_category_in_use\" => \"Nie można usunąć kategorii używanej w subskrypcji\",\r\n    \"currencies\" => \"Waluty\",\r\n    \"save_currency\" => \"Zapisz walutę\",\r\n    \"delete_currency\" => \"Usuń walutę\",\r\n    \"cant_delete_main_currency\" => \"Nie można usunąć głównej waluty\",\r\n    \"cant_delete_currency_in_use\" => \"Nie można usunąć waluty używanej w subskrypcji\",\r\n    \"exchange_update\" => \"Kursy wymiany walut ostatnio zaktualizowane w dniu\",\r\n    \"currency_info\" => \"Znajdź obsługiwane waluty i prawidłowe kody walut na\",\r\n    \"currency_performance\" => \"W celu poprawy wydajności przechowuj tylko te waluty, których używasz.\",\r\n    \"fixer_api_key\" => \"Klucz API Fixer'a\",\r\n    \"provider\" => \"Dostawca\",\r\n    \"fixer_info\" => \"Jeśli używasz wielu walut i chcesz dokładnych statystyk i sortowania subskrypcji, niezbędny jest DARMOWY klucz API z Fixer'a.\",\r\n    \"get_key\" => \"Zdobądź klucz na stronie\",\r\n    \"get_free_fixer_api_key\" => \"Uzyskaj bezpłatny klucz API Fixer'a\",\r\n    \"get_key_alternative\" => \"Alternatywnie, możesz uzyskać darmowy klucz api fixer'a od\",\r\n    \"ai_model\" => \"Model AI\",\r\n    \"select_ai_model\" => \"Wybierz model AI\",\r\n    \"run_schedule\" => \"Harmonogram uruchamiania\",\r\n    \"manually\" => \"Ręcznie\",\r\n    \"coming_soon\" => \"Wkrótce dostępne\",\r\n    \"invalid_host\" => \"Nieprawidłowy host\",\r\n    \"ai_recommendations_info\" => \"Rekomendacje AI są generowane na podstawie Twoich subskrypcji i członków gospodarstwa domowego.\",\r\n    \"may_take_time\" => \"W zależności od dostawcy, modelu i liczby subskrypcji, generowanie rekomendacji może zająć trochę czasu.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Rekomendacje będą widoczne na pulpicie.\",\r\n    \"generate_recommendations\" => \"Generuj rekomendacje\",\r\n    \"display_settings\" => \"Ustawienia wyświetlania\",\r\n    \"theme_settings\" => \"Ustawienia motywu\",\r\n    \"colors\" => \"Kolory\",\r\n    \"custom_colors\" => \"Kolory niestandardowe\",\r\n    \"theme\" => \"Motyw\",\r\n    \"dark_theme\" => \"Przełącz na jasny/ciemny motyw\",\r\n    \"light_theme\" => \"Przełącz na ciemny/jasny motyw\",\r\n    \"automatic\" => \"Automatycznie\",\r\n    \"main_color\" => \"Główny kolor\",\r\n    \"accent_color\" => \"Kolor akcentowy\",\r\n    \"hover_color\" => \"Kolor najechania\",\r\n    \"save_custom_colors\" => \"Zapisz niestandardowe kolory\",\r\n    \"reset_custom_colors\" => \"Resetuj niestandardowe kolory\",\r\n    \"custom_css\" => \"Niestandardowy CSS\",\r\n    \"save_custom_css\" => \"Zapisz niestandardowy CSS\",\r\n    \"calculate_monthly_price\" => \"Oblicz i pokaż miesięczną cenę wszystkich subskrypcji\",\r\n    \"convert_prices\" => \"Zawsze przeliczaj i pokazuj ceny w mojej głównej walucie (wolniej)\",\r\n    \"show_original_price\" => \"Pokaż również oryginalną cenę, gdy dokonywane są przeliczenia lub obliczenia\",\r\n    \"experience\" => \"Doświadczenie\",\r\n    \"show_subscription_progress\" => \"Pokaż postęp subskrypcji\",\r\n    \"disabled_subscriptions\" => \"Wyłączone subskrypcje\",\r\n    \"hide_disabled_subscriptions\" => \"Ukryj wyłączone subskrypcje\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Pokaż wyłączone subskrypcje na dole\",\r\n    \"experimental_settings\" => \"Ustawienia eksperymentalne\",\r\n    \"remove_background\" => \"Próba usunięcia tła logo z wyszukiwania obrazów\",\r\n    \"use_mobile_navigation_bar\" => \"Użyj paska nawigacyjnego na urządzeniach mobilnych\",\r\n    \"experimental_info\" => \"Ustawienia eksperymentalne prawdopodobnie nie będą działać idealnie.\",\r\n    \"payment_methods\" => \"Metody płatności\",\r\n    \"payment_methods_info\" => \"Kliknij metodę płatności, aby ją wyłączyć/włączyć..\",\r\n    \"rename_payment_methods_info\" => \"Kliknij nazwę metody płatności, aby zmienić jej nazwę.\",\r\n    \"cant_delete_payment_method_in_use\" => \"Nie można wyłączyć używanej metody płatnościd\",\r\n    \"add_custom_payment\" => \"Dodaj niestandardową metodę płatności\",\r\n    \"payment_method_name\" => \"Nazwa metody płatności\",\r\n    \"payment_method_added_successfuly\" => \"Metoda płatności dodana pomyślnie\",\r\n    \"payment_method_removed\" => \"Usunięto metodę płatności\",\r\n    \"disable\" => \"Wyłącz\",\r\n    \"enable\" => \"Włącz\",\r\n    \"rename_payment_method\" => \"Zmień nazwę metody płatności\",\r\n    \"payment_renamed\" => \"Zmieniono nazwę metody płatności\",\r\n    \"payment_not_renamed\" => \"Nazwa metody płatności nie została zmieniona\",\r\n    \"test\" => \"Test\",\r\n    \"add\" => \"Dodaj\",\r\n    \"save\" => \"Zapisz\",\r\n    \"reset\" => \"Resetuj\",\r\n    \"main_accent_color_error\" => \"Kolor główny i akcentowy nie mogą być takie same\",\r\n    \"backup_and_restore\" => \"Kopia zapasowa i przywracanie\",\r\n    \"backup\" => \"Kopia zapasowa\",\r\n    \"restore\" => \"Przywróć\",\r\n    \"restore_info\" => \"Przywrócenie bazy danych zastąpi wszystkie bieżące dane. Po przywróceniu zostaniesz wylogowany.\",\r\n    \"account\" => \"Konto\",\r\n    \"export_subscriptions\" => \"Eksportuj subskrypcje\",\r\n    \"export_as_json\" => \"Eksportuj jako JSON\",\r\n    \"export_as_csv\" => \"Eksportuj jako CSV\",\r\n    \"danger_zone\" => \"Strefa zagrożenia\",\r\n    \"delete_account\" => \"Usuń konto\",\r\n    \"delete_account_info\" => \"Usunięcie konta spowoduje również usunięcie wszystkich subskrypcji i ustawień.\",\r\n    // Filters menu\r\n    \"filter\" => \"Filtr\",\r\n    \"clear\" => \"Wyczyść\",\r\n    // Toast\r\n    \"success\" => \"Sukces\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"Twoja sesja wygasła. Zaloguj się ponownie\",\r\n    \"fields_missing\" => \"Brakuje niektórych pól\",\r\n    \"fill_all_fields\" => \"Proszę wypełnić wszystkie pola\",\r\n    \"fill_mandatory_fields\" => \"Proszę wypełnić wszystkie pola obowiązkowe\",\r\n    \"error\" => \"Błąd\",\r\n    // Category\r\n    \"failed_add_category\" => \"Nie udało się dodać kategorii\",\r\n    \"failed_edit_category\" => \"Nie udało się edytować kategorii\",\r\n    \"category_in_use\" => \"Kategoria jest używana w subskrypcjach i nie można jej usunąć\",\r\n    \"failed_remove_category\" => \"Nie udało się usunąć kategorii\",\r\n    \"category_saved\" => \"Kategoria zapisana\",\r\n    \"category_removed\" => \"Kategoria usunięta\",\r\n    \"sort_order_saved\" => \"Porządek sortowania został zapisany\",\r\n    // Currency\r\n    \"currency_saved\" => \"został zapisany.\",\r\n    \"error_adding_currency\" => \"Błąd podczas dodawania wpisu waluty.\",\r\n    \"failed_to_store_currency\" => \"Nie udało się zapisać waluty w bazie danych.\",\r\n    \"currency_in_use\" => \"Waluta jest używana w subskrypcjach i nie można jej usunąć.\",\r\n    \"currency_is_main\" => \"Waluta jest ustawiona jako waluta główna i nie można jej usunąć.\",\r\n    \"failed_to_remove_currency\" => \"Nie udało się usunąć waluty z bazy danych.\",\r\n    \"failed_to_store_api_key\" => \"Nie udało się zapisać klucza API w bazie danych.\",\r\n    \"invalid_api_key\" => \"Nieprawidłowy klucz API.\",\r\n    \"api_key_saved\" => \"Klucz API zapidsany pomyślnie\",\r\n    \"currency_removed\" => \"Waluta została usunięta\",\r\n    // Household\r\n    \"failed_add_household\" => \"Nie udało się dodać domownika\",\r\n    \"failed_edit_household\" => \"Nie udało się edytować domownika\",\r\n    \"failed_remove_household\" => \"Nie udało się usunąć domownika\",\r\n    \"household_in_use\" => \"Domownik jest używany w subskrypcjach i nie można go usunąć\",\r\n    \"member_saved\" => \"Użytkownik zapisany\",\r\n    \"member_removed\" => \"Użytkownik usunięty\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Błąd podczas zapisywania danych powiadomień.\",\r\n    \"wallos_notification\" => \"Powiadomienie Wallos\",\r\n    \"test_notification\" => \"To jest powiadomienie testowe. Jeśli to widzisz, konfiguracja jest prawidłowa.\",\r\n    \"email_error\" => \"Błąd podczas wysyłania wiadomości e-mail\",\r\n    \"notification_failed\" => \"Powiadomienie nie powiodło się\",\r\n    \"notification_sent_successfuly\" => \"Powiadomienie wysłane pomyślnie\",\r\n    \"notifications_settings_saved\" => \"Ustawienia powiadomień zostały zapisane.\",\r\n    // Payments\r\n    \"payment_in_use\" => \"Nie można wyłączyć użytej metody płatności\",\r\n    \"failed_update_payment\" => \"Nie udało się zaktualizować metody płatności w bazie danych\",\r\n    \"enabled\" => \"włączone\",\r\n    \"disabled\" => \"wyłączone\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Błąd podczas pobierania obrazu\",\r\n    \"subscription_updated_successfuly\" => \"Subskrypcja została pomyślnie zaktualizowana\",\r\n    \"subscription_added_successfuly\" => \"Subskrypcja dodana pomyślnie\",\r\n    \"error_deleting_subscription\" => \"Błąd podczas usuwania subskrypcji.\",\r\n    \"invalid_request_method\" => \"Nieprawidłowa metoda żądania.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Błąd podczas aktualizacji danych użytkownika.\",\r\n    \"user_details_saved\" => \"Dane użytkownika zostały zapisane\",\r\n    // Admin Page\r\n    \"registrations\" => \"Rejestracje\",\r\n    \"enable_user_registrations\" => \"Włącz rejestracje użytkowników\",\r\n    \"maximum_number_users\" => \"Maksymalna liczba użytkowników\",\r\n    \"require_email_verification\" => \"Wymagaj weryfikacji e-maila\",\r\n    \"configure_smtp_settings_to_enable\" => \"Skonfiguruj ustawienia SMTP, aby włączyć\",\r\n    \"server_url\" => \"Adres URL serwera\",\r\n    \"server_url_info\" => \"Służy do weryfikacji adresu e-mail i odzyskiwania hasła. Musi to być prawidłowy publiczny adres URL.\",\r\n    \"server_url_password_reset\" => \"Jeśli zostanie ustawiona, włączy również funkcję resetowania hasła.\",\r\n    \"disable_login\" => \"Wyłącz logowanie\",\r\n    \"disable_login_info\" => \"Obejście logowania. Jeśli serwer działa tylko w sieci lokalnej, bez dostępu z zewnątrz, można wyłączyć logowanie. Spowoduje to automatyczne zalogowanie użytkownika admin.\",\r\n    \"disable_login_info2\" => \"To ustawienie można włączyć tylko wtedy, gdy rejestracja użytkowników jest wyłączona i nie ma więcej niż konto administratora.\",\r\n    \"max_users_info\" => \"Jeśli ustawisz 0, nie będzie limitu użytkowników.\",\r\n    \"user_management\" => \"Zarządzanie użytkownikami\",\r\n    \"delete_user\" => \"Usuń użytkownika\",\r\n    \"delete_user_info\" => \"Usunięcie użytkownika spowoduje również usunięcie wszystkich jego subskrypcji i ustawień.\",\r\n    \"create_user\" => \"Utwórz użytkownika\",\r\n    \"oidc_settings\" => \"Ustawienia OIDC\",\r\n    \"oidc_auth_enabled\" => \"Włącz uwierzytelnianie OIDC\",\r\n    \"create_user_automatically\" => \"Automatycznie twórz użytkowników\",\r\n    \"disable_password_login\" => \"Wyłącz logowanie za pomocą hasła\",\r\n    \"smtp_settings\" => \"Ustawienia SMTP\",\r\n    \"smtp_usage_info\" => \"Będzie używany do odzyskiwania hasła i innych e-maili systemowych.\",\r\n    \"security_settings\" => \"Ustawienia zabezpieczeń\",\r\n    \"ssrf_protection_info\" => \"Aby zapobiec atakom Server-Side Request Forgery (SSRF), Wallos domyślnie blokuje powiadomienia webhook wysyłane na prywatne lub wewnętrzne adresy sieciowe.\",\r\n    \"local_webhook_info\" => \"Jeśli musisz wysyłać webhooki do lokalnych usług (np. Home Assistant, Gotify lub Node-RED), wprowadź ich adresy IP lub nazwy hostów powyżej jako listę oddzieloną przecinkami (np. <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"maintenance_tasks\" => \"Zadania konserwacyjne\",\r\n    \"orphaned_logos\" => \"Osierocone logo\",\r\n    \"update\" => \"Aktualizacja\",\r\n    \"new_version_available\" => \"Dostępna jest nowa wersja Wallos\",\r\n    \"current_version\" => \"Aktualna wersja\",\r\n    \"latest_version\" => \"Najnowsza wersja\",\r\n    \"on_current_version\" => \"Używasz najnowszej wersji Wallos.\",\r\n    \"show_update_notification\" => \"Pokaż powiadomienie o aktualizacjach na dashboardzie\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"E-mail został zweryfikowany\",\r\n    \"email_verification_failed\" => \"Weryfikacja e-maila nie powiodła się\",\r\n    // Calendar\r\n    \"calendar\" => \"Kalendarz\",\r\n    \"sun\" => \"Niedz.\",\r\n    \"mon\" => \"Pon.\",\r\n    \"tue\" => \"Wt.\",\r\n    \"wed\" => \"Śr.\",\r\n    \"thu\" => \"Czw.\",\r\n    \"fri\" => \"Pt.\",\r\n    \"sat\" => \"Sob.\",\r\n    \"month-01\" => \"Styczeń\",\r\n    \"month-02\" => \"Luty\",\r\n    \"month-03\" => \"Marzec\",\r\n    \"month-04\" => \"Kwiecień\",\r\n    \"month-05\" => \"Maj\",\r\n    \"month-06\" => \"Czerwiec\",\r\n    \"month-07\" => \"Lipiec\",\r\n    \"month-08\" => \"Sierpień\",\r\n    \"month-09\" => \"Wrzesień\",\r\n    \"month-10\" => \"Październik\",\r\n    \"month-11\" => \"Listopad\",\r\n    \"month-12\" => \"Grudzień\",\r\n    \"total_cost\" => \"Całkowity koszt\",\r\n    \"export_icalendar\" => \"Eksportuj do iCalendar\",\r\n    \"over_budget_warning\" => \"Przekroczyłeś budżet\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Wprowadź kod TOTP\",\r\n\r\n];\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/pt.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"Tem que criar uma conta antes de poder iniciar sessão\",\r\n    \"username\" => \"Nome de utilizador\",\r\n    \"password\" => \"Password\",\r\n    \"email\" => \"Email\",\r\n    \"firstname\" => \"Nome próprio\",\r\n    \"lastname\" => \"Apelido\",\r\n    \"confirm_password\" => \"Confirmar Password\",\r\n    \"main_currency\" => \"Moeda Principal\",\r\n    \"language\" => \"Linguagem\",\r\n    \"passwords_dont_match\" => \"As passwords não coincidem\",\r\n    \"username_exists\" => \"Nome de utilizador já existe\",\r\n    \"email_exists\" => \"Email já existe\",\r\n    \"registration_failed\" => \"O registo falhou. Tente novamente\",\r\n    \"register\" => \"Registar\",\r\n    \"restore_database\" => \"Restaurar base de dados\",\r\n    // Login Page\r\n    \"please_login\" => \"Por favor inicie sessão\",\r\n    \"stay_logged_in\" => \"Manter sessão (30 dias)\",\r\n    \"login\" => \"Iniciar Sessão\",\r\n    \"login_with\" => \"Iniciar sessão com\",\r\n    \"or\" => \"ou\",\r\n    \"login_failed\" => \"Dados de autenticação incorrectos\",\r\n    \"registration_successful\" => \"Registo efectuado com sucesso.\",\r\n    \"user_email_waiting_verification\" => \"O seu e-mail precisa de ser validado. Verifique o seu correio eletrónico\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Esqueceu-se da password?\",\r\n    \"reset_password\" => \"Repor Password\",\r\n    \"reset_sent_check_email\" => \"Pedido de reposição de password enviado. Verifique o seu email.\",\r\n    \"password_reset_successful\" => \"Password reposta com sucesso\",\r\n    // Header\r\n    \"profile\" => \"Perfil\",\r\n    \"dashboard\" => \"Painel\",\r\n    \"subscriptions\" => \"Subscrições\",\r\n    \"stats\" => \"Estatísticas\",\r\n    \"settings\" => \"Definições\",\r\n    \"admin\" => \"Administração\",\r\n    \"about\" => \"Sobre\",\r\n    \"logout\" => \"Terminar Sessão\",\r\n    // Dashboard\r\n    \"hello\" => \"Olá\",\r\n    \"upcoming_payments\" => \"Próximos Pagamentos\",\r\n    \"no_upcoming_payments\" => \"Você não tem pagamentos agendados\",\r\n    \"overdue_renewals\" => \"Renovações Atrasadas\",\r\n    \"ai_recommendations\" => \"Recomendações de IA\",\r\n    \"your_budget\" => \"Seu Orçamento\",\r\n    \"budget\" => \"Orçamento\",\r\n    \"budget_used\" => \"Orçamento Usado\",\r\n    \"over_budget\" => \"Acima do Orçamento\",\r\n    \"your_subscriptions\" => \"Suas subscrições\",\r\n    \"your_savings\" => \"Suas Poupanças\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Subscrição\",\r\n    \"no_subscriptions_yet\" => \"Ainda não tem subscrições\",\r\n    \"add_first_subscription\" => \"Adicionar primeira subscrição\",\r\n    \"new_subscription\" => \"Nova Subscrição\",\r\n    \"search\" => \"Pesquisar\",\r\n    \"state\" => \"Estado\",\r\n    \"alphanumeric\" => \"Alfanumérico\",\r\n    \"sort\" => \"Ordenar\",\r\n    \"name\" => \"Nome\",\r\n    \"last_added\" => \"Última Adicionada\",\r\n    \"price\" => \"Preço\",\r\n    \"next_payment\" => \"Próximo Pagamento\",\r\n    \"renewal_type\" => \"Tipo de Renovação\",\r\n    \"auto_renewal\" => \"Renovação Automática\",\r\n    \"automatically_renews\" => \"Renova automaticamente\",\r\n    \"manual_renewal\" => \"Renovação Manual\",\r\n    \"start_date\" => \"Data de Início\",\r\n    \"inactive\" => \"Desactivar Subscrição\",\r\n    \"replaced_with\" => \"Substituída por\",\r\n    \"none\" => \"Nenhuma\",\r\n    \"member\" => \"Membro\",\r\n    \"category\" => \"Categoria\",\r\n    \"payment_method\" => \"Metodo de Pagamento\",\r\n    \"Daily\" => \"Diario\",\r\n    \"Weekly\" => \"Semanal\",\r\n    \"Monthly\" => \"Mensal\",\r\n    \"Yearly\" => \"Anual\",\r\n    \"daily\" => \"Dia(s)\",\r\n    \"weekly\" => \"Semana(s)\",\r\n    \"monthly\" => \"Mês(es)\",\r\n    \"yearly\" => \"Ano(s)\",\r\n    \"days\" => \"dias\",\r\n    \"weeks\" => \"semanas\",\r\n    \"months\" => \"meses\",\r\n    \"years\" => \"anos\",\r\n    \"external_url\" => \"Visitar URL Externo\",\r\n    \"empty_page\" => \"Página Vazia\",\r\n    \"clear_filters\" => \"Limpar Filtros\",\r\n    \"no_matching_subscriptions\" => \"Sem subscrições correspondentes\",\r\n    \"clone\" => \"Clonar\",\r\n    \"renew\" => \"Renovar\",\r\n    \"calculate_next_payment_date\" => \"Calcular Próxima Data de Pagamento\",\r\n    // Subscription form\r\n    \"add_subscription\" => \"Adicionar subscrição\",\r\n    \"edit_subscription\" => \"Modificar subscrição\",\r\n    \"subscription_name\" => \"Nome da subscrição\",\r\n    \"logo_preview\" => \"Pre-visualisação do logo\",\r\n    \"search_logo\" => \"Pesquisar logo na internet\",\r\n    \"web_search\" => \"Pesquisa online\",\r\n    \"currency\" => \"Moeda\",\r\n    \"payment_every\" => \"Pagamento a cada\",\r\n    \"frequency\" => \"Frequencia\",\r\n    \"Cycle\" => \"Ciclo\",\r\n    \"no_category\" => \"Sem categoria\",\r\n    \"paid_by\" => \"Pago por\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Notas\",\r\n    \"enable_notifications\" => \"Activar notificações para esta subscrição\",\r\n    \"default_value_from_settings\" => \"Valor por defeito das definições\",\r\n    \"cancellation_notification\" => \"Notificação de cancelamento\",\r\n    \"delete\" => \"Remover\",\r\n    \"cancel\" => \"Cancelar\",\r\n    \"upload_logo\" => \"Enviar Logo\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"Está a utilizar várias moedas nas suas subscrições. Para obter estatísticas válidas e exactas, defina uma chave API do Fixer na página de definições.\",\r\n    \"general_statistics\" => \"Estatísticas Gerais\",\r\n    \"active_subscriptions\" => \"Subscrições Activas\",\r\n    \"inactive_subscriptions\" => \"Subscrições inactivas\",\r\n    \"monthly_cost\" => \"Custo Mensal\",\r\n    \"yearly_cost\" => \"Custo Anual\",\r\n    \"average_monthly\" => \"Custo Mensal Médio das Subscrições\",\r\n    \"most_expensive\" => \"Custo da Subscrição Mais Cara\",\r\n    \"amount_due\" => \"Quantia em dívida este mês\",\r\n    \"percentage_budget_used\" => \"Percentagem do orçamento usada\",\r\n    \"budget_remaining\" => \"Orçamento Restante\",\r\n    \"amount_over_budget\" => \"Quantia acima do orçamento\",\r\n    \"monthly_savings\" => \"Poupança Mensal (em subscrições inactivas)\",\r\n    \"yearly_savings\" => \"Poupança Anual (em subscrições inactivas)\",\r\n    \"split_views\" => \"Vistas Divididas\",\r\n    \"category_split\" => \"Por Categoria\",\r\n    \"household_split\" => \"Por Membro\",\r\n    \"payment_method_split\" => \"Por Método de Pagamento\",\r\n    \"total_cost_trend\" => \"Tendência de Custo Total\",\r\n    \"cost_vs_budget\" => \"Custo vs Orçamento\",\r\n    // About page\r\n    \"about_and_credits\" => \"Sobre e Créditos\",\r\n    \"credits\" => \"Créditos\",\r\n    \"license\" => \"Licença\",\r\n    \"release_notes\" => \"Notas de Lançamento\",\r\n    \"update_available\" => \"Atualização Disponível\",\r\n    \"issues_and_requests\" => \"Problemas e Pedidos\",\r\n    \"the_author\" => \"O Autor\",\r\n    \"icons\" => \"Ícones\",\r\n    \"payment_icons\" => \"Ícones de Pagamentos\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"Enviar avatar\",\r\n    \"file_type_error\" => \"Tipo de ficheiro não permitido\",\r\n    \"user_details\" => \"Detalhes do utilizador\",\r\n    \"two_factor_authentication\" => \"Autenticação de dois fatores\",\r\n    \"two_factor_info\" => \"A autenticação de dois factores acrescenta uma camada extra de segurança à sua conta.<br>Necessitará de uma aplicação de autenticação como o Google Authenticator, Authy ou Ente Auth para ler o código QR.\",\r\n    \"two_factor_enabled_info\" => \"A sua conta está segura com a autenticação de dois factores. Pode desactivá-la clicando no botão acima.\",\r\n    \"enable_two_factor_authentication\" => \"Activar autenticação de dois factores\",\r\n    \"2fa_already_enabled\" => \"A autenticação de dois factores já está activada\",\r\n    \"totp_code_incorrect\" => \"Código TOTP incorrecto\",\r\n    \"backup_codes\" => \"Códigos de Backup\",\r\n    \"download_backup_codes\" => \"Descarregar códigos de backup\",\r\n    \"copy_to_clipboard\" => \"Copiar para a área de transferência\",\r\n    \"totp_backup_codes_info\" => \"Guarde estes códigos num local seguro. Pode usá-los para aceder à sua conta se perder o acesso ao seu dispositivo de autenticação.\",\r\n    \"disable_two_factor_authentication\" => \"Desactivar autenticação de dois factores\",\r\n    \"totp_code\" => \"Código TOTP\",\r\n    \"api_key\" => \"API Key\",\r\n    \"regenerate\" => \"Regenerar\",\r\n    \"api_key_info\" => \"A sua API Key é usada para aceder à API do Wallos. Não a partilhe com ninguém.\",\r\n    // Settings page\r\n    \"monthly_budget\" => \"Orçamento Mensal\",\r\n    \"budget_info\" => \"Ao definir um orçamento pode comparar com os gastos reais na página de estatísticas.\",\r\n    \"household\" => \"Agregado\",\r\n    \"save_member\" => \"Guardar Membro\",\r\n    \"delete_member\" => \"Apagar Membro\",\r\n    \"cant_delete_member\" => \"Não pode apagar o membro principal\",\r\n    \"cant_delete_member_in_use\" => \"Não pode apagar membro em uso em subscrição\",\r\n    \"household_info\" => \"O campo E-mail permite que os membros do agregado sejam notificados das subscrições que estão prestes a expirar.\",\r\n    \"notifications\" => \"Notificações\",\r\n    \"enable_email_notifications\" => \"Activar notificações por email\",\r\n    \"notify_me\" => \"Notificar-me\",\r\n    \"day_before\" => \"dia antes\",\r\n    \"on_due_date\" => \"Na data de vencimento\",\r\n    \"days_before\" => \"dias antes\",\r\n    \"smtp_address\" => \"Endereço SMTP\",\r\n    \"port\" => \"Porto\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"Utilizador SMTP\",\r\n    \"smtp_password\" => \"Password SMTP\",\r\n    \"from_email\" => \"Email de envio (Opcional)\",\r\n    \"send_to_other_emails\" => \"Também enviar notificações para os seguintes endereços de email (use ; para os separar):\",\r\n    \"smtp_info\" => \"A Password é armazenada e transmitida em texto. Por segurança, crie uma conta só para esta finalidade.\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Token do Bot Telegram\",\r\n    \"telegram_chat_id\" => \"ID do Chat Telegram\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Token do Pushplus\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"URL do Hook\",\r\n    \"mattermost_bot_username\" => \"Nome de Utilizador do Bot\",\r\n    \"mattermost_bot_icon_emoji\" => \"Icon Emoji do Bot\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"URL do Webhook\",\r\n    \"request_method\" => \"Método de Pedido\",\r\n    \"custom_headers\" => \"Cabeçalhos Personalizados\",\r\n    \"webhook_payload\" => \"Payload do Webhook\",\r\n    \"payment_notifications_payload\" => \"Payload da notificação de pagamento\",\r\n    \"cancelation_notification_payload\" => \"Payload da notificação de cancelamento\",\r\n    \"variables_available\" => \"Variáveis Disponíveis\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Token\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Nome de Utilizador do Bot\",\r\n    \"discord_bot_avatar_url\" => \"URL do Avatar do Bot\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Chave de Utilizador Pushover\",\r\n    \"host\" => \"Host\",\r\n    \"topic\" => \"Tópico\",\r\n    \"ignore_ssl_errors\" => \"Ignorar erros SSL\",\r\n    \"categories\" => \"Categorias\",\r\n    \"save_category\" => \"Guardar Categoria\",\r\n    \"delete_category\" => \"Apagar Categoria\",\r\n    \"cant_delete_category_in_use\" => \"Não pode apagar categoria em uso em subscrição\",\r\n    \"currencies\" => \"Moedas\",\r\n    \"save_currency\" => \"Guardar moeda\",\r\n    \"delete_currency\" => \"Apagar moeda\",\r\n    \"cant_delete_main_currency\" => \"Não pode apagar a moeda principal\",\r\n    \"cant_delete_currency_in_use\" => \"Não pode apagar moeda em uso em subscrição\",\r\n    \"exchange_update\" => \"Taxas de conversão actualizadas em\",\r\n    \"currency_info\" => \"Encontre a lista de moedas e os respectivos códigos em\",\r\n    \"currency_performance\" => \"Por motivos de desempenho mantenha apenas as moedas que usa.\",\r\n    \"fixer_api_key\" => \"Fixer API Key\",\r\n    \"provider\" => \"Fornecedor\",\r\n    \"fixer_info\" => \"Se usa multiplas moedas e deseja estatísticas correctas é necessário uma API Key grátis do Fixer.\",\r\n    \"get_key\" => \"Obtenha a sua API Key em\",\r\n    \"get_free_fixer_api_key\" => \"Obtenha a sua API Key grátis do Fixer\",\r\n    \"get_key_alternative\" => \"Como alternativa obtenha a sua API Key em\",\r\n    \"ai_model\" => \"Modelo de IA\",\r\n    \"select_ai_model\" => \"Selecionar modelo de IA\",\r\n    \"run_schedule\" => \"Agendamento de execução\",\r\n    \"manually\" => \"Manual\",\r\n    \"coming_soon\" => \"Em breve\",\r\n    \"invalid_host\" => \"Host inválido\",\r\n    \"ai_recommendations_info\" => \"As recomendações de IA são geradas com base nas suas assinaturas e membros da família.\",\r\n    \"may_take_time\" => \"Dependendo do provedor, modelo e número de assinaturas, a geração de recomendações pode levar algum tempo.\",\r\n    \"recommendations_visible_on_dashboard\" => \"As recomendações serão visíveis no painel.\",\r\n    \"generate_recommendations\" => \"Gerar recomendações\",\r\n    \"display_settings\" => \"Definições de exibição\",\r\n    \"theme_settings\" => \"Definições de Tema\",\r\n    \"colors\" => \"Cores\",\r\n    \"custom_colors\" => \"Cores Personalizadas\",\r\n    \"theme\" => \"Tema\",\r\n    \"dark_theme\" => \"Tema Escuro\",\r\n    \"light_theme\" => \"Tema Claro\",\r\n    \"automatic\" => \"Automático\",\r\n    \"main_color\" => \"Cor Principal\",\r\n    \"accent_color\" => \"Cor de Destaque\",\r\n    \"hover_color\" => \"Cor de Hover\",\r\n    \"save_custom_colors\" => \"Guardar Cores Personalizadas\",\r\n    \"reset_custom_colors\" => \"Repor Cores Personalizadas\",\r\n    \"custom_css\" => \"CSS Personalizado\",\r\n    \"save_custom_css\" => \"Guardar CSS Personalizado\",\r\n    \"calculate_monthly_price\" => \"Calcular e mostrar preço mensal para todas as subscrições\",\r\n    \"convert_prices\" => \"Converter e mostrar todas as subscrições na moeda principal (mais lento)\",\r\n    \"show_original_price\" => \"Também mostrar o preço original quando são feitas conversões ou cálculos\",\r\n    \"experience\" => \"Experiência\",\r\n    \"show_subscription_progress\" => \"Mostrar progresso da subscrição\",\r\n    \"disabled_subscriptions\" => \"Subscrições Desactivadas\",\r\n    \"hide_disabled_subscriptions\" => \"Esconder subscrições desactivadas\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Mostrar subscrições desactivadas no fundo da lista\",\r\n    \"experimental_settings\" => \"Definições Experimentais\",\r\n    \"remove_background\" => \"Tentar remover o fundo dos logos na pesquisa de imagem\",\r\n    \"use_mobile_navigation_bar\" => \"Usar barra de navegação móvel\",\r\n    \"experimental_info\" => \"Definições experimentais provavelmente não funcionarão correctamente.\",\r\n    \"payment_methods\" => \"Métodos de Pagamento\",\r\n    \"payment_methods_info\" => \"Clique num método de pagamento para o activar / desactivar.\",\r\n    \"rename_payment_methods_info\" => \"Clique no nome do método de pagamento para o renomear.\",\r\n    \"cant_delete_payment_method_in_use\" => \"Não pode desactivar metodo de pagamento em uso\",\r\n    \"add_custom_payment\" => \"Adicionar método de pagamento personalizado\",\r\n    \"payment_method_name\" => \"Nome do método de pagamento\",\r\n    \"payment_method_added_successfuly\" => \"Método de pagamento adicionado com sucesso\",\r\n    \"payment_method_removed\" => \"Método de pagamento removido\",\r\n    \"disable\" => \"Desactivar\",\r\n    \"enable\" => \"Activar\",\r\n    \"rename_payment_method\" => \"Renomear método de pagamento\",\r\n    \"payment_renamed\" => \"Método de pagamento renomeado\",\r\n    \"payment_not_renamed\" => \"Método de pagamento não renomeado\",\r\n    \"test\" => \"Testar\",\r\n    \"add\" => \"Adicionar\",\r\n    \"save\" => \"Guardar\",\r\n    \"reset\" => \"Repor\",\r\n    \"main_accent_color_error\" => \"A cor principal e a cor de destaque não podem ser iguais\",\r\n    \"backup_and_restore\" => \"Backup e Restauro\",\r\n    \"backup\" => \"Backup\",\r\n    \"restore\" => \"Restauro\",\r\n    \"restore_info\" => \"O restauro da base de dados apagará todos os dados actuais. A sua sessão irá terminar após o restauro.\",\r\n    \"account\" => \"Conta\",\r\n    \"export_subscriptions\" => \"Exportar Subscrições\",\r\n    \"export_as_json\" => \"Exportar como JSON\",\r\n    \"export_as_csv\" => \"Exportar como CSV\",\r\n    \"danger_zone\" => \"Zona de Perigo\",\r\n    \"delete_account\" => \"Eliminar Conta\",\r\n    \"delete_account_info\" => \"A eliminação da sua conta também eliminará todas as suas subscrições e definições.\",\r\n    // Filters menu\r\n    \"filter\" => \"Filtro\",\r\n    \"clear\" => \"Limpar\",\r\n    // Toast\r\n    \"success\" => \"Sucesso\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"A sessão expirou. Por favor autentique-se.\",\r\n    \"fields_missing\" => \"Alguns campos em falta\",\r\n    \"fill_all_fields\" => \"Por favor preencha todos os campos\",\r\n    \"fill_mandatory_fields\" => \"Por favor preencha todos os campos obrigatórios\",\r\n    \"error\" => \"Erro\",\r\n    // Category\r\n    \"failed_add_category\" => \"Erro ao adicionar categoria\",\r\n    \"failed_edit_category\" => \"Erro ao modificar categoria\",\r\n    \"category_in_use\" => \"Categoria em uso em subscrição e não pode ser removida\",\r\n    \"failed_remove_category\" => \"Erro ao remover categoria\",\r\n    \"category_saved\" => \"Categoria guardada\",\r\n    \"category_removed\" => \"Categoria removida\",\r\n    \"sort_order_saved\" => \"Ordenação guardada\",\r\n    // Currency\r\n    \"currency_saved\" => \"guardada.\",\r\n    \"error_adding_currency\" => \"Erro ao adicionar moeda.\",\r\n    \"failed_to_store_currency\" => \"Erro ao guardar a moeda na base de dados.\",\r\n    \"currency_in_use\" => \"Moeda em uso em subscrição e não pode ser removida.\",\r\n    \"currency_is_main\" => \"A Moeda principal não pode ser removida.\",\r\n    \"failed_to_remove_currency\" => \"Erro ao remover a moeda da base de dados.\",\r\n    \"failed_to_store_api_key\" => \"Erro ao guardar API Key na base de dados.\",\r\n    \"invalid_api_key\" => \"API Key inválida.\",\r\n    \"api_key_saved\" => \"API key guardada\",\r\n    \"currency_removed\" => \"Moeda removida\",\r\n    // Household\r\n    \"failed_add_household\" => \"Erro ao adicionar membro\",\r\n    \"failed_edit_household\" => \"Erro ao modificar membro\",\r\n    \"failed_remove_household\" => \"Erro ao remover membro\",\r\n    \"household_in_use\" => \"Membro está em uso em subscrição e não pode er removido\",\r\n    \"member_saved\" => \"Membro guardado\",\r\n    \"member_removed\" => \"Membro removido\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Erro ao guardar os dados das notificaçoes.\",\r\n    \"wallos_notification\" => \"Notificação Wallos\",\r\n    \"test_notification\" => \"Isto é uma notificação de teste. Se está a ver isto a configuração está correcta.\",\r\n    \"email_error\" => \"Erro ao enviar email\",\r\n    \"notification_sent_successfuly\" => \"Notificação enviada com sucesso\",\r\n    \"notifications_settings_saved\" => \"Configuração de notificações guardada.\",\r\n    \"notification_failed\" => \"Erro ao enviar notificação\",\r\n    // Payments\r\n    \"payment_in_use\" => \"Não pode desactivar método de pagamento em uso\",\r\n    \"failed_update_payment\" => \"Erro ao actualizar método de pagamento na base de dados\",\r\n    \"enabled\" => \"activado\",\r\n    \"disabled\" => \"descativado\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Erro ao obter a imagem\",\r\n    \"subscription_updated_successfuly\" => \"Subscrição actualizada com sucesso\",\r\n    \"subscription_added_successfuly\" => \"Subscrição adicionada com sucesso\",\r\n    \"error_deleting_subscription\" => \"Erro ao remover subscrição.\",\r\n    \"invalid_request_method\" => \"Método invalido.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Erro ao actualizar dados do utilizador.\",\r\n    \"user_details_saved\" => \"Dados do utiliador actualizados.\",\r\n    // Admin Page\r\n    \"registrations\" => \"Registos\",\r\n    \"enable_user_registrations\" => \"Activar Registos de Utilizadores\",\r\n    \"maximum_number_users\" => \"Número Máximo de Utilizadores\",\r\n    \"require_email_verification\" => \"Requerer verificação de email\",\r\n    \"configure_smtp_settings_to_enable\" => \"Configure as definições SMTP para activar esta funcionalidade.\",\r\n    \"server_url\" => \"URL do Servidor\",\r\n    \"server_url_info\" => \"Usado para gerar links de verificação de email. Deve ser um URL público e válido.\",\r\n    \"server_url_password_reset\" => \"Se definido, também activará a funcionalidade de reposição da palavra-passe.\",\r\n    \"disable_login\" => \"Desactivar Inicio de Sessão\",\r\n    \"disable_login_info\" => \"Ultrapassar o início de sessão. Se o seu servidor funciona apenas numa rede local, sem acesso externo, pode desativar o início de sessão. Isto irá iniciar automaticamente a sessão do utilizador administrador.\",\r\n    \"disable_login_info2\" => \"Só pode ativar esta definição se o registo de utilizadores estiver desativado e se não houver mais do que a conta de utilizador administrador.\",\r\n    \"max_users_info\" => \"0 para ilimitado\",\r\n    \"user_management\" => \"Gestão de Utilizadores\",\r\n    \"delete_user\" => \"Apagar Utilizador\",\r\n    \"delete_user_info\" => \"Apagar utilizador irá remover todas as suas subscrições e dados associados.\",\r\n    \"create_user\" => \"Criar Utilizador\",\r\n    \"oidc_settings\" => \"Definições OIDC\",\r\n    \"oidc_auth_enabled\" => \"Activar autenticação OIDC\",\r\n    \"create_user_automatically\" => \"Criar utilizador automaticamente\",\r\n    \"disable_password_login\" => \"Desactivar login por password\",\r\n    \"smtp_settings\" => \"Definições SMTP\",\r\n    \"smtp_usage_info\" => \"Será usado para recuperações de password e outros emails do sistema.\",\r\n    \"security_settings\" => \"Configurações de Segurança\",\r\n    \"ssrf_protection_info\" => \"Para evitar ataques Server-Side Request Forgery (SSRF), o Wallos bloqueia por padrão notificações webhook para endereços de rede privados ou internos.\",\r\n    \"local_webhook_info\" => \"Se precisar enviar webhooks para serviços locais (como Home Assistant, Gotify ou Node-RED), insira seus endereços IP ou nomes de host acima como uma lista separada por vírgulas (por exemplo <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"maintenance_tasks\" => \"Tarefas de Manutenção\",\r\n    \"orphaned_logos\" => \"Logos Órfãos\",\r\n    \"update\" => \"Actualizar\",\r\n    \"new_version_available\" => \"Uma nova versão do Wallos está disponível\",\r\n    \"current_version\" => \"Versão Atual\",\r\n    \"latest_version\" => \"Última Versão\",\r\n    \"on_current_version\" => \"Está a usar a versão mais recente do Wallos.\",\r\n    \"show_update_notification\" => \"Mostrar notificação de atualizações no dashboard\",\r\n    \"Cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"Email verificado\",\r\n    \"email_verification_failed\" => \"Verificação de email falhou\",\r\n    // Calendar\r\n    \"calendar\" => \"Calendário\",\r\n    \"sun\" => \"Dom\",\r\n    \"mon\" => \"Seg\",\r\n    \"tue\" => \"Ter\",\r\n    \"wed\" => \"Qua\",\r\n    \"thu\" => \"Qui\",\r\n    \"fri\" => \"Sex\",\r\n    \"sat\" => \"Sáb\",\r\n    \"month-01\" => \"Janeiro\",\r\n    \"month-02\" => \"Fevereiro\",\r\n    \"month-03\" => \"Março\",\r\n    \"month-04\" => \"Abril\",\r\n    \"month-05\" => \"Maio\",\r\n    \"month-06\" => \"Junho\",\r\n    \"month-07\" => \"Julho\",\r\n    \"month-08\" => \"Agosto\",\r\n    \"month-09\" => \"Setembro\",\r\n    \"month-10\" => \"Outubro\",\r\n    \"month-11\" => \"Novembro\",\r\n    \"month-12\" => \"Dezembro\",\r\n    \"total_cost\" => \"Custo Total\",\r\n    \"export_icalendar\" => \"Exportar iCalendar\",\r\n    \"over_budget_warning\" => \"O orçamento foi ultrapassado\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Insira o código TOTP\",\r\n\r\n];\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/pt_br.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"É necessário criar uma conta antes de poder se logar\",\n    \"username\" => \"Usuário\",\n    \"password\" => \"Senha\",\n    \"email\" => \"Email\",\n    \"firstname\" => \"Primeiro nome\",\n    \"lastname\" => \"Sobrenome\",\n    \"confirm_password\" => \"Confirmar Senha\",\n    \"main_currency\" => \"Moeda principal\",\n    \"language\" => \"Idioma\",\n    \"passwords_dont_match\" => \"As senhas não são iguais\",\n    \"username_exists\" => \"O nome de usuário já existe\",\n    \"email_exists\" => \"O email já está em uso\",\n    \"registration_failed\" => \"O registro falhou. Por favor, tente novamente\",\n    \"register\" => \"Registrar\",\n    \"restore_database\" => \"Restaurar banco de dados\",\n    // Login Page\n    \"please_login\" => \"Por favor, faça o login\",\n    \"stay_logged_in\" => \"Me manter logado (30 dias)\",\n    \"login\" => \"Login\",\n    \"login_with\" => \"Entrar com\",\n    \"or\" => \"ou\",\n    \"login_failed\" => \"As informações de login estão incorretas\",\n    \"registration_successful\" => \"Registro bem-sucedido\",\n    \"user_email_waiting_verification\" => \"Seu e-mail precisa ser validado. Por favor, verifique seu e-mail\",\n    // Password Reset Page\n    \"forgot_password\" => \"Esqueceu a senha?\",\n    \"reset_password\" => \"Redefinir senha\",\n    \"reset_sent_check_email\" => \"Redefinição de senha enviada. Por favor, verifique seu email\",\n    \"password_reset_successful\" => \"Senha redefinida com sucesso\",\n    // Header\n    \"profile\" => \"Perfil\",\n    \"dashboard\" => \"Painel\",\n    \"subscriptions\" => \"Assinaturas\",\n    \"stats\" => \"Estatísticas\",\n    \"settings\" => \"Configurações\",\n    \"admin\" => \"Admin\",\n    \"about\" => \"Sobre\",\n    \"logout\" => \"Sair\",\n    // Dashboard\n    \"hello\" => \"Olá\",\n    \"upcoming_payments\" => \"Pagamentos Futuros\",\n    \"no_upcoming_payments\" => \"Você não tem pagamentos futuros\",\n    \"overdue_renewals\" => \"Renovações Atrasadas\",\n    \"ai_recommendations\" => \"Recomendações de IA\",\n    \"your_budget\" => \"Seu Orçamento\",\n    \"budget\" => \"Orçamento\",\n    \"budget_used\" => \"Orçamento Usado\",\n    \"over_budget\" => \"Acima do Orçamento\",\n    \"your_subscriptions\" => \"Suas Assinaturas\",\n    \"your_savings\" => \"Suas Economias\",\n    // Subscriptions page\n    \"subscription\" => \"Assinatura\",\n    \"no_subscriptions_yet\" => \"Você ainda não tem nenhuma assinatura\",\n    \"add_first_subscription\" => \"Adicionar a primeira assinatura\",\n    \"new_subscription\" => \"Nova assinatura\",\n    \"search\" => \"Pesquisar\",\n    \"state\" => \"Estado\",\n    \"alphanumeric\" => \"Alfanumérico\",\n    \"sort\" => \"Ordenar\",\n    \"name\" => \"Nome\",\n    \"last_added\" => \"Última adicionada\",\n    \"price\" => \"Preço\",\n    \"next_payment\" => \"Próximo pagamento\",\n    \"renewal_type\" => \"Tipo de renovação\",\n    \"auto_renewal\" => \"Renovação automática\",\n    \"automatically_renews\" => \"Renova automaticamente\",\n    \"manual_renewal\" => \"Renovação manual\",\n    \"start_date\" => \"Data de início\",\n    \"inactive\" => \"Assinatura inativa\",\n    \"replaced_with\" => \"Substituída por\",\n    \"none\" => \"Nenhuma\",\n    \"member\" => \"Membro\",\n    \"category\" => \"Categoria\",\n    \"payment_method\" => \"Método de Pagamento\",\n    \"Daily\" => \"Diário\",\n    \"Weekly\" => \"Semanal\",\n    \"Monthly\" => \"Mensal\",\n    \"Yearly\" => \"Anual\",\n    \"daily\" => \"Dia(s)\",\n    \"weekly\" => \"Semana(s)\",\n    \"monthly\" => \"Mês(es)\",\n    \"yearly\" => \"Ano(s)\",\n    \"days\" => \"dias\",\n    \"weeks\" => \"semanas\",\n    \"months\" => \"meses\",\n    \"years\" => \"anos\",\n    \"external_url\" => \"Abrir URL externa\",\n    \"empty_page\" => \"Página vazia\",\n    \"clear_filters\" => \"Limpar filtros\",\n    \"no_matching_subscriptions\" => \"Nenhuma assinatura encontrada\",\n    \"clone\" => \"Clonar\",\n    \"renew\" => \"Renovar\",\n    \"calculate_next_payment_date\" => \"Calcular próxima data de pagamento\",\n    // Subscription form\n    \"add_subscription\" => \"Adicionar assinatura\",\n    \"edit_subscription\" => \"Editar assinatura\",\n    \"subscription_name\" => \"Nome da assinatura\",\n    \"logo_preview\" => \"Preview do logo\",\n    \"search_logo\" => \"Pesquisar logo internet\",\n    \"web_search\" => \"Buscar na internet\",\n    \"currency\" => \"Moeda\",\n    \"payment_every\" => \"Pagamento a cada\",\n    \"frequency\" => \"Frequência\",\n    \"cycle\" => \"Ciclo\",\n    \"no_category\" => \"Sem categoria\",\n    \"paid_by\" => \"Pago por\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Anotações\",\n    \"enable_notifications\" => \"Ativar notificações para essa assinatura\",\n    \"default_value_from_settings\" => \"Valor padrão das configurações\",\n    \"cancellation_notification\" => \"Notificação de cancelamento\",\n    \"delete\" => \"Excluir\",\n    \"cancel\" => \"Cancelar\",\n    \"upload_logo\" => \"Enviar Logo\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Você está usando várias moedas em suas assinaturas. Para obter estatísticas válidas e precisas, defina uma chave de API do Fixer na página de configurações.\",\n    \"general_statistics\" => \"Estatísticas gerais\",\n    \"active_subscriptions\" => \"Assinaturas ativas\",\n    \"inactive_subscriptions\" => \"Assinaturas inativas\",\n    \"monthly_cost\" => \"Custo mensal\",\n    \"yearly_cost\" => \"Custo anual\",\n    \"average_monthly\" => \"Custom médio mensal\",\n    \"most_expensive\" => \"Assinatura mais cara\",\n    \"amount_due\" => \"Valor devido nesse mês\",\n    \"percentage_budget_used\" => \"Porcentagem do orçamento utilizado\",\n    \"budget_remaining\" => \"Orçamento restante\",\n    \"amount_over_budget\" => \"Valor acima do orçamento\",\n    \"monthly_savings\" => \"Economia mensal (em assinaturas inativas)\",\n    \"yearly_savings\" => \"Economia anual (em assinaturas inativas)\",\n    \"split_views\" => \"Visualizações\",\n    \"category_split\" => \"Por categoria\",\n    \"household_split\" => \"Por membro\",\n    \"payment_method_split\" => \"Por método de pagamento\",\n    \"total_cost_trend\" => \"Tendência de custo total\",\n    \"cost_vs_budget\" => \"Custo vs Orçamento\",\n    // About page\n    \"about_and_credits\" => \"Sobre e Créditos\",\n    \"credits\" => \"Créditos\",\n    \"license\" => \"Licença\",\n    \"release_notes\" => \"Notas de lançamento\",\n    \"update_available\" => \"Atualização disponível\",\n    \"issues_and_requests\" => \"Problemas e Pedidos\",\n    \"the_author\" => \"O autor\",\n    \"icons\" => \"Ícones\",\n    \"payment_icons\" => \"Ícones de pagamento\",\n    // Profile page\n    \"upload_avatar\" => \"Carregar avatar\",\n    \"file_type_error\" => \"Tipo de arquivo não permitido\",\n    \"user_details\" => \"Informações do Usuário\",\n    \"two_factor_authentication\" => \"Autenticação de dois fatores\",\n    \"two_factor_info\" => \"A autenticação de dois fatores adiciona uma camada extra de segurança à sua conta.<br>Você precisará de um aplicativo autenticador, como o Google Authenticator, Authy ou Ente Auth, para ler o código QR.\",\n    \"two_factor_enabled_info\" => \"Sua conta está segura com a autenticação de dois fatores. Você pode desativá-la clicando no botão acima.\",\n    \"enable_two_factor_authentication\" => \"Ativar autenticação de dois fatores\",\n    \"2fa_already_enabled\" => \"A autenticação de dois fatores já está ativada\",\n    \"totp_code_incorrect\" => \"Código TOTP incorreto\",\n    \"backup_codes\" => \"Códigos de backup\",\n    \"download_backup_codes\" => \"Baixar códigos de backup\",\n    \"copy_to_clipboard\" => \"Copiar para a área de transferência\",\n    \"totp_backup_codes_info\" => \"Guarde esses códigos em um lugar seguro. Eles podem ser usados para acessar sua conta se você perder o acesso ao aplicativo de autenticação.\",\n    \"disable_two_factor_authentication\" => \"Desativar autenticação de dois fatores\",\n    \"totp_code\" => \"Código TOTP\",\n    \"api_key\" => \"Chave da API\",\n    \"regenerate\" => \"Regenerar\",\n    \"api_key_info\" => \"A chave da API é usada para acessar a API do Wallos. Não compartilhe sua chave com ninguém.\",\n    // Settings page\n    \"monthly_budget\" => \"Orçamento mensal\",\n    \"budget_info\" => \"O orçamento mensal é usado para calcular estatísticas\",\n    \"household\" => \"Membros\",\n    \"save_member\" => \"Salvar membro\",\n    \"delete_member\" => \"Excluir membro\",\n    \"cant_delete_member\" => \"Não é possível excluir o membro principal\",\n    \"cant_delete_member_in_use\" => \"Não é possível excluir um membro em uso em uma assinatura\",\n    \"household_info\" => \"O email permite que os membros sejam notificados quando uma assinatura estiver para expirar.\",\n    \"notifications\" => \"Notificações\",\n    \"enable_email_notifications\" => \"Ativar notificações por email\",\n    \"notify_me\" => \"Me notificar\",\n    \"day_before\" => \"dia antes\",\n    \"on_due_date\" => \"Na data de vencimento\",\n    \"days_before\" => \"dias antes\",\n    \"smtp_address\" => \"Endereço SMTP\",\n    \"port\" => \"Porta\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Usuário SMTP\",\n    \"smtp_password\" => \"Senha SMTP\",\n    \"from_email\" => \"Email de envio (Opcional)\",\n    \"send_to_other_emails\" => \"Também enviar notificações para os seguintes endereços de email (use ; para separá-los):\",\n    \"smtp_info\" => \"A senha do SMTP é transmitida em texto puro. Por segurança, crie uma conta só para esta finalidade.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Token do Bot\",\n    \"telegram_chat_id\" => \"Chat ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Token do Pushplus\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost URL do Webhook\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Nome de Usuário\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Ícone Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"URL do Webhook\",\n    \"request_method\" => \"Método de requisição\",\n    \"custom_headers\" => \"Cabeçalhos personalizados\",\n    \"webhook_payload\" => \"Payload do Webhook\",\n    \"payment_notifications_payload\" => \"Payload da notificação de pagamento\",\n    \"cancelation_notification_payload\" => \"Payload da notificação de cancelamento\",\n    \"variables_available\" => \"Variáveis disponíveis\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Nome do Bot\",\n    \"discord_bot_avatar_url\" => \"URL do Avatar\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Chave do Usuário\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Tópico\",\n    \"ignore_ssl_errors\" => \"Ignorar erros SSL\",\n    \"categories\" => \"Categorias\",\n    \"save_category\" => \"Salvar categoria\",\n    \"delete_category\" => \"Excluir categoria\",\n    \"cant_delete_category_in_use\" => \"Não é possível excluir uma categoria em uso em uma assinatura\",\n    \"currencies\" => \"Moedas\",\n    \"save_currency\" => \"Salvar moeda\",\n    \"delete_currency\" => \"Excluir moeda\",\n    \"cant_delete_main_currency\" => \"Não é possível excluir a moeda principal\",\n    \"cant_delete_currency_in_use\" => \"Não é possível excluir uma moeda em uso em uma assinatura\",\n    \"exchange_update\" => \"Taxas de câmbio atualizadas em\",\n    \"currency_info\" => \"Encontre as moedas suportadas e os códigos de moeda em\",\n    \"currency_performance\" => \"Para um melhor desempenho, mantenha apenas as moedas que você utiliza.\",\n    \"fixer_api_key\" => \"Chave da API do Fixer\",\n    \"provider\" => \"Fornecedor\",\n    \"fixer_info\" => \"Se você utiliza múltiplas moedas e deseja ter estatísticas precisas e ordenação das assinaturas, uma chave GRATUÍTA da API do Fixer é necessária.\",\n    \"get_key\" => \"Obtenha a sua chave em\",\n    \"get_free_fixer_api_key\" => \"Obtenha a sua chave API do Fixer gratuitamente\",\n    \"get_key_alternative\" => \"Como alternativa, você pode obter uma chave de API grátis em\",\n    \"ai_model\" => \"Modelo de IA\",\n    \"select_ai_model\" => \"Selecionar modelo de IA\",\n    \"run_schedule\" => \"Agendamento de execução\",\n    \"manually\" => \"Manual\",\n    \"coming_soon\" => \"Em breve\",\n    \"invalid_host\" => \"Host inválido\",\n    \"ai_recommendations_info\" => \"As recomendações de IA são geradas com base em suas assinaturas e membros da família.\",\n    \"may_take_time\" => \"Dependendo do provedor, modelo e número de assinaturas, a geração de recomendações pode levar algum tempo.\",\n    \"recommendations_visible_on_dashboard\" => \"As recomendações serão visíveis no painel.\",\n    \"generate_recommendations\" => \"Gerar recomendações\",\n    \"display_settings\" => \"Configurações de exibição\",\n    \"theme_settings\" => \"Configurações de tema\",\n    \"colors\" => \"Cores\",\n    \"custom_colors\" => \"Cores personalizadas\",\n    \"theme\" => \"Tema\",\n    \"dark_theme\" => \"Tema Escuro\",\n    \"light_theme\" => \"Tema Claro\",\n    \"automatic\" => \"Automático\",\n    \"main_color\" => \"Cor principal\",\n    \"accent_color\" => \"Cor de destaque\",\n    \"hover_color\" => \"Cor de destaque (hover)\",\n    \"save_custom_colors\" => \"Salvar Cores Personalizadas\",\n    \"reset_custom_colors\" => \"Redefinir Cores Personalizadas\",\n    \"custom_css\" => \"CSS Personalizado\",\n    \"save_custom_css\" => \"Salvar CSS Personalizado\",\n    \"calculate_monthly_price\" => \"Calcular e exibir o custo mensal para todas as assinaturas\",\n    \"convert_prices\" => \"Sempre converter e exibir preços na moeda principal (mais lento)\",\n    \"show_original_price\" => \"Também mostrar o preço original quando conversões ou cálculos são feitos\",\n    \"experience\" => \"Experiência\",\n    \"show_subscription_progress\" => \"Mostrar progresso da assinatura\",\n    \"disabled_subscriptions\" => \"Assinaturas desativadas\",\n    \"hide_disabled_subscriptions\" => \"Ocultar assinaturas desativadas\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Mostre as assinaturas desativadas no final da lista\",\n    \"experimental_settings\" => \"Configurações experimentais\",\n    \"remove_background\" => \"Tentar remover o fundo de logos na pesquisa de imagem\",\n    \"use_mobile_navigation_bar\" => \"Usar barra de navegação móvel\",\n    \"experimental_info\" => \"As configurações experimentais provavelmente não funcionarão corretamente\",\n    \"payment_methods\" => \"Métodos de pagamento\",\n    \"payment_methods_info\" => \"Clique em um método de pagamento para ativá-lo ou desativá-lo\",\n    \"rename_payment_methods_info\" => \"Clique no nome de um método de pagamento para renomeá-lo\",\n    \"cant_delete_payment_method_in_use\" => \"Não é possível desativar um método de pagamento em uso\",\n    \"add_custom_payment\" => \"Adicionar um método de pagamento personalizado\",\n    \"payment_method_name\" => \"Nome do método de pagamento\",\n    \"payment_method_added_successfuly\" => \"Método de pagamento adicionado com sucesso\",\n    \"payment_method_removed\" => \"Método de pagamento excluído\",\n    \"disable\" => \"Desativar\",\n    \"enable\" => \"Ativar\",\n    \"rename_payment_method\" => \"Renomear método de pagamento\",\n    \"payment_renamed\" => \"Método de pagamento renomeado\",\n    \"payment_not_renamed\" => \"Método de pagamento não renomeado\",\n    \"test\" => \"Testar\",\n    \"add\" => \"Adicionar\",\n    \"save\" => \"Salvar\",\n    \"reset\" => \"Redefinir\",\n    \"main_accent_color_error\" => \"A cor principal e a cor de destaque não podem ser iguais\",\n    \"backup_and_restore\" => \"Backup e Restauração\",\n    \"backup\" => \"Backup\",\n    \"restore\" => \"Restaurar\",\n    \"restore_info\" => \"A restauração do banco de dados substituirá todos os dados atuais. Você será desconectado após a restauração.\",\n    \"account\" => \"Conta\",\n    \"export_subscriptions\" => \"Exportar assinaturas\",\n    \"export_as_json\" => \"Exportar como JSON\",\n    \"export_as_csv\" => \"Exportar como CSV\",\n    \"danger_zone\" => \"Zona de perigo\",\n    \"delete_account\" => \"Excluir conta\",\n    \"delete_account_info\" => \"Excluir sua conta também excluirá todas as assinaturas e configurações.\",\n    // Filters menu\n    \"filter\" => \"Filtrar\",\n    \"clear\" => \"Limpar\",\n    // Toast\n    \"success\" => \"Sucesso\",\n    // Endpoint responses\n    \"session_expired\" => \"Sua sessão expirou. Por favor, faça o login novamente\",\n    \"fields_missing\" => \"Alguns campos estão faltando\",\n    \"fill_all_fields\" => \"Por favor, preencha todos os campos\",\n    \"fill_mandatory_fields\" => \"Por favor, preencha todos os campos obrigatórios\",\n    \"error\" => \"Erro\",\n    // Category\n    \"failed_add_category\" => \"Erro ao adicionar categoria\",\n    \"failed_edit_category\" => \"Erro ao editar categoria\",\n    \"category_in_use\" => \"Essa categoria está em uso em uma assinatura e não pode ser removida\",\n    \"failed_remove_category\" => \"Erro ao remover categoria\",\n    \"category_saved\" => \"Categoria salva\",\n    \"category_removed\" => \"Categoria excluída\",\n    \"sort_order_saved\" => \"Direção de ordenação salva\",\n    // Currency\n    \"currency_saved\" => \"foi salva.\",\n    \"error_adding_currency\" => \"Erro ao adicionar moeda.\",\n    \"failed_to_store_currency\" => \"Erro ao armazenar moeda no banco de dados\",\n    \"currency_in_use\" => \"A moeda está em uso em uma assinatura e não pode ser excluída\",\n    \"currency_is_main\" => \"A moeda está configurada como principal e não pode ser excluída\",\n    \"failed_to_remove_currency\" => \"Erro ao excluir a moeda do banco de dados\",\n    \"failed_to_store_api_key\" => \"Erro ao armazenar a chave da API no banco de dados\",\n    \"invalid_api_key\" => \"Chave da API inválida\",\n    \"api_key_saved\" => \"Chave da API salva com sucesso\",\n    \"currency_removed\" => \"Moeda excluída\",\n    // Household\n    \"failed_add_household\" => \"Erro ao adicionar membro\",\n    \"failed_edit_household\" => \"Erro ao editar membro\",\n    \"failed_remove_household\" => \"Erro ao excluir membro\",\n    \"household_in_use\" => \"O membro está em uso em uma assinatura e não pode ser removido\",\n    \"member_saved\" => \"Membro salvo\",\n    \"member_removed\" => \"Membro excluído\",\n    // Notifications\n    \"error_saving_notifications\" => \"Error ao salvar dados de notificação\",\n    \"wallos_notification\" => \"Notificação do Wallos\",\n    \"test_notification\" => \"Essa é uma notificação de teste. Se você está vendo isso, a configuração está correta.\",\n    \"email_error\" => \"Erro ao enviar email\",\n    \"notification_sent_successfuly\" => \"Notificação enviada com sucesso\",\n    \"notifications_settings_saved\" => \"Configurações de notificação salvas com sucesso\",\n    \"notification_failed\" => \"Falha ao enviar notificação\",\n    // Payments\n    \"payment_in_use\" => \"Não é possível desativar o método de pagamento\",\n    \"failed_update_payment\" => \"Erro ao atualizar o método de pagamento no banco de dados.\",\n    \"enabled\" => \"ativado\",\n    \"disabled\" => \"desativado\",\n    // Subscription\n    \"error_fetching_image\" => \"Erro ao carregar imagem\",\n    \"subscription_updated_successfuly\" => \"Assinatura atualizada com sucesso\",\n    \"subscription_added_successfuly\" => \"Assinatura adicionar com sucesso\",\n    \"error_deleting_subscription\" => \"Erro ao excluir assinatura\",\n    \"invalid_request_method\" => \"Método de requisição inválido\",\n    // User\n    \"error_updating_user_data\" => \"Erro ao atualizar os dados do usuário\",\n    \"user_details_saved\" => \"Dados do usuário salvos\",\n    // Admin Page\n    \"registrations\" => \"Registros\",\n    \"enable_user_registrations\" => \"Ativar registros de usuários\",\n    \"maximum_number_users\" => \"Número máximo de usuários\",\n    \"require_email_verification\" => \"Requerer verificação de email\",\n    \"configure_smtp_settings_to_enable\" => \"Configure as configurações SMTP para ativar o envio de email\",\n    \"server_url\" => \"URL do servidor\",\n    \"server_url_info\" => \"Será usado para gerar links de verificação de email, deve ser um endereço público e válido.\",\n    \"server_url_password_reset\" => \"Se definido, também ativará a funcionalidade de redefinição de senha.\",\n    \"disable_login\" => \"Desativar login\",\n    \"disable_login_info\" => \"Ignorar login. Se você executar o servidor somente em uma rede local, sem acesso externo, poderá desativar o login. Isso fará o login automático do usuário administrador.\",\n    \"disable_login_info2\" => \"Só é possível ativar essa configuração se o registro de usuário estiver desativado e não houver mais do que a conta de usuário administrador.\",\n    \"max_users_info\" => \"0 para ilimitado\",\n    \"user_management\" => \"Gerenciamento de usuários\",\n    \"delete_user\" => \"Excluir usuário\",\n    \"delete_user_info\" => \"Excluir um usuário também excluirá todas as assinaturas e dados associados\",\n    \"create_user\" => \"Criar usuário\",\n    \"oidc_settings\" => \"Configurações OIDC\",\n    \"oidc_auth_enabled\" => \"Habilitar autenticação OIDC\",\n    \"create_user_automatically\" => \"Criar usuário automaticamente\",\n    \"disable_password_login\" => \"Desativar login por senha\",\n    \"smtp_settings\" => \"Configurações SMTP\",\n    \"smtp_usage_info\" => \"Será usado para recuperação de senha e outros e-mails do sistema.\",\n    \"security_settings\" => \"Configurações de Segurança\",\n    \"ssrf_protection_info\" => \"Para evitar ataques Server-Side Request Forgery (SSRF), o Wallos bloqueia por padrão notificações webhook para endereços de rede privados ou internos.\",\n    \"local_webhook_info\" => \"Se precisar enviar webhooks para serviços locais (como Home Assistant, Gotify ou Node-RED), insira seus endereços IP ou nomes de host acima como uma lista separada por vírgulas (por exemplo <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Tarefas de manutenção\",\n    \"orphaned_logos\" => \"Logos órfãos\",\n    \"update\" => \"Atualizar\",\n    \"new_version_available\" => \"Nova versão do Wallos disponível\",\n    \"current_version\" => \"Versão atual\",\n    \"latest_version\" =>\"Última versão\",\n    \"on_current_version\" => \"Você está na última versão do Wallos.\",\n    \"show_update_notification\" => \"Mostrar notificação de atualização no dashboard\",\n    \"Cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"Email verificado\",\n    \"email_verification_failed\" => \"Falha na verificação do email\",\n    // Calendar\n    \"calendar\" => \"Calendário\",\n    \"sun\" => \"Dom\",\n    \"mon\" => \"Seg\",\n    \"tue\" => \"Ter\",\n    \"wed\" => \"Qua\",\n    \"thu\" => \"Qui\",\n    \"fri\" => \"Sex\",\n    \"sat\" => \"Sáb\",\n    \"month-01\" => \"Janeiro\",\n    \"month-02\" => \"Fevereiro\",\n    \"month-03\" => \"Março\",\n    \"month-04\" => \"Abril\",\n    \"month-05\" => \"Maio\",\n    \"month-06\" => \"Junho\",\n    \"month-07\" => \"Julho\",\n    \"month-08\" => \"Agosto\",\n    \"month-09\" => \"Setembro\",\n    \"month-10\" => \"Outubro\",\n    \"month-11\" => \"Novembro\",\n    \"month-12\" => \"Dezembro\",\n    \"total_cost\" => \"Custo total\",\n    \"export_icalendar\" => \"Exportar iCalendar\",\n    \"over_budget_warning\" => \"Você está acima do orçamento\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Insira o código TOTP\",\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/ro.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Trebuie să creezi un cont înainte de a te putea conecta.\",\n    \"username\" => \"Nume utilizator\",\n    \"password\" => \"Parolă\",\n    \"firstname\" => \"Prenume\",\n    \"lastname\" => \"Nume\",\n    \"email\" => \"E-mail\",\n    \"confirm_password\" => \"Confirmă parola\",\n    \"main_currency\" => \"Valuta principală\",\n    \"language\" => \"Limbă\",\n    \"passwords_dont_match\" => \"Parolele nu sunt identice\",\n    \"username_exists\" => \"Acest nume de utilizator există deja\",\n    \"email_exists\" => \"Acest e-mail există deja\",\n    \"registration_failed\" => \"Înregistrarea a eșuat, încearcă din nou\",\n    \"register\" => \"Înregistrează-te\",\n    \"restore_database\" => \"Restaurare bază de date\",\n    // Login Page\n    \"please_login\" => \"Conectează-te\",\n    \"stay_logged_in\" => \"Rămâi conectat (30 zile)\",\n    \"login\" => \"Conectare\",\n    \"login_with\" => \"Conectează-te cu\",\n    \"or\" => \"sau\",\n    \"login_failed\" => \"Date de conectare incorecte\",\n    \"registration_successful\" => \"Înregistrare reușită!\",\n    \"user_email_waiting_verification\" => \"Adresa de e-mail trebuie confirmată. Te rugăm să-ți verifici e-mailul.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Ai uitat parola?\",\n    \"reset_password\" => \"Resetare Parolă\",\n    \"reset_sent_check_email\" => \"E-mail de resetare trimis. Te rugăm să-ți verifici e-mailul.\",\n    \"password_reset_successful\" => \"Parola a fost actualizată\",\n    // Header\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Acasă\",\n    \"subscriptions\" => \"Abonamente\",\n    \"stats\" => \"Statistici\",\n    \"settings\" => \"Setări\",\n    \"admin\" => \"Admin\",\n    \"about\" => \"Despre\",\n    \"logout\" => \"Deconectare\",\n    // Dashboard\n    \"hello\" => \"Salut\",\n    \"upcoming_payments\" => \"Plăți scadente\",\n    \"no_upcoming_payments\" => \"Nu există plăți scadente\",\n    \"overdue_renewals\" => \"Reînnoiri scadente\",\n    \"ai_recommendations\" => \"Recomandări AI\",\n    \"your_budget\" => \"Bugetul tău\",\n    \"budget\" => \"Buget\",\n    \"budget_used\" => \"Buget folosit\",\n    \"over_budget\" => \"Peste Buget\",\n    \"your_subscriptions\" => \"Abonamentele tale\",\n    \"your_savings\" => \"Economiile tale\",\n    // Subscriptions page\n    \"subscription\" => \"Abonament\",\n    \"no_subscriptions_yet\" => \"Nu ai încă niciun abonament\",\n    \"add_first_subscription\" => \"Adaugă primul abonament\",\n    \"new_subscription\" => \"Abonament nou\",\n    \"search\" => \"Caută\",\n    \"state\" => \"Status\",\n    \"alphanumeric\" => \"Alfanumeric\",\n    \"sort\" => \"Sortează\",\n    \"name\" => \"Nume\",\n    \"last_added\" => \"Ultimul adăugat\",\n    \"price\" => \"Cost\",\n    \"next_payment\" => \"Următoarea plată\",\n    \"renewal_type\" => \"Tip reînoire\",\n    \"auto_renewal\" => \"Reînoire automată\",\n    \"automatically_renews\" => \"Se reînoiește automat\",\n    \"manual_renewal\" => \"Reînoire manuală\",\n    \"start_date\" => \"Dată început\",\n    \"inactive\" => \"Dezactivează abonament\",\n    \"replaced_with\" => \"Înlocuit cu\",\n    \"none\" => \"Niciunul\",\n    \"member\" => \"Membru\",\n    \"category\" => \"Categorie\",\n    \"payment_method\" => \"Metodă de plată\",\n    \"Daily\" => \"Zilnic\",\n    \"Weekly\" => \"Săptămânal\",\n    \"Monthly\" => \"Lunar\",\n    \"Yearly\" => \"Anual\",\n    \"daily\" => \"Zile\",\n    \"weekly\" => \"Săptămâni\",\n    \"monthly\" => \"Luni\",\n    \"yearly\" => \"Ani\",\n    \"days\" => \"zile\",\n    \"weeks\" => \"săptămâni\",\n    \"months\" => \"luni\",\n    \"years\" => \"ani\",\n    \"external_url\" => \"Accesează URL extern\",\n    \"empty_page\" => \"Pagină goală\",\n    \"clear_filters\" => \"Golește filtre\",\n    \"no_matching_subscriptions\" => \"Nicio potrivire\",\n    \"clone\" => \"Dublează\",\n    \"renew\" => \"Reînoiește\",\n    \"calculate_next_payment_date\" => \"Calculează data următoare-i tranzacții\",\n    // Subscription form\n    \"add_subscription\" => \"Adaugă abonament\",\n    \"edit_subscription\" => \"Modifică abonament\",\n    \"subscription_name\" => \"Titlu abonament\",\n    \"logo_preview\" => \"Previzualizare Logo\",\n    \"search_logo\" => \"Caută Logo\",\n    \"web_search\" => \"Căutare Web\",\n    \"currency\" => \"Valută\",\n    \"payment_every\" => \"Plată la fiecare\",\n    \"frequency\" => \"Frecvență\",\n    \"cycle\" => \"Ciclu\",\n    \"no_category\" => \"Nicio categorie\",\n    \"paid_by\" => \"Plătit de\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Notițe\",\n    \"enable_notifications\" => \"Activează notificările pentru acest abonament\",\n    \"default_value_from_settings\" => \"Valoarea standard din setări\",\n    \"cancellation_notification\" => \"Notificare de anulare\",\n    \"delete\" => \"Șterge\",\n    \"cancel\" => \"Anulează\",\n    \"upload_logo\" => \"Încarcă Logo\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Folosești mai multe valute pentru abonamentele tale. Pentru a obține statistici valide și precise, te rugăm să setezi o cheie API Fixer pe pagina de setări.\",\n    \"general_statistics\" => \"Statistici generale\",\n    \"active_subscriptions\" => \"Abonamente active\",\n    \"inactive_subscriptions\" => \"Abonamente inactive\",\n    \"monthly_cost\" => \"Costuri lunare\",\n    \"yearly_cost\" => \"Costuri anuale\",\n    \"average_monthly\" => \"Cost mediu lunar de abonament\",\n    \"most_expensive\" => \"Cel mai scump abonament\",\n    \"amount_due\" => \"De plătit în acestă lună\",\n    \"percentage_budget_used\" => \"Procentul bugetului utilizat\",\n    \"budget_remaining\" => \"Rămas din buget\",\n    \"amount_over_budget\" => \"Suma peste buget\",\n    \"monthly_savings\" => \"Economii lunare (abonamente inactive)\",\n    \"yearly_savings\" => \"Economii anuale (abonamente inactive)\",\n    \"split_views\" => \"Vizualizare împărțită\",\n    \"category_split\" => \"După categorii\",\n    \"household_split\" => \"După membrii familiei\",\n    \"payment_method_split\" => \"După metoda de plată\",\n    \"total_cost_trend\" => \"Evoluția costului total\",\n    \"cost_vs_budget\" => \"Costuri vs Buget\",\n    // About page\n    \"about_and_credits\" => \"Despre și merite\",\n    \"credits\" => \"Merite\",\n    \"license\" => \"Licență\",\n    \"release_notes\" => \"Note despre versiune\",\n    \"update_available\" => \"Actualizare disponibilă\",\n    \"issues_and_requests\" => \"Probleme și cereri\",\n    \"the_author\" => \"Autorul\",\n    \"icons\" => \"Icon-uri\",\n    \"payment_icons\" => \"Icon plată\",\n    // Profile page\n    \"upload_avatar\" => \"Încarcă Avatar\",\n    \"file_type_error\" => \"Tipul de fișier nu este permis\",\n    \"user_details\" => \"Detalii utilizator\",\n    \"two_factor_authentication\" => \"Autentificare în doi pași\",\n    \"two_factor_info\" => \"Autentificarea în doi pași adaugă un nivel suplimentar de securitate contului tău. <br>Vei avea nevoie de o aplicație de autentificare, cum ar fi Google Authenticator, Authy sau Ente Auth, pentru a scana codul QR.\",\n    \"two_factor_enabled_info\" => \"Contul tău este securizat cu autentificarea în doi pași. Poți să o dezactivezi făcând clic pe butonul de mai sus.\",\n    \"enable_two_factor_authentication\" => \"Activează autentificarea în doi pași\",\n    \"2fa_already_enabled\" => \"Autentificarea în doi pași este deja activă\",\n    \"totp_code_incorrect\" => \"Codul TOTP este incorect\",\n    \"backup_codes\" => \"Coduri de rezervă\",\n    \"download_backup_codes\" => \"Descarcă codurile de rezervă\",\n    \"copy_to_clipboard\" => \"Copiază în Clipboard\",\n    \"totp_backup_codes_info\" => \"Aceste coduri pot fi utilizate pentru a te conecta dacă pierzi accesul la aplicația de autentificare.\",\n    \"disable_two_factor_authentication\" => \"Dezactivează autentificarea în doi pași\",\n    \"totp_code\" => \"Codul TOTP\",\n    \"api_key\" => \"Cheia API\",\n    \"regenerate\" => \"Regenerează\",\n    \"api_key_info\" => \"Cheia API este utilizată pentru a accesa API-ul. Păstreaz-o secretă!\",\n    // Settings page\n    \"monthly_budget\" => \"Buget lunar\",\n    \"budget_info\" => \"Bugetul lunar este utilizat pentru calcularea statisticilor.\",\n    \"household\" => \"Membrii familiei\",\n    \"save_member\" => \"Salvează membru\",\n    \"delete_member\" => \"Șterge membru\",\n    \"cant_delete_member\" => \"Nu poți șterge membrul principal\",\n    \"cant_delete_member_in_use\" => \"Nu poți șterge un membru cu un abonament activ\",\n    \"household_info\" => \"Câmpul de e-mail permite membrilor familiei să fie notificați cu privire la abonamentele care urmează să expire.\",\n    \"notifications\" => \"Notificări\",\n    \"enable_email_notifications\" => \"Activează notificări e-mail\",\n    \"notify_me\" => \"Notifică-mă\",\n    \"day_before\" => \"zi înainte\",\n    \"on_due_date\" => \"Data scadentă\",\n    \"days_before\" => \"Yile înainte\",\n    \"smtp_address\" => \"Adresa SMTP\",\n    \"port\" => \"Port\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Utilizator SMTP\",\n    \"smtp_password\" => \"Parola SMTP\",\n    \"from_email\" => \"De la e-mail (Optional)\",\n    \"send_to_other_emails\" => \"Trimiteți notificările și la următoarele adrese de e-mail (folosiți ; pentru a le separa):\",\n    \"other_emails_placeholder\" => \"user@domain.com;test@user.com\",\n    \"smtp_info\" => \"Parola SMTP este transmisă și stocată în format text simplu. Din motive de securitate, vă rugăm să creați un cont special pentru acest scop.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Token Telegram Bot\",\n    \"telegram_chat_id\" => \"ID-ul de Chat Telegram\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Token-ul Pushplus\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"WebHook URL\",\n    \"mattermost_bot_username\" => \"Bot nume utillizator\",\n    \"mattermost_bot_icon_emoji\" => \"Bot Icon Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"Metoda de solicitare\",\n    \"custom_headers\" => \"Titluri personalizate\",\n    \"webhook_payload\" => \"Titluri personalizate\",\n    \"payment_notifications_payload\" => \"Payload Notificare de plată\",\n    \"cancelation_notification_payload\" => \"Payload Notificare de anulare\",\n    \"variables_available\" => \"Variabile disponibile\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Nume utillizator Discord Bot\",\n    \"discord_bot_avatar_url\" => \"Discord Bot URL Avatar \",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover User Key\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Topic\",\n    \"ignore_ssl_errors\" => \"Ignoră erori SSL\",\n    \"categories\" => \"Categorii\",\n    \"save_category\" => \"Salvează categorie\",\n    \"delete_category\" => \"Șterge categorie\",\n    \"cant_delete_category_in_use\" => \"Nu poți șterge o categorie utilizată într-un abonament\",\n    \"currencies\" => \"Valută\",\n    \"save_currency\" => \"Salvează valută\",\n    \"delete_currency\" => \"Șterge valută\",\n    \"cant_delete_main_currency\" => \"Nu poți șterge valuta principală\",\n    \"cant_delete_currency_in_use\" => \"Nu poți șterge o valută utilizată într-un abonament\",\n    \"exchange_update\" => \"Cursurile de schimb actualizate ultima dată la\",\n    \"currency_info\" => \"Găsești valutele acceptate și codurile valutare corecte pe\",\n    \"currency_performance\" => \"Pentru o performanță îmbunătățită, păstreapăstrează numai valutele pe care le utilizezi.\",\n    \"fixer_api_key\" => \"Cheie API Fixer \",\n    \"provider\" => \"Furnizor\",\n    \"fixer_info\" => \"Dacă utilizezi mai multe monede și dorești statistici precise și sortarea abonamentelor, este necesară o cheie API GRATUITĂ de la Fixer.\",\n    \"get_key\" => \"Obține cheia la\",\n    \"get_free_fixer_api_key\" => \"Obține gratuit cheia API Fixer\",\n    \"get_key_alternative\" => \"Alternativ, poți obține o cheie API fixer gratuită de la\",\n    \"ai_model\" => \"Model AI\",\n    \"select_ai_model\" => \"Selectează modelul AI\",\n    \"run_schedule\" => \"Rulează\",\n    \"manually\" => \"Manual\",\n    \"coming_soon\" => \"Disponibil curând\",\n    \"invalid_host\" => \"Host invalid\",\n    \"ai_recommendations_info\" => \"Recomandările AI sunt generate pe baza abonamentelor dvs. și a membrilor familiei.\",\n    \"may_take_time\" => \"În funcție de furnizor, model și numărul de abonamente, generarea recomandărilor poate dura ceva timp.\",\n    \"recommendations_visible_on_dashboard\" => \"Recomandările vor fi vizibile pe panoul Acasă.\",\n    \"generate_recommendations\" => \"Generează recomandări\",\n    \"display_settings\" => \"Afișează setări\",\n    \"theme_settings\" => \"Setări temă\",\n    \"colors\" => \"Culori\",\n    \"custom_colors\" => \"Culori personalizate\",\n    \"theme\" => \"Temă\",\n    \"dark_theme\" => \"Întunecat\",\n    \"light_theme\" => \"Luminos\",\n    \"automatic\"=> \"Automat\",\n    \"main_color\" => \"Culoare principală\",\n    \"accent_color\" => \"Culoare de accent\",\n    \"hover_color\" => \"Culoare la Hover\",\n    \"save_custom_colors\" => \"Salvează culori personalizate\",\n    \"reset_custom_colors\" => \"Resetează culori personalizate\",\n    \"custom_css\" => \"CSS personalizat\",\n    \"save_custom_css\" => \"Salvează CSS personalizat\",\n    \"calculate_monthly_price\" => \"Calculează și afișează prețul lunar pentru toate abonamentele\",\n    \"convert_prices\" => \"Convertește și afișează întotdeauna prețurile în moneda mea principală (mai lent)\",\n    \"show_original_price\" => \"Afișează și prețul inițial atunci când se efectuează conversii sau calcule.\",\n    \"experience\" => \"Experiență\",\n    \"show_subscription_progress\" => \"Afișează progresul abonamentului\",\n    \"disabled_subscriptions\" => \"Abonamente dezactivate\",\n    \"hide_disabled_subscriptions\" => \"Ascunde abonamentele dezactivate\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Afișează abonamentele dezactivate în partea de jos\",\n    \"experimental_settings\" => \"Setări experimentale\",\n    \"remove_background\" => \"Încearcă să elimini fundalul logo-urilor din căutarea de imagini\",\n    \"use_mobile_navigation_bar\" => \"Folosește bara de navigare mobilă\",\n    \"experimental_info\" => \"Setările experimentale probabil nu vor funcționa perfect.\",\n    \"payment_methods\" => \"Metode de plată\",\n    \"payment_methods_info\" => \"Faceți clic pe o metodă de plată pentru a o dezactiva/activa.\",\n    \"rename_payment_methods_info\" => \"Faceți clic pe numele unei metode de plată pentru a o redenumi.\",\n    \"cant_delete_payment_method_in_use\" => \"Nu se poate dezactiva metodă de plată în uz\",\n    \"add_custom_payment\" => \"Adaugă metodă de plată personalizată\",\n    \"payment_method_name\" => \"Numele metodei de plată\",\n    \"payment_method_added_successfuly\" => \"Metoda de plată adăugată cu succes\",\n    \"payment_method_removed\" => \"Metodă de plată eliminată\",\n    \"disable\" => \"Dezactivează\",\n    \"enable\" => \"Activează\",\n    \"rename_payment_method\" => \"Redenumirea metodei de plată\",\n    \"payment_renamed\" => \"Metodă de plată redenumită\",\n    \"payment_not_renamed\" => \"Metoda de plată nu a fost redenumită\",\n    \"test\" => \"Test\",\n    \"add\" => \"Adaugă\",\n    \"save\" => \"Salvează\",\n    \"reset\" => \"Resetează\",\n    \"main_accent_color_error\" => \"Culoarea principală și culoarea de accent nu pot fi identice.\",\n    \"backup_and_restore\" => \"Backup și restaurare\",\n    \"backup\" => \"Backup\",\n    \"restore\" => \"Restaurează\",\n    \"restore_info\" => \"Restaurarea bazei de date va suprascrie toate datele actuale. Vei fi deconectat după restaurare.\",\n    \"account\" => \"Cont\",\n    \"export_subscriptions\" => \"Export Abonamente\",\n    \"export_as_json\" => \"Export ca JSON\",\n    \"export_as_csv\" => \"Export ca CSV\",\n    \"danger_zone\" => \"Zona periculoasă\",\n    \"delete_account\" => \"Șterge cont\",\n    \"delete_account_info\" => \"Ștergerea contului va șterge și toate abonamentele și setările.\",\n    // Filters menu\n    \"filter\" => \"Filtrează\",\n    \"clear\" => \"Golește\",\n    // Toast\n    \"success\" => \"Succes\",\n    // Endpoint responses\n    \"session_expired\" => \"Sesiunea a expirat. Te rugăm să te autentifici din nou.\",\n    \"fields_missing\" => \"Unele câmpuri lipsesc\",\n    \"fill_all_fields\" => \"Te rugăm, completează toate câmpurile\",\n    \"fill_mandatory_fields\" => \"Te rugăm, completează toate câmpurile obligatorii\",\n    \"error\" => \"Erori\",\n    // Category\n    \"failed_add_category\" => \"Nu s-a reușit adăugarea categoriei\",\n    \"failed_edit_category\" => \"Nu s-a reușit editarea categoriei\",\n    \"category_in_use\" => \"Categoria este în uz și nu poate fi eliminată.\",\n    \"failed_remove_category\" => \"Nu s-a reușit eliminarea categoriei\",\n    \"category_saved\" => \"Categorie salvată\",\n    \"category_removed\" => \"Categorie eliminată\",\n    \"sort_order_saved\" => \"Sortare salvată\",\n    // Currency\n    \"currency_saved\" => \"a fost salvată.\",\n    \"error_adding_currency\" => \"Eroare la adăugarea valutei.\",\n    \"failed_to_store_currency\" => \"Nu s-a reușit stocarea monedei în baza de date.\",\n    \"currency_in_use\" => \"Valuta este în uz și nu poate fi ștersă.\",\n    \"currency_is_main\" => \"Valuta este setată ca monedă principală și nu poate fi ștersă.\",\n    \"failed_to_remove_currency\" => \"Nu s-a reușit eliminarea valutei din baza de date\",\n    \"failed_to_store_api_key\" => \"Nu s-a reușit stocarea cheii API în baza de date\",\n    \"invalid_api_key\" => \"Cheie API invalidă.\",\n    \"api_key_saved\" => \"Cheie API salvată cu succes\",\n    \"currency_removed\" => \"Valută ștearsă\",\n    // Household\n    \"failed_add_household\" => \"Nu s-a reușit adăugarea membrului familiei\",\n    \"failed_edit_household\" => \"Nu s-a putut edita membrul familiei\",\n    \"failed_remove_household\" => \"Nu s-a reușit eliminarea membrului familiei\",\n    \"household_in_use\" => \"Membrul familiei este în uz în abonamente și nu poate fi eliminat\",\n    \"member_saved\" => \"Membru adăugat\",\n    \"member_removed\" => \"Membru șters\",\n    // Notifications\n    \"error_saving_notifications\" => \"Eroare la salvarea datelor notificărilor.\",\n    \"wallos_notification\" => \"Notificare Wallos\",\n    \"test_notification\" => \"Aceasta este o notificare de test. Dacă vedeți acest mesaj, configurația este corectă.\",\n    \"email_error\" => \"Eroare la trimiterea e-mailului\",\n    \"notification_sent_successfuly\" => \"Notificare trimisă cu succes\",\n    \"notifications_settings_saved\" => \"Setările de notificare au fost salvate cu succes.\",\n    \"notification_failed\" => \"Notificare eșusată\",\n    // Payments\n    \"payment_in_use\" => \"Nu se poate dezactiva o metodă de plată în uz\",\n    \"failed_update_payment\" => \"Nu s-a reușit actualizarea metodei de plată în baza de date\",\n    \"enabled\" => \"activat\",\n    \"disabled\" => \"dezactivat\",\n    // Subscription\n    \"error_fetching_image\" => \"Eroare la afișarea imagini\",\n    \"subscription_updated_successfuly\" => \"Abonamentul a fost actualizat cu succes\",\n    \"subscription_added_successfuly\" => \"Abonamentul a fost adăugat cu succes\",\n    \"error_deleting_subscription\" => \"Eroare la ștergerea abonamentului.\",\n    \"invalid_request_method\" => \"Metodă de solicitare invalidă.\",\n    // User\n    \"error_updating_user_data\" => \"Eroare la actualizarea datelor utilizatorului.\",\n    \"user_details_saved\" => \"Detalii utilizator salvate\",\n    // Admin Page\n    \"registrations\" => \"Înregistrări\",\n    \"enable_user_registrations\" => \"Activează înregistrarea utilizatorilor\",\n    \"maximum_number_users\" => \"Număr maxim de utlizatori\",\n    \"require_email_verification\" => \"Solicită verificarea adresei de e-mail\",\n    \"configure_smtp_settings_to_enable\" => \"Configurează setările SMTP pentru a activa\",\n    \"server_url\" => \"URL Server\",\n    \"server_url_info\" => \"Utilizat pentru verificarea adresei de e-mail și recuperarea parolei. Trebuie să fie o adresă URL publică validă.\",\n    \"server_url_password_reset\" => \"Dacă este setată, va activa și funcționalitatea de resetare a parolei.\",\n    \"disable_login\" => \"Dezactivează autentificarea\",\n    \"disable_login_info\" => \"Omite autentificarea. Dacă rulezi serverul într-o rețea locală, fără acces extern, poți dezactiva autentificarea. Acest lucru va autentifica automat utilizatorul administrator.\",\n    \"disable_login_info2\" => \"Poți activa această setare numai dacă înregistrarea utilizatorilor este dezactivată și nu există alt cont decât cel de administrator.\",\n    \"max_users_info\" => \"0 înseamnă nelimitat\",\n    \"user_management\" => \"Management utilizatori\",\n    \"delete_user\" => \"Șterge utilizator\",\n    \"delete_user_info\" => \"Ștergerea unui utilizator va șterge implicit toate abonamentele și setările acestuia.\",\n    \"create_user\" => \"Crează utilizator\",\n    \"oidc_settings\" => \"Setări OIDC\",\n    \"oidc_oauth_enabled\" => \"Activează OIDC/OAuth\",\n    \"create_user_automatically\" => \"Crează utilizatori automat\",\n    \"disable_password_login\" => \"Dezactivează logarea cu parolă\",\n    \"smtp_settings\" => \"Setări SMTP\",\n    \"smtp_usage_info\" => \"Va fi utilizată pentru recuperarea parolei și alte e-mailuri de sistem.\",\n    \"security_settings\" => \"Setări de srcuritate\",\n    \"ssrf_protection_info\" => \"Pentru a preveni atacurile de tip Server-Side Request Forgery (SSRF), Wallos blochează în mod implicit notificările webhook către adrese de rețea private sau interne.\",\n    \"local_webhook_info\" => \"Dacă trebuie să trimți webhook-uri către servicii locale (cum ar fi Home Assistant, Gotify sau Node-RED), introdu adresele IP sau numele de gazdă ale acestora mai sus, sub forma unei liste separate prin virgule (de exemplu, <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Sarcini întreținere\",\n    \"orphaned_logos\" => \"Logo-uri fără uz\",\n    \"update\" => \"Actualizare\",\n    \"new_version_available\" => \"O nouă versiune Wallos este disponibilă\",\n    \"current_version\" => \"Versiunea curentă\",\n    \"latest_version\" => \"Ultima versiune\",\n    \"on_current_version\" => \"Folosești cea mai recentă versiune Wallos.\",\n    \"show_update_notification\" => \"Afișează notificări pentru actualizări pe panoul Acasă\",\n    \"cronjobs\" => \"Cronjobs\",\n    // e-mail Verification\n    \"email_verified\" => \"Succes! e-mailul a fost verificat\",\n    \"email_verification_failed\" => \"Verificarea e-mailului a eșuat\",\n    // Calendar\n    \"calendar\" => \"Calendar\",\n    \"sun\" => \"Dum\",\n    \"mon\" => \"Lu\",\n    \"tue\" => \"Ma\",\n    \"wed\" => \"Mie\",\n    \"thu\" => \"Joi\",\n    \"fri\" => \"Vin\",\n    \"sat\" => \"Sâm\",\n    \"month-01\" => \"Ianuarie\",\n    \"month-02\" => \"Februarie\",\n    \"month-03\" => \"Marz\",\n    \"month-04\" => \"Aprilie\",\n    \"month-05\" => \"Mai\",\n    \"month-06\" => \"Iunie\",\n    \"month-07\" => \"Iulie\",\n    \"month-08\" => \"August\",\n    \"month-09\" => \"Septembrie\",\n    \"month-10\" => \"Octombrie\",\n    \"month-11\" => \"Noiembrie\",\n    \"month-12\" => \"Decembrie\",\n    \"total_cost\" => \"Cost total\",\n    \"export_icalendar\" => \"Exportează iCalendar\",\n    \"over_budget_warning\" => \"Ai trecut peste buget\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Inserează codul TOTP\",\n\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/ru.php",
    "content": "<?php\r\n\r\n$i18n = [\r\n    // Registration page\r\n    \"create_account\" => \"Вам необходимо создать учетную запись, прежде чем вы сможете войти в систему\",\r\n    \"username\" => \"Имя пользователя\",\r\n    \"password\" => \"Пароль\",\r\n    \"email\" => \"E-mail\",\r\n    \"firstname\" => \"Имя\",\r\n    \"lastname\" => \"Фамилия\",\r\n    \"confirm_password\" => \"Подтвердите пароль\",\r\n    \"main_currency\" => \"Основная валюта\",\r\n    \"language\" => \"Язык\",\r\n    \"passwords_dont_match\" => \"Пароли не совпадают\",\r\n    \"username_exists\" => \"Имя пользователя уже существует\",\r\n    \"email_exists\" => \"E-mail уже существует\",\r\n    \"registration_failed\" => \"Регистрация не удалась, попробуйте еще раз.\",\r\n    \"register\" => \"Регистрация\",\r\n    \"restore_database\" => \"Восстановить базу данных\",\r\n    // Login Page\r\n    \"please_login\" => \"Пожалуйста, войдите\",\r\n    \"stay_logged_in\" => \"Оставаться в системе (30 дней)\",\r\n    \"login\" => \"Авторизоваться\",\r\n    \"login_with\" => \"Войти с помощью\",\r\n    \"or\" => \"или\",\r\n    \"login_failed\" => \"Данные для входа неверны\",\r\n    \"registration_successful\" => \"Регистрация прошла успешно\",\r\n    \"user_email_waiting_verification\" => \"Ваша электронная почта нуждается в проверке. Пожалуйста, проверьте свою электронную почту\",\r\n    // Password Reset Page\r\n    \"forgot_password\" => \"Забыли пароль?\",\r\n    \"reset_password\" => \"Сбросить пароль\",\r\n    \"reset_sent_check_email\" => \"Ссылка для сброса пароля отправлена на вашу электронную почту\",\r\n    \"password_reset_successful\" => \"Пароль успешно сброшен\",\r\n    // Header\r\n    \"profile\" => \"Профиль\",\r\n    \"dashboard\" => \"Панель\",\r\n    \"subscriptions\" => \"Подписки\",\r\n    \"stats\" => \"Статистика\",\r\n    \"settings\" => \"Настройки\",\r\n    \"admin\" => \"Администратор\",\r\n    \"about\" => \"О программе\",\r\n    \"logout\" => \"Выйти\",\r\n    // Dashboard\r\n    \"hello\" => \"Привет\",\r\n    \"upcoming_payments\" => \"Предстоящие платежи\",\r\n    \"no_upcoming_payments\" => \"У вас нет предстоящих платежей\",\r\n    \"overdue_renewals\" => \"Просроченные продления\",\r\n    \"ai_recommendations\" => \"Рекомендации ИИ\",\r\n    \"your_budget\" => \"Ваш бюджет\",\r\n    \"budget\" => \"Бюджет\",\r\n    \"budget_used\" => \"Использованный бюджет\",\r\n    \"over_budget\" => \"Превышение бюджета\",\r\n    \"your_subscriptions\" => \"Ваши подписки\",\r\n    \"your_savings\" => \"Ваши сбережения\",\r\n    // Subscriptions page\r\n    \"subscription\" => \"Подписка\",\r\n    \"no_subscriptions_yet\" => \"У вас пока нет подписок\",\r\n    \"add_first_subscription\" => \"Добавить первую подписку\",\r\n    \"new_subscription\" => \"Новая подписка\",\r\n    \"search\" => \"Поиск\",\r\n    \"state\" => \"Состояние\",\r\n    \"alphanumeric\" => \"Алфавитный порядок\",\r\n    \"sort\" => \"Сортировка\",\r\n    \"name\" => \"Имя\",\r\n    \"last_added\" => \"Дата создания\",\r\n    \"price\" => \"Стоимость\",\r\n    \"next_payment\" => \"Следующий платеж\",\r\n    \"renewal_type\" => \"Тип продления\",\r\n    \"auto_renewal\" => \"Автоматическое продление\",\r\n    \"automatically_renews\" => \"Автоматическое продление\",\r\n    \"manual_renewal\" => \"Ручное продление\",\r\n    \"start_date\" => \"Дата начала\",\r\n    \"inactive\" => \"Отключить подписку\",\r\n    \"replaced_with\" => \"Заменена на\",\r\n    \"none\" => \"Нет\",\r\n    \"member\" => \"Член семьи\",\r\n    \"category\" => \"Категория\",\r\n    \"payment_method\" => \"Способ оплаты\",\r\n    \"Daily\" => \"День\",\r\n    \"Weekly\" => \"Неделя\",\r\n    \"Monthly\" => \"Месяц\",\r\n    \"Yearly\" => \"Год\",\r\n    \"daily\" => \"День\",\r\n    \"weekly\" => \"Неделя\",\r\n    \"monthly\" => \"Месяц\",\r\n    \"yearly\" => \"Год\",\r\n    \"days\" => \"дней\",\r\n    \"weeks\" => \"недель\",\r\n    \"months\" => \"месяцев\",\r\n    \"years\" => \"года\",\r\n    \"external_url\" => \"Посетите внешний URL-адрес\",\r\n    \"empty_page\" => \"Пустая страница\",\r\n    \"clear_filters\" => \"Очистить фильтры\",\r\n    \"no_matching_subscriptions\" => \"Нет подходящих подписок\",\r\n    \"clone\" => \"Клонировать\",\r\n    \"renew\" => \"Продлить\",\r\n    \"calculate_next_payment_date\" => \"Рассчитать дату следующего платежа\",\r\n    // Subscription form\r\n    \"add_subscription\" => \"Добавить подписку\",\r\n    \"edit_subscription\" => \"Изменить подписку\",\r\n    \"subscription_name\" => \"Название подписки\",\r\n    \"logo_preview\" => \"Предварительный просмотр логотипа\",\r\n    \"search_logo\" => \"Поиск логотипа в Интернете\",\r\n    \"web_search\" => \"Веб-поиск\",\r\n    \"currency\" => \"Валюта\",\r\n    \"payment_every\" => \"Оплата каждые\",\r\n    \"frequency\" => \"Частота\",\r\n    \"cycle\" => \"Цикл\",\r\n    \"no_category\" => \"Нет категории\",\r\n    \"paid_by\" => \"Оплачивает\",\r\n    \"url\" => \"URL\",\r\n    \"notes\" => \"Примечания\",\r\n    \"enable_notifications\" => \"Включить уведомления для этой подписки\",\r\n    \"default_value_from_settings\" => \"Значение по умолчанию из настроек\",\r\n    \"cancellation_notification\" => \"Уведомление об отмене\",\r\n    \"delete\" => \"Удалить\",\r\n    \"cancel\" => \"Отмена\",\r\n    \"upload_logo\" => \"Загрузить логотип\",\r\n    // Statistics page\r\n    \"cant_convert_currency\" => \"Вы используете несколько валют в своих подписках. Для получения достоверной и точной статистики, пожалуйста, установите API-ключ Fixer на странице настроек.\",\r\n    \"general_statistics\" => \"Общая статистика\",\r\n    \"active_subscriptions\" => \"Активные подписки\",\r\n    \"inactive_subscriptions\" => \"Неактивные подписки\",\r\n    \"monthly_cost\" => \"Ежемесячная стоимость\",\r\n    \"yearly_cost\" => \"Годовая стоимость\",\r\n    \"average_monthly\" => \"Средняя ежемесячная стоимость подписки\",\r\n    \"most_expensive\" => \"Самая дорогая стоимость подписки\",\r\n    \"amount_due\" => \"Сумма к оплате в этом месяце\",\r\n    \"percentage_budget_used\" => \"Процент использования бюджета\",\r\n    \"budget_remaining\" => \"Оставшийся бюджет\",\r\n    \"amount_over_budget\" => \"Сумма превышения бюджета\",\r\n    \"monthly_savings\" => \"Ежемесячная экономия (при неактивных подписках)\",\r\n    \"yearly_savings\" => \"Годовая экономия (при неактивных подписках)\",\r\n    \"split_views\" => \"Подробная статистика\",\r\n    \"category_split\" => \"По категориям\",\r\n    \"household_split\" => \"По членам семьи\",\r\n    \"payment_method_split\" => \"По способам оплаты\",\r\n    \"total_cost_trend\" => \"Тенденция общей стоимости\",\r\n    \"cost_vs_budget\" => \"Стоимость по сравнению с бюджетом\",\r\n    // About page\r\n    \"about_and_credits\" => \"О компании и авторах\",\r\n    \"credits\" => \"Благодарности\",\r\n    \"license\" => \"Лицензия\",\r\n    \"release_notes\" => \"Заметки о выпуске\",\r\n    \"update_available\" => \"Доступно обновление\",\r\n    \"issues_and_requests\" => \"Проблемы и вопросы\",\r\n    \"the_author\" => \"Автор\",\r\n    \"icons\" => \"Значки\",\r\n    \"payment_icons\" => \"Значки способов оплаты\",\r\n    // Profile page\r\n    \"upload_avatar\" => \"Загрузить аватар\",\r\n    \"file_type_error\" => \"Указанный тип файла не поддерживается.\",\r\n    \"user_details\" => \"Данные пользователя\",\r\n    \"two_factor_authentication\" => \"Двухфакторная аутентификация\",\r\n    \"two_factor_info\" => \"Двухфакторная аутентификация добавляет дополнительный уровень безопасности к вашей учетной записи.<br>Для сканирования QR-кода вам понадобится приложение-аутентификатор, например Google Authenticator, Authy или Ente Auth.\",\r\n    \"two_factor_enabled_info\" => \"Ваш аккаунт защищен с помощью двухфакторной аутентификации. Вы можете отключить ее, нажав на кнопку выше.\",\r\n    \"enable_two_factor_authentication\" => \"Включить двухфакторную аутентификацию\",\r\n    \"2fa_already_enabled\" => \"Двухфакторная аутентификация уже включена\",\r\n    \"totp_code_incorrect\" => \"Код TOTP неверен\",\r\n    \"backup_codes\" => \"Резервные коды\",\r\n    \"download_backup_codes\" => \"Скачать резервные коды\",\r\n    \"copy_to_clipboard\" => \"Скопировать в буфер обмена\",\r\n    \"totp_backup_codes_info\" => \"Сохраните эти коды в безопасном месте. Они могут быть использованы для входа в систему, если вы потеряете доступ к приложению аутентификации.\",\r\n    \"disable_two_factor_authentication\" => \"Отключить двухфакторную аутентификацию\",\r\n    \"totp_code\" => \"Код TOTP\",\r\n    \"monthly_budget\" => \"Ежемесячный бюджет\",\r\n    \"api_key\" => \"API ключ\",\r\n    \"regenerate\" => \"Сгенерировать\",\r\n    \"api_key_info\" => \"API ключ используется для доступа к вашим данным через API. Не передавайте его третьим лицам.\",\r\n    // Settings page\r\n    \"budget_info\" => \"Если вы укажете бюджет, Wallos будет отображать вашу текущую стоимость подписок в сравнении с вашим бюджетом.\",\r\n    \"household\" => \"Семья\",\r\n    \"save_member\" => \"Сохранить члена семьи\",\r\n    \"delete_member\" => \"Удалить члена семьи\",\r\n    \"cant_delete_member\" => \"Не могу удалить основного члена семьи\",\r\n    \"cant_delete_member_in_use\" => \"Невозможно удалить члена семьи, используемого в подписке.\",\r\n    \"household_info\" => \"Поле электронной почты позволяет членам семьи получать уведомления об истечении срока действия подписки.\",\r\n    \"notifications\" => \"Уведомления\",\r\n    \"enable_email_notifications\" => \"Включить уведомления по электронной почте\",\r\n    \"notify_me\" => \"Уведомить меня за\",\r\n    \"day_before\" => \"день до события\",\r\n    \"on_due_date\" => \"в день события\",\r\n    \"days_before\" => \"дня(дней) до события\",\r\n    \"smtp_address\" => \"SMTP-адрес\",\r\n    \"port\" => \"Порт\",\r\n    \"tls\" => \"TLS\",\r\n    \"ssl\" => \"SSL\",\r\n    \"smtp_username\" => \"Имя пользователя SMTP\",\r\n    \"smtp_password\" => \"Пароль SMTP\",\r\n    \"from_email\" => \"От кого E-Mail (необязательно)\",\r\n    \"send_to_other_emails\" => \"Также отправлять уведомления на следующие адреса электронной почты (используйте ; для их разделения):\",\r\n    \"smtp_info\" => \"Пароль SMTP передается и сохраняется в виде открытого текста. В целях безопасности создайте учетную запись только для Wallos.\",\r\n    \"telegram\" => \"Telegram\",\r\n    \"telegram_bot_token\" => \"Токен Telegram-бота\",\r\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\r\n    \"pushplus\" => \"Pushplus\",\r\n    \"pushplus_token\" => \"Токен Pushplus\",\r\n    \"serverchan\" => \"Serverchan\",\r\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\r\n    \"mattermost\" => \"Mattermost\",\r\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\r\n    \"mattermost_bot_username\" => \"Mattermost Bot Имя пользователя\",\r\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Иконка Emoji\",\r\n    \"webhook\" => \"Webhook\",\r\n    \"webhook_url\" => \"Webhook URL\",\r\n    \"request_method\" => \"Метод запроса\",\r\n    \"custom_headers\" => \"Пользовательские заголовки\",\r\n    \"webhook_payload\" => \"Webhook Payload\",\r\n    \"payment_notifications_payload\" => \"Payload уведомлений о платеже\",\r\n    \"cancelation_notification_payload\" => \"Уведомление об отмене подписки Payload\",\r\n    \"variables_available\" => \"Доступные переменные\",\r\n    \"gotify\" => \"Gotify\",\r\n    \"token\" => \"Токен\",\r\n    \"discord\" => \"Discord\",\r\n    \"discord_bot_username\" => \"Имя пользователя бота Discord\",\r\n    \"discord_bot_avatar_url\" => \"URL-адрес аватара бота Discord\",\r\n    \"pushover\" => \"Pushover\",\r\n    \"pushover_user_key\" => \"Ключ пользователя Pushover\",\r\n    \"host\" => \"Хост\",\r\n    \"topic\" => \"Тема\",\r\n    \"ignore_ssl_errors\" => \"Игнорировать ошибки SSL\",\r\n    \"categories\" => \"Категории\",\r\n    \"save_category\" => \"Сохранить категорию\",\r\n    \"delete_category\" => \"Удалить категорию\",\r\n    \"cant_delete_category_in_use\" => \"Невозможно удалить категорию, используемую в подписке.\",\r\n    \"currencies\" => \"Валюты\",\r\n    \"save_currency\" => \"Сохранить валюту\",\r\n    \"delete_currency\" => \"Удалить валюту\",\r\n    \"cant_delete_main_currency\" => \"Не могу удалить основную валюту\",\r\n    \"cant_delete_currency_in_use\" => \"Невозможно удалить валюту, используемую в подписке.\",\r\n    \"exchange_update\" => \"Курсы валют последний раз обновлялись\",\r\n    \"currency_info\" => \"Найдите поддерживаемые валюты и правильные коды валют на\",\r\n    \"currency_performance\" => \"Для повышения производительности сохраняйте только те валюты, которые вы используете.\",\r\n    \"fixer_api_key\" => \"Ключ Fixer API\",\r\n    \"provider\" => \"Провайдер\",\r\n    \"fixer_info\" => \"Если вы используете несколько валют и хотите получить точную статистику и сортировку подписок, вам необходим БЕСПЛАТНЫЙ ключ API от Fixer.\",\r\n    \"get_key\" => \"Получите ключ по адресу\",\r\n    \"get_free_fixer_api_key\" => \"Получите бесплатный ключ API Fixer\",\r\n    \"get_key_alternative\" => \"Кроме того, вы можете получить бесплатный ключ API Fixer на сайте\",\r\n    \"ai_model\" => \"Модель ИИ\",\r\n    \"select_ai_model\" => \"Выбрать модель ИИ\",\r\n    \"run_schedule\" => \"Запустить расписание\",\r\n    \"manually\" => \"Вручную\",\r\n    \"coming_soon\" => \"Скоро\",\r\n    \"invalid_host\" => \"Неверный хост\",\r\n    \"ai_recommendations_info\" => \"Рекомендации ИИ генерируются на основе ваших подписок и членов семьи.\",\r\n    \"may_take_time\" => \"В зависимости от провайдера, модели и количества подписок генерация рекомендаций может занять некоторое время.\",\r\n    \"recommendations_visible_on_dashboard\" => \"Рекомендации будут видны на панели управления.\",\r\n    \"generate_recommendations\" => \"Сгенерировать рекомендации\",\r\n    \"display_settings\" => \"Настройки отображения\",\r\n    \"theme_settings\" => \"Настройки темы\",\r\n    \"colors\" => \"Цвета\",\r\n    \"custom_colors\" => \"Пользовательские цвета\",\r\n    \"theme\" => \"Тема\",\r\n    \"dark_theme\" => \"Темная тема\",\r\n    \"light_theme\" => \"Светлая тема\",\r\n    \"automatic\" => \"Автоматически\",\r\n    \"main_color\" => \"Основной цвет\",\r\n    \"accent_color\" => \"Акцентный цвет\",\r\n    \"hover_color\" => \"Цвет при наведении\",\r\n    \"save_custom_colors\" => \"Сохранить пользовательские цвета\",\r\n    \"reset_custom_colors\" => \"Сбросить пользовательские цвета\",\r\n    \"custom_css\" => \"Пользовательский CSS\",\r\n    \"save_custom_css\" => \"Сохранить пользовательский CSS\",\r\n    \"calculate_monthly_price\" => \"Рассчитать и показать ежемесячную цену для всех подписок\",\r\n    \"convert_prices\" => \"Всегда конвертировать и показывать цены в моей основной валюте (медленнее)\",\r\n    \"show_original_price\" => \"Также показывать оригинальную цену при выполнении конверсий или расчетов\",\r\n    \"experience\" => \"Опыт\",\r\n    \"show_subscription_progress\" => \"Показать прогресс подписки\",\r\n    \"disabled_subscriptions\" => \"Отключенные подписки\",\r\n    \"hide_disabled_subscriptions\" => \"Скрыть отключенные подписки\",\r\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Показать отключенные подписки внизу списка\",\r\n    \"experimental_settings\" => \"Экспериментальные настройки\",\r\n    \"remove_background\" => \"Попытка удалить фон логотипов из поиска изображений.\",\r\n    \"use_mobile_navigation_bar\" => \"Использовать мобильную панель навигации\",\r\n    \"experimental_info\" => \"Экспериментальные настройки, вероятно, не будут работать идеально.\",\r\n    \"payment_methods\" => \"Способы оплаты\",\r\n    \"payment_methods_info\" => \"Нажмите на способ оплаты, чтобы отключить/включить его.\",\r\n    \"rename_payment_methods_info\" => \"Нажмите на название способа оплаты, чтобы переименовать его.\",\r\n    \"cant_delete_payment_method_in_use\" => \"Невозможно отключить используемый способ оплаты\",\r\n    \"add_custom_payment\" => \"Добавить собственный способ оплаты\",\r\n    \"payment_method_name\" => \"Название способа оплаты\",\r\n    \"payment_method_added_successfuly\" => \"Способ оплаты успешно добавлен\",\r\n    \"payment_method_removed\" => \"Способ оплаты удален.\",\r\n    \"disable\" => \"Отключить\",\r\n    \"enable\" => \"Включить\",\r\n    \"rename_payment_method\" => \"Переименовать способ оплаты\",\r\n    \"payment_renamed\" => \"Способ оплаты переименован\",\r\n    \"payment_not_renamed\" => \"Способ оплаты не переименован\",\r\n    \"test\" => \"Тест\",\r\n    \"add\" => \"Добавить\",\r\n    \"save\" => \"Сохранить\",\r\n    \"reset\" => \"Перезагрузить\",\r\n    \"main_accent_color_error\" => \"Основной и акцентный цвет не могут быть одинаковыми.\",\r\n    \"backup_and_restore\" => \"Резервное копирование и восстановление\",\r\n    \"backup\" => \"Резервное копирование\",\r\n    \"restore\" => \"Восстановление\",\r\n    \"restore_info\" => \"Восстановление базы данных отменит все текущие данные. После восстановления вы выйдете из системы.\",\r\n    \"account\" => \"Учетная запись\",\r\n    \"export_subscriptions\" => \"Экспорт подписок\",\r\n    \"export_as_json\" => \"Экспорт в JSON\",\r\n    \"export_as_csv\" => \"Экспорт в CSV\",\r\n    \"danger_zone\" => \"Опасная зона\",\r\n    \"delete_account\" => \"Удалить учетную запись\",\r\n    \"delete_account_info\" => \"При удалении аккаунта также будут удалены все ваши подписки и настройки.\",\r\n    // Filters menu\r\n    \"filter\" => \"Фильтр\",\r\n    \"clear\" => \"Очистить\",\r\n    // Toast\r\n    \"success\" => \"Успешно\",\r\n    // Endpoint responses\r\n    \"session_expired\" => \"Срок действия вашей сессии истек. Пожалуйста, войдите снова\",\r\n    \"fields_missing\" => \"Некоторые поля отсутствуют\",\r\n    \"fill_all_fields\" => \"Пожалуйста заполните все поля\",\r\n    \"fill_mandatory_fields\" => \"Пожалуйста, заполните все обязательные поля\",\r\n    \"error\" => \"Ошибка\",\r\n    // Category\r\n    \"failed_add_category\" => \"Не удалось добавить категорию\",\r\n    \"failed_edit_category\" => \"Не удалось изменить категорию\",\r\n    \"category_in_use\" => \"Категория используется в подписках и не может быть удалена.\",\r\n    \"failed_remove_category\" => \"Не удалось удалить категорию\",\r\n    \"category_saved\" => \"Категория сохранена\",\r\n    \"category_removed\" => \"Категория удалена\",\r\n    \"sort_order_saved\" => \"Порядок сортировки сохранен.\",\r\n    // Currency\r\n    \"currency_saved\" => \"сохранено.\",\r\n    \"error_adding_currency\" => \"Ошибка добавления валюты.\",\r\n    \"failed_to_store_currency\" => \"Не удалось сохранить валюту в базе данных.\",\r\n    \"currency_in_use\" => \"Валюта используется в подписках и не может быть удалена.\",\r\n    \"currency_is_main\" => \"Валюта установлена ​​в качестве основной и не может быть удалена.\",\r\n    \"failed_to_remove_currency\" => \"Не удалось удалить валюту из базы данных.\",\r\n    \"failed_to_store_api_key\" => \"Не удалось сохранить ключ API в базе данных.\",\r\n    \"invalid_api_key\" => \"Неверный ключ API.\",\r\n    \"api_key_saved\" => \"Ключ API успешно сохранен\",\r\n    \"currency_removed\" => \"Валюта удалена\",\r\n    // Household\r\n    \"failed_add_household\" => \"Не удалось добавить члена семьи.\",\r\n    \"failed_edit_household\" => \"Не удалось изменить члена семьи.\",\r\n    \"failed_remove_household\" => \"Не удалось удалить члена семьи.\",\r\n    \"household_in_use\" => \"Член семьи используется в подписках и не может быть удален.\",\r\n    \"member_saved\" => \"Член семьи сохранен\",\r\n    \"member_removed\" => \"Член семьи удален\",\r\n    // Notifications\r\n    \"error_saving_notifications\" => \"Ошибка сохранения данных уведомлений.\",\r\n    \"wallos_notification\" => \"Уведомление от Wallos\",\r\n    \"test_notification\" => \"Это тестовое уведомление. Если вы видите это, значит, конфигурация правильная.\",\r\n    \"email_error\" => \"Ошибка отправки электронной почты\",\r\n    \"notification_sent_successfuly\" => \"Уведомление успешно отправлено\",\r\n    \"notifications_settings_saved\" => \"Настройки уведомлений успешно сохранены.\",\r\n    \"notification_failed\" => \"Уведомление не удалось\",\r\n    // Payments\r\n    \"payment_in_use\" => \"Невозможно отключить используемый способ оплаты\",\r\n    \"failed_update_payment\" => \"Не удалось обновить способ оплаты в базе данных.\",\r\n    \"enabled\" => \"включено\",\r\n    \"disabled\" => \"отключено\",\r\n    // Subscription\r\n    \"error_fetching_image\" => \"Ошибка при загрузке изображения.\",\r\n    \"subscription_updated_successfuly\" => \"Подписка успешно обновлена\",\r\n    \"subscription_added_successfuly\" => \"Подписка успешно добавлена\",\r\n    \"error_deleting_subscription\" => \"Ошибка удаления подписки.\",\r\n    \"invalid_request_method\" => \"Неверный метод запроса.\",\r\n    // User\r\n    \"error_updating_user_data\" => \"Ошибка обновления пользовательских данных.\",\r\n    \"user_details_saved\" => \"Данные пользователя сохранены.\",\r\n    // Admin Page\r\n    \"registrations\" => \"Регистрации\",\r\n    \"enable_user_registrations\" => \"Включить регистрацию пользователей\",\r\n    \"maximum_number_users\" => \"Максимальное количество пользователей\",\r\n    \"require_email_verification\" => \"Требовать подтверждение по электронной почте\",\r\n    \"configure_smtp_settings_to_enable\" => \"Настройте SMTP, чтобы включить эту функцию.\",\r\n    \"server_url\" => \"URL-адрес сервера\",\r\n    \"server_url_info\" => \"Используется для проверки электронной почты и восстановления пароля. Должен быть действительным публичным URL.\",\r\n    \"server_url_password_reset\" => \"Если этот параметр установлен, он также включает функцию сброса пароля.\",\r\n    \"disable_login\" => \"Отключить вход\",\r\n    \"disable_login_info\" => \"Обход входа в систему. Если вы используете свой сервер только в локальной сети, без доступа извне, вы можете отключить вход в систему. При этом будет автоматически входить пользователь admin.\",\r\n    \"disable_login_info2\" => \"Этот параметр можно включить только в том случае, если регистрация пользователей отключена и их количество не превышает учетную запись администратора.\",\r\n    \"max_users_info\" => \"Установите 0 для неограниченного количества пользователей.\",\r\n    \"user_management\" => \"Управление пользователями\",\r\n    \"delete_user\" => \"Удалить пользователя\",\r\n    \"delete_user_info\" => \"Удаление пользователя также приведет к удалению всех его подписок и настроек.\",\r\n    \"create_user\" => \"Создать пользователя\",\r\n    \"oidc_settings\" => \"Настройки OIDC\",\r\n    \"oidc_auth_enabled\" => \"Включить OIDC аутентификацию\",\r\n    \"create_user_automatically\" => \"Автоматически создавать пользователей\",\r\n    \"disable_password_login\" => \"Отключить вход по паролю\",\r\n    \"smtp_settings\" => \"Настройки SMTP\",\r\n    \"security_settings\" => \"Параметры безопасности\",\r\n    \"ssrf_protection_info\" => \"Чтобы предотвратить атаки Server-Side Request Forgery (SSRF), Wallos по умолчанию блокирует уведомления webhook на приватные или внутренние сетевые адреса.\",\r\n    \"local_webhook_info\" => \"Если вам нужно отправлять вебхуки на локальные сервисы (например, Home Assistant, Gotify или Node-RED), введите их IP-адреса или имена хостов выше через запятую (например <code>192.168.1.100,192.168.1.101</code>).\",\r\n    \"smtp_usage_info\" => \"Будет использоваться для восстановления пароля и других системных писем.\",\r\n    \"maintenance_tasks\" => \"Задачи обслуживания\",\r\n    \"orphaned_logos\" => \"Потерянные логотипы\",\r\n    \"update\" => \"Обновить\",\r\n    \"new_version_available\" => \"Доступна новая версия Wallos\",\r\n    \"current_version\" => \"Текущая версия\",\r\n    \"latest_version\" => \"Последняя версия\",\r\n    \"on_current_version\" => \"Вы используете последнюю версию Wallos.\",\r\n    \"show_update_notification\" => \"Показывать уведомление об обновлениях на дашборде\",\r\n    \"cronjobs\" => \"Cronjobs\",\r\n    // Email Verification\r\n    \"email_verified\" => \"Ваш адрес электронной почты подтвержден. Теперь вы можете войти.\",\r\n    \"email_verification_failed\" => \"Не удалось подтвердить ваш адрес электронной почты.\",\r\n    // Calendar\r\n    \"calendar\" => \"Календарь\",\r\n    \"sun\" => \"Вс\",\r\n    \"mon\" => \"Пн\",\r\n    \"tue\" => \"Вт\",\r\n    \"wed\" => \"Ср\",\r\n    \"thu\" => \"Чт\",\r\n    \"fri\" => \"Пт\",\r\n    \"sat\" => \"Сб\",\r\n    \"month-01\" => \"Январь\",\r\n    \"month-02\" => \"Февраль\",\r\n    \"month-03\" => \"Март\",\r\n    \"month-04\" => \"Апрель\",\r\n    \"month-05\" => \"Май\",\r\n    \"month-06\" => \"Июнь\",\r\n    \"month-07\" => \"Июль\",\r\n    \"month-08\" => \"Август\",\r\n    \"month-09\" => \"Сентябрь\",\r\n    \"month-10\" => \"Октябрь\",\r\n    \"month-11\" => \"Ноябрь\",\r\n    \"month-12\" => \"Декабрь\",\r\n    \"total_cost\" => \"Общая стоимость\",\r\n    \"export_icalendar\" => \"Экспорт в iCalendar\",\r\n    \"over_budget_warning\" => \"Вы превысили бюджет\",\r\n    // TOTP Page\r\n    \"insert_totp_code\" => \"Введите код TOTP\",\r\n\r\n];\r\n\r\n\r\n?>\r\n"
  },
  {
    "path": "includes/i18n/sl.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Preden se lahko prijavite, morate ustvariti račun\",\n    \"username\" => \"Uporabniško ime\",\n    \"password\" => \"Geslo\",\n    \"email\" => \"E-pošta\",\n    \"firstname\" => \"Ime\",\n    \"lastname\" => \"Priimek\",\n    \"confirm_password\" => \"Potrdite geslo\",\n    \"main_currency\" => \"Glavna valuta\",\n    \"language\" => \"Jezik\",\n    \"passwords_dont_match\" => \"Gesli se ne ujema\",\n    \"username_exists\" => \"Uporabniško ime že obstaja\",\n    \"email_exists\" => \"E-pošta že obstaja\",\n    \"registration_failed\" => \"Registracija ni uspela, poskusite znova.\",\n    \"register\" => \"Registrirajte se\",\n    \"restore_database\" => \"Obnovi bazo podatkov\",\n    // Login Page\n    \"please_login\" => \"Prosim prijavite se\",\n    \"stay_logged_in\" => \"Ostanite prijavljeni (30 dni)\",\n    \"login\" => \"Prijava\",\n    \"login_with\" => \"Prijavite se z\",\n    \"or\" => \"ali\",\n    \"login_failed\" => \"Podatki za prijavo so napačni\",\n    \"registration_successful\" => \"Registracija uspešna\",\n    \"user_email_waiting_verification\" => \"Vaš e-poštni naslov je treba preveriti. Prosim, preglejte vašo e-pošto.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Ste pozabili geslo\",\n    \"reset_password\" => \"Ponastavitev gesla\",\n    \"reset_sent_check_email\" => \"E-pošta ponastavitev gesla je bila poslana. Prosim, preglejte vašo e-pošto.\",\n    \"password_reset_successful\" => \"Ponastavitev gesla je uspela\",\n    // Header\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Panel\",\n    \"subscriptions\" => \"Naročnine\",\n    \"stats\" => \"Statistika\",\n    \"settings\" => \"Nastavitve\",\n    \"admin\" => \"Skrbnik\",\n    \"about\" => \"O programu\",\n    \"logout\" => \"Odjava\",\n    // Dashboard\n    \"hello\" => \"Pozdravljen\",\n    \"upcoming_payments\" => \"Prihajajoča plačila\",\n    \"no_upcoming_payments\" => \"Nimate prihodnjih plačil\",\n    \"overdue_renewals\" => \"Zapadla podaljšanja\",\n    \"ai_recommendations\" => \"Priporočila AI\",\n    \"your_budget\" => \"Vaš proračun\",\n    \"budget\" => \"Proračun\",\n    \"budget_used\" => \"Porabljen proračun\",\n    \"over_budget\" => \"Prekoračen proračun\",\n    \"your_subscriptions\" => \"Vaše naročnine\",\n    \"your_savings\" => \"Vaše prihranke\",\n    // Subscriptions page\n    \"subscription\" => \"Naročnina\",\n    \"no_subscriptions_yet\" => \"Nimate še nobene naročnine\",\n    \"add_first_subscription\" => \"Dodaj prvo naročnino\",\n    \"new_subscription\" => \"Nova naročnina\",\n    \"search\" => \"Iskanje\",\n    \"state\" => \"Stanje\",\n    \"alphanumeric\" => \"Abecedno\",\n    \"sort\" => \"Razvrsti\",\n    \"name\" => \"Ime\",\n    \"last_added\" => \"Zadnje dodano\",\n    \"price\" => \"Cena\",\n    \"next_payment\" => \"Naslednje plačilo\",\n    \"renewal_type\" => \"Vrsta obnove\",\n    \"auto_renewal\" => \"Samodejno obnavljanje\",\n    \"automatically_renews\" => \"Se samodejno obnavlja\",\n    \"manual_renewal\" => \"Ročno obnavljanje\",\n    \"start_date\" => \"Datum začetka\",\n    \"inactive\" => \"Onemogoči naročnino\",\n    \"replaced_with\" => \"Zamenjano z\",\n    \"none\" => \"brez\",\n    \"member\" => \"Član\",\n    \"category\" => \"Kategorija\",\n    \"payment_method\" => \"Način plačila\",\n    \"Daily\" => \"Dnevno\",\n    \"Weekly\" => \"Tedensko\",\n    \"Monthly\" => \"Mesečno\",\n    \"Yearly\" => \"Letno\",\n    \"daily\" => \"Dan (dni)\",\n    \"weekly\" => \"Teden (tednov)\",\n    \"monthly\" => \"Mesec (mesecev)\",\n    \"yearly\" => \"Leto (leta)\",\n    \"days\" => \"dnevi\",\n    \"weeks\" => \"tedni\",\n    \"months\" => \"meseci\",\n    \"years\" => \"leta\",\n    \"external_url\" => \"Obiščite zunanji URL\",\n    \"empty_page\" => \"Prazna stran\",\n    \"clear_filters\" => \"Počisti filter\",\n    \"no_matching_subscriptions\" => \"Ni ustreznih naročnin\",\n    \"clone\" => \"Klon\",\n    \"renew\" => \"Obnovi\",\n    \"calculate_next_payment_date\" => \"Izračunaj datum naslednjega plačila\",\n    // Subscription form\n    \"add_subscription\" => \"Dodaj naročnino\",\n    \"edit_subscription\" => \"Uredi naročnino\",\n    \"subscription_name\" => \"Ime naročnine\",\n    \"logo_preview\" => \"Predogled logotipa\",\n    \"search_logo\" => \"Poišči logotip v spletu\",\n    \"web_search\" => \"Spletno iskanje\",\n    \"currency\" => \"Valuta\",\n    \"payment_every\" => \"Plačilo vsakih\",\n    \"frequency\" => \"Ponavljanje\",\n    \"cycle\" => \"cikel\",\n    \"no_category\" => \"Brez kategorije\",\n    \"paid_by\" => \"Plačal\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Opombe\",\n    \"enable_notifications\" => \"Omogoči obvestila za to naročnino\",\n    \"default_value_from_settings\" => \"Privzeta vrednost iz nastavitev\",\n    \"cancellation_notification\" => \"Obvestilo o preklicu\",\n    \"delete\" => \"Izbriši\",\n    \"cancel\" => \"Prekliči\",\n    \"upload_logo\" => \"Naloži logotip\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Pri naročninah uporabljate več valut. Če želite imeti veljavne in točne statistične podatke, na strani z nastavitvami nastavite ključ API Fixer.\",\n    \"general_statistics\" => \"Splošna statistika\",\n    \"active_subscriptions\" => \"Aktivne naročnine\",\n    \"inactive_subscriptions\" => \"Neaktivne naročnine\",\n    \"monthly_cost\" => \"Mesečni stroški\",\n    \"yearly_cost\" => \"Letni stroški\",\n    \"average_monthly\" => \"Povprečni mesečni stroški naročnine\",\n    \"most_expensive\" => \"Najdražja cena naročnine\",\n    \"amount_due\" => \"Zapadli znesek ta mesec\",\n    \"percentage_budget_used\" => \"Odstotek porabljenega proračuna\",\n    \"budget_remaining\" => \"Preostali proračun\",\n    \"amount_over_budget\" => \"Znesek nad proračunom\",\n    \"monthly_savings\" => \"Mesečni prihranek (pri neaktivnih naročninah)\",\n    \"yearly_savings\" => \"Letni prihranki (pri neaktivnih naročninah)\",\n    \"split_views\" => \"Razdeljeni pogledi\",\n    \"category_split\" => \"Razdelitev kategorije\",\n    \"household_split\" => \"Razdelitev gospodinjstva\",\n    \"payment_method_split\" => \"Razdelitev načina plačila\",\n    \"total_cost_trend\" => \"Trend skupnih stroškov\",\n    \"cost_vs_budget\" => \"Stroški v primerjavi s proračunom\",\n    // About page\n    \"about_and_credits\" => \"O programu in zahvale\",\n    \"credits\" => \"Zahvale\",\n    \"license\" => \"Licenca\",\n    \"release_notes\" => \"Opombe o izdaji\",\n    \"update_available\" => \"Na voljo je posodobitev\",\n    \"issues_and_requests\" => \"Težave in zahteve\",\n    \"the_author\" => \"Avtor\",\n    \"icons\" => \"Ikone\",\n    \"payment_icons\" => \"Ikone plačil\",\n    // Profile page\n    \"upload_avatar\" => \"Naloži avatar\",\n    \"file_type_error\" => \"Vrsta datoteke ni podprta.\",\n    \"user_details\" => \"Podrobnosti o uporabniku\",\n    \"two_factor_authentication\" => \"Dvojna preverba pristnosti\",\n    \"two_factor_info\" => \"Two Factor Authentication adds an extra layer of security to your account.<br>Za optično branje kode QR potrebujete aplikacijo za preverjanje pristnosti, kot so Google Authenticator, Authy ali Ente Auth.\",\n    \"two_factor_enabled_info\" => \"Vaš račun je varen z dvostopenjskim preverjanjem pristnosti. Onemogočite jo lahko tako, da kliknete zgornji gumb.\",\n    \"enable_two_factor_authentication\" => \"Omogoči dvostopenjsko preverjanje pristnosti\",\n    \"2fa_already_enabled\" => \"Dvostopenjsko preverjanje pristnosti je že omogočeno\",\n    \"totp_code_incorrect\" => \"Koda TOTP je napačna\",\n    \"backup_codes\" => \"Rezervne kode\",\n    \"download_backup_codes\" => \"Prenesi rezervne kode\",\n    \"copy_to_clipboard\" => \"Kopiraj v odložišče\",\n    \"totp_backup_codes_info\" => \"Shranite te rezervne kode na varno mesto. Uporabite jih lahko, če izgubite dostop do svoje aplikacije za preverjanje pristnosti.\",\n    \"disable_two_factor_authentication\" => \"Onemogoči dvostopenjsko preverjanje pristnosti\",\n    \"totp_code\" => \"TOTP koda\",\n    \"api_key\" => \"API ključ\",\n    \"regenerate\" => \"Ponovno generiraj\",\n    \"api_key_info\" => \"API ključ se uporablja za dostop do vaših podatkov prek API-ja. Če mislite, da je vaš ključ kompromitiran, ga lahko ponovno generirate.\",\n    // Settings page\n    \"monthly_budget\" => \"Mesečni proračun\",\n    \"budget_info\" => \"Mesečni proračun se uporablja za izračun statistike\",\n    \"household\" => \"Gospodinjstvo\",\n    \"save_member\" => \"Shrani člana\",\n    \"delete_member\" => \"Izbriši člana\",\n    \"cant_delete_member\" => \"Ne morem izbrisati glavnega člana\",\n    \"cant_delete_member_in_use\" => \"Ne morem izbrisati člana, ki je v uporabi v naročnini\",\n    \"household_info\" => \"E-poštno polje omogoča članom gospodinjstva, da so obveščeni o naročninah, ki bodo potekle.\",\n    \"notifications\" => \"Obvestila\",\n    \"enable_email_notifications\" => \"Omogoči e-poštna obvestila\",\n    \"notify_me\" => \"Obvesti me\",\n    \"day_before\" => \"dan prej\",\n    \"on_due_date\" => \"Na dan zapadlosti\",\n    \"days_before\" => \"dni prej\",\n    \"smtp_address\" => \"SMTP naslov\",\n    \"port\" => \"vrata\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Uporabniško ime SMTP\",\n    \"smtp_password\" => \"Geslo SMTP\",\n    \"from_email\" => \"Iz e-pošte (izbirno)\",\n    \"send_to_other_emails\" => \"Pošlji obvestila tudi na naslednje e-poštne naslove (uporabi ; za ločevanje):\",\n    \"smtp_info\" => \"Geslo SMTP se prenaša in shranjuje v navadnem besedilu. Zaradi varnosti ustvarite račun samo za to.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegram Bot žeton\",\n    \"telegram_chat_id\" => \"ID klepeta Telegrama\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus žeton\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Uporabniško ime\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Ikona Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"Metoda zahteve\",\n    \"custom_headers\" => \"Glave po meri\",\n    \"webhook_payload\" => \"Webhook Payload\",\n    \"payment_notifications_payload\" => \"Obvestilo o plačilu\",\n    \"cancelation_notification_payload\" => \"Obvestilo o preklicu\",\n    \"variables_available\" => \"Spremenljivke, ki so na voljo\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Žeton\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Uporabniško ime za Discord Bot\",\n    \"discord_bot_avatar_url\" => \"URL avatarja Discordovega bota\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Uporabniški ključ Pushover\",\n    \"host\" => \"Gostitelj\",\n    \"topic\" => \"Tema\",\n    \"ignore_ssl_errors\" => \"Prezri napake SSL\",\n    \"categories\" => \"Kategorije\",\n    \"save_category\" => \"Shrani kategorijo\",\n    \"delete_category\" => \"Izbriši kategorijo\",\n    \"cant_delete_category_in_use\" => \"Kategorija, ki se uporablja v naročnini, ni mogoče izbrisati\",\n    \"currencies\" => \"Valute\",\n    \"save_currency\" => \"Shrani valuto\",\n    \"delete_currency\" => \"Izbriši valuto\",\n    \"cant_delete_main_currency\" => \"Glavne valute ni mogoče izbrisati\",\n    \"cant_delete_currency_in_use\" => \"V naročnini ni mogoče izbrisati valute, ki je v uporabi\",\n    \"exchange_update\" => \"Menjalni tečaji so bili zadnjič posodobljeni dne\",\n    \"currency_info\" => \"Poiščite podprte valute in pravilne kode valut na\",\n    \"currency_performance\" => \"Za izboljšano delovanje obdržite samo valute, ki jih uporabljate.\",\n    \"fixer_api_key\" => \"API ključ za Fixer\",\n    \"provider\" => \"Ponudnik\",\n    \"fixer_info\" => \"Če uporabljate več valut in želite natančno statistiko in razvrščanje naročnin, potrebujete BREZPLAČNI API ključ od Fixerja.\",\n    \"get_key\" => \"Pridobite svoj ključ pri\",\n    \"get_free_fixer_api_key\" => \"Pridobite brezplačen ključ API Fixer\",\n    \"get_key_alternative\" => \"Lahko pa tudi dobite brezplačni Fixer API od\",\n    \"ai_model\" => \"Model AI\",\n    \"select_ai_model\" => \"Izberite model AI\",\n    \"run_schedule\" => \"Zaženi urnik\",\n    \"manually\" => \"Ročno\",\n    \"coming_soon\" => \"Kmalu\",\n    \"invalid_host\" => \"Neveljavna gostiteljska naprava\",\n    \"ai_recommendations_info\" => \"Priporočila AI so generirana na podlagi vaših naročnin in članov gospodinjstva.\",\n    \"may_take_time\" => \"Odvisno od ponudnika, modela in števila naročnin lahko generiranje priporočil traja nekaj časa.\",\n    \"recommendations_visible_on_dashboard\" => \"Priporočila bodo vidna na nadzorni plošči.\",\n    \"generate_recommendations\" => \"Generiraj priporočila\",\n    \"display_settings\" => \"Nastavitve zaslona\",\n    \"theme_settings\" => \"Nastavitve teme\",\n    \"colors\" => \"Barve\",\n    \"custom_colors\" => \"Barve po meri\",\n    \"theme\" => \"Tema\",\n    \"dark_theme\" => \"Temna tema\",\n    \"light_theme\" => \"Svetla tema\",\n    \"automatic\" => \"Samodejno\",\n    \"main_color\" => \"Glavna barva\",\n    \"accent_color\" => \"Poudarna barva\",\n    \"hover_color\" => \"Barva ob hoverju\",\n    \"save_custom_colors\" => \"Shrani barve po meri\",\n    \"reset_custom_colors\" => \"Ponastavi barve po meri\",\n    \"custom_css\" => \"CSS po meri\",\n    \"save_custom_css\" => \"Shrani CSS po meri\",\n    \"calculate_monthly_price\" => \"Izračunaj in prikaži mesečno ceno za vse naročnine\",\n    \"convert_prices\" => \"Vedno pretvori in prikaži cene v moji glavni valuti (počasneje)\",\n    \"show_original_price\" => \"Prikaži tudi originalno ceno, ko se izvajajo pretvorbe ali izračuni\",\n    \"experience\" => \"Izkušnja\",\n    \"show_subscription_progress\" => \"Prikaži napredek naročnine\",\n    \"disabled_subscriptions\" => \"Onemogočene naročnine\",\n    \"hide_disabled_subscriptions\" => \"Skrij onemogočene naročnine\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Prikaži onemogočene naročnine na dnu seznama\",\n    \"experimental_settings\" => \"Eksperimentalne nastavitve\",\n    \"remove_background\" => \"Poskusi odstraniti ozadje logotipov iz iskanja slik\",\n    \"use_mobile_navigation_bar\" => \"Uporabi mobilno navigacijsko vrstico\",\n    \"experimental_info\" => \"Poskusne nastavitve verjetno ne bodo popolnoma delovale.\",\n    \"payment_methods\" => \"Načini plačila\",\n    \"payment_methods_info\" => \"Kliknite način plačila, da ga onemogočite/omogočite.\",\n    \"rename_payment_methods_info\" => \"Kliknite ime plačilnega sredstva, da ga preimenujete.\",\n    \"cant_delete_payment_method_in_use\" => \"Uporabljenega plačilnega sredstva ni mogoče onemogočiti\",\n    \"add_custom_payment\" => \"Dodaj način plačila po meri\",\n    \"payment_method_name\" => \"Ime plačilnega sredstva\",\n    \"payment_method_added_successfuly\" => \"Plačilno sredstvo je uspešno dodano\",\n    \"payment_method_removed\" => \"Plačilno sredstvo odstranjeno\",\n    \"disable\" => \"Onemogoči\",\n    \"enable\" => \"Omogoči\",\n    \"rename_payment_method\" => \"Preimenuj način plačila\",\n    \"payment_renamed\" => \"Plačilno sredstvo je preimenovano\",\n    \"payment_not_renamed\" => \"Način plačila ni preimenovan\",\n    \"test\" => \"Test\",\n    \"add\" => \"Dodaj\",\n    \"save\" => \"Shrani\",\n    \"reset\" => \"Ponastavi\",\n    \"main_accent_color_error\" => \"Glavna in poudarna barva se ne sme ujemati\",\n    \"backup_and_restore\" => \"Varnostno kopiranje in obnovitev\",\n    \"backup\" => \"Varnostna kopija\",\n    \"restore\" => \"Obnovitev\",\n    \"restore_info\" => \"Obnovitev baze podatkov bo prepisala vse trenutne podatke. Po obnovitvi boste odjavljeni.\",\n    // Filters menu\n    \"filter\" => \"Filter\",\n    \"clear\" => \"Počisti\",\n    // Toast\n    \"success\" => \"Uspeh\",\n    // Endpoint responses\n    \"session_expired\" => \"Vaša seja je potekla. Ponovno se prijavite\",\n    \"fields_missing\" => \"Nekatere polja niso izpoljnena\",\n    \"fill_all_fields\" => \"Prosim, izpolnite vsa polja\",\n    \"fill_mandatory_fields\" => \"Prosim, izpolnite vsa obvezna polja\",\n    \"error\" => \"Napaka\",\n    // Category\n    \"failed_add_category\" => \"Dodajanje kategorije ni uspelo\",\n    \"failed_edit_category\" => \"Urejanje kategorije ni uspelo\",\n    \"category_in_use\" => \"Kategorija je v uporabi v naročninah in je ni mogoče odstraniti\",\n    \"failed_remove_category\" => \"Odstranitev kategorije ni uspela\",\n    \"category_saved\" => \"Kategorija je shranjena\",\n    \"category_removed\" => \"Kategorija je odstranjena\",\n    \"sort_order_saved\" => \"Vrstni red je shranjen\",\n    // Currency\n    \"currency_saved\" => \"je bila shranjen.\",\n    \"error_adding_currency\" => \"Napaka pri dodajanju zapisa valute.\",\n    \"failed_to_store_currency\" => \"Shranjevanje valute v zbirko podatkov ni uspelo.\",\n    \"currency_in_use\" => \"Valuta je v uporabi v naročninah in je ni mogoče izbrisati.\",\n    \"currency_is_main\" => \"Valuta je nastavljena kot glavna valuta in je ni mogoče izbrisati.\",\n    \"failed_to_remove_currency\" => \"Odstranitev valute iz zbirke podatkov ni uspela.\",\n    \"failed_to_store_api_key\" => \"Ključa API ni bilo mogoče shraniti v zbirko podatkov.\",\n    \"invalid_api_key\" => \"Neveljaven API ključ.\",\n    \"api_key_saved\" => \"API ključ je uspešno shranjen\",\n    \"currency_removed\" => \"Valuta je odstranjena\",\n    // Household\n    \"failed_add_household\" => \"Dodajanje člana gospodinjstva ni uspelo\",\n    \"failed_edit_household\" => \"Urejanje člana gospodinjstva ni uspelo\",\n    \"failed_remove_household\" => \"Odstranitev člana gospodinjstva ni uspela\",\n    \"household_in_use\" => \"Član gospodinjstva je v uporabi v naročninah in ga ni mogoče odstraniti\",\n    \"member_saved\" => \"Član je shranjen\",\n    \"member_removed\" => \"Član je odstranjen\",\n    // Notifications\n    \"error_saving_notifications\" => \"Napaka pri shranjevanju podatkov obvestil.\",\n    \"wallos_notification\" => \"Wallosovo obvestilo\",\n    \"test_notification\" => \"To je preizkusno obvestilo. Če ga vidite, je konfiguracija pravilna.\",\n    \"email_error\" => \"Napaka pri pošiljanju e-pošte\",\n    \"notification_sent_successfuly\" => \"Obvestilo je bilo uspešno poslano\",\n    \"notifications_settings_saved\" => \"Nastavitve obvestil so uspešno shranjene.\",\n    \"notification_failed\" => \"Pošiljanje obvestila ni uspelo\",\n    // Payments\n    \"payment_in_use\" => \"Uporabljenega plačilnega sredstva ni mogoče onemogočiti\",\n    \"failed_update_payment\" => \"Posodobitev plačilnega sredstva v bazi podatkov ni uspela\",\n    \"enabled\" => \"omogočeno\",\n    \"disabled\" => \"onemogočeno\",\n    // Subscription\n    \"error_fetching_image\" => \"Napaka pri pridobivanju slike\",\n    \"subscription_updated_successfuly\" => \"Naročnina je bila uspešno posodobljena\",\n    \"subscription_added_successfuly\" => \"Naročnina je bila uspešno dodana\",\n    \"error_deleting_subscription\" => \"Napaka pri brisanju naročnine.\",\n    \"invalid_request_method\" => \"Neveljavna metoda zahteve.\",\n    // User\n    \"error_updating_user_data\" => \"Napaka pri posodabljanju uporabniških podatkov.\",\n    \"user_details_saved\" => \"Podrobnosti o uporabniku so shranjene\",\n    // Admin Page\n    \"registrations\" => \"Registracije\",\n    \"enable_user_registrations\" => \"Omogoči registracije uporabnikov\",\n    \"maximum_number_users\" => \"Največje število uporabnikov\",\n    \"require_email_verification\" => \"Zahtevaj preverjanje elektronske pošte\",\n    \"configure_smtp_settings_to_enable\" => \"Za omogočanje nastavitve SMTP nastavitve\",\n    \"server_url\" => \"URL strežnika\",\n    \"server_url_info\" => \"Uporablja se za preverjanje e-pošte in obnovitev gesla. Biti mora veljaven javni URL.\",\n    \"server_url_password_reset\" => \"Če je nastavljeno, bo omogočena tudi funkcija ponastavitve gesla.\",\n    \"disable_login\" => \"Onemogoči prijavo\",\n    \"disable_login_info\" => \"Obidite prijavo. Če strežnik uporabljate samo v lokalnem omrežju brez zunanjega dostopa, lahko onemogočite prijavo. Tako se bo samodejno prijavil uporabnik administrator.\",\n    \"disable_login_info2\" => \"To nastavitev lahko omogočite le, če je registracija uporabnikov izklopljena in če ni več uporabniških računov, razen uporabniškega računa upravitelja.\",\n    \"max_users_info\" => \"0 pomeni neomejeno\",\n    \"user_management\" => \"Upravljanje uporabnikov\",\n    \"delete_user\" => \"Izbriši uporabnika\",\n    \"delete_user_info\" => \"Če izbrišete uporabnika, boste izbrisali tudi vse njegove naročnine in nastavitve.\",\n    \"create_user\" => \"Ustvari uporabnika\",\n    \"oidc_settings\" => \"OIDC nastavitve\",\n    \"oidc_auth_enabled\" => \"Omogoči OIDC prijavo\",\n    \"create_user_automatically\" => \"Samodejno ustvari uporabnika\",\n    \"disable_password_login\" => \"Onemogoči prijavo z geslom\",\n    \"smtp_settings\" => \"Nastavitve SMTP\",\n    \"smtp_usage_info\" => \"Uporabljeno bo za obnovitev gesla in druge sistemske e-pošte.\",\n    \"security_settings\" => \"Varnostne nastavitve\",\n    \"ssrf_protection_info\" => \"Za preprečevanje napadov Server-Side Request Forgery (SSRF) Wallos privzeto blokira webhook obvestila na zasebne ali notranje omrežne naslove.\",\n    \"local_webhook_info\" => \"Če morate pošiljati webhooke na lokalne storitve (npr. Home Assistant, Gotify ali Node-RED), vnesite njihove IP naslove ali imena gostiteljev zgoraj kot seznam, ločen z vejico (npr. <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Vzdrževalne naloge\",\n    \"orphaned_logos\" => \"Osamljeni logotipi\",\n    \"update\" => \"Posodobi\",\n    \"new_version_available\" => \"Na voljo je nova različica Wallos\",\n    \"current_version\" => \"Trenutna različica\",\n    \"latest_version\" => \"Najnovejša različica\",\n    \"on_current_version\" => \"Uporabljate najnovejšo različico Wallos.\",\n    \"show_update_notification\" => \"Prikaži obvestilo o posodobitvah na dashboardu\",\n    \"cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"E-pošta je bila uspešno preverjena\",\n    \"email_verification_failed\" => \"Preverjanje e-pošte ni uspelo\",\n    // Calendar\n    \"calendar\" => \"Koledar\",\n    \"sun\" => \"Ned\",\n    \"mon\" => \"Pon\",\n    \"tue\" => \"Tor\",\n    \"wed\" => \"Sre\",\n    \"thu\" => \"Čet\",\n    \"fri\" => \"Pet\",\n    \"sat\" => \"Sob\",\n    \"month-01\" => \"Januar\",\n    \"month-02\" => \"Februar\",\n    \"month-03\" => \"Marec\",\n    \"month-04\" => \"April\",\n    \"month-05\" => \"Maj\",\n    \"month-06\" => \"Junij\",\n    \"month-07\" => \"Julij\",\n    \"month-08\" => \"Avgust\",\n    \"month-09\" => \"September\",\n    \"month-10\" => \"Oktober\",\n    \"month-11\" => \"November\",\n    \"month-12\" => \"December\",\n    \"total_cost\" => \"Skupni stroški\",\n    \"export_icalendar\" => \"Izvozi iCalendar\",\n    \"over_budget_warning\" => \"Presegli ste proračun\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Vnesite kodo TOTP\",\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/sr.php",
    "content": "<?php\n\n$i18n = [\n    // Страница за регистрацију\n    \"create_account\" => \"Морате креирати налог пре него што можете пријавити\",\n    \"username\" => \"Корисничко име\",\n    \"password\" => \"Лозинка\",\n    \"email\" => \"И-пошта\",\n    \"firstname\" => \"Име\",\n    \"lastname\" => \"Презиме\",\n    \"confirm_password\" => \"Потврди лозинку\",\n    \"main_currency\" => \"Главна валута\",\n    \"language\" => \"Језик\",\n    \"passwords_dont_match\" => \"Лозинке се не поклапају\",\n    \"username_exists\" => \"Корисничко име већ постоји\",\n    \"email_exists\" => \"Е-пошта већ постоји\",\n    \"registration_failed\" => \"Регистрација није успела, покушајте поново.\",\n    \"register\" => \"Региструј се\",\n    \"restore_database\" => \"Врати базу података\",\n    // Страница за пријаву\n    \"please_login\" => \"Молимо вас да се пријавите\",\n    \"stay_logged_in\" => \"Остани пријављен (30 дана)\",\n    \"login\" => \"Пријави се\",\n    \"login_with\" => \"Пријави се са\",\n    \"or\" => \"или\",\n    \"login_failed\" => \"Подаци за пријаву нису исправни\",\n    \"registration_successful\" => \"Пријава успешна\",\n    \"user_email_waiting_verification\" => \"Ваша е-пошта треба да буде верификована. Молимо прегледајте Е-пошту\",\n    // Password Reset Page\n    \"forgot_password\" => \"Заборављена лозинка\",\n    \"reset_password\" => \"Ресетуј лозинку\",\n    \"reset_sent_check_email\" => \"Ресетовање лозинке је послато на вашу е-пошту\",\n    \"password_reset_successful\" => \"Ресетовање лозинке је успешно\",\n    // Header\n    \"profile\" => \"Профил\",\n    \"dashboard\" => \"Панел\",\n    \"subscriptions\" => \"Претплате\",\n    \"stats\" => \"Статистике\",\n    \"settings\" => \"Подешавања\",\n    \"admin\" => \"Админ\",\n    \"about\" => \"О апликацији\",\n    \"logout\" => \"Одјава\",\n    // Dashboard\n    \"hello\" => \"Здраво\",\n    \"upcoming_payments\" => \"Предстојећа плаћања\",\n    \"no_upcoming_payments\" => \"Немате предстојећих плаћања\",\n    \"overdue_renewals\" => \"Засадне обнове\",\n    \"ai_recommendations\" => \"AI препоруке\",\n    \"your_budget\" => \"Ваш буџет\",\n    \"budget\" => \"Буџет\",\n    \"budget_used\" => \"Искоришћен буџет\",\n    \"over_budget\" => \"Прекорачен буџет\",\n    \"your_subscriptions\" => \"Ваше претплате\",\n    \"your_savings\" => \"Ваша уштеда\",\n    // Страница са претплатама\n    \"subscription\" => \"Претплата\",\n    \"no_subscriptions_yet\" => \"Још увек немате ниједну претплату\",\n    \"add_first_subscription\" => \"Додајте прву претплату\",\n    \"new_subscription\" => \"Нова претплата\",\n    \"search\" => \"Претрага\",\n    \"state\" => \"Статус\",\n    \"alphanumeric\" => \"Алфанумерички\",\n    \"sort\" => \"Сортирај\",\n    \"name\" => \"Назив\",\n    \"last_added\" => \"Последње додато\",\n    \"price\" => \"Цена\",\n    \"next_payment\" => \"Следећа уплата\",    \n    \"renewal_type\" => \"Тип обнове\",\n    \"auto_renewal\" => \"Аутоматско обновљење\",\n    \"automatically_renews\" => \"Аутоматски обновља\",\n    \"manual_renewal\" => \"Ручно обновљење\",\n    \"start_date\" => \"Датум почетка\",\n    \"inactive\" => \"Онемогући претплату\",\n    \"replaced_with\" => \"Замењено са\",\n    \"none\" => \"Ништа\",\n    \"member\" => \"Члан\",\n    \"category\" => \"Категорија\",\n    \"payment_method\" => \"Начин плаћања\",\n    \"Daily\" => \"Дневно\",\n    \"Weekly\" => \"Недељно\",\n    \"Monthly\" => \"Месечно\",\n    \"Yearly\" => \"Годишње\",\n    \"daily\" => \"дана\",\n    \"weekly\" => \"недеља\",\n    \"monthly\" => \"месеци\",\n    \"yearly\" => \"година\",\n    \"days\" => \"дана\",\n    \"weeks\" => \"недеља\",\n    \"months\" => \"месеци\",\n    \"years\" => \"године\",\n    \"external_url\" => \"Посети спољни URL\",\n    \"empty_page\" => \"Празна страница\",\n    \"clear_filters\" => \"Очисти филтере\",\n    \"no_matching_subscriptions\" => \"Нема подударајућих претплата\",\n    \"clone\" => \"Клонирај\",\n    \"renew\" => \"Обнови\",\n    \"calculate_next_payment_date\" => \"Израчунајте датум следеће уплате\",\n    // Форма за претплату\n    \"add_subscription\" => \"Додај претплату\",\n    \"edit_subscription\" => \"Уреди претплату\",\n    \"subscription_name\" => \"Назив претплате\",\n    \"logo_preview\" => \"Преглед логотипа\",\n    \"search_logo\" => \"Претражи логотип на интернету\",\n    \"web_search\" => \"Интернет претрага\",\n    \"currency\" => \"Валута\",\n    \"payment_every\" => \"Плаћање сваког\",\n    \"frequency\" => \"Фреквенција\",\n    \"cycle\" => \"Циклус\",\n    \"no_category\" => \"Без категорије\",\n    \"paid_by\" => \"Плаћено од\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Напомене\",\n    \"enable_notifications\" => \"Омогући обавештења за ову претплату\",\n    \"default_value_from_settings\" => \"Подразумевана вредност из подешавања\",\n    \"cancellation_notification\" => \"Обавештење о отказивању\",\n    \"delete\" => \"Обриши\",\n    \"cancel\" => \"Откажи\",\n    \"upload_logo\" => \"Постави логотип\",\n    // Страница са статистикама\n    \"cant_convert_currency\" => \"На својим претплатама користите више валута. Да бисте имали валидну и тачну статистику, поставите Фикер АПИ кључ на страници подешавања.\",\n    \"general_statistics\" => \"Опште статистике\",\n    \"active_subscriptions\" => \"Активне претплате\",\n    \"inactive_subscriptions\" => \"Неактивне претплате\",\n    \"monthly_cost\" => \"Месечни трошак\",\n    \"yearly_cost\" => \"Годишњи трошак\",\n    \"average_monthly\" => \"Просечни месечни трошак претплате\",\n    \"most_expensive\" => \"Најскупља претплата\",\n    \"amount_due\" => \"Износ за уплату овог месеца\",\n    \"percentage_budget_used\" => \"Проценат искоришћеног буџета\",\n    \"budget_remaining\" => \"Преостали буџет\",\n    \"amount_over_budget\" => \"Износ преко буџета\",\n    \"monthly_savings\" => \"Месечне уштеде (на неактивним претплатама)\",\n    \"yearly_savings\" => \"Годишње уштеде (на неактивним претплатама)\",\n    \"split_views\" => \"Подељени прикази\",\n    \"category_split\" => \"Подела по категоријама\",\n    \"household_split\" => \"Подела по домаћинству\",\n    \"payment_method_split\" => \"Подела по начинима плаћања\",\n    \"total_cost_trend\" => \"Тренд укупних трошкова\",\n    \"cost_vs_budget\" => \"Трошак у односу на буџет\",\n    // Страница о апликацији\n    \"about_and_credits\" => \"О апликацији и заслугама\",\n    \"credits\" => \"Заслуге\",\n    \"license\" => \"Лиценца\",\n    \"release_notes\" => \"Белешке о издању\",\n    \"update_available\" => \"Доступно је ажурирање\",\n    \"issues_and_requests\" => \"Проблеми и захтеви\",\n    \"the_author\" => \"Аутор\",\n    \"icons\" => \"Иконе\",\n    \"payment_icons\" => \"Иконе плаћања\",\n    // Profile page\n    \"upload_avatar\" => \"Постави аватар\",\n    \"file_type_error\" => \"Датотека није у подржаном формату.\",\n    \"user_details\" => \"Кориснички детаљи\",\n    \"two_factor_authentication\" => \"Двофакторска аутентикација\",\n    \"two_factor_info\" => \"Двофакторска аутентификација додаје додатни ниво сигурности вашем налогу. <бр>Биће вам потребна апликација за аутентификацију као што је Гоогле Аутхентицатор, Аутхи или Енте Аутх да бисте скенирали КР код.\",\n    \"two_factor_enabled_info\" => \"Ваш налог је сигуран са двофакторском аутентификацијом. Можете га онемогућити кликом на дугме изнад.\",\n    \"enable_two_factor_authentication\" => \"Омогући двофакторску аутентикацију\",\n    \"2fa_already_enabled\" => \"Двофакторска аутентикација је већ омогућена\",\n    \"totp_code_incorrect\" => \"ТОТП код није исправан\",\n    \"backup_codes\" => \"Резервни кодови\",\n    \"download_backup_codes\" => \"Преузми резервне кодове\",\n    \"copy_to_clipboard\" => \"Копирај у клипборд\",\n    \"totp_backup_codes_info\" => \"Сачувајте ове кодове на безбедно место. Користићете их када изгубите приступ апликацији за аутентификацију.\",\n    \"disable_two_factor_authentication\" => \"Онемогући двофакторску аутентикацију\",\n    \"totp_code\" => \"ТОТП код\",\n    \"api_key\" => \"API кључ\",\n    \"regenerate\" => \"Генериши\",\n    \"api_key_info\" => \"API кључ је потребан за коришћење API-ја за приступ вашим подацима. Не делите овај кључ са другима.\",\n    // Страница са подешавањима\n    \"monthly_budget\" => \"Месечни буџет\",\n    \"budget_info\" => \"Унесите месечни буџет да бисте видели препоручену максималну цену претплате на почетној страници.\",\n    \"household\" => \"Домаћинство\",\n    \"save_member\" => \"Сачувај члана\",\n    \"delete_member\" => \"Обриши члана\",\n    \"cant_delete_member\" => \"Главни члан не може бити обрисан\",\n    \"cant_delete_member_in_use\" => \"Члана који се користи у претплати не може бити обрисан\",\n    \"household_info\" => \"Поље за е-пошту омогућава члановима домаћинства да буду обавештени о претплатама које ће ускоро истећи.\",\n    \"notifications\" => \"Обавештења\",\n    \"enable_email_notifications\" => \"Омогући обавештења е-поштом\",\n    \"notify_me\" => \"Обавести ме\",\n    \"day_before\" => \"дан пре\",\n    \"on_due_date\" => \"на дан доспећа\",\n    \"days_before\" => \"дана пре\",\n    \"smtp_address\" => \"SMTP адреса\",\n    \"port\" => \"Порт\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP корисничко име\",\n    \"smtp_password\" => \"SMTP лозинка\",\n    \"from_email\" => \"Од е-поште (Опционо)\",\n    \"send_to_other_emails\" => \"Такође пошаљите обавештења на следеће адресе е-поште (користите ; за њихово раздвајање):\",\n    \"smtp_info\" => \"SMTP лозинка се преноси и чува у обичном тексту. Из сигурносних разлога, молимо вас да направите налог само за ово.\",\n    \"telegram\" => \"Телеграм\",\n    \"telegram_bot_token\" => \"Телеграм бот токен\",\n    \"telegram_chat_id\" => \"Телеграм чет ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus токен\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Име корисника\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Икона Emoji\",\n    \"webhook\" => \"Вебхук\",\n    \"webhook_url\" => \"Вебхук URL\",\n    \"request_method\" => \"Метод захтева\",\n    \"custom_headers\" => \"Прилагођени заглавља\",\n    \"webhook_payload\" => \"Вебхук Пајлоад\",\n    \"payment_notifications_payload\" => \"Обавештење о плаћању Пејлоад\",\n    \"cancelation_notification_payload\" => \"Отказивање обавештење Пејлоад\",\n    \"variables_available\" => \"Доступне променљиве\",\n    \"gotify\" => \"Готифи\",\n    \"token\" => \"Токен\",\n    \"discord\" => \"Дискорд\",\n    \"discord_bot_username\" => \"Дискорд бот корисничко име\",\n    \"discord_bot_avatar_url\" => \"Дискорд бот URL аватара\",\n    \"pushover\" => \"Пушовер\",\n    \"pushover_user_key\" => \"Пушовер кориснички кључ\",\n    \"host\" => \"Домаћин\",\n    \"topic\" => \"Тема\",\n    \"ignore_ssl_errors\" => \"Игнориши SSL грешке\",\n    \"categories\" => \"Категорије\",\n    \"save_category\" => \"Сачувај категорију\",\n    \"delete_category\" => \"Избриши категорију\",\n    \"cant_delete_category_in_use\" => \"Категорија која се користи у претплати не може бити избрисана\",\n    \"currencies\" => \"Валуте\",\n    \"save_currency\" => \"Сачувај валуту\",\n    \"delete_currency\" => \"Избриши валуту\",\n    \"cant_delete_main_currency\" => \"Главна валута не може бити избрисана\",\n    \"cant_delete_currency_in_use\" => \"Валута која се користи у претплати не може бити избрисана\",\n    \"exchange_update\" => \"Курсне стопе последњи пут ажуриране\",\n    \"currency_info\" => \"Пронађите подржане валуте и исправне кодове валута на\",\n    \"currency_performance\" => \"За побољшану перформансу, задржите само валуте које користите.\",\n    \"fixer_api_key\" => \"Fixer API кључ\",\n    \"provider\" => \"Провајдер\",\n    \"fixer_info\" => \"Ако користите више валута и желите тачне статистике и сортирање претплата, неопходан је БЕСПЛАТНИ API кључ од Fixer.\",\n    \"get_key\" => \"Добијте свој кључ на\",\n    \"get_free_fixer_api_key\" => \"Добијте бесплатни Fixer API кључ\",\n    \"get_key_alternative\" => \"Алтернативно, можете добити бесплатни Fixer API кључ са\",\n    \"ai_model\" => \"AI Модел\",\n    \"select_ai_model\" => \"Изаберите AI Модел\",\n    \"run_schedule\" => \"Покрените распоред\",\n    \"manually\" => \"Ручно\",\n    \"coming_soon\" => \"Ускоро\",\n    \"invalid_host\" => \"Неважећи хост\",\n    \"ai_recommendations_info\" => \"AI препоруке се генеришу на основу ваших претплата и чланова домаћинства.\",\n    \"may_take_time\" => \"У зависности од провајдера, модела и броја претплата, генерисање препорука може потрајати.\",\n    \"recommendations_visible_on_dashboard\" => \"Препоруке ће бити видљиве на контролној табли.\",\n    \"generate_recommendations\" => \"Генериши препоруке\",\n    \"display_settings\" => \"Подешавања приказа\",\n    \"theme_settings\" => \"Подешавања теме\",\n    \"colors\" => \"Боје\",\n    \"custom_colors\" => \"Прилагођене боје\",\n    \"theme\" => \"Тема\",\n    \"dark_theme\" => \"Тамна тема\",\n    \"light_theme\" => \"Светла тема\",\n    \"automatic\"=> \"Аутоматски\",\n    \"main_color\" => \"Главна боја\",\n    \"accent_color\" => \"Акцент боја\",\n    \"hover_color\" => \"Боја приликом преласка\",\n    \"save_custom_colors\" => \"Сачувај прилагођене боје\",\n    \"reset_custom_colors\" => \"Ресетуј прилагођене боје\",\n    \"custom_css\" => \"Прилагођени CSS\",\n    \"save_custom_css\" => \"Сачувај прилагођени CSS\",\n    \"calculate_monthly_price\" => \"Израчунајте и прикажите месечну цену за све претплате\",\n    \"convert_prices\" => \"Увек конвертујте и прикажите цене на мојој главној валути (спорије)\",\n    \"show_original_price\" => \"Прикажи и оригиналну цену када се врше конверзије или прорачуни\",\n    \"experience\" => \"Искуство\",\n    \"show_subscription_progress\" => \"Прикажи прогрес претплате\",\n    \"disabled_subscriptions\" => \"Онемогућене претплате\",\n    \"hide_disabled_subscriptions\" => \"Сакриј онемогућене претплате\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Прикажи онемогућене претплате на дну\",\n    \"experimental_settings\" => \"Експериментална подешавања\",\n    \"remove_background\" => \"Покушајте уклонити позадину логотипа са слика претраге\",\n    \"use_mobile_navigation_bar\" => \"Користите мобилну навигациону траку\",\n    \"experimental_info\" => \"Експериментална подешавања вероватно неће радити савршено.\",\n    \"payment_methods\" => \"Начини плаћања\",\n    \"payment_methods_info\" => \"Кликните на начин плаћања да бисте га онемогућили / омогућили.\",\n    \"rename_payment_methods_info\" => \"Кликните на име начина плаћања да бисте га преименовали.\",\n    \"cant_delete_payment_method_in_use\" => \"Не може се онемогућити коришћени начин плаћања\",\n    \"add_custom_payment\" => \"Додајте прилагођени начин плаћања\",\n    \"payment_method_name\" => \"Име начина плаћања\",\n    \"payment_method_added_successfuly\" => \"Начин плаћања успешно додат\",\n    \"payment_method_removed\" => \"Начин плаћања уклоњен\",\n    \"disable\" => \"Онемогући\",\n    \"enable\" => \"Омогући\",\n    \"rename_payment_method\" => \"Преименуј начин плаћања\",\n    \"payment_renamed\" => \"Начин плаћања преименован\",\n    \"payment_not_renamed\" => \"Начин плаћања није преименован\",\n    \"test\" => \"Тест\",\n    \"add\" => \"Додај\",\n    \"save\" => \"Сачувај\",\n    \"reset\" => \"Ресетуј\",\n    \"main_accent_color_error\" => \"Главна и акцент боја не могу бити исте\",\n    \"backup_and_restore\" => \"Бекап и ресторе\",\n    \"backup\" => \"Бекап\",\n    \"restore\" => \"Ресторе\",\n    \"restore_info\" => \"Враћање базе података ће заменити све тренутне податке. Бићете одјављени након враћања.\",\n    \"account\" => \"Налог\",\n    \"export_subscriptions\" => \"Извоз претплата\",\n    \"export_as_json\" => \"Извоз као JSON\",\n    \"export_as_csv\" => \"Извоз као CSV\",\n    \"danger_zone\" => \"Зона опасности\",\n    \"delete_account\" => \"Обриши налог\",\n    \"delete_account_info\" => \"Брисање налога ће обрисати све ваше податке, укључујући претплате, подешавања и чланове домаћинства.\",\n    // Мени са филтерима\n    \"filter\" => \"Филтер\",\n    \"clear\" => \"Очисти\",\n    // Тост\n    \"success\" => \"Успех\",\n    // Одговори са сервера\n    \"session_expired\" => \"Ваша сесија је истекла. Молимо вас да се поново пријавите\",\n    \"fields_missing\" => \"Неки подаци недостају\",\n    \"fill_all_fields\" => \"Молимо вас да попуните сва поља\",\n    \"fill_mandatory_fields\" => \"Молимо вас да попуните сва обавезна поља\",\n    \"error\" => \"Грешка\",\n    // Категорија\n    \"failed_add_category\" => \"Додавање категорије није успело\",\n    \"failed_edit_category\" => \"Измена категорије није успела\",\n    \"category_in_use\" => \"Категорија се користи у претплатама и не може бити уклоњена\",\n    \"failed_remove_category\" => \"Уклањање категорије није успело\",\n    \"category_saved\" => \"Категорија сачувана\",\n    \"category_removed\" => \"Категорија уклоњена\",\n    \"sort_order_saved\" => \"Редослед сортирања сачуван\",\n    // Валута\n    \"currency_saved\" => \"је сачувана.\",\n    \"error_adding_currency\" => \"Грешка при додавању валутне ставке.\",\n    \"failed_to_store_currency\" => \"Није успело смештање валуте у базу података.\",\n    \"currency_in_use\" => \"Валута се користи у претплатама и не може бити избрисана.\",\n    \"currency_is_main\" => \"Валута је постављена као главна и не може бити избрисана.\",\n    \"failed_to_remove_currency\" => \"Није успело уклањање валуте из базе података.\",\n    \"failed_to_store_api_key\" => \"Није успело смештање API кључа у базу података.\",\n    \"invalid_api_key\" => \"Неисправан API кључ.\",\n    \"api_key_saved\" => \"API кључ успешно сачуван\",\n    \"currency_removed\" => \"Валута уклоњена\",\n    // Домаћинство\n    \"failed_add_household\" => \"Додавање члана домаћинства није успело\",\n    \"failed_edit_household\" => \"Измена члана домаћинства није успела\",\n    \"failed_remove_household\" => \"Уклањање члана домаћинства није успело\",\n    \"household_in_use\" => \"Члан домаћинства се користи у претплатама и не може бити уклоњен\",\n    \"member_saved\" => \"Члан сачуван\",\n    \"member_removed\" => \"Члан уклоњен\",\n    // Обавештења\n    \"error_saving_notifications\" => \"Грешка при чувању података за обавештења.\",\n    \"wallos_notification\" => \"Валос обавештење\",\n    \"test_notification\" => \"Ово је тест обавештење. Ако га видите, конфигурација је исправна.\",\n    \"email_error\" => \"Грешка при слању е-поште\",\n    \"notification_sent_successfuly\" => \"Обавештење успешно послато\",\n    \"notifications_settings_saved\" => \"Подешавања обавештења успешно сачувана.\",\n    \"notification_failed\" => \"Обавештење није послато\",\n    // Плаћања\n    \"payment_in_use\" => \"Не може се онемогућити коришћени начин плаћања\",\n    \"failed_update_payment\" => \"Ажурирање начина плаћања у бази података није успело\",\n    \"enabled\" => \"омогућен\",\n    \"disabled\" => \"онемогућен\",\n    // Претплата\n    \"error_fetching_image\" => \"Грешка при преузимању слике\",\n    \"subscription_updated_successfuly\" => \"Претплата успешно ажурирана\",\n    \"subscription_added_successfuly\" => \"Претплата успешно додата\",\n    \"error_deleting_subscription\" => \"Грешка при брисању претплате.\",\n    \"invalid_request_method\" => \"Неисправан метод захтева.\",\n    // Корисник\n    \"error_updating_user_data\" => \"Грешка при ажурирању корисничких података.\",\n    \"user_details_saved\" => \"Кориснички подаци сачувани\",\n    // Admin Page\n    \"registrations\" => \"Регистрације\",\n    \"enable_user_registrations\" => \"Омогући регистрације корисника\",\n    \"maximum_number_users\" => \"Максималан број корисника\",\n    \"require_email_verification\" => \"Захтевај верификацију е-поште\",\n    \"configure_smtp_settings_to_enable\" => \"Конфигуришите SMTP подешавања да бисте омогућили ову опцију\",\n    \"server_url\" => \"URL сервера\",\n    \"server_url_info\" => \"Користи се за верификацију е-поште и опоравак лозинке. Мора да буде важећи јавни УРЛ.\",\n    \"server_url_password_reset\" => \"Ако је подешено, такође ће се омогућити функција ресетовања лозинке.\",\n    \"disable_login\" => \"Онемогући пријаву\",\n    \"disable_login_info\" => \"Заобиђите пријаву. Ако свој сервер покрећете само на локалној мрежи, без спољног приступа можете да онемогућите пријаву. Ово ће аутоматски пријавити корисника администратора.\",\n    \"disable_login_info2\" => \"Ово подешавање се може омогућити само ако је регистрација корисника затворена и број корисничких налога не прелази администраторске налоге.\",\n    \"max_users_info\" => \"Максималан број корисника који могу бити регистровани. 0 за неограничено.\",\n    \"user_management\" => \"Управљање корисницима\",\n    \"delete_user\" => \"Обриши корисника\",\n    \"delete_user_info\" => \"Брисање корисника ће такође обрисати све његове претплате и податке.\",\n    \"create_user\" => \"Креирај корисника\",\n    \"oidc_settings\" => \"OIDC подешавања\",\n    \"oidc_auth_enabled\" => \"OIDC аутентификација је омогућена\",\n    \"create_user_automatically\" => \"Креирај корисника аутоматски\",\n    \"disable_password_login\" => \"Онемогући пријаву лозинком\",\n    \"smtp_settings\" => \"SMTP подешавања\",\n    \"smtp_usage_info\" => \"SMTP се користи за слање е-поште за обавештења.\",\n    \"security_settings\" => \"Поставке безбедности\",\n    \"ssrf_protection_info\" => \"Како би се спречили напади Server-Side Request Forgery (SSRF), Wallos подразумевано блокира webhook обавештења ка приватним или унутрашњим мрежним адресама.\",\n    \"local_webhook_info\" => \"Ако треба да шаљете вебхуке ка локалним сервисима (нпр. Home Assistant, Gotify или Node-RED), унесите њихове IP адресе или имена хостова горе, одвојене зарезима (нпр. <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Одржавање\",\n    \"orphaned_logos\" => \"Породични логотипови\",\n    \"update\" => \"Ажурирај\",\n    \"new_version_available\" => \"Нова верзија Wallos-а је доступна\",\n    \"current_version\" => \"Тренутна верзија\",\n    \"latest_version\" => \"Најновија верзија\",\n    \"on_current_version\" => \"Користите најновију верзију Wallos-а.\",\n    \"show_update_notification\" => \"Прикажи обавештење о ажурирањима на dashboardu\",\n    \"cronjobs\" => \"Цроњобс\",\n    // Email Verification\n    \"email_verified\" => \"Е-пошта је верификована\",\n    \"email_verification_failed\" => \"Верификација е-поште није успела\",\n    // Calendar\n    \"calendar\" => \"Календар\",\n    \"sun\" => \"Нед\",\n    \"mon\" => \"Пон\",\n    \"tue\" => \"Уто\",\n    \"wed\" => \"Сре\",\n    \"thu\" => \"Чет\",\n    \"fri\" => \"Пет\",\n    \"sat\" => \"Суб\",\n    \"month-01\" => \"Јануар\",\n    \"month-02\" => \"Фебруар\",\n    \"month-03\" => \"Март\",\n    \"month-04\" => \"Април\",\n    \"month-05\" => \"Мај\",\n    \"month-06\" => \"Јун\",\n    \"month-07\" => \"Јул\",\n    \"month-08\" => \"Август\",\n    \"month-09\" => \"Септембар\",\n    \"month-10\" => \"Октобар\",\n    \"month-11\" => \"Новембар\",\n    \"month-12\" => \"Децембар\",\n    \"total_cost\" => \"Укупан трошак\",\n    \"export_icalendar\" => \"Извоз у iCalendar формат\",\n    \"over_budget_warning\" => \"Прекорачили сте буџет\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Унесите ТОТП код\",\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/sr_lat.php",
    "content": "<?php\n\n$i18n = [\n    // Stranica za registraciju\n    \"create_account\" => \"Morate kreirati nalog pre nego što se možete prijaviti\",\n    \"username\" => \"Korisničko ime\",\n    \"password\" => \"Lozinka\",\n    \"email\" => \"E-pošta\",\n    \"firstname\" => \"Име\",\n    \"lastname\" => \"Презиме\",\n    \"confirm_password\" => \"Potvrdi lozinku\",\n    \"main_currency\" => \"Glavna valuta\",\n    \"language\" => \"Jezik\",\n    \"passwords_dont_match\" => \"Lozinke se ne poklapaju\",\n    \"username_exists\" => \"Korisničko ime već postoji\",\n    \"email_exists\" => \"E-pošta već postoji\",\n    \"registration_failed\" => \"Registracija nije uspela, pokušajte ponovo.\",\n    \"register\" => \"Registruj se\",\n    \"restore_database\" => \"Vrati bazu podataka\",\n    // Stranica za prijavu\n    \"please_login\" => \"Molimo vas da se prijavite\",\n    \"stay_logged_in\" => \"Ostani prijavljen (30 dana)\",\n    \"login\" => \"Prijavi se\",\n    \"login_with\" => \"Prijavi se sa\",\n    \"or\" => \"ili\",\n    \"login_failed\" => \"Podaci za prijavu nisu ispravni\",\n    \"registration_successful\" => \"Registracija uspešna\",\n    \"user_email_waiting_verification\" => \"Vaša e-pošta treba da bude verifikovana. Molimo pregledajte E-poštu\",\n    // Password Reset Page\n    \"forgot_password\" => \"Zaboravili ste lozinku?\",\n    \"reset_password\" => \"Resetuj lozinku\",\n    \"reset_sent_check_email\" => \"Poslali smo vam e-poštu sa uputstvima za resetovanje lozinke\",\n    \"password_reset_successful\" => \"Lozinka uspešno resetovana\",\n    // Header\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Panel\",\n    \"subscriptions\" => \"Pretplate\",\n    \"stats\" => \"Statistike\",\n    \"settings\" => \"Podešavanja\",\n    \"admin\" => \"Admin\",\n    \"about\" => \"O aplikaciji\",\n    \"logout\" => \"Odjava\",\n    // Dashboard\n    \"hello\" => \"Zdravo\",\n    \"upcoming_payments\" => \"Predstojeće uplate\",\n    \"no_upcoming_payments\" => \"Nemate predstojećih uplata\",\n    \"overdue_renewals\" => \"Zakasne obnove\",\n    \"ai_recommendations\" => \"AI preporuke\",\n    \"your_budget\" => \"Vaš budžet\",\n    \"budget\" => \"Budžet\",\n    \"budget_used\" => \"Iskorišćen budžet\",\n    \"over_budget\" => \"Prekoračen budžet\",\n    \"your_subscriptions\" => \"Vaše pretplate\",\n    \"your_savings\" => \"Vaša ušteda\",\n    // Stranica sa pretplatama\n    \"subscription\" => \"Pretplata\",\n    \"no_subscriptions_yet\" => \"Još uvek nemate nijednu pretplatu\",\n    \"add_first_subscription\" => \"Dodajte prvu pretplatu\",\n    \"new_subscription\" => \"Nova pretplata\",\n    \"search\" => \"Pretraga\",\n    \"state\" => \"Stanje\",\n    \"alphanumeric\" => \"Alfanumerički\",\n    \"sort\" => \"Sortiraj\",\n    \"name\" => \"Naziv\",\n    \"last_added\" => \"Poslednje dodato\",\n    \"price\" => \"Cena\",\n    \"next_payment\" => \"Sledeća uplata\",\n    \"renewal_type\" => \"Tip obnove\",\n    \"auto_renewal\" => \"Automatsko obnavljanje\",\n    \"automatically_renews\" => \"Automatsko obnavljanje\",\n    \"manual_renewal\" => \"Ručno obnavljanje\",\n    \"start_date\" => \"Datum početka\",\n    \"inactive\" => \"Onemogući pretplatu\",\n    \"replaced_with\" => \"Zamenjeno sa\",\n    \"none\" => \"Nijedna\",\n    \"member\" => \"Član\",\n    \"category\" => \"Kategorija\",\n    \"payment_method\" => \"Način plaćanja\",\n    \"Daily\" => \"Dnevno\",\n    \"Weekly\" => \"Nedeljno\",\n    \"Monthly\" => \"Mesečno\",\n    \"Yearly\" => \"Godišnje\",\n    \"daily\" => \"dana\",\n    \"weekly\" => \"nedelja\",\n    \"monthly\" => \"meseci\",\n    \"yearly\" => \"godina\",\n    \"days\" => \"dana\",\n    \"weeks\" => \"nedelja\",\n    \"months\" => \"meseci\",\n    \"years\" => \"godina\",\n    \"external_url\" => \"Poseti spoljni URL\",\n    \"empty_page\" => \"Prazna stranica\",\n    \"clear_filters\" => \"Očisti filtere\",\n    \"no_matching_subscriptions\" => \"Nema podudarajućih pretplata\",\n    \"clone\" => \"Kloniraj\",\n    \"renew\" => \"Obnovi\",\n    \"calculate_next_payment_date\" => \"Izračunaj datum sledeće uplate\",\n    // Forma za pretplatu\n    \"add_subscription\" => \"Dodaj pretplatu\",\n    \"edit_subscription\" => \"Uredi pretplatu\",\n    \"subscription_name\" => \"Naziv pretplate\",\n    \"logo_preview\" => \"Pregled logotipa\",\n    \"search_logo\" => \"Pretraži logo na internetu\",\n    \"web_search\" => \"Internet pretraga\",\n    \"currency\" => \"Valuta\",\n    \"payment_every\" => \"Plaćanje svakog\",\n    \"frequency\" => \"Frekvencija\",\n    \"cycle\" => \"Ciklus\",\n    \"no_category\" => \"Bez kategorije\",\n    \"paid_by\" => \"Plaćeno od strane\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Beleške\",\n    \"enable_notifications\" => \"Omogući obaveštenja za ovu pretplatu\",\n    \"default_value_from_settings\" => \"Podrazumevana vrednost iz podešavanja\",\n    \"cancellation_notification\" => \"Obaveštenje o otkazivanju\",\n    \"delete\" => \"Izbriši\",\n    \"cancel\" => \"Otkaži\",\n    \"upload_logo\" => \"Učitaj logo\",\n    // Stranica sa statistikama\n    \"cant_convert_currency\" => \"Na svojim pretplatama koristite više valuta. Da biste imali validnu i tačnu statistiku, postavite Fiker API ključ na stranici podešavanja.\",\n    \"general_statistics\" => \"Opšte statistike\",\n    \"active_subscriptions\" => \"Aktivne pretplate\",\n    \"inactive_subscriptions\" => \"Neaktivne pretplate\",\n    \"monthly_cost\" => \"Mesečni trošak\",\n    \"yearly_cost\" => \"Godišnji trošak\",\n    \"average_monthly\" => \"Prosečni mesečni trošak pretplate\",\n    \"most_expensive\" => \"Najskuplja pretplata\",\n    \"amount_due\" => \"Iznos za plaćanje ovog meseca\",\n    \"percentage_budget_used\" => \"Procenat budžeta iskorišćen\",\n    \"budget_remaining\" => \"Preostali budžet\",\n    \"amount_over_budget\" => \"Iznos preko budžeta\",\n    \"monthly_savings\" => \"Mesečne uštede (na neaktivnim pretplatama)\",\n    \"yearly_savings\" => \"Godišnje uštede (na neaktivnim pretplatama)\",\n    \"split_views\" => \"Podeljene statistike\",\n    \"category_split\" => \"Podela po kategorijama\",\n    \"household_split\" => \"Podela po domaćinstvima\",\n    \"payment_method_split\" => \"Podela po načinu plaćanja\",\n    \"total_cost_trend\" => \"Trend ukupnog troška\",\n    \"cost_vs_budget\" => \"Trošak vs Budžet\",\n    // Stranica O aplikaciji\n    \"about_and_credits\" => \"O aplikaciji i zasluge\",\n    \"credits\" => \"Zasluge\",\n    \"license\" => \"Licenca\",\n    \"release_notes\" => \"Beleške o izdanju\",\n    \"update_available\" => \"Dostupno ažuriranje\",\n    \"issues_and_requests\" => \"Problemi i zahtevi\",\n    \"the_author\" => \"Autor\",\n    \"icons\" => \"Ikone\",\n    \"payment_icons\" => \"Ikone za plaćanje\",\n    // Stranica sa profilom\n    \"upload_avatar\" => \"Učitaj avatar\",\n    \"file_type_error\" => \"Tip datoteke koji ste priložili nije podržan.\",\n    \"user_details\" => \"Detalji korisnika\",\n    \"two_factor_authentication\" => \"Dvostruka autentifikacija\",\n    \"two_factor_info\" => \"Dvofaktorska autentifikacija dodaje dodatni nivo sigurnosti vašem nalogu. <br>Biće vam potrebna aplikacija za autentifikaciju kao što je Google Authenticator, Authi ili Ente Auth da biste skenirali KR kod.\",\n    \"two_factor_enabled_info\" => \"Vaš nalog je siguran sa dvofaktorskom autentifikacijom. Možete ga onemogućiti klikom na dugme iznad.\",\n    \"enable_two_factor_authentication\" => \"Omogući dvofaktorsku autentifikaciju\",\n    \"2fa_already_enabled\" => \"Dvofaktorska autentifikacija je već omogućena\",\n    \"totp_code_incorrect\" => \"Kod za dvofaktorsku autentifikaciju nije tačan\",\n    \"backup_codes\" => \"Rezervni kodovi\",\n    \"download_backup_codes\" => \"Preuzmi rezervne kodove\",\n    \"copy_to_clipboard\" => \"Kopiraj u clipboard\",\n    \"totp_backup_codes_info\" => \"Ovo su vaši rezervni kodovi za dvofaktorsku autentifikaciju. Sačuvajte ih na sigurnom mestu.\",\n    \"disable_two_factor_authentication\" => \"Onemogući dvofaktorsku autentifikaciju\",\n    \"totp_code\" => \"Kod za dvofaktorsku autentifikaciju\",\n    \"api_key\" => \"API ključ\",\n    \"regenerate\" => \"Regeneriši\",\n    \"api_key_info\" => \"API ključ se koristi za pristup Wallos API-ju. Ako mislite da je vaš ključ kompromitovan, možete ga regenerisati.\",\n    // Stranica sa podešavanjima\n    \"monthly_budget\" => \"Mesečni budžet\",\n    \"budget_info\" => \"Ovo je vaš mesečni budžet za sve pretplate. Ovo je samo informativno i ne ograničava vas.\",\n    \"household\" => \"Domaćinstvo\",\n    \"save_member\" => \"Sačuvaj člana\",\n    \"delete_member\" => \"Izbriši člana\",\n    \"cant_delete_member\" => \"Nemoguće brisanje glavnog člana\",\n    \"cant_delete_member_in_use\" => \"Nemoguće brisanje člana koji je u upotrebi u pretplati\",\n    \"household_info\" => \"Polje za e-poštu omogućava članovima domaćinstva da budu obavešteni o pretplatama koje će uskoro isteći.\",\n    \"notifications\" => \"Obaveštenja\",\n    \"enable_email_notifications\" => \"Omogući obaveštenja e-poštom\",\n    \"notify_me\" => \"Obavesti me\",\n    \"day_before\" => \"dan pre\",\n    \"on_due_date\" => \"Na dan dospeća\",\n    \"days_before\" => \"dana pre\",\n    \"smtp_address\" => \"SMTP adresa\",\n    \"port\" => \"Port\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP korisničko ime\",\n    \"smtp_password\" => \"SMTP lozinka\",\n    \"from_email\" => \"Od e-pošte (Opciono)\",\n    \"send_to_other_emails\" => \"Takođe pošaljite obaveštenja na sledeće e-mail adrese (koristite ; za razdvajanje):\",\n    \"smtp_info\" => \"SMTP lozinka se prenosi i čuva u običnom tekstu. Iz sigurnosnih razloga, molimo vas da napravite nalog samo za ovo.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegram bot token\",\n    \"telegram_chat_id\" => \"Telegram chat ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus token\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Korisničko ime\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Ikona Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"Metod zahteva\",\n    \"custom_headers\" => \"Prilagođeni zaglavlja\",\n    \"webhook_payload\" => \"Webhook payload\",\n    \"payment_notifications_payload\" => \"Obaveštenje o uplati payload\",\n    \"cancelation_notification_payload\" => \"Obaveštenje o otkazivanju payload\",\n    \"variables_available\" => \"Dostupne promenljive\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Discord bot korisničko ime\",\n    \"discord_bot_avatar_url\" => \"Discord bot URL avatara\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover korisnički ključ\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Tema\",\n    \"ignore_ssl_errors\" => \"Ignoriši SSL greške\",\n    \"categories\" => \"Kategorije\",\n    \"save_category\" => \"Sačuvaj kategoriju\",\n    \"delete_category\" => \"Izbriši kategoriju\",\n    \"cant_delete_category_in_use\" => \"Nemoguće brisanje kategorije koja je u upotrebi u pretplati\",\n    \"currencies\" => \"Valute\",\n    \"save_currency\" => \"Sačuvaj valutu\",\n    \"delete_currency\" => \"Izbriši valutu\",\n    \"cant_delete_main_currency\" => \"Nemoguće brisanje glavne valute\",\n    \"cant_delete_currency_in_use\" => \"Nemoguće brisanje valute koja je u upotrebi u pretplati\",\n    \"exchange_update\" => \"Kursne stope poslednji put ažurirane\",\n    \"currency_info\" => \"Pronađite podržane valute i ispravne kodove valuta na\",\n    \"currency_performance\" => \"Za poboljšanu performansu, zadržite samo valute koje koristite.\",\n    \"fixer_api_key\" => \"Fixer API ključ\",\n    \"provider\" => \"Provajder\",\n    \"fixer_info\" => \"Ako koristite više valuta i želite tačne statistike i sortiranje pretplata, neophodan je BESPLATNI API ključ sa Fixer-a.\",\n    \"get_key\" => \"Pronađite svoj ključ na\",\n    \"get_free_fixer_api_key\" => \"Pronađite besplatni Fixer API ključ\",\n    \"get_key_alternative\" => \"Alternativno, možete dobiti besplatni Fixer API ključ na\",\n    \"ai_model\" => \"AI Model\",\n    \"select_ai_model\" => \"Izaberite AI Model\",\n    \"run_schedule\" => \"Pokreni raspored\",\n    \"manually\" => \"Ručno\",\n    \"coming_soon\" => \"Uskoro\",\n    \"invalid_host\" => \"Nevažeći host\",\n    \"ai_recommendations_info\" => \"AI preporuke se generišu na osnovu vaših pretplata i članova domaćinstva.\",\n    \"may_take_time\" => \"U zavisnosti od provajdera, modela i broja pretplata, generisanje preporuka može potrajati.\",\n    \"recommendations_visible_on_dashboard\" => \"Preporuke će biti vidljive na kontrolnoj tabli.\",\n    \"generate_recommendations\" => \"Generiši preporuke\",\n    \"display_settings\" => \"Podešavanja prikaza\",\n    \"theme_settings\" => \"Podešavanja teme\",\n    \"colors\" => \"Boje\",\n    \"custom_colors\" => \"Prilagođene boje\",\n    \"theme\" => \"Tema\",\n    \"dark_theme\" => \"Tamna tema\",\n    \"light_theme\" => \"Svetla tema\",\n    \"automatic\" => \"Automatski\",\n    \"main_color\" => \"Glavna boja\",\n    \"accent_color\" => \"Akcentna boja\",\n    \"hover_color\" => \"Boja prilikom prelaska\",\n    \"save_custom_colors\" => \"Sačuvaj prilagođene boje\",\n    \"reset_custom_colors\" => \"Resetuj prilagođene boje\",\n    \"custom_css\" => \"Prilagođeni CSS\",\n    \"save_custom_css\" => \"Sačuvaj prilagođeni CSS\",\n    \"calculate_monthly_price\" => \"Izračunaj i prikaži mesečnu cenu za sve pretplate\",\n    \"convert_prices\" => \"Uvek konvertuj i prikaži cene u mojoj glavnoj valuti (sporije)\",\n    \"show_original_price\" => \"Prikaži i originalnu cenu kada se vrše konverzije ili proračuni\",\n    \"experience\" => \"Iskustvo\",\n    \"show_subscription_progress\" => \"Prikaži napredak pretplate\",\n    \"disabled_subscriptions\" => \"Onemogućene pretplate\",\n    \"hide_disabled_subscriptions\" => \"Sakrij onemogućene pretplate\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Prikaži onemogućene pretplate na dnu\",\n    \"experimental_settings\" => \"Eksperimentalna podešavanja\",\n    \"remove_background\" => \"Pokušajte ukloniti pozadinu logotipa sa pretrage slika\",\n    \"use_mobile_navigation_bar\" => \"Koristi navigacionu traku za mobilne uređaje\",\n    \"experimental_info\" => \"Eksperimentalna podešavanja verovatno neće savršeno funkcionisati.\",\n    \"payment_methods\" => \"Načini plaćanja\",\n    \"payment_methods_info\" => \"Kliknite na način plaćanja da biste ga onemogućili / omogućili.\",\n    \"rename_payment_methods_info\" => \"Kliknite na ime načina plaćanja da biste ga preimenovali.\",\n    \"cant_delete_payment_method_in_use\" => \"Nemoguće onemogućiti korišćeni način plaćanja\",\n    \"add_custom_payment\" => \"Dodaj prilagođeni način plaćanja\",\n    \"payment_method_name\" => \"Ime načina plaćanja\",\n    \"payment_method_added_successfuly\" => \"Način plaćanja uspešno dodat\",\n    \"payment_method_removed\" => \"Način plaćanja uklonjen\",\n    \"disable\" => \"Onemogući\",\n    \"enable\" => \"Omogući\",\n    \"rename_payment_method\" => \"Preimenuj način plaćanja\",\n    \"payment_renamed\" => \"Način plaćanja preimenovan\",\n    \"payment_not_renamed\" => \"Način plaćanja nije preimenovan\",\n    \"test\" => \"Test\",\n    \"add\" => \"Dodaj\",\n    \"save\" => \"Sačuvaj\",\n    \"reset\" => \"Resetuj\",\n    \"main_accent_color_error\" => \"Glavna i akcentna boja ne mogu biti iste\",\n    \"backup_and_restore\" => \"Backup i restore\",\n    \"backup\" => \"Backup\",\n    \"restore\" => \"Restore\",\n    \"restore_info\" => \"Vraćanje baze podataka će zameniti sve trenutne podatke. Bićete odjavljeni nakon vraćanja.\",\n    \"account\" => \"Nalog\",\n    \"export_subscriptions\" => \"Izvezi pretplate\",\n    \"export_as_json\" => \"Izvezi kao JSON\",\n    \"export_as_csv\" => \"Izvezi kao CSV\",\n    \"danger_zone\" => \"Opasna zona\",\n    \"delete_account\" => \"Izbriši nalog\",\n    \"delete_account_info\" => \"Brisanjem naloga izbrisaćete sve podatke, uključujući pretplate, podešavanja, članove domaćinstva i načine plaćanja.\",\n    // Meni sa filterima\n    \"filter\" => \"Filter\",\n    \"clear\" => \"Očisti\",\n    // Toast\n    \"success\" => \"Uspeh\",\n    // Odgovori sa servera\n    \"session_expired\" => \"Vaša sesija je istekla. Molimo vas da se ponovo prijavite\",\n    \"fields_missing\" => \"Neki podaci nedostaju\",\n    \"fill_all_fields\" => \"Molimo vas da popunite sva polja\",\n    \"fill_mandatory_fields\" => \"Molimo vas da popunite sva obavezna polja\",\n    \"error\" => \"Greška\",\n    // Kategorija\n    \"failed_add_category\" => \"Dodavanje kategorije nije uspelo\",\n    \"failed_edit_category\" => \"Izmena kategorije nije uspela\",\n    \"category_in_use\" => \"Kategorija se koristi u pretplatama i ne može biti uklonjena\",\n    \"failed_remove_category\" => \"Uklanjanje kategorije nije uspelo\",\n    \"category_saved\" => \"Kategorija sačuvana\",\n    \"category_removed\" => \"Kategorija uklonjena\",\n    \"sort_order_saved\" => \"Redosled sortiranja sačuvan\",\n    // Valuta\n    \"currency_saved\" => \"je sačuvan.\",\n    \"error_adding_currency\" => \"Greška pri dodavanju valutne stavke.\",\n    \"failed_to_store_currency\" => \"Nije uspelo skladištenje valute u bazi podataka.\",\n    \"currency_in_use\" => \"Valuta se koristi u pretplatama i ne može biti izbrisana.\",\n    \"currency_is_main\" => \"Valuta je postavljena kao glavna i ne može biti izbrisana.\",\n    \"failed_to_remove_currency\" => \"Nije uspelo uklanjanje valute iz baze podataka.\",\n    \"failed_to_store_api_key\" => \"Nije uspelo skladištenje API ključa u bazi podataka.\",\n    \"invalid_api_key\" => \"Nevažeći API ključ.\",\n    \"api_key_saved\" => \"API ključ je uspešno sačuvan\",\n    \"currency_removed\" => \"Valuta uklonjena\",\n    // Domaćinstvo\n    \"failed_add_household\" => \"Dodavanje člana domaćinstva nije uspelo\",\n    \"failed_edit_household\" => \"Izmena člana domaćinstva nije uspela\",\n    \"failed_remove_household\" => \"Uklanjanje člana domaćinstva nije uspelo\",\n    \"household_in_use\" => \"Član domaćinstva se koristi u pretplatama i ne može biti uklonjen\",\n    \"member_saved\" => \"Član sačuvan\",\n    \"member_removed\" => \"Član uklonjen\",\n    // Obaveštenja\n    \"error_saving_notifications\" => \"Greška pri čuvanju podataka za obaveštenja.\",\n    \"wallos_notification\" => \"Obaveštenje od Wallos-a\",\n    \"test_notification\" => \"Ovo je testno obaveštenje. Ako ga vidite, konfiguracija je ispravna.\",\n    \"email_error\" => \"Greška pri slanju e-pošte\",\n    \"notification_sent_successfuly\" => \"Obaveštenje uspešno poslato\",\n    \"notifications_settings_saved\" => \"Podešavanja obaveštenja uspešno sačuvana.\",\n    \"notification_failed\" => \"Obaveštenje nije poslato\",\n    // Plaćanja\n    \"payment_in_use\" => \"Nije moguće onemogućiti korišćeni način plaćanja\",\n    \"failed_update_payment\" => \"Nije uspelo ažuriranje načina plaćanja u bazi podataka\",\n    \"enabled\" => \"omogućen\",\n    \"disabled\" => \"onemogućen\",\n    // Pretplata\n    \"error_fetching_image\" => \"Greška pri preuzimanju slike\",\n    \"subscription_updated_successfuly\" => \"Pretplata uspešno ažurirana\",\n    \"subscription_added_successfuly\" => \"Pretplata uspešno dodata\",\n    \"error_deleting_subscription\" => \"Greška pri brisanju pretplate.\",\n    \"invalid_request_method\" => \"Nevažeći metod zahteva.\",\n    // Korisnik\n    \"error_updating_user_data\" => \"Greška pri ažuriranju korisničkih podataka.\",\n    \"user_details_saved\" => \"Korisnički podaci sačuvani\",\n    // Admin Page\n    \"registrations\" => \"Registracije\",\n    \"enable_user_registrations\" => \"Omogući registracije korisnika\",\n    \"maximum_number_users\" => \"Maksimalan broj korisnika\",\n    \"require_email_verification\" => \"Zahtevaj verifikaciju e-pošte\",\n    \"configure_smtp_settings_to_enable\" => \"Konfigurišite SMTP podešavanja da biste omogućili ovu opciju\",\n    \"server_url\" => \"URL servera\",\n    \"server_url_info\" => \"Koristi se za verifikaciju e-pošte i oporavak lozinke. Mora da bude važeći javni URL.\",\n    \"server_url_password_reset\" => \"Ako je podešeno, takođe će se omogućiti funkcija resetovanja lozinke.\",\n    \"disable_login\" => \"Onemogući prijavu\",\n    \"disable_login_info\" => \"Zaobiđite prijavu. Ako svoj server pokrećete samo na lokalnoj mreži, bez spoljnog pristupa možete da onemogućite prijavu. Ovo će automatski prijaviti korisnika administratora.\",\n    \"disable_login_info2\" => \"Ovo podešavanje se može omogućiti samo ako je registracija korisnika zatvorena i broj korisničkih naloga ne prelazi administratorske naloge.\",\n    \"max_users_info\" => \"0 za neograničen broj korisnika\",\n    \"user_management\" => \"Upravljanje korisnicima\",\n    \"delete_user\" => \"Izbriši korisnika\",\n    \"delete_user_info\" => \"Brisanjem korisnika izbrisaće se i sve njegove pretplate i podešavanja.\",\n    \"create_user\" => \"Kreiraj korisnika\",\n    \"oidc_settings\" => \"OIDC podešavanja\",\n    \"oidc_auth_enabled\" => \"Omogući OIDC autentifikaciju\",\n    \"create_user_automatically\" => \"Kreiraj korisnika automatski\",\n    \"disable_password_login\" => \"Onemoguči prijavu z geslom\",\n    \"smtp_settings\" => \"SMTP podešavanja\",\n    \"smtp_usage_info\" => \"Koristiće se za oporavak lozinke i druge sistemske e-poruke.\",\n    \"security_settings\" => \"Podešavanja bezbednosti\",\n    \"ssrf_protection_info\" => \"Kako bi se sprečile Server-Side Request Forgery (SSRF) napadi, Wallos podrazumevano blokira webhook obaveštenja ka privatnim ili internim mrežnim adresama.\",\n    \"local_webhook_info\" => \"Ako treba da šaljete webhooks ka lokalnim servisima (npr. Home Assistant, Gotify ili Node-RED), unesite njihove IP adrese ili imena hostova gore, odvojene zarezima (npr. <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Održavanje\",\n    \"orphaned_logos\" => \"Nepovezani logotipi\",\n    \"update\" => \"Ažuriraj\",\n    \"new_version_available\" => \"Nova verzija Wallos-a je dostupna\",\n    \"current_version\" => \"Trenutna verzija\",\n    \"latest_version\" => \"Najnovija verzija\",\n    \"on_current_version\" => \"Koristite najnoviju verziju Wallos-a.\",\n    \"show_update_notification\" => \"Prikaži obaveštenje o ažuriranjima na dashboardu\",\n    \"cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"E-pošta je uspešno verifikovana\",\n    \"email_verification_failed\" => \"Verifikacija e-pošte nije uspela\",\n    // Calendar\n    \"calendar\" => \"Kalendar\",\n    \"sun\" => \"Ned\",\n    \"mon\" => \"Pon\",\n    \"tue\" => \"Uto\",\n    \"wed\" => \"Sre\",\n    \"thu\" => \"Čet\",\n    \"fri\" => \"Pet\",\n    \"sat\" => \"Sub\",\n    \"month-01\" => \"Januar\",\n    \"month-02\" => \"Februar\",\n    \"month-03\" => \"Mart\",\n    \"month-04\" => \"April\",\n    \"month-05\" => \"Maj\",\n    \"month-06\" => \"Jun\",\n    \"month-07\" => \"Jul\",\n    \"month-08\" => \"Avgust\",\n    \"month-09\" => \"Septembar\",\n    \"month-10\" => \"Oktobar\",\n    \"month-11\" => \"Novembar\",\n    \"month-12\" => \"Decembar\",\n    \"total_cost\" => \"Ukupan trošak\",\n    \"export_icalendar\" => \"Izvezi iCalendar\",\n    \"over_budget_warning\" => \"Prekoračili ste budžet\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Unesite TOTP kod\",\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/tr.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Giriş yapabilmeniz için önce bir hesap oluşturmanız gerekiyor\",\n    \"username\" => \"Kullanıcı Adı\",\n    \"password\" => \"Şifre\",\n    \"email\" => \"E-posta\",\n    \"firstname\" => \"Ad\",\n    \"lastname\" => \"Soy isim\",\n    \"confirm_password\" => \"Şifreyi Onayla\",\n    \"main_currency\" => \"Ana Para Birimi\",\n    \"language\" => \"Dil\",\n    \"passwords_dont_match\" => \"Şifreler eşleşmiyor\",\n    \"username_exists\" => \"Bu kullanıcı adı zaten mevcut\",\n    \"email_exists\" => \"Bu e-posta zaten mevcut\",\n    \"registration_failed\" => \"Kayıt başarısız, lütfen tekrar deneyin.\",\n    \"register\" => \"Kayıt Ol\",\n    \"restore_database\" => \"Veritabanını geri yükle\",\n    // Login Page\n    \"please_login\" => \"Lütfen giriş yapın\",\n    \"stay_logged_in\" => \"Oturumu açık tut (30 gün)\",\n    \"login\" => \"Giriş Yap\",\n    \"login_with\" => \"Şununla giriş yap\",\n    \"or\" => \"veya\",\n    \"login_failed\" => \"Giriş bilgileri hatalı\",\n    \"registration_successful\" => \"Kayıt başarılı\",\n    \"user_email_waiting_verification\" => \"E-postanızın doğrulanması gerekiyor. Lütfen e-postanızı kontrol edin\",\n    // Password Reset Page\n    \"forgot_password\" => \"Şifremi Unuttum\",\n    \"reset_password\" => \"Şifreyi Sıfırla\",\n    \"reset_sent_check_email\" => \"Şifre sıfırlama bağlantısı e-posta adresinize gönderildi. Lütfen e-postanızı kontrol edin.\",\n    \"password_reset_successful\" => \"Şifre sıfırlama başarılı\",\n    // Header\n    \"profile\" => \"Profil\",\n    \"dashboard\" => \"Panel\",\n    \"subscriptions\" => \"Abonelikler\",\n    \"stats\" => \"İstatistikler\",\n    \"settings\" => \"Ayarlar\",\n    \"admin\" => \"Yönetici\",\n    \"about\" => \"Hakkında\",\n    \"logout\" => \"Çıkış Yap\",\n    // Dashboard\n    \"hello\" => \"Merhaba\",\n    \"upcoming_payments\" => \"Yaklaşan Ödemeler\",\n    \"no_upcoming_payments\" => \"Yaklaşan ödemeniz yok\",\n    \"overdue_renewals\" => \"Gecikmiş Yenilemeler\",\n    \"ai_recommendations\" => \"AI Önerileri\",\n    \"your_budget\" => \"Bütçeniz\",\n    \"budget\" => \"Bütçe\",\n    \"budget_used\" => \"İşletilen Bütçe\",\n    \"over_budget\" => \"Bütçeyi Aşma\",\n    \"your_subscriptions\" => \"Abonelikleriniz\",\n    \"your_savings\" => \"Tasarruflarınız\",\n    // Subscriptions page\n    \"subscription\" => \"Abonelik\",\n    \"no_subscriptions_yet\" => \"Henüz herhangi bir aboneliğiniz yok\",\n    \"add_first_subscription\" => \"İlk aboneliği ekle\",\n    \"new_subscription\" => \"Yeni Abonelik\",\n    \"search\" => \"Ara\",\n    \"state\" => \"Durum\",\n    \"alphanumeric\" => \"Alfanümerik\",\n    \"sort\" => \"Sırala\",\n    \"name\" => \"İsim\",\n    \"last_added\" => \"Son Eklenen\",\n    \"price\" => \"Fiyat\",\n    \"next_payment\" => \"Sonraki Ödeme\",\n    \"renewal_type\" => \"Yenileme Türü\",\n    \"auto_renewal\" => \"Otomatik Yenileme\",\n    \"automatically_renews\" => \"Otomatik Yenileme\",\n    \"manual_renewal\" => \"Manuel Yenileme\",\n    \"start_date\" => \"Başlangıç Tarihi\",\n    \"inactive\" => \"Aboneliği Devre Dışı Bırak\",\n    \"replaced_with\" => \"Şununla değiştirildi\",\n    \"none\" => \"Yok\",\n    \"member\" => \"Üye\",\n    \"category\" => \"Kategori\",\n    \"payment_method\" => \"Ödeme Yöntemi\",\n    \"Daily\" => \"Günlük\",\n    \"Weekly\" => \"Haftalık\",\n    \"Monthly\" => \"Aylık\",\n    \"Yearly\" => \"Yıllık\",\n    \"daily\" => \"Gün(ler)\",\n    \"weekly\" => \"Hafta(lar)\",\n    \"monthly\" => \"Ay(lar)\",\n    \"yearly\" => \"Yıl(lar)\",\n    \"days\" => \"günler\",\n    \"weeks\" => \"haftalar\",\n    \"months\" => \"aylar\",\n    \"years\" => \"yıllar\",\n    \"external_url\" => \"Harici URL'yi Ziyaret Et\",\n    \"empty_page\" => \"Boş Sayfa\",\n    \"clear_filters\" => \"Filtreleri Temizle\",\n    \"no_matching_subscriptions\" => \"Eşleşen abonelik bulunamadı\",\n    \"clone\" => \"Kopyala\",\n    \"renew\" => \"Yenile\",\n    \"calculate_next_payment_date\" => \"Sonraki ödeme tarihini hesapla\",\n    // Subscription form\n    \"add_subscription\" => \"Abonelik ekle\",\n    \"edit_subscription\" => \"Aboneliği düzenle\",\n    \"subscription_name\" => \"Abonelik adı\",\n    \"logo_preview\" => \"Logo Önizlemesi\",\n    \"search_logo\" => \"Logoyu webde ara\",\n    \"web_search\" => \"Web araması\",\n    \"currency\" => \"Para Birimi\",\n    \"payment_every\" => \"Ödeme Sıklığı\",\n    \"frequency\" => \"Frekans\",\n    \"cycle\" => \"Döngü\",\n    \"no_category\" => \"Kategori yok\",\n    \"paid_by\" => \"Ödeyen\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Notlar\",\n    \"enable_notifications\" => \"Bu abonelik için bildirimleri etkinleştir\",\n    \"default_value_from_settings\" => \"Ayarlar'dan varsayılan değeri al\",\n    \"cancellation_notification\" => \"İptal Bildirimi\",\n    \"delete\" => \"Sil\",\n    \"cancel\" => \"İptal\",\n    \"upload_logo\" => \"Logo Yükle\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Aboneliklerinizde birden fazla para birimi kullanıyorsunuz. Geçerli ve doğru istatistiklere sahip olmak için lütfen ayarlar sayfasında bir Fixer API Anahtarı ayarlayın.\",\n    \"general_statistics\" => \"Genel İstatistikler\",\n    \"active_subscriptions\" => \"Aktif Abonelikler\",\n    \"inactive_subscriptions\" => \"Aktif Olmayan Abonelikler\",\n    \"monthly_cost\" => \"Aylık Maliyet\",\n    \"yearly_cost\" => \"Yıllık Maliyet\",\n    \"average_monthly\" => \"Ortalama Aylık Abonelik Maliyeti\",\n    \"most_expensive\" => \"En Pahalı Abonelik Maliyeti\",\n    \"amount_due\" => \"Bu ay ödenecek miktar\",\n    \"percentage_budget_used\" => \"Bütçe Kullanımı\",\n    \"budget_remaining\" => \"Kalan Bütçe\",\n    \"amount_over_budget\" => \"Bütçe Aşımı\",\n    \"monthly_savings\" => \"Aylık Tasarruf (aktif olmayan aboneliklerde)\",\n    \"yearly_savings\" => \"Yıllık Tasarruf (aktif olmayan aboneliklerde)\",\n    \"split_views\" => \"Bölünmüş Görünümler\",\n    \"category_split\" => \"Kategori Bölümü\",\n    \"household_split\" => \"Hane Bölümü\",\n    \"payment_method_split\" => \"Ödeme Yöntemi Bölümü\",\n    \"total_cost_trend\" => \"Toplam Maliyet Eğilimi\",\n    \"cost_vs_budget\" => \"Bütçe ile Maliyet\",\n    // About page\n    \"about_and_credits\" => \"Hakkında ve Teşekkürler\",\n    \"credits\" => \"Teşekkürler\",\n    \"license\" => \"Lisans\",\n    \"release_notes\" => \"Sürüm Notları\",\n    \"update_available\" => \"Güncelleme mevcut\",\n    \"issues_and_requests\" => \"Sorunlar ve İstekler\",\n    \"the_author\" => \"Yazar\",\n    \"icons\" => \"İkonlar\",\n    \"payment_icons\" => \"Ödeme İkonları\",\n    // Profile page\n    \"upload_avatar\" => \"Avatarı yükle\",\n    \"file_type_error\" => \"Dosya türü izin verilmiyor\",\n    \"user_details\" => \"Kullanıcı Detayları\",\n    \"two_factor_authentication\" => \"İki Faktörlü Kimlik Doğrulama\",\n    \"two_factor_info\" => \"İki Faktörlü Kimlik Doğrulama, hesabınıza ekstra bir güvenlik katmanı ekler.<br>Karekodu taramak için Google Authenticator, Authy veya Ente Auth gibi bir kimlik doğrulayıcı uygulamasına ihtiyacınız olacaktır.\",\n    \"two_factor_enabled_info\" => \"Hesabınız İki Faktörlü Kimlik Doğrulama ile güvendedir. Yukarıdaki düğmeye tıklayarak devre dışı bırakabilirsiniz.\",\n    \"enable_two_factor_authentication\" => \"İki Faktörlü Kimlik Doğrulamayı Etkinleştir\",\n    \"2fa_already_enabled\" => \"İki Faktörlü Kimlik Doğrulama zaten etkinleştirildi\",\n    \"totp_code_incorrect\" => \"TOTP kodu yanlış\",\n    \"backup_codes\" => \"Yedek Kodlar\",\n    \"download_backup_codes\" => \"Yedek Kodları İndir\",\n    \"copy_to_clipboard\" => \"Panoya Kopyala\",\n    \"totp_backup_codes_info\" => \"Yedek kodları güvenli bir yerde saklayın. Her biri yalnızca bir kez kullanılabilir.\",\n    \"disable_two_factor_authentication\" => \"İki Faktörlü Kimlik Doğrulamayı Devre Dışı Bırak\",\n    \"totp_code\" => \"TOTP Kodu\",\n    \"api_key\" => \"API Anahtarı\",\n    \"regenerate\" => \"Yeniden Oluştur\",\n    \"api_key_info\" => \"API Anahtarı, Wallos'un API'sine erişmek için kullanılır. Bu anahtarı kimseyle paylaşmayın.\",\n    // Settings page\n    \"monthly_budget\" => \"Aylık Bütçe\",\n    \"budget_info\" => \"Bir bütçe belirlemek, istatistik sayfasında bütçe ve gerçek harcamaları karşılaştırmanıza olanak tanır.\",\n    \"household\" => \"Hane\",\n    \"save_member\" => \"Üyeyi Kaydet\",\n    \"delete_member\" => \"Üyeyi Sil\",\n    \"cant_delete_member\" => \"Ana üyeyi silemezsiniz\",\n    \"cant_delete_member_in_use\" => \"Abonelikte kullanılan üyeyi silemezsiniz\",\n    \"household_info\" => \"E-posta alanı, hane üyelerinin süresi dolmak üzere olan aboneliklerden haberdar edilmesini sağlar.\",\n    \"notifications\" => \"Bildirimler\",\n    \"enable_email_notifications\" => \"E-posta bildirimlerini etkinleştir\",\n    \"notify_me\" => \"Beni bilgilendir\",\n    \"day_before\" => \"bir gün önce\",\n    \"on_due_date\" => \"Vadesinde\",\n    \"days_before\" => \"günler önce\",\n    \"smtp_address\" => \"SMTP Adresi\",\n    \"port\" => \"Port\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP Kullanıcı Adı\",\n    \"smtp_password\" => \"SMTP Şifresi\",\n    \"from_email\" => \"Gönderen e-posta (İsteğe bağlı)\",\n    \"send_to_other_emails\" => \"Bildirimleri aşağıdaki e-posta adreslerine de gönder (ayırmak için ; kullanın):\",\n    \"smtp_info\" => \"SMTP Şifresi düz metin olarak iletilir ve saklanır. Güvenlik için, lütfen bunun için özel bir hesap oluşturun.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegram Bot Token\",\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus Token\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Kullanıcı Adı\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot İkon Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"İstek Metodu\",\n    \"custom_headers\" => \"Özel Başlıklar\",\n    \"webhook_payload\" => \"Webhook Payload\",\n    \"payment_notifications_payload\" => \"Ödeme Bildirimi Payload\",\n    \"cancelation_notification_payload\" => \"İptal Bildirimi Payload\",\n    \"variables_available\" => \"Kullanılabilir Değişkenler\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Token\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Discord Bot Kullanıcı Adı\",\n    \"discord_bot_avatar_url\" => \"Discord Bot Avatar URL\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover Kullanıcı Anahtarı\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Konu\",\n    \"ignore_ssl_errors\" => \"SSL Hatalarını Yoksay\",\n    \"categories\" => \"Kategoriler\",\n    \"save_category\" => \"Kategoriyi Kaydet\",\n    \"delete_category\" => \"Kategoriyi Sil\",\n    \"cant_delete_category_in_use\" => \"Abonelikte kullanılan kategoriyi silemezsiniz\",\n    \"currencies\" => \"Para Birimleri\",\n    \"save_currency\" => \"Para birimini kaydet\",\n    \"delete_currency\" => \"Para birimini sil\",\n    \"cant_delete_main_currency\" => \"Ana para birimini silemezsiniz\",\n    \"cant_delete_currency_in_use\" => \"Abonelikte kullanılan para birimini silemezsiniz\",\n    \"exchange_update\" => \"Döviz kurları son güncelleme tarihi\",\n    \"currency_info\" => \"Desteklenen para birimlerini ve doğru para birimi kodlarını burada bulun\",\n    \"currency_performance\" => \"Performansı artırmak için sadece kullandığınız para birimlerini tutun.\",\n    \"fixer_api_key\" => \"Fixer API Anahtarı\",\n    \"provider\" => \"Sağlayıcı\",\n    \"fixer_info\" => \"Birden fazla para birimi kullanıyorsanız ve aboneliklerde doğru istatistikler ve sıralama istiyorsanız, Fixer'dan ÜCRETSİZ bir API Anahtarı gereklidir.\",\n    \"get_key\" => \"Anahtarınızı şuradan alın\",\n    \"get_free_fixer_api_key\" => \"Ücretsiz Fixer API Anahtarı alın\",\n    \"get_key_alternative\" => \"Alternatif olarak, şu adresten ücretsiz bir fixer api anahtarı edinebilirsiniz\",\n    \"ai_model\" => \"AI Modeli\",\n    \"select_ai_model\" => \"AI Modelini Seçin\",\n    \"run_schedule\" => \"Programı Çalıştır\",\n    \"manually\" => \"Manuel Olarak\",\n    \"coming_soon\" => \"Çok Yakında\",\n    \"invalid_host\" => \"Geçersiz Host\",\n    \"ai_recommendations_info\" => \"AI önerileri, abonelikleriniz ve hane üyeleriniz temel alınarak oluşturulur.\",\n    \"may_take_time\" => \"Sağlayıcıya, modele ve abonelik sayısına bağlı olarak önerilerin oluşturulması biraz zaman alabilir.\",\n    \"recommendations_visible_on_dashboard\" => \"Öneriler panelde görüntülenecektir.\",\n    \"generate_recommendations\" => \"Önerileri Oluştur\",\n    \"display_settings\" => \"Görüntüleme Ayarları\",\n    \"theme_settings\" => \"Tema Ayarları\",\n    \"colors\" => \"Renkler\",\n    \"custom_colors\" => \"Özel Renkler\",\n    \"theme\" => \"Tema\",\n    \"dark_theme\" => \"Koyu Temayı\",\n    \"light_theme\" => \"Açık Temayı\",\n    \"automatic\"=> \"Otomatik\",\n    \"main_color\" => \"Ana\",\n    \"accent_color\" => \"Vurgu\",\n    \"hover_color\" => \"Üzerine gelindiğinde\",\n    \"save_custom_colors\" => \"Özel Renkleri Kaydet\",\n    \"reset_custom_colors\" => \"Özel Renkleri Sıfırla\",\n    \"custom_css\" => \"Özel CSS\",\n    \"save_custom_css\" => \"Özel CSS'yi Kaydet\",\n    \"calculate_monthly_price\" => \"Tüm aboneliklerin aylık fiyatını hesaplayın ve gösterin\",\n    \"convert_prices\" => \"Fiyatları her zaman ana para birimimde dönüştürün ve gösterin (daha yavaş)\",\n    \"show_original_price\" => \"Dönüşümler veya hesaplamalar yapıldığında orijinal fiyatı da göster\",\n    \"experience\" => \"Deneyim\",\n    \"show_subscription_progress\" => \"Aboneliklerin ilerlemesini göster\",\n    \"disabled_subscriptions\" => \"Devre Dışı Bırakılan Abonelikler\",\n    \"hide_disabled_subscriptions\" => \"Devre dışı bırakılan abonelikleri gizle\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Devre dışı bırakılan abonelikleri altta göster\",\n    \"experimental_settings\" => \"Deneysel Ayarlar\",\n    \"remove_background\" => \"Görsel aramadan logoların arka planını kaldırmayı deneyin\",\n    \"use_mobile_navigation_bar\" => \"Mobil cihazlarda gezinme çubuğunu kullan\",\n    \"experimental_info\" => \"Deneysel ayarlar muhtemelen mükemmel çalışmayacak.\",\n    \"payment_methods\" => \"Ödeme Yöntemleri\",\n    \"payment_methods_info\" => \"Bir ödeme yöntemini devre dışı bırakmak / etkinleştirmek için tıklayın.\",\n    \"rename_payment_methods_info\" => \"Yeniden adlandırmak için bir ödeme yönteminin adına tıklayın.\",\n    \"cant_delete_payment_method_in_use\" => \"Kullanımda olan ödeme yöntemini devre dışı bırakamazsınız\",\n    \"add_custom_payment\" => \"Özel ödeme yöntemi ekle\",\n    \"payment_method_name\" => \"Ödeme Yöntemi Adı\",\n    \"payment_method_added_successfuly\" => \"Ödeme yöntemi başarıyla eklendi\",\n    \"payment_method_removed\" => \"Ödeme yöntemi kaldırıldı\",\n    \"disable\" => \"Devre Dışı Bırak\",\n    \"enable\" => \"Etkinleştir\",\n    \"rename_payment_method\" => \"Ödeme yöntemi adını değiştir\",\n    \"payment_renamed\" => \"Ödeme yöntemi adı değiştirildi\",\n    \"payment_not_renamed\" => \"Ödeme yöntemi adı değiştirilemedi\",\n    \"test\" => \"Test Et\",\n    \"add\" => \"Ekle\",\n    \"save\" => \"Kaydet\",\n    \"reset\" => \"Sıfırla\",\n    \"main_accent_color_error\" => \"Ana ve vurgu rengi aynı olamaz\",\n    \"backup_and_restore\" => \"Yedekle ve Geri Yükle\",\n    \"backup\" => \"Yedekle\",\n    \"restore\" => \"Geri Yükle\",\n    \"restore_info\" => \"Veritabanının geri yüklenmesi tüm mevcut verileri geçersiz kılacaktır. Geri yüklemeden sonra oturumunuz kapatılacaktır.\",\n    \"account\" => \"Hesap\",\n    \"export_subscriptions\" => \"Abonelikleri dışa aktar\",\n    \"export_as_json\" => \"JSON olarak dışa aktar\",\n    \"export_as_csv\" => \"CSV olarak dışa aktar\",\n    \"danger_zone\" => \"Tehlikeli Bölge\",\n    \"delete_account\" => \"Hesabı Sil\",\n    \"delete_account_info\" => \"Hesabınızı sildiğinizde tüm abonelikleriniz ve ayarlarınız da silinecektir.\",\n    // Filters menu\n    \"filter\" => \"Filtre\",\n    \"clear\" => \"Temizle\",\n    // Toast\n    \"success\" => \"Başarılı\",\n    // Endpoint responses\n    \"session_expired\" => \"Oturumunuz sona erdi. Lütfen tekrar giriş yapın\",\n    \"fields_missing\" => \"Bazı alanlar eksik\",\n    \"fill_all_fields\" => \"Lütfen tüm alanları doldurun\",\n    \"fill_mandatory_fields\" => \"Lütfen zorunlu alanları doldurun\",\n    \"error\" => \"Hata\",\n    // Category\n    \"failed_add_category\" => \"Kategori eklenemedi\",\n    \"failed_edit_category\" => \"Kategori düzenlenemedi\",\n    \"category_in_use\" => \"Kategori aboneliklerde kullanımda olduğu için kaldırılamaz\",\n    \"failed_remove_category\" => \"Kategori kaldırılamadı\",\n    \"category_saved\" => \"Kategori kaydedildi\",\n    \"category_removed\" => \"Kategori kaldırıldı\",\n    \"sort_order_saved\" => \"Sıralama düzeni kaydedildi\",\n    // Currency\n    \"currency_saved\" => \"kaydedildi.\",\n    \"error_adding_currency\" => \"Para birimi girişi eklenirken hata oluştu.\",\n    \"failed_to_store_currency\" => \"Para birimi Veritabanına kaydedilemedi.\",\n    \"currency_in_use\" => \"Para birimi aboneliklerde kullanımda olduğu için silinemez.\",\n    \"currency_is_main\" => \"Para birimi ana para birimi olarak ayarlandığı için silinemez.\",\n    \"failed_to_remove_currency\" => \"Para birimi Veritabanından kaldırılamadı.\",\n    \"failed_to_store_api_key\" => \"API Anahtarı Veritabanına kaydedilemedi.\",\n    \"invalid_api_key\" => \"Geçersiz API Anahtarı.\",\n    \"api_key_saved\" => \"API anahtarı başarıyla kaydedildi\",\n    \"currency_removed\" => \"Para birimi kaldırıldı\",\n    // Household\n    \"failed_add_household\" => \"Hane üyesi eklenemedi\",\n    \"failed_edit_household\" => \"Hane üyesi düzenlenemedi\",\n    \"failed_remove_household\" => \"Hane üyesi kaldırılamadı\",\n    \"household_in_use\" => \"Hane üyesi aboneliklerde kullanımda olduğu için kaldırılamaz\",\n    \"member_saved\" => \"Üye kaydedildi\",\n    \"member_removed\" => \"Üye kaldırıldı\",\n    // Notifications\n    \"error_saving_notifications\" => \"Bildirim verileri kaydedilirken hata oluştu.\",\n    \"wallos_notification\" => \"Wallos Bildirimi\",\n    \"test_notification\" => \"Bu bir test bildirimidir. Bunu görüyorsanız, yapılandırma doğrudur.\",\n    \"email_error\" => \"E-posta gönderilirken hata oluştu\",\n    \"notification_sent_successfuly\" => \"Bildirim başarıyla gönderildi\",\n    \"notifications_settings_saved\" => \"Bildirim ayarları başarıyla kaydedildi.\",\n    \"notification_failed\" => \"Bildirim gönderilemedi\",\n    // Payments\n    \"payment_in_use\" => \"Kullanımda olan ödeme yöntemi devre dışı bırakılamaz\",\n    \"failed_update_payment\" => \"Ödeme yöntemi veritabanında güncellenemedi\",\n    \"enabled\" => \"etkinleştirildi\",\n    \"disabled\" => \"devre dışı bırakıldı\",\n    // Subscription\n    \"error_fetching_image\" => \"Görüntü alınırken hata oluştu\",\n    \"subscription_updated_successfuly\" => \"Abonelik başarıyla güncellendi\",\n    \"subscription_added_successfuly\" => \"Abonelik başarıyla eklendi\",\n    \"error_deleting_subscription\" => \"Abonelik silinirken hata oluştu.\",\n    \"invalid_request_method\" => \"Geçersiz istek metodu.\",\n    // User\n    \"error_updating_user_data\" => \"Kullanıcı verileri güncellenirken hata oluştu.\",\n    \"user_details_saved\" => \"Kullanıcı detayları kaydedildi\",\n    // Admin Page\n    \"registrations\" => \"Kayıtlar\",\n    \"enable_user_registrations\" => \"Kullanıcı kayıtlarını etkinleştir\",\n    \"maximum_number_users\" => \"Maksimum kullanıcı sayısı\",\n    \"require_email_verification\" => \"E-posta doğrulaması gerektir\",\n    \"configure_smtp_settings_to_enable\" => \"E-posta doğrulamasını etkinleştirmek için SMTP ayarlarını yapılandırın\",\n    \"server_url\" => \"Sunucu URL'si\",\n    \"server_url_info\" => \"E-posta doğrulama ve şifre kurtarma için kullanılır. Geçerli bir genel URL olmalıdır.\",\n    \"server_url_password_reset\" => \"Ayarlanırsa şifre sıfırlama işlevini de etkinleştirir.\",\n    \"disable_login\" => \"Girişi devre dışı bırak\",\n    \"disable_login_info\" => \"Girişi atlayın. Sunucunuzu yalnızca yerel bir ağ üzerinde, harici erişim olmadan çalıştırıyorsanız, oturum açmayı devre dışı bırakabilirsiniz. Bu, yönetici kullanıcıyı otomatik olarak oturum açacaktır.\",\n    \"disable_login_info2\" => \"Bu ayarı yalnızca kullanıcı kaydı kapalıysa ve yönetici kullanıcı hesabından başka kullanıcı yoksa etkinleştirebilirsiniz.\",\n    \"max_users_info\" => \"0 veya boş bırakıldığında sınırsız kullanıcı sayısı\",\n    \"user_management\" => \"Kullanıcı Yönetimi\",\n    \"delete_user\" => \"Kullanıcıyı Sil\",\n    \"delete_user_info\" => \"Bir kullanıcının silinmesi aynı zamanda tüm aboneliklerinin ve ayarlarının da silinmesine neden olur.\",\n    \"create_user\" => \"Kullanıcı Oluştur\",\n    \"oidc_settings\" => \"OpenID Connect Ayarları\",\n    \"oidc_auth_enabled\" => \"OpenID Connect Kimlik Doğrulaması Etkinleştirildi\",\n    \"create_user_automatically\" => \"OpenID Connect ile giriş yapıldığında kullanıcı otomatik olarak oluşturulsun\",\n    \"disable_password_login\" => \"Parola ile giriş devre dışı bırakılsın\",\n    \"smtp_settings\" => \"SMTP Ayarları\",\n    \"smtp_usage_info\" => \"Şifre kurtarma ve diğer sistem e-postaları için kullanılacaktır.\",\n    \"security_settings\" => \"Güvenlik Ayarları\",\n    \"ssrf_protection_info\" => \"Sunucu tarafı istek sahteciliği (SSRF) saldırılarını önlemek için Wallos, varsayılan olarak webhook bildirimlerini özel veya dahili ağ adreslerine engeller.\",\n    \"local_webhook_info\" => \"Webhooks'u yerel hizmetlere (ör. Home Assistant, Gotify veya Node-RED) göndermeniz gerekiyorsa, IP adreslerini veya ana bilgisayar adlarını yukarıya virgülle ayrılmış bir liste olarak girin (ör. <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Bakım Görevleri\",\n    \"orphaned_logos\" => \"Yetim Logolar\",\n    \"update\" => \"Güncelle\",\n    \"new_version_available\" => \"Yeni bir Wallos sürümü mevcut\",\n    \"current_version\" => \"Mevcut Sürüm\",\n    \"latest_version\" => \"En Son Sürüm\",\n    \"on_current_version\" => \"Wallos'un en son sürümünü kullanıyorsunuz.\",\n    \"show_update_notification\" => \"Gösterge panelinde güncelleme bildirimini göster\",\n    \"cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"E-posta doğrulandı\",\n    \"email_verification_failed\" => \"E-posta doğrulaması başarısız oldu\",\n    // Calendar\n    \"calendar\" => \"Takvim\",\n    \"sun\" => \"Paz\",\n    \"mon\" => \"Pzt\",\n    \"tue\" => \"Sal\",\n    \"wed\" => \"Çar\",\n    \"thu\" => \"Per\",\n    \"fri\" => \"Cum\",\n    \"sat\" => \"Cmt\",\n    \"month-01\" => \"Ocak\",\n    \"month-02\" => \"Şubat\",\n    \"month-03\" => \"Mart\",\n    \"month-04\" => \"Nisan\",\n    \"month-05\" => \"Mayıs\",\n    \"month-06\" => \"Haziran\",\n    \"month-07\" => \"Temmuz\",\n    \"month-08\" => \"Ağustos\",\n    \"month-09\" => \"Eylül\",\n    \"month-10\" => \"Ekim\",\n    \"month-11\" => \"Kasım\",\n    \"month-12\" => \"Aralık\",\n    \"total_cost\" => \"Toplam Maliyet\",\n    \"export_icalendar\" => \"iCalendar olarak dışa aktar\",\n    \"over_budget_warning\" => \"Bütçenizi aştınız\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Lütfen TOTP kodunuzu girin\",\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/i18n/uk.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Вам необхідно створити обліковий запис, перш ніж ви зможете увійти в систему\",\n    \"username\" => \"Ім'я користувача\",\n    \"password\" => \"Пароль\",\n    \"email\" => \"E-mail\",\n    \"firstname\" => \"Ім'я\",\n    \"lastname\" => \"Прізвище\",\n    \"confirm_password\" => \"Підтвердьте пароль\",\n    \"main_currency\" => \"Основна валюта\",\n    \"language\" => \"Мова\",\n    \"passwords_dont_match\" => \"Паролі не співпадають\",\n    \"username_exists\" => \"Ім'я користувача вже існує\",\n    \"email_exists\" => \"E-mail вже існує\",\n    \"registration_failed\" => \"Реєстрація не вдалася, спробуйте ще раз.\",\n    \"register\" => \"Реєстрація\",\n    \"restore_database\" => \"Відновити базу даних\",\n    // Login Page\n    \"please_login\" => \"Будь ласка, увійдіть\",\n    \"stay_logged_in\" => \"Залишатися в системі (30 днів)\",\n    \"login\" => \"Авторизуватися\",\n    \"login_with\" => \"Увійти з\",\n    \"or\" => \"або\",\n    \"login_failed\" => \"Дані для входу невірні\",\n    \"registration_successful\" => \"Реєстрація пройшла успішно\",\n    \"user_email_waiting_verification\" => \"Ваша електронна адреса потребує перевірки. Будь ласка, перевірте свою електронну скриньку.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Забули пароль?\",\n    \"reset_password\" => \"Скинути пароль\",\n    \"reset_sent_check_email\" => \"Посилання для скидання паролю надіслано на вашу електронну пошту\",\n    \"password_reset_successful\" => \"Пароль успішно скинуто\",\n    // Header\n    \"profile\" => \"Профіль\",\n    \"dashboard\" => \"Панель\",\n    \"subscriptions\" => \"Підписки\",\n    \"stats\" => \"Статистика\",\n    \"settings\" => \"Налаштування\",\n    \"admin\" => \"Адміністратор\",\n    \"about\" => \"Про програму\",\n    \"logout\" => \"Вийти\",\n    // Dashboard\n    \"hello\" => \"Привіт\",\n    \"upcoming_payments\" => \"Предстоять платежі\",\n    \"no_upcoming_payments\" => \"У вас немає предстоять платежів\",\n    \"overdue_renewals\" => \"Прострочені поновлення\",\n    \"ai_recommendations\" => \"AI рекомендації\",\n    \"your_budget\" => \"Ваш бюджет\",\n    \"budget\" => \"Бюджет\",\n    \"budget_used\" => \"Використаний бюджет\",\n    \"over_budget\" => \"Перевищений бюджет\",\n    \"your_subscriptions\" => \"Ваші підписки\",\n    \"your_savings\" => \"Ваші заощадження\",\n    // Subscriptions page\n    \"subscription\" => \"Підписка\",\n    \"no_subscriptions_yet\" => \"У вас поки немає підписок\",\n    \"add_first_subscription\" => \"Додати першу підписку\",\n    \"new_subscription\" => \"Нова підписка\",\n    \"search\" => \"Пошук\",\n    \"state\" => \"Стан\",\n    \"alphanumeric\" => \"Алфавітний порядок\",\n    \"sort\" => \"Сортування\",\n    \"name\" => \"Ім'я\",\n    \"last_added\" => \"Дата створення\",\n    \"price\" => \"Вартість\",\n    \"next_payment\" => \"Наступний платіж\",\n    \"renewal_type\" => \"Тип продовження\",\n    \"auto_renewal\" => \"Автоматичне продовження\",\n    \"automatically_renews\" => \"Автоматичне продовження\",\n    \"manual_renewal\" => \"Ручне продовження\",\n    \"start_date\" => \"Дата початку\",\n    \"inactive\" => \"Відключити підписку\",\n    \"replaced_with\" => \"Замінена на\",\n    \"none\" => \"Немає\",\n    \"member\" => \"Член сім'ї\",\n    \"category\" => \"Категорія\",\n    \"payment_method\" => \"Спосіб оплати\",\n    \"Daily\" => \"День\",\n    \"Weekly\" => \"Тиждень\",\n    \"Monthly\" => \"Місяць\",\n    \"Yearly\" => \"Рік\",\n    \"daily\" => \"День\",\n    \"weekly\" => \"Тиждень\",\n    \"monthly\" => \"Місяць\",\n    \"yearly\" => \"Рік\",\n    \"days\" => \"днів\",\n    \"weeks\" => \"тижнів\",\n    \"months\" => \"місяців\",\n    \"years\" => \"років\",\n    \"external_url\" => \"Відвідайте зовнішню URL-адресу\",\n    \"empty_page\" => \"Порожня сторінка\",\n    \"clear_filters\" => \"Очистити фільтри\",\n    \"no_matching_subscriptions\" => \"Немає відповідних підписок\",\n    \"clone\" => \"Клонувати\",\n    \"renew\" => \"Продовжити\",\n    \"calculate_next_payment_date\" => \"Розрахувати дату наступного платежу\",\n    // Subscription form\n    \"add_subscription\" => \"Додати підписку\",\n    \"edit_subscription\" => \"Змінити підписку\",\n    \"subscription_name\" => \"Назва підписки\",\n    \"logo_preview\" => \"Попередній перегляд логотипу\",\n    \"search_logo\" => \"Пошук логотипу в Інтернеті\",\n    \"web_search\" => \"Веб-пошук\",\n    \"currency\" => \"Валюта\",\n    \"payment_every\" => \"Оплата кожні\",\n    \"frequency\" => \"Частота\",\n    \"cycle\" => \"Цикл\",\n    \"no_category\" => \"Немає категорії\",\n    \"paid_by\" => \"Оплачує\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Примітки\",\n    \"enable_notifications\" => \"Увімкнути сповіщення для цієї підписки\",\n    \"default_value_from_settings\" => \"Значення за замовчуванням з налаштувань\",\n    \"cancellation_notification\" => \"Сповіщення про скасування\",\n    \"delete\" => \"Видалити\",\n    \"cancel\" => \"Скасувати\",\n    \"upload_logo\" => \"Завантажити логотип\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Ви використовуєте кілька валют у своїх підписках. Для отримання достовірної та точної статистики, будь ласка, встановіть API-ключ Fixer на сторінці налаштувань.\",\n    \"general_statistics\" => \"Загальна статистика\",\n    \"active_subscriptions\" => \"Активні підписки\",\n    \"inactive_subscriptions\" => \"Неактивні підписки\",\n    \"monthly_cost\" => \"Щомісячна вартість\",\n    \"yearly_cost\" => \"Річна вартість\",\n    \"average_monthly\" => \"Середня щомісячна вартість підписки\",\n    \"most_expensive\" => \"Найдорожча вартість підписки\",\n    \"amount_due\" => \"Сума до оплати цього місяця\",\n    \"percentage_budget_used\" => \"Відсоток використання бюджету\",\n    \"budget_remaining\" => \"Залишок бюджету\",\n    \"amount_over_budget\" => \"Сума перевищення бюджету\",\n    \"monthly_savings\" => \"Щомісячна економія (при неактивних підписках)\",\n    \"yearly_savings\" => \"Річна економія (при неактивних підписках)\",\n    \"split_views\" => \"Детальна статистика\",\n    \"category_split\" => \"За категоріями\",\n    \"household_split\" => \"За членами сім'ї\",\n    \"payment_method_split\" => \"За способами оплати\",\n    \"total_cost_trend\" => \"Тенденція загальної вартості\",\n    \"cost_vs_budget\" => \"Вартість у порівнянні з бюджетом\",\n    // About page\n    \"about_and_credits\" => \"Про компанію та авторів\",\n    \"credits\" => \"Подяки\",\n    \"license\" => \"Ліцензія\",\n    \"release_notes\" => \"Примітки до випуску\",\n    \"update_available\" => \"Доступне оновлення\",\n    \"issues_and_requests\" => \"Проблеми та запити\",\n    \"the_author\" => \"Автор\",\n    \"icons\" => \"Значки\",\n    \"payment_icons\" => \"Значки способів оплати\",\n    // Profile page\n    \"upload_avatar\" => \"Завантажити аватар\",\n    \"file_type_error\" => \"Зазначений тип файлу не підтримується.\",\n    \"user_details\" => \"Дані користувача\",\n    \"two_factor_authentication\" => \"Двофакторна автентифікація\",\n    \"two_factor_info\" => \"Двофакторна автентифікація додає додатковий рівень безпеки до вашого облікового запису.<br>Для сканування QR-коду вам знадобиться застосунок-автентифікатор, наприклад Google Authenticator, Authy або Ente Auth.\",\n    \"two_factor_enabled_info\" => \"Ваш обліковий запис захищено за допомогою двофакторної автентифікації. Ви можете вимкнути її, натиснувши на кнопку вище.\",\n    \"enable_two_factor_authentication\" => \"Увімкнути двофакторну аутентифікацію\",\n    \"2fa_already_enabled\" => \"Двофакторна автентифікація вже увімкнена\",\n    \"totp_code_incorrect\" => \"Код TOTP невірний\",\n    \"backup_codes\" => \"Резервні коди\",\n    \"download_backup_codes\" => \"Завантажити резервні коди\",\n    \"copy_to_clipboard\" => \"Скопіювати в буфер обміну\",\n    \"totp_backup_codes_info\" => \"Збережіть ці коди в безпечному місці. Вони можуть бути використані для входу в систему, якщо ви втратите доступ до додатку автентифікації.\",\n    \"disable_two_factor_authentication\" => \"Вимкнути двофакторну автентифікацію\",\n    \"totp_code\" => \"Код TOTP\",\n    \"monthly_budget\" => \"Щомісячний бюджет\",\n    \"api_key\" => \"API ключ\",\n    \"regenerate\" => \"Згенерувати\",\n    \"api_key_info\" => \"API ключ використовується для доступу до ваших даних через API. Не передавайте його третім особам.\",\n    // Settings page\n    \"budget_info\" => \"Якщо ви вкажете бюджет, Wallos буде відображати вашу поточну вартість підписок у порівнянні з вашим бюджетом.\",\n    \"household\" => \"Сім'я\",\n    \"save_member\" => \"Зберегти члена сім'ї\",\n    \"delete_member\" => \"Видалити члена сім'ї\",\n    \"cant_delete_member\" => \"Не можу видалити основного члена сім'ї\",\n    \"cant_delete_member_in_use\" => \"Неможливо видалити члена сім'ї, який використовується в підписці.\",\n    \"household_info\" => \"Поле електронної пошти дозволяє членам сім'ї отримувати сповіщення про закінчення терміну дії підписки.\",\n    \"notifications\" => \"Сповіщення\",\n    \"enable_email_notifications\" => \"Увімкнути сповіщення по електронній пошті\",\n    \"notify_me\" => \"Повідомити мене за\",\n    \"day_before\" => \"день до події\",\n    \"on_due_date\" => \"в день події\",\n    \"days_before\" => \"дня(днів) до події\",\n    \"smtp_address\" => \"SMTP-адреса\",\n    \"port\" => \"Порт\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Ім'я користувача SMTP\",\n    \"smtp_password\" => \"Пароль SMTP\",\n    \"from_email\" => \"Від кого E-Mail (необов'язково)\",\n    \"send_to_other_emails\" => \"Також надсилати сповіщення на наступні адреси електронної пошти (використовуйте ; для їх розділення):\",\n    \"smtp_info\" => \"Пароль SMTP передається і зберігається у вигляді відкритого тексту. З метою безпеки створіть обліковий запис тільки для Wallos.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Токен Telegram-бота\",\n    \"telegram_chat_id\" => \"Telegram Chat ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Pushplus токен\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Ім'я користувача\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Ікона Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"Метод запиту\",\n    \"custom_headers\" => \"Користувацькі заголовки\",\n    \"webhook_payload\" => \"Webhook Payload\",\n    \"payment_notifications_payload\" => \"Payload для сповіщення про платіж\",\n    \"cancelation_notification_payload\" => \"Payload для сповіщення про скасування\",\n    \"variables_available\" => \"Доступні змінні\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Токен\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Ім'я користувача бота Discord\",\n    \"discord_bot_avatar_url\" => \"URL-адреса аватара бота Discord\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Ключ користувача Pushover\",\n    \"host\" => \"Хост\",\n    \"topic\" => \"Тема\",\n    \"ignore_ssl_errors\" => \"Ігнорувати помилки SSL\",\n    \"categories\" => \"Категорії\",\n    \"save_category\" => \"Зберегти категорію\",\n    \"delete_category\" => \"Видалити категорію\",\n    \"cant_delete_category_in_use\" => \"Неможливо видалити категорію, яка використовується в підписці.\",\n    \"currencies\" => \"Валюти\",\n    \"save_currency\" => \"Зберегти валюту\",\n    \"delete_currency\" => \"Видалити валюту\",\n    \"cant_delete_main_currency\" => \"Не можу видалити основну валюту\",\n    \"cant_delete_currency_in_use\" => \"Неможливо видалити валюту, яка використовується в підписці.\",\n    \"exchange_update\" => \"Курси валют востаннє оновлювалися\",\n    \"currency_info\" => \"Знайдіть підтримувані валюти та правильні коди валют на\",\n    \"currency_performance\" => \"Для підвищення продуктивності зберігайте тільки ті валюти, які ви використовуєте.\",\n    \"fixer_api_key\" => \"Ключ Fixer API\",\n    \"provider\" => \"Провайдер\",\n    \"fixer_info\" => \"Якщо ви використовуєте кілька валют і хочете отримати точну статистику та сортування підписок, вам необхідний БЕЗКОШТОВНИЙ ключ API від Fixer.\",\n    \"get_key\" => \"Отримайте ключ за адресою\",\n    \"get_free_fixer_api_key\" => \"Отримайте безкоштовний ключ API Fixer\",\n    \"get_key_alternative\" => \"Крім того, ви можете отримати безкоштовний ключ API Fixer на сайті\",\n    \"ai_model\" => \"AI Модель\",\n    \"select_ai_model\" => \"Виберіть AI Модель\",\n    \"run_schedule\" => \"Запустити Розклад\",\n    \"manually\" => \"Вручну\",\n    \"coming_soon\" => \"Незабаром\",\n    \"invalid_host\" => \"Неправильний Хост\",\n    \"ai_recommendations_info\" => \"AI рекомендації створюються на основі ваших підписок та членів домогосподарства.\",\n    \"may_take_time\" => \"В залежності від постачальника, моделі та кількості підписок, створення рекомендацій може зайняти деякий час.\",\n    \"recommendations_visible_on_dashboard\" => \"Рекомендації будуть видимі на панелі приладів.\",\n    \"generate_recommendations\" => \"Створити рекомендації\",\n    \"display_settings\" => \"Налаштування відображення\",\n    \"theme_settings\" => \"Налаштування теми\",\n    \"colors\" => \"Кольори\",\n    \"custom_colors\" => \"Користувацькі кольори\",\n    \"theme\" => \"Тема\",\n    \"dark_theme\" => \"Темна тема\",\n    \"light_theme\" => \"Світла тема\",\n    \"automatic\" => \"Автоматично\",\n    \"main_color\" => \"Основний колір\",\n    \"accent_color\" => \"Акцентний колір\",\n    \"hover_color\" => \"Колір при наведенні\",\n    \"save_custom_colors\" => \"Зберегти користувацькі кольори\",\n    \"reset_custom_colors\" => \"Скинути користувацькі кольори\",\n    \"custom_css\" => \"Користувацький CSS\",\n    \"save_custom_css\" => \"Зберегти користувацький CSS\",\n    \"calculate_monthly_price\" => \"Розрахувати та показати щомісячну ціну для всіх підписок\",\n    \"convert_prices\" => \"Завжди конвертувати та показувати ціни в моїй основній валюті (повільніше)\",\n    \"show_original_price\" => \"Також показувати оригінальну ціну при виконанні конверсій або розрахунків\",\n    \"experience\" => \"Досвід\",\n    \"show_subscription_progress\" => \"Показати прогрес підписки\",\n    \"disabled_subscriptions\" => \"Відключені підписки\",\n    \"hide_disabled_subscriptions\" => \"Приховати відключені підписки\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Показати відключені підписки внизу списку\",\n    \"experimental_settings\" => \"Експериментальні налаштування\",\n    \"remove_background\" => \"Спроба видалити фон логотипів із пошуку зображень.\",\n    \"use_mobile_navigation_bar\" => \"Використовувати мобільну панель навігації\",\n    \"experimental_info\" => \"Експериментальні налаштування, ймовірно, не будуть працювати ідеально.\",\n    \"payment_methods\" => \"Способи оплати\",\n    \"payment_methods_info\" => \"Натисніть на спосіб оплати, щоб відключити/включити його.\",\n    \"rename_payment_methods_info\" => \"Натисніть на назву способу оплати, щоб перейменувати його.\",\n    \"cant_delete_payment_method_in use\" => \"Неможливо відключити використовуваний спосіб оплати\",\n    \"add_custom_payment\" => \"Додати власний спосіб оплати\",\n    \"payment_method_name\" => \"Назва способу оплати\",\n    \"payment_method_added_successfuly\" => \"Спосіб оплати успішно додано\",\n    \"payment_method_removed\" => \"Спосіб оплати видалено.\",\n    \"disable\" => \"Відключити\",\n    \"enable\" => \"Увімкнути\",\n    \"rename_payment_method\" => \"Перейменувати спосіб оплати\",\n    \"payment_renamed\" => \"Спосіб оплати перейменовано\",\n    \"payment_not_renamed\" => \"Спосіб оплати не перейменовано\",\n    \"test\" => \"Тест\",\n    \"add\" => \"Додати\",\n    \"save\" => \"Зберегти\",\n    \"reset\" => \"Перезавантажити\",\n    \"main_accent_color_error\" => \"Основний і акцентний колір не можуть бути однаковими.\",\n    \"backup_and_restore\" => \"Резервне копіювання та відновлення\",\n    \"backup\" => \"Резервне копіювання\",\n    \"restore\" => \"Відновлення\",\n    \"restore_info\" => \"Відновлення бази даних скасує всі поточні дані. Після відновлення ви вийдете з системи.\",\n    \"account\" => \"Обліковий запис\",\n    \"export_subscriptions\" => \"Експорт підписок\",\n    \"export_as_json\" => \"Експорт у JSON\",\n    \"export_as_csv\" => \"Експорт у CSV\",\n    \"danger_zone\" => \"Небезпечна зона\",\n    \"delete_account\" => \"Видалити обліковий запис\",\n    \"delete_account_info\" => \"При видаленні облікового запису також будуть видалені всі ваші підписки та налаштування.\",\n    // Filters menu\n    \"filter\" => \"Фільтр\",\n    \"clear\" => \"Очистити\",\n    // Toast\n    \"success\" => \"Успішно\",\n    // Endpoint responses\n    \"session_expired\" => \"Термін дії вашої сесії закінчився. Будь ласка, увійдіть знову\",\n    \"fields_missing\" => \"Деякі поля відсутні\",\n    \"fill_all_fields\" => \"Будь ласка, заповніть усі поля\",\n    \"fill_mandatory_fields\" => \"Будь ласка, заповніть усі обов'язкові поля\",\n    \"error\" => \"Помилка\",\n    // Category\n    \"failed_add_category\" => \"Не вдалося додати категорію\",\n    \"failed_edit_category\" => \"Не вдалося змінити категорію\",\n    \"category_in_use\" => \"Категорія використовується в підписках і не може бути видалена.\",\n    \"failed_remove_category\" => \"Не вдалося видалити категорію\",\n    \"category_saved\" => \"Категорія збережена\",\n    \"category_removed\" => \"Категорія видалена\",\n    \"sort_order_saved\" => \"Порядок сортування збережено.\",\n    // Currency\n    \"currency_saved\" => \"збережено.\",\n    \"error_adding_currency\" => \"Помилка додавання валюти.\",\n    \"failed_to_store_currency\" => \"Не вдалося зберегти валюту в базі даних.\",\n    \"currency_in_use\" => \"Валюта використовується в підписках і не може бути видалена.\",\n    \"currency_is_main\" => \"Валюта встановлена ​​як основна і не може бути видалена.\",\n    \"failed_to_remove_currency\" => \"Не вдалося видалити валюту з бази даних.\",\n    \"failed_to_store_api_key\" => \"Не вдалося зберегти ключ API в базі даних.\",\n    \"invalid_api_key\" => \"Невірний ключ API.\",\n    \"api_key_saved\" => \"Ключ API успішно збережено\",\n    \"currency_removed\" => \"Валюта видалена\",\n    // Household\n    \"failed_add_household\" => \"Не вдалося додати члена сім'ї.\",\n    \"failed_edit_household\" => \"Не вдалося змінити члена сім'ї.\",\n    \"failed_remove_household\" => \"Не вдалося видалити члена сім'ї.\",\n    \"household_in_use\" => \"Член сім'ї використовується в підписках і не може бути видалений.\",\n    \"member_saved\" => \"Член сім'ї збережений\",\n    \"member_removed\" => \"Член сім'ї видалений\",\n    // Notifications\n    \"error_saving_notifications\" => \"Помилка збереження даних сповіщень.\",\n    \"wallos_notification\" => \"Сповіщення від Wallos\",\n    \"test_notification\" => \"Це тестове сповіщення. Якщо ви бачите це, значить, конфігурація правильна.\",\n    \"email_error\" => \"Помилка надсилання електронної пошти\",\n    \"notification_sent_successfuly\" => \"Сповіщення успішно надіслано\",\n    \"notifications_settings_saved\" => \"Налаштування сповіщень успішно збережено.\",\n    \"notification_failed\" => \"Сповіщення не вдалося\",\n    // Payments\n    \"payment_in_use\" => \"Неможливо відключити використовуваний спосіб оплати\",\n    \"failed_update_payment\" => \"Не вдалося оновити спосіб оплати в базі даних.\",\n    \"enabled\" => \"увімкнено\",\n    \"disabled\" => \"відключено\",\n    // Subscription\n    \"error_fetching_image\" => \"Помилка завантаження зображення.\",\n    \"subscription_updated_successfuly\" => \"Підписка успішно оновлена\",\n    \"subscription_added_successfuly\" => \"Підписка успішно додана\",\n    \"error_deleting_subscription\" => \"Помилка видалення підписки.\",\n    \"invalid_request_method\" => \"Невірний метод запиту.\",\n    // User\n    \"error_updating_user_data\" => \"Помилка оновлення даних користувача.\",\n    \"user_details_saved\" => \"Дані користувача збережено.\",\n    // Admin Page\n    \"registrations\" => \"Реєстрації\",\n    \"enable_user_registrations\" => \"Увімкнути реєстрацію користувачів\",\n    \"maximum_number_users\" => \"Максимальна кількість користувачів\",\n    \"require_email_verification\" => \"Вимагати підтвердження електронною поштою\",\n    \"configure_smtp_settings_to_enable\" => \"Налаштуйте SMTP, щоб увімкнути цю функцію.\",\n    \"server_url\" => \"URL-адреса сервера\",\n    \"server_url_info\" => \"Використовується для перевірки електронної пошти та відновлення пароля. Повинен бути дійсним публічним URL.\",\n    \"server_url_password_reset\" => \"Якщо цей параметр встановлено, він також увімкне функцію скидання пароля.\",\n    \"disable_login\" => \"Відключити вхід\",\n    \"disable_login_info\" => \"Обхід входу в систему. Якщо ви використовуєте свій сервер тільки в локальній мережі, без доступу ззовні, ви можете відключити вхід в систему. При цьому буде автоматично входити користувач admin.\",\n    \"disable_login_info2\" => \"Цей параметр можна увімкнути тільки в тому випадку, якщо реєстрація користувачів відключена і їх кількість не перевищує обліковий запис адміністратора.\",\n    \"max_users_info\" => \"Встановіть 0 для необмеженої кількості користувачів.\",\n    \"user_management\" => \"Управління користувачами\",\n    \"delete_user\" => \"Видалити користувача\",\n    \"delete_user_info\" => \"Видалення користувача також призведе до видалення всіх його підписок та налаштувань.\",\n    \"create_user\" => \"Створити користувача\",\n    \"smtp_settings\" => \"Налаштування SMTP\",\n    \"oidc_settings\" => \"Налаштування OIDC\",\n    \"oidc_auth_enabled\" => \"Увімкнути OIDC автентифікацію\",\n    \"create_user_automatically\" => \"Автоматично створювати користувача при вході\",\n    \"disable_password_login\" => \"Відключити вхід за паролем\",\n    \"smtp_usage_info\" => \"Буде використовуватися для відновлення пароля та інших системних листів.\",\n    \"security_settings\" => \"Налаштування безпеки\",\n    \"ssrf_protection_info\" => \"Щоб запобігти атакам Server-Side Request Forgery (SSRF), Wallos за замовчуванням блокує вебхуки до приватних або внутрішніх мережевих адрес.\",\n    \"local_webhook_info\" => \"Якщо вам потрібно надсилати вебхуки до локальних сервісів (наприклад, Home Assistant, Gotify або Node-RED), введіть їх IP-адреси або імена хостів вище як список, розділений комами (наприклад <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Завдання обслуговування\",\n    \"orphaned_logos\" => \"Втрачений логотип\",\n    \"update\" => \"Оновити\",\n    \"new_version_available\" => \"Доступна нова версія Wallos\",\n    \"current_version\" => \"Поточна версія\",\n    \"latest_version\" => \"Остання версія\",\n    \"on_current_version\" => \"Ви використовуєте останню версію Wallos.\",\n    \"show_update_notification\" => \"Показувати сповіщення про оновлення на дашборді\",\n    \"cronjobs\" => \"Cronjobs\",\n    // Email Verification\n    \"email_verified\" => \"Вашу електронну пошту підтверджено. Тепер ви можете увійти.\",\n    \"email_verification_failed\" => \"Не вдалося підтвердити вашу електронну пошту.\",\n    // Calendar\n    \"calendar\" => \"Календар\",\n    \"sun\" => \"Нд\",\n    \"mon\" => \"Пн\",\n    \"tue\" => \"Вт\",\n    \"wed\" => \"Ср\",\n    \"thu\" => \"Чт\",\n    \"fri\" => \"Пт\",\n    \"sat\" => \"Сб\",\n    \"month-01\" => \"Січень\",\n    \"month-02\" => \"Лютий\",\n    \"month-03\" => \"Березень\",\n    \"month-04\" => \"Квітень\",\n    \"month-05\" => \"Травень\",\n    \"month-06\" => \"Червень\",\n    \"month-07\" => \"Липень\",\n    \"month-08\" => \"Серпень\",\n    \"month-09\" => \"Вересень\",\n    \"month-10\" => \"Жовтень\",\n    \"month-11\" => \"Листопад\",\n    \"month-12\" => \"Грудень\",\n    \"total_cost\" => \"Загальна вартість\",\n    \"export_icalendar\" => \"Експорт у iCalendar\",\n    \"over_budget_warning\" => \"Ви перевищили бюджет\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Введіть код TOTP\",\n\n];\n\n?>\n"
  },
  {
    "path": "includes/i18n/vi.php",
    "content": "<?php\n\n$i18n = [\n    // Registration page\n    \"create_account\" => \"Bạn cần tạo tài khoản trước khi có thể đăng nhập\",\n    \"username\" => \"Tên người dùng\",\n    \"password\" => \"Mật khẩu\",\n    \"email\" => \"Email\",\n    \"firstname\" => \"Tên\",\n    \"lastname\" => \"Họ\",\n    \"confirm_password\" => \"Xác nhận mật khẩu\",\n    \"main_currency\" => \"Tiền tệ chính\",\n    \"language\" => \"Ngôn ngữ\",\n    \"passwords_dont_match\" => \"Mật khẩu không khớp\",\n    \"username_exists\" => \"Tên người dùng đã tồn tại\",\n    \"email_exists\" => \"Email đã tồn tại\",\n    \"registration_failed\" => \"Đăng ký thất bại, vui lòng thử lại.\",\n    \"register\" => \"Đăng ký\",\n    \"restore_database\" => \"Khôi phục cơ sở dữ liệu\",\n    // Login Page\n    \"please_login\" => \"Vui lòng đăng nhập\",\n    \"stay_logged_in\" => \"Giữ đăng nhập (30 ngày)\",\n    \"login\" => \"Đăng nhập\",\n    \"login_with\" => \"Đăng nhập với\",\n    \"or\" => \"hoặc\",\n    \"login_failed\" => \"Thông tin đăng nhập không chính xác\",\n    \"registration_successful\" => \"Đăng ký thành công\",\n    \"user_email_waiting_verification\" => \"Email của bạn cần được xác minh. Vui lòng kiểm tra email.\",\n    // Password Reset Page\n    \"forgot_password\" => \"Quên mật khẩu\",\n    \"reset_password\" => \"Đặt lại mật khẩu\",\n    \"reset_sent_check_email\" => \"Email đặt lại đã được gửi. Vui lòng kiểm tra email.\",\n    \"password_reset_successful\" => \"Đặt lại mật khẩu thành công\",\n    // Header\n    \"profile\" => \"Hồ sơ\",\n    \"dashboard\" => \"Bảng\",\n    \"subscriptions\" => \"Đăng ký\",\n    \"stats\" => \"Thống kê\",\n    \"settings\" => \"Cài đặt\",\n    \"admin\" => \"Quản trị viên\",\n    \"about\" => \"Giới thiệu\",\n    \"logout\" => \"Đăng xuất\",\n    // Dashboard\n    \"hello\" => \"Xin chào\",\n    \"upcoming_payments\" => \"Các khoản thanh toán sắp tới\",\n    \"no_upcoming_payments\" => \"Bạn không có khoản thanh toán nào sắp tới\",\n    \"overdue_renewals\" => \"Gia hạn quá hạn\",\n    \"ai_recommendations\" => \"Khuyến nghị AI\",\n    \"your_budget\" => \"Ngân sách của bạn\",\n    \"budget\" => \"Ngân sách\",\n    \"budget_used\" => \"Ngân sách đã sử dụng\",\n    \"over_budget\" => \"Vượt ngân sách\",\n    \"your_subscriptions\" => \"Đăng ký của bạn\",\n    \"your_savings\" => \"Tiết kiệm của bạn\",\n    // Subscriptions page\n    \"subscription\" => \"Đăng ký\",\n    \"no_subscriptions_yet\" => \"Bạn chưa có đăng ký nào\",\n    \"add_first_subscription\" => \"Thêm đăng ký đầu tiên\",\n    \"new_subscription\" => \"Đăng ký mới\",\n    \"search\" => \"Tìm kiếm\",\n    \"state\" => \"Trạng thái\",\n    \"alphanumeric\" => \"Chữ và số\",\n    \"sort\" => \"Sắp xếp\",\n    \"name\" => \"Tên\",\n    \"last_added\" => \"Thêm gần đây\",\n    \"price\" => \"Giá\",\n    \"next_payment\" => \"Thanh toán tiếp theo\",\n    \"renewal_type\" => \"Loại gia hạn\",\n    \"auto_renewal\" => \"Tự động gia hạn\",\n    \"automatically_renews\" => \"Tự động gia hạn\",\n    \"manual_renewal\" => \"Gia hạn thủ công\",\n    \"start_date\" => \"Ngày bắt đầu\",\n    \"inactive\" => \"Vô hiệu hóa đăng ký\",\n    \"replaced_with\" => \"Thay thế bằng\",\n    \"none\" => \"Không\",\n    \"member\" => \"Thành viên\",\n    \"category\" => \"Danh mục\",\n    \"payment_method\" => \"Phương thức thanh toán\",\n    \"Daily\" => \"Hàng ngày\",\n    \"Weekly\" => \"Hàng tuần\",\n    \"Monthly\" => \"Hàng tháng\",\n    \"Yearly\" => \"Hàng năm\",\n    \"daily\" => \"Ngày\",\n    \"weekly\" => \"Tuần\",\n    \"monthly\" => \"Tháng\",\n    \"yearly\" => \"Năm\",\n    \"days\" => \"ngày\",\n    \"weeks\" => \"tuần\",\n    \"months\" => \"tháng\",\n    \"years\" => \"năm\",\n    \"external_url\" => \"Truy cập URL ngoài\",\n    \"empty_page\" => \"Trang trống\",\n    \"clear_filters\" => \"Xóa bộ lọc\",\n    \"no_matching_subscriptions\" => \"Không có đăng ký phù hợp\",\n    \"clone\" => \"Nhân bản\",\n    \"renew\" => \"Gia hạn\",\n    \"calculate_next_payment_date\" => \"Tính toán ngày thanh toán tiếp theo\",\n    // Subscription form\n    \"add_subscription\" => \"Thêm đăng ký\",\n    \"edit_subscription\" => \"Chỉnh sửa đăng ký\",\n    \"subscription_name\" => \"Tên đăng ký\",\n    \"logo_preview\" => \"Xem trước logo\",\n    \"search_logo\" => \"Tìm logo trên web\",\n    \"web_search\" => \"Tìm kiếm web\",\n    \"currency\" => \"Tiền tệ\",\n    \"payment_every\" => \"Thanh toán mỗi\",\n    \"frequency\" => \"Tần suất\",\n    \"cycle\" => \"Chu kỳ\",\n    \"no_category\" => \"Không có danh mục\",\n    \"paid_by\" => \"Người thanh toán\",\n    \"url\" => \"URL\",\n    \"notes\" => \"Ghi chú\",\n    \"enable_notifications\" => \"Bật thông báo cho đăng ký này\",\n    \"default_value_from_settings\" => \"Giá trị mặc định từ cài đặt\",\n    \"cancellation_notification\" => \"Thông báo hủy\",\n    \"delete\" => \"Xóa\",\n    \"cancel\" => \"Hủy\",\n    \"upload_logo\" => \"Tải logo\",\n    // Statistics page\n    \"cant_convert_currency\" => \"Bạn đang sử dụng nhiều loại tiền tệ trên các đăng ký của mình. Để có thống kê hợp lệ và chính xác, vui lòng đặt một API Key Fixer trên trang cài đặt.\",\n    \"general_statistics\" => \"Thống kê chung\",\n    \"active_subscriptions\" => \"Đăng ký hoạt động\",\n    \"inactive_subscriptions\" => \"Đăng ký không hoạt động\",\n    \"monthly_cost\" => \"Chi phí hàng tháng\",\n    \"yearly_cost\" => \"Chi phí hàng năm\",\n    \"average_monthly\" => \"Chi phí đăng ký trung bình hàng tháng\",\n    \"most_expensive\" => \"Chi phí đăng ký đắt nhất\",\n    \"amount_due\" => \"Số tiền phải trả tháng này\",\n    \"percentage_budget_used\" => \"Phần trăm ngân sách đã sử dụng\",\n    \"budget_remaining\" => \"Ngân sách còn lại\",\n    \"amount_over_budget\" => \"Số tiền vượt ngân sách\",\n    \"monthly_savings\" => \"Tiết kiệm hàng tháng (trên các đăng ký không hoạt động)\",\n    \"yearly_savings\" => \"Tiết kiệm hàng năm (trên các đăng ký không hoạt động)\",\n    \"split_views\" => \"Chia tách lượt xem\",\n    \"category_split\" => \"Phân chia theo danh mục\",\n    \"household_split\" => \"Phân chia theo hộ gia đình\",\n    \"payment_method_split\" => \"Phân chia theo phương thức thanh toán\",\n    \"total_cost_trend\" => \"Xu hướng chi phí tổng cộng\",\n    \"cost_vs_budget\" => \"Chi phí so với ngân sách\",\n    //  About page\n    \"about_and_credits\" => \"Giới thiệu và cảm ơn\",\n    \"credits\" => \"Cảm ơn\",\n    \"license\" => \"Giấy phép\",\n    \"release_notes\" => \"Ghi chú phát hành\",\n    \"update_available\" => \"Cập nhật có sẵn\",\n    \"issues_and_requests\" => \"Vấn đề và yêu cầu\",\n    \"the_author\" => \"Tác giả\",\n    \"icons\" => \"Biểu tượng\",\n    \"payment_icons\" => \"Biểu tượng thanh toán\",\n    //Profile page\n    \"upload_avatar\" => \"Tải ảnh đại diện\",\n    \"file_type_error\" => \"Loại tệp không được hỗ trợ.\",\n    \"user_details\" => \"Chi tiết người dùng\",\n    \"two_factor_authentication\" => \"Xác thực hai yếu tố\",\n    \"two_factor_info\" => \"Xác thực hai yếu tố thêm một lớp bảo mật cho tài khoản của bạn.<br>Bạn sẽ cần một ứng dụng xác thực như Google Authenticator, Authy hoặc Ente Auth để quét mã QR.\",\n    \"two_factor_enabled_info\" => \"Tài khoản của bạn được bảo vệ với xác thực hai yếu tố. Bạn có thể vô hiệu hóa bằng cách nhấp vào nút phía trên.\",\n    \"enable_two_factor_authentication\" => \"Bật xác thực hai yếu tố\",\n    \"2fa_already_enabled\" => \"Xác thực hai yếu tố đã được bật\",\n    \"totp_code_incorrect\" => \"Mã TOTP không chính xác\",\n    \"backup_codes\" => \"Mã sao lưu\",\n    \"download_backup_codes\" => \"Tải xuống mã sao lưu\",\n    \"copy_to_clipboard\" => \"Sao chép vào bảng tạm\",\n    \"totp_backup_codes_info\" => \"Những mã này có thể được sử dụng để đăng nhập nếu bạn mất quyền truy cập vào ứng dụng xác thực của mình.\",\n    \"disable_two_factor_authentication\" => \"Vô hiệu hóa xác thực hai yếu tố\",\n    \"totp_code\" => \"Mã TOTP\",\n    \"api_key\" => \"API Key\",\n    \"regenerate\" => \"Tạo lại\",\n    \"api_key_info\" => \"API Key được sử dụng để truy cập API. Giữ nó bí mật.\",\n    // Settings page\n    \"monthly_budget\" => \"Ngân sách hàng tháng\",\n    \"budget_info\" => \"Ngân sách hàng tháng được sử dụng để tính toán thống kê\",\n    \"household\" => \"Hộ gia đình\",\n    \"save_member\" => \"Lưu thành viên\",\n    \"delete_member\" => \"Xóa thành viên\",\n    \"cant_delete_member\" => \"Không thể xóa thành viên chính\",\n    \"cant_delete_member_in_use\" => \"Không thể xóa thành viên đang được sử dụng trong đăng ký\",\n    \"household_info\" => \"Trường Email cho phép thành viên trong hộ gia đình được thông báo về các đăng ký sắp hết hạn.\",\n    \"notifications\" => \"Thông báo\",\n    \"enable_email_notifications\" => \"Bật thông báo qua email\",\n    \"notify_me\" => \"Thông báo cho tôi\",\n    \"day_before\" => \"ngày trước\",\n    \"on_due_date\" => \"Vào ngày đáo hạn\",\n    \"days_before\" => \"ngày trước\",\n    \"smtp_address\" => \"Địa chỉ SMTP\",\n    \"port\" => \"Cổng\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"Tên người dùng SMTP\",\n    \"smtp_password\" => \"Mật khẩu SMTP\",\n    \"from_email\" => \"Email gửi (Tùy chọn)\",\n    \"send_to_other_emails\" => \"Cũng gửi thông báo đến các địa chỉ email sau (sử dụng ; để phân tách chúng):\",\n    \"other_emails_placeholder\" => \"user@domain.com;test@user.com\",\n    \"smtp_info\" => \"Mật khẩu SMTP được truyền và lưu trữ dưới dạng văn bản thuần túy. Vì lý do bảo mật, vui lòng tạo tài khoản chỉ dành cho việc này.\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Mã thông báo Bot Telegram\",\n    \"telegram_chat_id\" => \"ID cuộc trò chuyện Telegram\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"Mã thông báo Pushplus\",\n    \"serverchan\" => \"Serverchan\",\n    \"serverchan_sendkey\" => \"Serverchan SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot Tên người dùng\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot Biểu tượng Emoji\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"URL Webhook\",\n    \"request_method\" => \"Phương thức yêu cầu\",\n    \"custom_headers\" => \"Tiêu đề tùy chỉnh\",\n    \"webhook_payload\" => \"Payload Webhook\",\n    \"payment_notifications_payload\" => \"Payload thông báo thanh toán\",\n    \"cancelation_notification_payload\" => \"Payload thông báo hủy\",\n    \"variables_available\" => \"Các biến khả dụng\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"Mã thông báo\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Tên người dùng Bot Discord\",\n    \"discord_bot_avatar_url\" => \"URL ảnh đại diện Bot Discord\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Khóa người dùng Pushover\",\n    \"host\" => \"Host\",\n    \"topic\" => \"Chủ đề\",\n    \"ignore_ssl_errors\" => \"Bỏ qua lỗi SSL\",\n    \"categories\" => \"Danh mục\",\n    \"save_category\" => \"Lưu danh mục\",\n    \"delete_category\" => \"Xóa danh mục\",\n    \"cant_delete_category_in_use\" => \"Không thể xóa danh mục đang sử dụng trong đăng ký\",\n    \"currencies\" => \"Tiền tệ\",\n    \"save_currency\" => \"Lưu tiền tệ\",\n    \"delete_currency\" => \"Xóa tiền tệ\",\n    \"cant_delete_main_currency\" => \"Không thể xóa tiền tệ chính\",\n    \"cant_delete_currency_in_use\" => \"Không thể xóa tiền tệ đang sử dụng trong đăng ký\",\n    \"exchange_update\" => \"Tỷ giá được cập nhật lần cuối vào\",\n    \"currency_info\" => \"Tìm các loại tiền tệ được hỗ trợ và mã tiền tệ chính xác tại\",\n    \"currency_performance\" => \"Để có hiệu suất tốt hơn, chỉ giữ lại các loại tiền tệ bạn sử dụng.\",\n    \"fixer_api_key\" => \"API Key Fixer\",\n    \"provider\" => \"Nhà cung cấp\",\n    \"fixer_info\" => \"Nếu bạn sử dụng nhiều loại tiền tệ và muốn có thống kê và sắp xếp chính xác về đăng ký, một API Key miễn phí từ Fixer là cần thiết.\",\n    \"get_key\" => \"Nhận khóa của bạn tại\",\n    \"get_free_fixer_api_key\" => \"Nhận API Key Fixer miễn phí\",\n    \"get_key_alternative\" => \"Ngoài ra, bạn có thể nhận API Key Fixer miễn phí từ\",\n    \"ai_model\" => \"Mô hình AI\",\n    \"select_ai_model\" => \"Chọn Mô hình AI\",\n    \"run_schedule\" => \"Chạy Lịch Trình\",\n    \"manually\" => \"Thủ Công\",\n    \"coming_soon\" => \"Sắp Có\",\n    \"invalid_host\" => \"Máy Chủ Không Hợp Lệ\",\n    \"ai_recommendations_info\" => \"Các đề xuất AI được tạo dựa trên các đăng ký và thành viên hộ gia đình của bạn.\",\n    \"may_take_time\" => \"Tùy thuộc vào nhà cung cấp, mô hình và số lượng đăng ký, việc tạo đề xuất có thể mất một khoảng thời gian.\",\n    \"recommendations_visible_on_dashboard\" => \"Các đề xuất sẽ hiển thị trên bảng điều khiển.\",\n    \"generate_recommendations\" => \"Tạo Đề Xuất\",\n    \"display_settings\" => \"Cài đặt hiển thị\",\n    \"theme_settings\" => \"Cài đặt giao diện\",\n    \"colors\" => \"Màu sắc\",\n    \"custom_colors\" => \"Màu sắc tùy chỉnh\",\n    \"theme\" => \"Giao diện\",\n    \"dark_theme\" => \"Giao diện tối\",\n    \"light_theme\" => \"Giao diện sáng\",\n    \"automatic\"=> \"Tự động\",\n    \"main_color\" => \"Màu chính\",\n    \"accent_color\" => \"Màu nhấn\",\n    \"hover_color\" => \"Màu khi rê chuột\",\n    \"save_custom_colors\" => \"Lưu màu tùy chỉnh\",\n    \"reset_custom_colors\" => \"Đặt lại màu tùy chỉnh\",\n    \"custom_css\" => \"CSS tùy chỉnh\",\n    \"save_custom_css\" => \"Lưu CSS tùy chỉnh\",\n    \"calculate_monthly_price\" => \"Tính toán và hiển thị giá hàng tháng cho tất cả các đăng ký\",\n    \"convert_prices\" => \"Luôn chuyển đổi và hiển thị giá theo tiền tệ chính của tôi (chậm hơn)\",\n    \"show_original_price\" => \"Cũng hiển thị giá gốc khi thực hiện chuyển đổi hoặc tính toán\",\n    \"experience\" => \"Trải nghiệm\",\n    \"show_subscription_progress\" => \"Hiển thị tiến độ đăng ký\",\n    \"disabled_subscriptions\" => \"Các đăng ký đã vô hiệu hóa\",\n    \"hide_disabled_subscriptions\" => \"Ẩn các đăng ký đã vô hiệu hóa\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"Hiển thị các đăng ký đã vô hiệu hóa ở cuối\",\n    \"experimental_settings\" => \"Cài đặt thử nghiệm\",\n    \"remove_background\" => \"Cố gắng loại bỏ nền của logo từ tìm kiếm hình ảnh\",\n    \"use_mobile_navigation_bar\" => \"Sử dụng thanh điều hướng di động\",\n    \"experimental_info\" => \"Cài đặt thử nghiệm có thể không hoạt động hoàn hảo.\",\n    \"payment_methods\" => \"Phương thức thanh toán\",\n    \"payment_methods_info\" => \"Nhấp vào phương thức thanh toán để vô hiệu hóa / bật nó.\",\n    \"rename_payment_methods_info\" => \"Nhấp vào tên của một phương thức thanh toán để đổi tên nó.\",\n    \"cant_delete_payment_method_in_use\" => \"Không thể vô hiệu hóa phương thức thanh toán đang sử dụng\",\n    \"add_custom_payment\" => \"Thêm phương thức thanh toán tùy chỉnh\",\n    \"payment_method_name\" => \"Tên phương thức thanh toán\",\n    \"payment_method_added_successfuly\" => \"Phương thức thanh toán được thêm thành công\",\n    \"payment_method_removed\" => \"Phương thức thanh toán đã bị xóa\",\n    \"disable\" => \"Vô hiệu hóa\",\n    \"enable\" => \"Kích hoạt\",\n    \"rename_payment_method\" => \"Đổi tên phương thức thanh toán\",\n    \"payment_renamed\" => \"Phương thức thanh toán đã được đổi tên\",\n    \"payment_not_renamed\" => \"Không thể đổi tên phương thức thanh toán\",\n    \"test\" => \"Kiểm tra\",\n    \"add\" => \"Thêm\",\n    \"save\" => \"Lưu\",\n    \"reset\" => \"Đặt lại\",\n    \"main_accent_color_error\" => \"Màu chính và màu nhấn không thể giống nhau\",\n    \"backup_and_restore\" => \"Sao lưu và Khôi phục\",\n    \"backup\" => \"Sao lưu\",\n    \"restore\" => \"Khôi phục\",\n    \"restore_info\" => \"Khôi phục cơ sở dữ liệu sẽ ghi đè tất cả dữ liệu hiện tại. Bạn sẽ bị đăng xuất sau khi khôi phục.\",\n    \"account\" => \"Tài khoản\",\n    \"export_subscriptions\" => \"Xuất đăng ký\",\n    \"export_as_json\" => \"Xuất dưới dạng JSON\",\n    \"export_as_csv\" => \"Xuất dưới dạng CSV\",\n    \"danger_zone\" => \"Vùng nguy hiểm\",\n    \"delete_account\" => \"Xóa tài khoản\",\n    \"delete_account_info\" => \"Xóa tài khoản của bạn sẽ xóa tất cả các đăng ký và cài đặt của bạn.\",\n    // Filters menu\n    \"filter\" => \"Bộ lọc\",\n    \"clear\" => \"Xóa\",\n    // Toast\n    \"success\" => \"Thành công\",\n    // Endpoint responses\n    \"session_expired\" => \"Phiên của bạn đã hết hạn. Vui lòng đăng nhập lại\",\n    \"fields_missing\" => \"Một số trường bị thiếu\",\n    \"fill_all_fields\" => \"Vui lòng điền vào tất cả các trường\",\n    \"fill_mandatory_fields\" => \"Vui lòng điền vào tất cả các trường bắt buộc\",\n    \"error\" => \"Lỗi\",\n    // Category\n    \"failed_add_category\" => \"Thêm danh mục thất bại\",\n    \"failed_edit_category\" => \"Chỉnh sửa danh mục thất bại\",\n    \"category_in_use\" => \"Danh mục đang được sử dụng trong đăng ký và không thể bị xóa\",\n    \"failed_remove_category\" => \"Xóa danh mục thất bại\",\n    \"category_saved\" => \"Danh mục đã được lưu\",\n    \"category_removed\" => \"Danh mục đã bị xóa\",\n    \"sort_order_saved\" => \"Thứ tự sắp xếp đã được lưu\",\n    // Currency\n    \"currency_saved\" => \"đã được lưu.\",\n    \"error_adding_currency\" => \"Lỗi khi thêm mục tiền tệ.\",\n    \"failed_to_store_currency\" => \"Không thể lưu tiền tệ vào cơ sở dữ liệu.\",\n    \"currency_in_use\" => \"Tiền tệ đang được sử dụng trong các đăng ký và không thể bị xóa.\",\n    \"currency_is_main\" => \"Tiền tệ được đặt làm tiền tệ chính và không thể bị xóa.\",\n    \"failed_to_remove_currency\" => \"Không thể xóa tiền tệ khỏi cơ sở dữ liệu.\",\n    \"failed_to_store_api_key\" => \"Không thể lưu API Key vào cơ sở dữ liệu.\",\n    \"invalid_api_key\" => \"API Key không hợp lệ.\",\n    \"api_key_saved\" => \"API Key đã được lưu thành công\",\n    \"currency_removed\" => \"Tiền tệ đã bị xóa\",\n    // Household\n    \"failed_add_household\" => \"Thêm thành viên hộ gia đình thất bại\",\n    \"failed_edit_household\" => \"Chỉnh sửa thành viên hộ gia đình thất bại\",\n    \"failed_remove_household\" => \"Xóa thành viên hộ gia đình thất bại\",\n    \"household_in_use\" => \"Thành viên hộ gia đình đang được sử dụng trong đăng ký và không thể bị xóa\",\n    \"member_saved\" => \"Thành viên đã được lưu\",\n    \"member_removed\" => \"Thành viên đã bị xóa\",\n    // Notifications\n    \"error_saving_notifications\" => \"Lỗi khi lưu dữ liệu thông báo.\",\n    \"wallos_notification\" => \"Thông báo Wallos\",\n    \"test_notification\" => \"Đây là thông báo thử nghiệm. Nếu bạn thấy điều này, cấu hình đã chính xác.\",\n    \"email_error\" => \"Lỗi khi gửi email\",\n    \"notification_sent_successfuly\" => \"Thông báo đã được gửi thành công\",\n    \"notifications_settings_saved\" => \"Cài đặt thông báo đã được lưu thành công.\",\n    \"notification_failed\" => \"Thông báo thất bại\",\n    // Payments\n    \"payment_in_use\" => \"Không thể vô hiệu hóa phương thức thanh toán đang sử dụng\",\n    \"failed_update_payment\" => \"Cập nhật phương thức thanh toán trong cơ sở dữ liệu thất bại\",\n    \"enabled\" => \"đã được bật\",\n    \"disabled\" => \"đã bị vô hiệu hóa\",\n    // Subscription\n    \"error_fetching_image\" => \"Lỗi khi tìm nạp hình ảnh\",\n    \"subscription_updated_successfuly\" => \"Đăng ký đã được cập nhật thành công\",\n    \"subscription_added_successfuly\" => \"Đăng ký đã được thêm thành công\",\n    \"error_deleting_subscription\" => \"Lỗi khi xóa đăng ký.\",\n    \"invalid_request_method\" => \"Phương thức yêu cầu không hợp lệ.\",\n    // User\n    \"error_updating_user_data\" => \"Lỗi khi cập nhật dữ liệu người dùng.\",\n    \"user_details_saved\" => \"Chi tiết người dùng đã được lưu\",\n    // Admin Page\n    \"registrations\" => \"Đăng ký\",\n    \"enable_user_registrations\" => \"Bật đăng ký người dùng\",\n    \"maximum_number_users\" => \"Số lượng người dùng tối đa\",\n    \"require_email_verification\" => \"Yêu cầu xác minh email\",\n    \"configure_smtp_settings_to_enable\" => \"Cấu hình cài đặt SMTP để bật\",\n    \"server_url\" => \"URL của máy chủ\",\n    \"server_url_info\" => \"Được sử dụng để xác minh email và khôi phục mật khẩu. Phải là một URL công cộng hợp lệ.\",\n    \"server_url_password_reset\" => \"Nếu được thiết lập, cũng sẽ bật chức năng khôi phục mật khẩu.\",\n    \"disable_login\" => \"Vô hiệu hóa đăng nhập\",\n    \"disable_login_info\" => \"Bỏ qua đăng nhập. Nếu bạn chạy máy chủ của mình trên mạng cục bộ mà không có quyền truy cập từ bên ngoài, bạn có thể vô hiệu hóa đăng nhập. Điều này sẽ tự động đăng nhập người dùng quản trị.\",\n    \"disable_login_info2\" => \"Bạn chỉ có thể bật cài đặt này nếu việc đăng ký người dùng bị vô hiệu hóa và không có tài khoản người dùng nào ngoài tài khoản quản trị viên.\",\n    \"max_users_info\" => \"0 có nghĩa là không giới hạn\",\n    \"user_management\" => \"Quản lý người dùng\",\n    \"delete_user\" => \"Xóa người dùng\",\n    \"delete_user_info\" => \"Xóa một người dùng cũng sẽ xóa tất cả các đăng ký và cài đặt của họ.\",\n    \"create_user\" => \"Tạo người dùng\",\n    \"oidc_settings\" => \"Cài đặt OIDC\",\n    \"oidc_auth_enabled\" => \"Xác thực OIDC đã được bật\",\n    \"create_user_automatically\" => \"Tạo người dùng tự động\",\n    \"disable_password_login\" => \"Vô hiệu hóa đăng nhập bằng mật khẩu\",\n    \"smtp_settings\" => \"Cài đặt SMTP\",\n    \"smtp_usage_info\" => \"Sẽ được sử dụng cho việc khôi phục mật khẩu và các email hệ thống khác.\",\n    \"security_settings\" => \"Cài đặt bảo mật\",\n    \"ssrf_protection_info\" => \"Để ngăn chặn các cuộc tấn công Server-Side Request Forgery (SSRF), Wallos mặc định chặn các thông báo webhook đến các địa chỉ mạng riêng hoặc nội bộ.\",\n    \"local_webhook_info\" => \"Nếu bạn cần gửi webhooks đến các dịch vụ cục bộ (như Home Assistant, Gotify hoặc Node-RED), hãy nhập địa chỉ IP hoặc tên máy chủ của chúng ở trên dưới dạng danh sách cách nhau bởi dấu phẩy (ví dụ: <code>192.168.1.100,192.168.1.101</code>).\",\n    \"maintenance_tasks\" => \"Nhiệm vụ bảo trì\",\n    \"orphaned_logos\" => \"Logo bị bỏ rơi\",\n    \"update\" => \"Cập nhật\",\n    \"new_version_available\" => \"Một phiên bản mới của Wallos đã có sẵn\",\n    \"current_version\" => \"Phiên bản hiện tại\",\n    \"latest_version\" => \"Phiên bản mới nhất\",\n    \"on_current_version\" => \"Bạn đang sử dụng phiên bản mới nhất của Wallos.\",\n    \"show_update_notification\" => \"Hiển thị thông báo cập nhật trên bảng điều khiển\",\n    \"cronjobs\" => \"Công việc định kỳ\",\n    // Email Verification\n    \"email_verified\" => \"Xác minh email thành công\",\n    \"email_verification_failed\" => \"Xác minh email thất bại\",\n    // Calendar\n    \"calendar\" => \"Lịch\",\n    \"sun\" => \"CN\",\n    \"mon\" => \"Thứ 2\",\n    \"tue\" => \"Thứ 3\",\n    \"wed\" => \"Thứ 4\",\n    \"thu\" => \"Thứ 5\",\n    \"fri\" => \"Thứ 6\",\n    \"sat\" => \"Thứ 7\",\n    \"month-01\" => \"Tháng Giêng\",\n    \"month-02\" => \"Tháng Hai\",\n    \"month-03\" => \"Tháng Ba\",\n    \"month-04\" => \"Tháng Tư\",\n    \"month-05\" => \"Tháng Năm\",\n    \"month-06\" => \"Tháng Sáu\",\n    \"month-07\" => \"Tháng Bảy\",\n    \"month-08\" => \"Tháng Tám\",\n    \"month-09\" => \"Tháng Chín\",\n    \"month-10\" => \"Tháng Mười\",\n    \"month-11\" => \"Tháng Mười Một\",\n    \"month-12\" => \"Tháng Mười Hai\",\n    \"total_cost\" => \"Tổng chi phí\",\n    \"export_icalendar\" => \"Xuất iCalendar\",\n    \"over_budget_warning\" => \"Bạn đang vượt quá ngân sách\",\n    // TOTP Page\n    \"insert_totp_code\" => \"Nhập mã TOTP\",\n];\n\n?>\n"
  },
  {
    "path": "includes/i18n/zh_cn.php",
    "content": "<?php\n\n$i18n = [\n    // 注册页面\n    \"create_account\" => \"请创建帐号后登录\",\n    \"username\" => \"用户名\",\n    \"password\" => \"密码\",\n    \"email\" => \"电子邮箱\",\n    \"firstname\" => \"名\",\n    \"lastname\" => \"姓\",\n    \"confirm_password\" => \"确认密码\",\n    \"main_currency\" => \"主要货币\",\n    \"language\" => \"语言\",\n    \"passwords_dont_match\" => \"密码不匹配\",\n    \"username_exists\" => \"用户名已存在\",\n    \"email_exists\" => \"电子邮箱已存在\",\n    \"registration_failed\" => \"注册失败，请重试。\",\n    \"register\" => \"注册\",\n    \"restore_database\" => \"恢复数据库\",\n\n    // 登录页面\n    \"please_login\" => \"请登录\",\n    \"stay_logged_in\" => \"30 天内免登录\",\n    \"login\" => \"登录\",\n    \"login_with\" => \"使用以下方式登录\",\n    \"or\" => \"或\",\n    \"login_failed\" => \"登录信息错误\",\n    \"registration_successful\" => \"注册成功\",\n    \"user_email_waiting_verification\" => \"您的电子邮件需要验证。请检查您的电子邮件\",\n\n    // Password Reset Page\n    \"forgot_password\" => \"忘记密码\",\n    \"reset_password\" => \"重置密码\",\n    \"reset_sent_check_email\" => \"重置密码链接已发送到您的电子邮箱\",\n    \"password_reset_successful\" => \"密码重置成功\",\n\n    // 页眉\n    \"profile\" => \"个人资料\",\n    \"dashboard\" => \"仪表盘\",\n    \"subscriptions\" => \"订阅\",\n    \"stats\" => \"统计\",\n    \"settings\" => \"设置\",\n    \"admin\" => \"管理员\",\n    \"about\" => \"关于\",\n    \"logout\" => \"登出\",\n\n    // Dashboard\n    \"hello\" => \"你好\",\n    \"upcoming_payments\" => \"即将到期的付款\",\n    \"no_upcoming_payments\" => \"您没有任何即将到期的付款\",\n    \"overdue_renewals\" => \"逾期续订\",\n    \"ai_recommendations\" => \"AI 推荐\",\n    \"your_budget\" => \"您的预算\",\n    \"budget\" => \"预算\",\n    \"budget_used\" => \"预算已使用\",\n    \"over_budget\" => \"超出预算\",\n    \"your_subscriptions\" => \"您的订阅\",\n    \"your_savings\" => \"您的储蓄\",\n\n    // 订阅页面\n    \"subscription\" => \"订阅\",\n    \"no_subscriptions_yet\" => \"您还没有任何订阅\",\n    \"add_first_subscription\" => \"添加首个订阅\",\n    \"new_subscription\" => \"新订阅\",\n    \"search\" => \"搜索\",\n    \"state\" => \"状态\",\n    \"alphanumeric\" => \"名称\",\n    \"sort\" => \"排序\",\n    \"name\" => \"名称\",\n    \"last_added\" => \"创建时间\",\n    \"price\" => \"价格\",\n    \"next_payment\" => \"下次支付时间\",\n    \"renewal_type\" => \"续订类型\",\n    \"auto_renewal\" => \"自动续订\",\n    \"automatically_renews\" => \"自动续订\",\n    \"manual_renewal\" => \"手动续订\",\n    \"start_date\" => \"开始日期\",\n    \"inactive\" => \"停用订阅\",\n    \"replaced_with\" => \"替换为\",\n    \"none\" => \"无\",\n    \"member\" => \"成员\",\n    \"category\" => \"分类\",\n    \"payment_method\" => \"支付方式\",\n    \"Daily\" => \"每日\",\n    \"Weekly\" => \"每周\",\n    \"Monthly\" => \"每月\",\n    \"Yearly\" => \"每年\",\n    \"daily\" => \"日\",\n    \"weekly\" => \"周\",\n    \"monthly\" => \"月\",\n    \"yearly\" => \"年\",\n    \"days\" => \"天\",\n    \"weeks\" => \"周\",\n    \"months\" => \"月\",\n    \"years\" => \"年\",\n    \"external_url\" => \"访问外部链接\",\n    \"empty_page\" => \"空白页面\",\n    \"clear_filters\" => \"清除筛选\",\n    \"no_matching_subscriptions\" => \"没有匹配的订阅\",\n    \"clone\" => \"克隆\",\n    \"renew\" => \"续订\",\n    \"calculate_next_payment_date\" => \"计算下次支付日期\",\n\n    // 订阅表单\n    \"add_subscription\" => \"添加订阅\",\n    \"edit_subscription\" => \"编辑订阅\",\n    \"subscription_name\" => \"订阅名称\",\n    \"logo_preview\" => \"Logo 预览\",\n    \"search_logo\" => \"在网上搜索 Logo\",\n    \"web_search\" => \"网页搜索\",\n    \"currency\" => \"货币\",\n    \"payment_every\" => \"支付频率\",\n    \"frequency\" => \"频率\",\n    \"cycle\" => \"周期\",\n    \"no_category\" => \"无分类\",\n    \"paid_by\" => \"付款人\",\n    \"url\" => \"链接\",\n    \"notes\" => \"备注\",\n    \"enable_notifications\" => \"为此订阅启用通知\",\n    \"default_value_from_settings\" => \"默认值从设置中获取\",\n    \"cancellation_notification\" => \"取消通知\",\n    \"delete\" => \"删除\",\n    \"cancel\" => \"取消\",\n    \"upload_logo\" => \"上传 Logo\",\n\n    // 统计页面\n    \"cant_convert_currency\" => \"您在订阅中使用了多种货币。要获得有效、准确的统计数据，请在设置页面设置 Fixer API 密钥。\",\n    \"general_statistics\" => \"总体统计\",\n    \"active_subscriptions\" => \"活跃订阅\",\n    \"inactive_subscriptions\" => \"非活动订阅\",\n    \"monthly_cost\" => \"月费用\",\n    \"yearly_cost\" => \"年费用\",\n    \"average_monthly\" => \"平均每月订阅费用\",\n    \"most_expensive\" => \"最昂贵订阅费用\",\n    \"amount_due\" => \"本月应付金额\",\n    \"percentage_budget_used\" => \"预算使用百分比\",\n    \"budget_remaining\" => \"剩余预算\",\n    \"amount_over_budget\" => \"超出预算\",\n    \"monthly_savings\" => \"每月节省\",\n    \"yearly_savings\" => \"每年节省\",\n    \"split_views\" => \"拆分视图\",\n    \"category_split\" => \"分类视图\",\n    \"household_split\" => \"家庭视图\",\n    \"payment_method_split\" => \"支付方式视图\",\n    \"total_cost_trend\" => \"总费用趋势\",\n    \"cost_vs_budget\" => \"费用与预算\",\n\n    // 关于页面\n    \"about_and_credits\" => \"关于和鸣谢\",\n    \"credits\" => \"鸣谢\",\n    \"license\" => \"许可证\",\n    \"release_notes\" => \"发布说明\",\n    \"update_available\" => \"可用更新\",\n    \"issues_and_requests\" => \"问题反馈与功能请求\",\n    \"the_author\" => \"作者\",\n    \"icons\" => \"图标\",\n    \"payment_icons\" => \"支付图标\",\n\n    // Profile Page\n    \"upload_avatar\" => \"上传头像\",\n    \"file_type_error\" => \"文件类型不允许\",\n    \"user_details\" => \"用户详情\",\n    \"two_factor_authentication\" => \"双因素认证\",\n    \"two_factor_info\" => \"双因素身份验证为您的账户增加了一层额外的安全保护。您需要使用 Google Authenticator、Authy 或 Ente Auth 等认证程序来扫描二维码。\",\n    \"two_factor_enabled_info\" => \"双因素身份验证确保您的账户安全。您可以单击上面的按钮禁用它。\",\n    \"enable_two_factor_authentication\" => \"启用双因素身份验证\",\n    \"2fa_already_enabled\" => \"双因素身份验证已启用\",\n    \"totp_code_incorrect\" => \"TOTP 代码不正确\",\n    \"backup_codes\" => \"备份代码\",\n    \"download_backup_codes\" => \"下载备份代码\",\n    \"copy_to_clipboard\" => \"复制到剪贴板\",\n    \"totp_backup_codes_info\" => \"请务必保存这些备份代码。如果您丢失了双因素身份验证设备，您将需要这些备份代码来登录。\",\n    \"disable_two_factor_authentication\" => \"禁用双因素身份验证\",\n    \"totp_code\" => \"TOTP 代码\",\n    \"api_key\" => \"API 密钥\",\n    \"regenerate\" => \"重新生成\",\n    \"api_key_info\" => \"API 密钥用于与 Wallos API 通信。请勿将此密钥分享给任何人。\",\n    // 设置页面\n    \"monthly_budget\" => \"每月预算\",\n    \"budget_info\" => \"设置预算后，您可以在统计页面上比较预算和实际支出。\",\n    \"household\" => \"家庭\",\n    \"save_member\" => \"保存成员\",\n    \"delete_member\" => \"删除成员\",\n    \"cant_delete_member\" => \"不能删除主要成员\",\n    \"cant_delete_member_in_use\" => \"不能删除拥有订阅的成员\",\n    \"household_info\" => \"电子邮件字段允许通知家庭成员订阅即将过期。\",\n    \"notifications\" => \"通知\",\n    \"enable_email_notifications\" => \"启用电子邮件通知\",\n    \"notify_me\" => \"通知提前时间\",\n    \"day_before\" => \"天\", // 设置标题（`notify_me`）中已经表明是提前多少天，因此这里直接用单位即可\n    \"on_due_date\" => \"到期日\",\n    \"days_before\" => \"天\",\n    \"smtp_address\" => \"SMTP 地址\",\n    \"port\" => \"端口\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP 用户名\",\n    \"smtp_password\" => \"SMTP 密码\",\n    \"from_email\" => \"发件人邮箱（可选）\",\n    \"send_to_other_emails\" => \"还发送通知到以下电子邮件地址（使用 ; 分隔它们）：\",\n    \"smtp_info\" => \"SMTP 密码以明文传输和存储。为安全起见，建议专门为 Wallos 创建一个账户。\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegram 机器人令牌\",\n    \"telegram_chat_id\" => \"Telegram 聊天 ID\",\n    \"pushplus\" => \"Pushplus\",\n    \"pushplus_token\" => \"消息令牌或者是用户令牌\",\n    \"serverchan\" => \"Server酱\",\n    \"serverchan_sendkey\" => \"SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook URL\",\n    \"mattermost_bot_username\" => \"Mattermost Bot 用户名\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot 表情图标\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook URL\",\n    \"request_method\" => \"请求方法\",\n    \"custom_headers\" => \"自定义标头\",\n    \"webhook_payload\" => \"Webhook 负载\",\n    \"payment_notifications_payload\" => \"付款通知负载\",\n    \"cancelation_notification_payload\" => \"取消通知负载\",\n    \"variables_available\" => \"可用变量\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"令牌\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Discord 机器人用户名\",\n    \"discord_bot_avatar_url\" => \"Discord 机器人头像 URL\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover 用户密钥\",\n    \"host\" => \"主机\",\n    \"topic\" => \"主题\",\n    \"ignore_ssl_errors\" => \"忽略 SSL 错误\",\n    \"categories\" => \"分类\",\n    \"save_category\" => \"保存分类\",\n    \"delete_category\" => \"删除分类\",\n    \"cant_delete_category_in_use\" => \"不能删除正在订阅中的分类\",\n    \"currencies\" => \"货币\",\n    \"save_currency\" => \"保存货币\",\n    \"delete_currency\" => \"删除货币\",\n    \"cant_delete_main_currency\" => \"不能删除主要货币\",\n    \"cant_delete_currency_in_use\" => \"不能删除正在使用中的货币\",\n    \"exchange_update\" => \"汇率最后更新于\",\n    \"currency_info\" => \"如要查找支持的货币与对应代码，请前往\",\n    \"currency_performance\" => \"为提高性能，建议您只保留常用货币。\",\n    \"fixer_api_key\" => \"Fixer API 密钥\",\n    \"provider\" => \"提供商\",\n    \"fixer_info\" => \"如果您使用多种货币，希望统计信息和订阅排序更精确，则需要 Fixer API 密钥来查询汇率（可免费申请）。\",\n    \"get_key\" => \"申请密钥\",\n    \"get_free_fixer_api_key\" => \"申请免费 Fixer API 密钥\",\n    \"get_key_alternative\" => \"或者，您也可以从以下网站获取免费的修复程序 api 密钥\",\n    \"ai_model\" => \"AI 模型\",\n    \"select_ai_model\" => \"选择 AI 模型\",\n    \"run_schedule\" => \"运行计划\",\n    \"manually\" => \"手动\",\n    \"coming_soon\" => \"即将推出\",\n    \"invalid_host\" => \"无效的主机\",\n    \"ai_recommendations_info\" => \"AI 推荐是基于您的订阅和家庭成员生成的。\",\n    \"may_take_time\" => \"根据提供商、模型和订阅数量，推荐生成可能需要一些时间。\",\n    \"recommendations_visible_on_dashboard\" => \"推荐将在仪表板上可见。\",\n    \"generate_recommendations\" => \"生成推荐\",\n    \"display_settings\" => \"显示设置\",\n    \"theme_settings\" => \"主题设置\",\n    \"colors\" => \"颜色\",\n    \"custom_colors\" => \"自定义颜色\",\n    \"theme\" => \"主题\",\n    \"dark_theme\" => \"深色主题\",\n    \"light_theme\" => \"浅色主题\",\n    \"automatic\" => \"自动\",\n    \"main_color\" => \"主色\",\n    \"accent_color\" => \"强调色\",\n    \"hover_color\" => \"悬停颜色\",\n    \"save_custom_colors\" => \"保存自定义颜色\",\n    \"reset_custom_colors\" => \"重置自定义颜色\",\n    \"custom_css\" => \"自定义 CSS\",\n    \"save_custom_css\" => \"保存自定义 CSS\",\n    \"calculate_monthly_price\" => \"计算并显示所有订阅的月价格\",\n    \"convert_prices\" => \"始终按我的主要货币转换和显示价格（较慢）\",\n    \"show_original_price\" => \"当进行转换或计算时，也显示原始价格\",\n    \"experience\" => \"体验\",\n    \"show_subscription_progress\" => \"显示订阅进度\",\n    \"disabled_subscriptions\" => \"已停用的订阅\",\n    \"hide_disabled_subscriptions\" => \"隐藏已停用的订阅\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"在订阅列表底部显示已停用的订阅\",\n    \"experimental_settings\" => \"实验性设置\",\n    \"remove_background\" => \"尝试从图片搜索中移除标志的背景\",\n    \"use_mobile_navigation_bar\" => \"使用移动导航栏\",\n    \"experimental_info\" => \"实验性设置，可能存在问题。\",\n    \"payment_methods\" => \"支付方式\",\n    \"payment_methods_info\" => \"点击支付方式以禁用/启用。\",\n    \"rename_payment_methods_info\" => \"点击付款方式名称，重新命名该付款方式。\",\n    \"cant_delete_payment_method_in_use\" => \"不能禁用正在使用的支付方式\",\n    \"add_custom_payment\" => \"添加自定义支付方式\",\n    \"payment_method_name\" => \"支付方式名称\",\n    \"payment_method_added_successfuly\" => \"支付方式已成功添加\",\n    \"payment_method_removed\" => \"支付方式已移除\",\n    \"disable\" => \"禁用\",\n    \"enable\" => \"启用\",\n    \"rename_payment_method\" => \"重命名支付方式\",\n    \"payment_renamed\" => \"支付方式已重命名\",\n    \"payment_not_renamed\" => \"支付方式未重命名\",\n    \"test\" => \"测试\",\n    \"add\" => \"添加\",\n    \"save\" => \"保存\",\n    \"reset\" => \"重置\",\n    \"main_accent_color_error\" => \"主色和强调色不能相同\",\n    \"backup_and_restore\" => \"备份和恢复\",\n    \"backup\" => \"备份\",\n    \"restore\" => \"恢复\",\n    \"restore_info\" => \"还原数据库将覆盖所有当前数据。还原后，您将退出登录。\",\n    \"account\" => \"账户\",\n    \"export_subscriptions\" => \"导出订阅\",\n    \"export_as_json\" => \"导出为 JSON\",\n    \"export_as_csv\" => \"导出为 CSV\",\n    \"danger_zone\" => \"危险区\",\n    \"delete_account\" => \"删除账户\",\n    \"delete_account_info\" => \"删除账户将删除所有数据，包括订阅、设置和家庭成员。此操作不可逆。\",\n    // Filters menu\n    \"filter\" => \"筛选\",\n    \"clear\" => \"清除\",\n\n    // Toast\n    \"success\" => \"成功\",\n\n    // Endpoint responses\n    \"session_expired\" => \"您的会话已过期，请重新登录\",\n    \"fields_missing\" => \"部分字段未填写\",\n    \"fill_all_fields\" => \"请填写所有字段\",\n    \"fill_mandatory_fields\" => \"请填写所有必填字段\",\n    \"error\" => \"错误\",\n\n    // Category\n    \"failed_add_category\" => \"添加分类失败\",\n    \"failed_edit_category\" => \"编辑分类失败\",\n    \"category_in_use\" => \"分类正在被订阅使用中，无法移除\",\n    \"failed_remove_category\" => \"移除分类失败\",\n    \"category_saved\" => \"分类已保存\",\n    \"category_removed\" => \"分类已移除\",\n    \"sort_order_saved\" => \"排序顺序已保存\",\n\n    // Currency\n    \"currency_saved\" => \"货币已保存。\",\n    \"error_adding_currency\" => \"添加货币时出错。\",\n    \"failed_to_store_currency\" => \"存储货币到数据库失败。\",\n    \"currency_in_use\" => \"货币正在被订阅使用中，无法删除。\",\n    \"currency_is_main\" => \"货币已被设置为主货币，无法删除。\",\n    \"failed_to_remove_currency\" => \"从数据库删除货币失败。\",\n    \"failed_to_store_api_key\" => \"存储 API 密钥到数据库失败。\",\n    \"invalid_api_key\" => \"API 密钥无效。\",\n    \"api_key_saved\" => \"API 密钥已成功保存\",\n    \"currency_removed\" => \"货币已移除\",\n\n    // Household\n    \"failed_add_household\" => \"添加家庭成员失败\",\n    \"failed_edit_household\" => \"编辑家庭成员失败\",\n    \"failed_remove_household\" => \"移除家庭成员失败\",\n    \"household_in_use\" => \"此成员有相关的订阅，无法移除\",\n    \"member_saved\" => \"成员已保存\",\n    \"member_removed\" => \"成员已移除\",\n\n    // Notifications\n    \"error_saving_notifications\" => \"保存通知数据时出错。\",\n    \"wallos_notification\" => \"Wallos 通知\",\n    \"test_notification\" => \"这是一条测试通知。如果您看到此消息，说明 Wallos 通知邮件配置正确。\",\n    \"email_error\" => \"发送电子邮件时出错\",\n    \"notification_failed\" => \"通知发送失败\",\n    \"notification_sent_successfuly\" => \"通知已成功发送\",\n    \"notifications_settings_saved\" => \"通知设置已成功保存。\",\n\n    // Payments\n    \"payment_in_use\" => \"无法禁用正在使用的支付方式\",\n    \"failed_update_payment\" => \"更新数据库中的支付方式失败\",\n    \"enabled\" => \"已启用\",\n    \"disabled\" => \"已禁用\",\n\n    // Subscription\n    \"error_fetching_image\" => \"获取图片时出错\",\n    \"subscription_updated_successfuly\" => \"订阅已成功更新\",\n    \"subscription_added_successfuly\" => \"订阅已成功添加\",\n    \"error_deleting_subscription\" => \"删除订阅时出错。\",\n    \"invalid_request_method\" => \"请求方法无效。\",\n\n    // User\n    \"error_updating_user_data\" => \"更新用户数据时出错。\",\n    \"user_details_saved\" => \"用户详细信息已保存\",\n\n    // Admin Page\n    \"registrations\" => \"注册\",\n    \"enable_user_registrations\" => \"启用用户注册\",\n    \"maximum_number_users\" => \"最大用户数\",\n    \"require_email_verification\" => \"需要电子邮件验证\",\n    \"configure_smtp_settings_to_enable\" => \"要启用此功能，请配置 SMTP 设置。\",\n    \"server_url\" => \"服务器 URL\",\n    \"server_url_info\" => \"用于电子邮件验证和密码恢复。必须是有效的公共 URL。\",\n    \"server_url_password_reset\" => \"如果设置，还将启用密码重置功能。\",\n    \"disable_login\" => \"禁用登录\",\n    \"disable_login_info\" => \"旁路登录。如果服务器只在本地网络上运行，没有外部访问，则可以禁用登录。这会自动登录管理员用户。\",\n    \"disable_login_info2\" => \"只有在用户注册关闭且用户账户数不超过管理员账户时，才能启用此设置。\",\n    \"max_users_info\" => \"设置为 0 以无限制用户数\",\n    \"user_management\" => \"用户管理\",\n    \"delete_user\" => \"删除用户\",\n    \"delete_user_info\" => \"删除用户也会删除其所有订阅和设置。\",\n    \"create_user\" => \"创建用户\",\n    \"oidc_settings\" => \"OIDC 设置\",\n    \"oidc_auth_enabled\" => \"启用 OIDC 身份验证\",\n    \"create_user_automatically\" => \"当使用 OIDC 登录时自动创建用户\",\n    \"disable_password_login\" => \"禁用密码登录\",\n    \"smtp_settings\" => \"SMTP 设置\",\n    \"smtp_usage_info\" => \"将用于密码恢复和其他系统电子邮件。\",\n    \"security_settings\" => \"安全设置\",\n    \"ssrf_protection_info\" => \"为防止服务端请求伪造（SSRF）攻击，Wallos 默认阻止发送到私有或内部网络地址的 webhook 通知。\",\n    \"local_webhook_info\" => \"如果需要将 webhook 发送到本地服务（例如 Home Assistant、Gotify 或 Node-RED），请在上方以逗号分隔的列表中输入它们的 IP 地址或主机名（例如 <code>192.168.1.100,192.168.1.101</code>）。\",\n    \"maintenance_tasks\" => \"维护任务\",\n    \"orphaned_logos\" => \"孤立的 Logo\",\n    \"update\" => \"更新\",\n    \"new_version_available\" => \"新的 Wallos 版本可用\",\n    \"current_version\" => \"当前版本\",\n    \"latest_version\" => \"最新版本\",\n    \"on_current_version\" => \"您正在运行最新版本的 Wallos。\",\n    \"show_update_notification\" => \"在仪表板上显示更新通知\",\n    \"cronjobs\" => \"Cronjobs\",\n\n    // Email Verification\n    \"email_verified\" => \"电子邮件已验证\",\n    \"email_verification_failed\" => \"电子邮件验证失败\",\n\n    // Calendar\n    \"calendar\" => \"日历\",\n    \"sun\" => \"周日\",\n    \"mon\" => \"周一\",\n    \"tue\" => \"周二\",\n    \"wed\" => \"周三\",\n    \"thu\" => \"周四\",\n    \"fri\" => \"周五\",\n    \"sat\" => \"周六\",\n    \"month-01\" => \"一月\",\n    \"month-02\" => \"二月\",\n    \"month-03\" => \"三月\",\n    \"month-04\" => \"四月\",\n    \"month-05\" => \"五月\",\n    \"month-06\" => \"六月\",\n    \"month-07\" => \"七月\",\n    \"month-08\" => \"八月\",\n    \"month-09\" => \"九月\",\n    \"month-10\" => \"十月\",\n    \"month-11\" => \"十一月\",\n    \"month-12\" => \"十二月\",\n    \"total_cost\" => \"总费用\",\n    \"export_icalendar\" => \"导出 iCalendar\",\n    \"over_budget_warning\" => \"您超出预算\",\n\n    // TOTP Page\n    \"insert_totp_code\" => \"请输入 TOTP 代码\",\n\n];\n\n?>\n"
  },
  {
    "path": "includes/i18n/zh_tw.php",
    "content": "<?php\n\n$i18n = [\n    // 註冊頁面\n    \"create_account\" => \"您需要先建立帳號才能登入\",\n    \"username\" => \"使用者名稱\",\n    \"password\" => \"密碼\",\n    \"email\" => \"電子郵件\",\n    \"firstname\" => \"名\",\n    \"lastname\" => \"姓\",\n    \"confirm_password\" => \"確認密碼\",\n    \"main_currency\" => \"主要貨幣\",\n    \"language\" => \"語言\",\n    \"passwords_dont_match\" => \"密碼不一致\",\n    \"username_exists\" => \"使用者名稱已存在\",\n    \"email_exists\" => \"電子郵件已存在\",\n    \"registration_failed\" => \"註冊失敗，請再試一次。\",\n    \"register\" => \"註冊\",\n    \"restore_database\" => \"還原資料庫\",\n    // 登入頁面\n    \"please_login\" => \"請先登入\",\n    \"stay_logged_in\" => \"保持登入狀態（30 天）\",\n    \"login\" => \"登入\",\n    \"login_with\" => \"使用以下方式登入\",\n    \"or\" => \"或\",\n    \"login_failed\" => \"登入資訊錯誤\",\n    \"registration_successful\" => \"註冊成功\",\n    \"user_email_waiting_verification\" => \"您的電子郵件需要驗證。請檢查您的電子郵件信箱。\",\n    // 密碼重設頁面\n    \"forgot_password\" => \"忘記密碼\",\n    \"reset_password\" => \"重設密碼\",\n    \"reset_sent_check_email\" => \"重設密碼郵件已寄出。請檢查您的電子郵件信箱。\",\n    \"password_reset_successful\" => \"密碼重設成功\",\n    // 頁首\n    \"profile\" => \"個人檔案\",\n    \"dashboard\" => \"儀表板\",\n    \"subscriptions\" => \"訂閱服務\",\n    \"stats\" => \"統計資訊\",\n    \"settings\" => \"設定\",\n    \"admin\" => \"管理員\",\n    \"about\" => \"關於\",\n    \"logout\" => \"登出\",\n    // Dashboard\n    \"hello\" => \"你好\",\n    \"upcoming_payments\" => \"即將到期的付款\",\n    \"no_upcoming_payments\" => \"您沒有任何即將到期的付款\",\n    \"overdue_renewals\" => \"逾期續訂\",\n    \"ai_recommendations\" => \"AI 推荐\",\n    \"your_budget\" => \"您的預算\",\n    \"budget\" => \"預算\",\n    \"budget_used\" => \"預算已使用\",\n    \"over_budget\" => \"超出預算\",\n    \"your_subscriptions\" => \"您的訂閱\",\n    \"your_savings\" => \"您的儲蓄\",\n    // 訂閱頁面\n    \"subscription\" => \"訂閱服務\",\n    \"no_subscriptions_yet\" => \"您目前沒有任何訂閱服務\",\n    \"add_first_subscription\" => \"新增第一筆訂閱\",\n    \"new_subscription\" => \"新增訂閱\",\n    \"search\" => \"搜尋\",\n    \"state\" => \"狀態\",\n    \"alphanumeric\" => \"英數字元\",\n    \"sort\" => \"排序\",\n    \"name\" => \"名稱\",\n    \"last_added\" => \"最近新增\",\n    \"price\" => \"價格\",\n    \"next_payment\" => \"下次付款日期\",\n    \"renewal_type\" => \"續訂方式\",\n    \"auto_renewal\" => \"自動續訂\",\n    \"automatically_renews\" => \"自動續訂\",\n    \"manual_renewal\" => \"手動續訂\",\n    \"start_date\" => \"開始日期\",\n    \"inactive\" => \"停用訂閱\",\n    \"replaced_with\" => \"取代為\",\n    \"none\" => \"無\",\n    \"member\" => \"成員\",\n    \"category\" => \"分類\",\n    \"payment_method\" => \"付款方式\",\n    \"Daily\" => \"每日\",\n    \"Weekly\" => \"每週\",\n    \"Monthly\" => \"每月\",\n    \"Yearly\" => \"每年\",\n    \"daily\" => \"天\",\n    \"weekly\" => \"週\",\n    \"monthly\" => \"月\",\n    \"yearly\" => \"年\",\n    \"days\" => \"天\",\n    \"weeks\" => \"週\",\n    \"months\" => \"月\",\n    \"years\" => \"年\",\n    \"external_url\" => \"開啟外部連結\",\n    \"empty_page\" => \"空白頁面\",\n    \"clear_filters\" => \"清除篩選條件\",\n    \"no_matching_subscriptions\" => \"沒有符合的訂閱服務\",\n    \"clone\" => \"複製\",\n    \"renew\" => \"續訂\",\n    \"calculate_next_payment_date\" => \"計算下次付款日期\",\n    // 訂閱表單\n    \"add_subscription\" => \"新增訂閱服務\",\n    \"edit_subscription\" => \"編輯訂閱服務\",\n    \"subscription_name\" => \"訂閱名稱\",\n    \"logo_preview\" => \"圖示預覽\",\n    \"search_logo\" => \"在網路上搜尋圖示\",\n    \"web_search\" => \"網頁搜尋\",\n    \"currency\" => \"貨幣\",\n    \"payment_every\" => \"付款週期\",\n    \"frequency\" => \"頻率\",\n    \"cycle\" => \"週期\",\n    \"no_category\" => \"未分類\",\n    \"paid_by\" => \"付款人\",\n    \"url\" => \"網址\",\n    \"notes\" => \"備註\",\n    \"enable_notifications\" => \"啟用此訂閱的通知\",\n    \"default_value_from_settings\" => \"使用設定中的預設值\",\n    \"cancellation_notification\" => \"取消通知\",\n    \"delete\" => \"刪除\",\n    \"cancel\" => \"取消\",\n    \"upload_logo\" => \"上傳圖示\",\n    // 統計頁面\n    \"cant_convert_currency\" => \"您的訂閱使用了多種貨幣。為了獲得有效且準確的統計資訊，請在設定頁面設定 Fixer API 金鑰。\",\n    \"general_statistics\" => \"整體統計\",\n    \"active_subscriptions\" => \"訂閱中\",\n    \"inactive_subscriptions\" => \"已停用的訂閱\",\n    \"monthly_cost\" => \"每月費用\",\n    \"yearly_cost\" => \"每年費用\",\n    \"average_monthly\" => \"平均每月訂閱費用\",\n    \"most_expensive\" => \"最高的訂閱費用\",\n    \"amount_due\" => \"本月應付金額\",\n    \"percentage_budget_used\" => \"預算使用率\",\n    \"budget_remaining\" => \"剩餘預算\",\n    \"amount_over_budget\" => \"預算超支\",\n    \"monthly_savings\" => \"每月節省金額（已停用的訂閱）\",\n    \"yearly_savings\" => \"每年節省金額（已停用的訂閱）\",\n    \"split_views\" => \"分類檢視\",\n    \"category_split\" => \"分類分析\",\n    \"household_split\" => \"家庭成員分析\",\n    \"payment_method_split\" => \"付款方式分析\",\n    \"total_cost_trend\" => \"總費用趨勢\",\n    \"cost_vs_budget\" => \"費用與預算比較\",\n    // 關於頁面\n    \"about_and_credits\" => \"關於和致謝\",\n    \"credits\" => \"致謝\",\n    \"license\" => \"授權條款\",\n    \"release_notes\" => \"版本資訊\",\n    \"update_available\" => \"有新版本可用\",\n    \"issues_and_requests\" => \"問題回報與功能建議\",\n    \"the_author\" => \"作者\",\n    \"icons\" => \"圖示\",\n    \"payment_icons\" => \"付款圖示\",\n    // 個人檔案頁面\n    \"upload_avatar\" => \"上傳大頭貼\",\n    \"file_type_error\" => \"不支援此檔案類型。\",\n    \"user_details\" => \"使用者資訊\",\n    \"two_factor_authentication\" => \"兩步驟驗證\",\n    \"two_factor_info\" => \"兩步驟驗證為您的帳號增加額外的安全防護。<br>您需要使用 Google Authenticator、Authy 或 Ente Auth 等驗證碼應用程式來掃描 QR Code。\",\n    \"two_factor_enabled_info\" => \"您的帳號已啟用兩步驟驗證，安全性較高。您可以點選上方按鈕來停用。\",\n    \"enable_two_factor_authentication\" => \"啟用兩步驟驗證\",\n    \"2fa_already_enabled\" => \"已啟用兩步驟驗證\",\n    \"totp_code_incorrect\" => \"TOTP 驗證碼不正確\",\n    \"backup_codes\" => \"備用驗證碼\",\n    \"download_backup_codes\" => \"下載備用驗證碼\",\n    \"copy_to_clipboard\" => \"複製到剪貼簿\",\n    \"totp_backup_codes_info\" => \"當您無法使用驗證碼應用程式時，可以使用這些備用驗證碼登入，請妥善保管、勿將此金鑰分享給任何人。\",\n    \"disable_two_factor_authentication\" => \"停用兩步驟驗證\",\n    \"totp_code\" => \"TOTP 驗證碼\",\n    \"api_key\" => \"API 金鑰\",\n    \"regenerate\" => \"重新產生\",\n    \"api_key_info\" => \"API 金鑰用於存取 Wallos API，請妥善保管、勿將此金鑰分享給任何人。\",\n    // 設定頁面\n    \"monthly_budget\" => \"每月預算\",\n    \"budget_info\" => \"每月預算用於計算統計資訊\",\n    \"household\" => \"家庭成員\",\n    \"save_member\" => \"儲存成員\",\n    \"delete_member\" => \"刪除成員\",\n    \"cant_delete_member\" => \"無法刪除主要成員\",\n    \"cant_delete_member_in_use\" => \"無法刪除擁有訂閱的成員\",\n    \"household_info\" => \"電子郵件欄位可讓家庭成員收到訂閱即將到期的通知。\",\n    \"notifications\" => \"通知\",\n    \"enable_email_notifications\" => \"啟用電子郵件通知\",\n    \"notify_me\" => \"通知時間\",\n    \"day_before\" => \"天前\",\n    \"on_due_date\" => \"到期當天\",\n    \"days_before\" => \"天前\",\n    \"smtp_address\" => \"SMTP 伺服器位址\",\n    \"port\" => \"連接埠\",\n    \"tls\" => \"TLS\",\n    \"ssl\" => \"SSL\",\n    \"smtp_username\" => \"SMTP 使用者名稱\",\n    \"smtp_password\" => \"SMTP 密碼\",\n    \"from_email\" => \"寄件人電子郵件（選填）\",\n    \"send_to_other_emails\" => \"同時傳送通知至以下電子郵件地址（使用分號 ; 分隔）：\",\n    \"other_emails_placeholder\" => \"user@domain.com;test@user.com\",\n    \"smtp_info\" => \"SMTP 密碼將以明文傳輸和儲存。為了安全起見，建議為 Wallos 建立專用帳號。\",\n    \"telegram\" => \"Telegram\",\n    \"telegram_bot_token\" => \"Telegram 機器人令牌\",\n    \"telegram_chat_id\" => \"Telegram 聊天 ID\",\n    \"pushplus\" => \"PushPlus\",\n    \"pushplus_token\" => \"消息令牌或者是用户令牌\",\n    \"serverchan\" => \"Server醬\",\n    \"serverchan_sendkey\" => \"SendKey\",\n    \"mattermost\" => \"Mattermost\",\n    \"mattermost_webhook_url\" => \"Mattermost Webhook 網址\",\n    \"mattermost_bot_username\" => \"Mattermost Bot 使用者名稱\",\n    \"mattermost_bot_icon_emoji\" => \"Mattermost Bot 表情圖示\",\n    \"webhook\" => \"Webhook\",\n    \"webhook_url\" => \"Webhook 網址\",\n    \"request_method\" => \"請求方法\",\n    \"custom_headers\" => \"自訂標頭\",\n    \"webhook_payload\" => \"Webhook 內容\",\n    \"payment_notifications_payload\" => \"付款通知內容\",\n    \"cancelation_notification_payload\" => \"取消通知內容\",\n    \"variables_available\" => \"可用變數\",\n    \"gotify\" => \"Gotify\",\n    \"token\" => \"令牌\",\n    \"discord\" => \"Discord\",\n    \"discord_bot_username\" => \"Discord 機器人使用者名稱\",\n    \"discord_bot_avatar_url\" => \"Discord 機器人大頭貼網址\",\n    \"pushover\" => \"Pushover\",\n    \"pushover_user_key\" => \"Pushover 使用者金鑰\",\n    \"host\" => \"主機\",\n    \"topic\" => \"主題\",\n    \"ignore_ssl_errors\" => \"忽略 SSL 錯誤\",\n    \"categories\" => \"分類\",\n    \"save_category\" => \"儲存分類\",\n    \"delete_category\" => \"刪除分類\",\n    \"cant_delete_category_in_use\" => \"無法刪除正在使用中的分類\",\n    \"currencies\" => \"貨幣\",\n    \"save_currency\" => \"儲存貨幣\",\n    \"delete_currency\" => \"刪除貨幣\",\n    \"cant_delete_main_currency\" => \"無法刪除主要貨幣\",\n    \"cant_delete_currency_in_use\" => \"無法刪除正在使用中的貨幣\",\n    \"exchange_update\" => \"匯率最後更新時間：\",\n    \"currency_info\" => \"如要查詢支援的貨幣和對應的貨幣代碼，請前往\",\n    \"currency_performance\" => \"為提升效能，建議僅保留您常使用的貨幣。\",\n    \"fixer_api_key\" => \"Fixer API 金鑰\",\n    \"provider\" => \"提供者\",\n    \"fixer_info\" => \"如果您使用多種貨幣，且想要取得準確的統計資訊和訂閱排序，則需要設定免費的 Fixer API 金鑰來查詢匯率。\",\n    \"get_key\" => \"取得金鑰請至\",\n    \"get_free_fixer_api_key\" => \"取得免費 Fixer API 金鑰\",\n    \"get_key_alternative\" => \"或者，您可以從以下網址取得免費的 Fixer API 金鑰\",\n    \"ai_model\" => \"AI 模型\",\n    \"select_ai_model\" => \"選擇 AI 模型\",\n    \"run_schedule\" => \"運行計劃\",\n    \"manually\" => \"手動\",\n    \"coming_soon\" => \"即將推出\",\n    \"invalid_host\" => \"無效的主機\",\n    \"ai_recommendations_info\" => \"AI 推荐是基于您的订阅和家庭成员生成的。\",\n    \"may_take_time\" => \"根据提供商、模型和订阅数量，推荐生成可能需要一些时间。\",\n    \"recommendations_visible_on_dashboard\" => \"推荐将在仪表板上可见。\",\n    \"generate_recommendations\" => \"生成推荐\",\n    \"display_settings\" => \"显示设置\",\n    \"theme_settings\" => \"主題設定\",\n    \"colors\" => \"顏色\",\n    \"custom_colors\" => \"自訂顏色\",\n    \"theme\" => \"主題\",\n    \"dark_theme\" => \"深色主題\",\n    \"light_theme\" => \"淺色主題\",\n    \"automatic\" => \"自動\",\n    \"main_color\" => \"主要顏色\",\n    \"accent_color\" => \"強調顏色\",\n    \"hover_color\" => \"游標停留顏色\",\n    \"save_custom_colors\" => \"儲存自訂顏色\",\n    \"reset_custom_colors\" => \"重設自訂顏色\",\n    \"custom_css\" => \"自訂 CSS\",\n    \"save_custom_css\" => \"儲存自訂 CSS\",\n    \"calculate_monthly_price\" => \"計算並顯示所有訂閱的每月價格\",\n    \"convert_prices\" => \"永遠以我的主要貨幣顯示價格（較慢）\",\n    \"show_original_price\" => \"在轉換或計算時同時顯示原始價格\",\n    \"experience\" => \"使用體驗\",\n    \"show_subscription_progress\" => \"顯示訂閱進度\",\n    \"disabled_subscriptions\" => \"已停用的訂閱\",\n    \"hide_disabled_subscriptions\" => \"隱藏已停用的訂閱\",\n    \"show_disabled_subscriptions_at_the_bottom\" => \"在底部顯示已停用的訂閱\",\n    \"experimental_settings\" => \"實驗性功能\",\n    \"remove_background\" => \"嘗試從圖片搜尋中移除搜尋結果的背景\",\n    \"use_mobile_navigation_bar\" => \"使用行動版導覽列\",\n    \"experimental_info\" => \"實驗性功能可能無法完美運作。\",\n    \"payment_methods\" => \"付款方式\",\n    \"payment_methods_info\" => \"點選付款方式以停用/啟用。\",\n    \"rename_payment_methods_info\" => \"點選付款方式名稱以重新命名。\",\n    \"cant_delete_payment_method_in_use\" => \"無法停用正在使用中的付款方式\",\n    \"add_custom_payment\" => \"新增自訂付款方式\",\n    \"payment_method_name\" => \"付款方式名稱\",\n    \"payment_method_added_successfuly\" => \"已成功新增付款方式\",\n    \"payment_method_removed\" => \"付款方式已移除\",\n    \"disable\" => \"停用\",\n    \"enable\" => \"啟用\",\n    \"rename_payment_method\" => \"更改付款方式名稱\",\n    \"payment_renamed\" => \"付款方式名稱已更改\",\n    \"payment_not_renamed\" => \"付款方式名稱未更改\",\n    \"test\" => \"測試\",\n    \"add\" => \"新增\",\n    \"save\" => \"儲存\",\n    \"reset\" => \"重設\",\n    \"main_accent_color_error\" => \"主要顏色和強調顏色不能相同\",\n    \"backup_and_restore\" => \"備份與還原\",\n    \"backup\" => \"備份\",\n    \"restore\" => \"還原\",\n    \"restore_info\" => \"還原資料庫將覆蓋所有目前資料。還原後您將被登出。\",\n    \"account\" => \"帳號\",\n    \"export_subscriptions\" => \"匯出訂閱資料\",\n    \"export_as_json\" => \"匯出為 JSON\",\n    \"export_as_csv\" => \"匯出為 CSV\",\n    \"danger_zone\" => \"危險區域\",\n    \"delete_account\" => \"刪除帳號\",\n    \"delete_account_info\" => \"刪除帳號將同時刪除所有訂閱和設定，此動作無法復原。\",\n    // 篩選選單\n    \"filter\" => \"篩選\",\n    \"clear\" => \"清除\",\n    // 提示訊息\n    \"success\" => \"成功\",\n    // 端點回應\n    \"session_expired\" => \"您的工作階段已過期，請重新登入\",\n    \"fields_missing\" => \"部分必填欄位未填寫\",\n    \"fill_all_fields\" => \"請填寫所有欄位\",\n    \"fill_mandatory_fields\" => \"請填寫所有必填欄位\",\n    \"error\" => \"錯誤\",\n    // 分類\n    \"failed_add_category\" => \"新增分類失敗\",\n    \"failed_edit_category\" => \"編輯分類失敗\",\n    \"category_in_use\" => \"分類正在使用中，無法移除\",\n    \"failed_remove_category\" => \"移除分類失敗\",\n    \"category_saved\" => \"已儲存分類\",\n    \"category_removed\" => \"已移除分類\",\n    \"sort_order_saved\" => \"已儲存排序順序\",\n    // 貨幣\n    \"currency_saved\" => \"已儲存。\",\n    \"error_adding_currency\" => \"新增貨幣時發生錯誤。\",\n    \"failed_to_store_currency\" => \"儲存貨幣到資料庫失敗。\",\n    \"currency_in_use\" => \"貨幣正在使用中，無法刪除。\",\n    \"currency_is_main\" => \"無法刪除主要貨幣。\",\n    \"failed_to_remove_currency\" => \"從資料庫中刪除貨幣失敗。\",\n    \"failed_to_store_api_key\" => \"儲存 API 金鑰到資料庫失敗。\",\n    \"invalid_api_key\" => \"無效的 API 金鑰。\",\n    \"api_key_saved\" => \"已成功儲存 API 金鑰\",\n    \"currency_removed\" => \"已移除貨幣\",\n    // 家庭成員\n    \"failed_add_household\" => \"新增家庭成員失敗\",\n    \"failed_edit_household\" => \"編輯家庭成員失敗\",\n    \"failed_remove_household\" => \"移除家庭成員失敗\",\n    \"household_in_use\" => \"此成員有正在使用中的訂閱，無法移除\",\n    \"member_saved\" => \"已儲存成員\",\n    \"member_removed\" => \"已移除成員\",\n    // 通知\n    \"error_saving_notifications\" => \"儲存通知設定時發生錯誤。\",\n    \"wallos_notification\" => \"Wallos 通知\",\n    \"test_notification\" => \"這是一則測試通知。如果您看到此訊息，表示 Wallos 通知功能設定正確。\",\n    \"email_error\" => \"寄送電子郵件時發生錯誤\",\n    \"notification_sent_successfuly\" => \"已成功送出通知\",\n    \"notifications_settings_saved\" => \"已成功儲存通知設定。\",\n    \"notification_failed\" => \"通知傳送失敗\",\n    // 付款方式\n    \"payment_in_use\" => \"無法停用正在使用中的付款方式\",\n    \"failed_update_payment\" => \"更新資料庫中的付款方式失敗\",\n    \"enabled\" => \"已啟用\",\n    \"disabled\" => \"已停用\",\n    // 訂閱\n    \"error_fetching_image\" => \"讀取圖片時發生錯誤\",\n    \"subscription_updated_successfuly\" => \"已成功更新訂閱\",\n    \"subscription_added_successfuly\" => \"已成功新增訂閱\",\n    \"error_deleting_subscription\" => \"刪除訂閱時發生錯誤。\",\n    \"invalid_request_method\" => \"請求方法無效。\",\n    // 使用者\n    \"error_updating_user_data\" => \"更新使用者資訊時發生錯誤。\",\n    \"user_details_saved\" => \"已儲存使用者資訊\",\n    // 管理頁面\n    \"registrations\" => \"註冊\",\n    \"enable_user_registrations\" => \"啟用使用者註冊\",\n    \"maximum_number_users\" => \"最大使用者數\",\n    \"require_email_verification\" => \"需要電子郵件驗證\",\n    \"configure_smtp_settings_to_enable\" => \"需要設定 SMTP 才能啟用\",\n    \"server_url\" => \"伺服器網址\",\n    \"server_url_info\" => \"用於電子郵件驗證和密碼重設。必須是有效的公開網址。\",\n    \"server_url_password_reset\" => \"如果設定，也將啟用密碼重設功能。\",\n    \"disable_login\" => \"停用登入\",\n    \"disable_login_info\" => \"略過登入。如果您的伺服器只在區域網路中執行且無外部存取，可以停用登入。這將自動以管理員身分登入。\",\n    \"disable_login_info2\" => \"只有在停用使用者註冊且使用者數不超過管理員帳號時，才能啟用此設定。\",\n    \"max_users_info\" => \"0 表示無限制\",\n    \"user_management\" => \"使用者管理\",\n    \"delete_user\" => \"刪除使用者\",\n    \"delete_user_info\" => \"刪除使用者也會刪除其所有訂閱和設定。\",\n    \"create_user\" => \"建立使用者\",\n    \"oidc_settings\" => \"OIDC 設定\",\n    \"oidc_auth_enabled\" => \"啟用 OIDC 身份驗證\",\n    \"create_user_automatically\" => \"當使用 OIDC 登入時自動建立使用者\",\n    \"disable_password_login\" => \"停用密碼登入\",\n    \"smtp_settings\" => \"SMTP 設定\",\n    \"smtp_usage_info\" => \"用於密碼重設和其他系統郵件。\",\n    \"security_settings\" => \"安全設定\",\n    \"ssrf_protection_info\" => \"為了防止服務端請求偽造（SSRF）攻擊，Wallos 預設會阻止發送到私人或內部網路位址的 webhook 通知。\",\n    \"local_webhook_info\" => \"如果您需要將 webhook 發送到本地服務（例如 Home Assistant、Gotify 或 Node-RED），請在上方以逗號分隔的列表中輸入它們的 IP 位址或主機名稱（例如 <code>192.168.1.100,192.168.1.101</code>）。\",\n    \"maintenance_tasks\" => \"維護工作\",\n    \"orphaned_logos\" => \"未使用的圖示\",\n    \"update\" => \"更新\",\n    \"new_version_available\" => \"有新版本的 Wallos 可供使用\",\n    \"current_version\" => \"目前版本\",\n    \"latest_version\" => \"最新版本\",\n    \"on_current_version\" => \"您使用的是最新版本的 Wallos。\",\n    \"show_update_notification\" => \"在儀表板上顯示更新通知\",\n    \"cronjobs\" => \"定時工作\",\n    // 電子郵件驗證\n    \"email_verified\" => \"電子郵件驗證成功\",\n    \"email_verification_failed\" => \"電子郵件驗證失敗\",\n    // 日曆\n    \"calendar\" => \"日曆\",\n    \"sun\" => \"日\",\n    \"mon\" => \"一\",\n    \"tue\" => \"二\",\n    \"wed\" => \"三\",\n    \"thu\" => \"四\",\n    \"fri\" => \"五\",\n    \"sat\" => \"六\",\n    \"month-01\" => \"一月\",\n    \"month-02\" => \"二月\",\n    \"month-03\" => \"三月\",\n    \"month-04\" => \"四月\",\n    \"month-05\" => \"五月\",\n    \"month-06\" => \"六月\",\n    \"month-07\" => \"七月\",\n    \"month-08\" => \"八月\",\n    \"month-09\" => \"九月\",\n    \"month-10\" => \"十月\",\n    \"month-11\" => \"十一月\",\n    \"month-12\" => \"十二月\",\n    \"total_cost\" => \"總費用\",\n    \"export_icalendar\" => \"匯出 iCalendar\",\n    \"over_budget_warning\" => \"您超出預算\",\n    // TOTP 頁面\n    \"insert_totp_code\" => \"請輸入 TOTP 驗證碼\",\n\n\n];\n\n\n?>\n"
  },
  {
    "path": "includes/inputvalidation.php",
    "content": "<?php\r\n\r\nfunction validate($value)\r\n{\r\n    $value = trim($value);\r\n    $value = stripslashes($value);\r\n    $value = htmlspecialchars($value);\r\n    return $value;\r\n}\r\n\r\n?>"
  },
  {
    "path": "includes/list_subscriptions.php",
    "content": "<?php\r\n\r\nrequire_once 'i18n/getlang.php';\r\n\r\nfunction getBillingCycle($cycle, $frequency, $i18n)\r\n{\r\n    switch ($cycle) {\r\n        case 1:\r\n            return $frequency == 1 ? translate('Daily', $i18n) : $frequency . \" \" . translate('days', $i18n);\r\n        case 2:\r\n            return $frequency == 1 ? translate('Weekly', $i18n) : $frequency . \" \" . translate('weeks', $i18n);\r\n        case 3:\r\n            return $frequency == 1 ? translate('Monthly', $i18n) : $frequency . \" \" . translate('months', $i18n);\r\n        case 4:\r\n            return $frequency == 1 ? translate('Yearly', $i18n) : $frequency . \" \" . translate('years', $i18n);\r\n    }\r\n}\r\n\r\nfunction getSubscriptionProgress($cycle, $frequency, $next_payment)\r\n{\r\n    $nextPaymentDate = new DateTime($next_payment);\r\n    $currentDate = new DateTime('now');\r\n\r\n    $paymentCycleDays = 30; // Default to monthly\r\n    if ($cycle === 1) {\r\n        $paymentCycleDays = 1 * $frequency;\r\n    } else if ($cycle === 2) {\r\n        $paymentCycleDays = 7 * $frequency;\r\n    } else if ($cycle === 3) {\r\n        $paymentCycleDays = 30 * $frequency;\r\n    } else if ($cycle === 4) {\r\n        $paymentCycleDays = 365 * $frequency;\r\n    }\r\n\r\n    $lastPaymentDate = clone $nextPaymentDate;\r\n    $lastPaymentDate->modify(\"-$paymentCycleDays days\");\r\n\r\n    $totalCycleDays = $lastPaymentDate->diff($nextPaymentDate)->days;\r\n    $daysSinceLastPayment = $lastPaymentDate->diff($currentDate)->days;\r\n\r\n    $subscriptionProgress = 0;\r\n    if ($totalCycleDays > 0) {\r\n        $subscriptionProgress = ($daysSinceLastPayment / $totalCycleDays) * 100;\r\n    }\r\n\r\n    return floor($subscriptionProgress);\r\n}\r\n\r\nfunction getPricePerMonth($cycle, $frequency, $price)\r\n{\r\n    switch ($cycle) {\r\n        case 1:\r\n            $numberOfPaymentsPerMonth = (30 / $frequency);\r\n            return $price * $numberOfPaymentsPerMonth;\r\n        case 2:\r\n            $numberOfPaymentsPerMonth = (4.35 / $frequency);\r\n            return $price * $numberOfPaymentsPerMonth;\r\n        case 3:\r\n            $numberOfPaymentsPerMonth = (1 / $frequency);\r\n            return $price * $numberOfPaymentsPerMonth;\r\n        case 4:\r\n            $numberOfMonths = (12 * $frequency);\r\n            return $price / $numberOfMonths;\r\n    }\r\n}\r\n\r\n\r\nfunction getPriceConverted($price, $currency, $database)\r\n{\r\n    $query = \"SELECT rate FROM currencies WHERE id = :currency\";\r\n    $stmt = $database->prepare($query);\r\n    $stmt->bindParam(':currency', $currency, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $exchangeRate = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($exchangeRate === false) {\r\n        return $price;\r\n    } else {\r\n        $fromRate = $exchangeRate['rate'];\r\n        return $price / $fromRate;\r\n    }\r\n}\r\n\r\nfunction formatPrice($price, $currencyCode, $currencies)\r\n{\r\n    $formattedPrice = CurrencyFormatter::format($price, $currencyCode);\r\n    if (strstr($formattedPrice, $currencyCode)) {\r\n        $symbol = $currencyCode;\r\n        \r\n        foreach ($currencies as $currency) {\r\n\r\n            if ($currency['code'] === $currencyCode) {\r\n                if ($currency['symbol'] != \"\") {\r\n                    $symbol = $currency['symbol'];\r\n                }\r\n                break;\r\n            }\r\n        }\r\n        $formattedPrice = str_replace($currencyCode, $symbol, $formattedPrice);\r\n    }\r\n\r\n    return $formattedPrice;\r\n}\r\n\r\nfunction formatDate($date, $lang = 'en')\r\n{\r\n    $currentYear = date('Y');\r\n    $dateYear = date('Y', strtotime($date));\r\n\r\n    // Determine the date format based on whether the year matches the current year\r\n    $dateFormat = ($currentYear == $dateYear) ? 'MMM d' : 'MMM yyyy';\r\n\r\n    // Validate the locale and fallback to 'en' if unsupported\r\n    if (!in_array($lang, ResourceBundle::getLocales(''))) {\r\n        $lang = 'en'; // Fallback to English\r\n    }\r\n\r\n    // Create an IntlDateFormatter instance for the specified language\r\n    $formatter = new IntlDateFormatter(\r\n        $lang,\r\n        IntlDateFormatter::SHORT,\r\n        IntlDateFormatter::NONE,\r\n        null,\r\n        null,\r\n        $dateFormat\r\n    );\r\n\r\n    // Format the date\r\n    $formattedDate = $formatter->format(new DateTime($date));\r\n\r\n    return $formattedDate;\r\n}\r\n\r\nfunction printSubscriptions($subscriptions, $sort, $categories, $members, $i18n, $colorTheme, $imagePath, $disabledToBottom, $mobileNavigation, $showSubscriptionProgress, $currencies, $lang)\r\n{\r\n    if ($sort === \"price\") {\r\n        usort($subscriptions, function ($a, $b) {\r\n            return $a['price'] < $b['price'] ? 1 : -1;\r\n        });\r\n        if ($disabledToBottom === 'true') {\r\n            usort($subscriptions, function ($a, $b) {\r\n                return $a['inactive'] - $b['inactive'];\r\n            });\r\n        }\r\n    }\r\n\r\n    $currentCategory = 0;\r\n    $currentPayerUserId = 0;\r\n    $currentPaymentMethodId = 0;\r\n    foreach ($subscriptions as $subscription) {\r\n        if ($sort == \"category_id\" && $subscription['category_id'] != $currentCategory) {\r\n            ?>\r\n            <div class=\"subscription-list-title\">\r\n                <?php\r\n                if ($subscription['category_id'] == 1) {\r\n                    echo translate('no_category', $i18n);\r\n                } else {\r\n                    echo $categories[$subscription['category_id']]['name'];\r\n                }\r\n                ?>\r\n            </div>\r\n            <?php\r\n            $currentCategory = $subscription['category_id'];\r\n        }\r\n        if ($sort == \"payer_user_id\" && $subscription['payer_user_id'] != $currentPayerUserId) {\r\n            ?>\r\n            <div class=\"subscription-list-title\">\r\n                <?= $members[$subscription['payer_user_id']]['name'] ?>\r\n            </div>\r\n            <?php\r\n            $currentPayerUserId = $subscription['payer_user_id'];\r\n        }\r\n        if ($sort == \"payment_method_id\" && $subscription['payment_method_id'] != $currentPaymentMethodId) {\r\n            ?>\r\n            <div class=\"subscription-list-title\">\r\n                <?= $subscription['payment_method_name'] ?>\r\n            </div>\r\n            <?php\r\n            $currentPaymentMethodId = $subscription['payment_method_id'];\r\n        }\r\n        ?>\r\n        <div class=\"subscription-container\">\r\n            <?php\r\n            if ($mobileNavigation === 'true') {\r\n                ?>\r\n                <div class=\"mobile-actions\" data-id=\"<?= $subscription['id'] ?>\">\r\n                    <button class=\"mobile-action-clone\"></button>\r\n                    <button class=\"mobile-action-clone\" onClick=\"cloneSubscription(event, <?= $subscription['id'] ?>)\">\r\n                        <?php include $imagePath . \"images/siteicons/svg/mobile-menu/clone.php\"; ?>\r\n                        Clone\r\n                    </button>\r\n                    <button class=\"mobile-action-delete\" onClick=\"deleteSubscription(event, <?= $subscription['id'] ?>)\">\r\n                        <?php include $imagePath . \"images/siteicons/svg/mobile-menu/delete.php\"; ?>\r\n                        Delete\r\n                    </button>\r\n                    <?php\r\n                    if ($subscription['auto_renew'] != 1) {\r\n                        ?>\r\n                        <button class=\"mobile-action-renew\" onClick=\"renewSubscription(event, <?= $subscription['id'] ?>)\">\r\n                            <?php include $imagePath . \"images/siteicons/svg/mobile-menu/renew.php\"; ?>\r\n                            Renew\r\n                        </button>\r\n                        <?php\r\n                    }\r\n                    ?>\r\n                    <button class=\"mobile-action-edit\" onClick=\"openEditSubscription(event, <?= $subscription['id'] ?>)\">\r\n                        <?php include $imagePath . \"images/siteicons/svg/mobile-menu/edit.php\"; ?>\r\n                        Edit\r\n                    </button>\r\n                </div>\r\n                <?php\r\n            }\r\n\r\n            $subscriptionExtraClasses = \"\";\r\n            if ($subscription['inactive']) {\r\n                $subscriptionExtraClasses .= \" inactive\";\r\n            }\r\n            if ($subscription['auto_renew'] != 1) {\r\n                $subscriptionExtraClasses .= \" manual\";\r\n            }\r\n\r\n            $hasLogo = false;\r\n            if ($subscription['logo'] != \"\") {\r\n                $hasLogo = true;\r\n            }\r\n\r\n            ?>\r\n\r\n            <div class=\"subscription<?= $subscriptionExtraClasses ?>\"\r\n                onClick=\"toggleOpenSubscription(<?= $subscription['id'] ?>)\" data-id=\"<?= $subscription['id'] ?>\"\r\n                data-name=\"<?= $subscription['name'] ?>\">\r\n                <div class=\"subscription-main\">\r\n                    <span class=\"logo <?= !$hasLogo ? 'hideOnMobile' : '' ?>\">\r\n                        <?php\r\n                        if ($hasLogo) {\r\n                            ?>\r\n                            <img src=\"<?= $subscription['logo'] ?>\">\r\n                            <?php\r\n                        } else {\r\n                            include $imagePath . \"images/siteicons/svg/logo.php\";\r\n                        }\r\n                        ?>\r\n                    </span>\r\n                    <span class=\"name <?= $hasLogo ? 'hideOnMobile' : '' ?>\"><?= $subscription['name'] ?></span>\r\n                    <span class=\"cycle\"\r\n                        title=\"<?= $subscription['auto_renew'] ? translate(\"automatically_renews\", $i18n) : translate(\"manual_renewal\", $i18n) ?>\">\r\n                        <?php\r\n                        if ($subscription['auto_renew']) {\r\n                            include $imagePath . \"images/siteicons/svg/automatic.php\";\r\n                        } else {\r\n                            include $imagePath . \"images/siteicons/svg/manual.php\";\r\n                        }\r\n                        ?>\r\n                        <?= $subscription['billing_cycle'] ?>\r\n                    </span>\r\n                    <span class=\"next\"><?= formatDate($subscription['next_payment'], $lang) ?></span>\r\n                    <span class=\"price\">\r\n                        <span class=\"value\">\r\n                            <?= formatPrice($subscription['price'], $subscription['currency_code'], $currencies) ?>\r\n                            <?php\r\n                            if (isset($subscription['original_price']) && $subscription['original_price'] != $subscription['price']) {\r\n                                ?>\r\n                                <span\r\n                                    class=\"original_price\">(<?= formatPrice($subscription['original_price'], $subscription['original_currency_code'], $currencies) ?>)</span>\r\n                                <?php\r\n                            }\r\n                            ?>\r\n                        </span>\r\n\r\n                    </span>\r\n                    <span class=\"payment_method\">\r\n                        <img src=\"<?= $subscription['payment_method_icon'] ?>\"\r\n                            title=\"<?= translate('payment_method', $i18n) ?>: <?= $subscription['payment_method_name'] ?>\" />\r\n                    </span>\r\n                    <?php\r\n                    $desktopMenuButtonClass = \"\"; {\r\n                    }\r\n                    if ($mobileNavigation === \"true\") {\r\n                        $desktopMenuButtonClass = \"mobileNavigationHideOnMobile\";\r\n                    }\r\n                    ?>\r\n                    <button type=\"button\" class=\"actions-expand <?= $desktopMenuButtonClass ?>\"\r\n                        onClick=\"expandActions(event, <?= $subscription['id'] ?>)\">\r\n                        <i class=\"fas fa-ellipsis-v\"></i>\r\n                    </button>\r\n                    <ul class=\"actions\">\r\n                        <li class=\"edit\" title=\"<?= translate('edit_subscription', $i18n) ?>\"\r\n                            onClick=\"openEditSubscription(event, <?= $subscription['id'] ?>)\">\r\n                            <?php include $imagePath . \"images/siteicons/svg/edit.php\"; ?>\r\n                            <?= translate('edit_subscription', $i18n) ?>\r\n                        </li>\r\n                        <li class=\"delete\" title=\"<?= translate('delete', $i18n) ?>\"\r\n                            onClick=\"deleteSubscription(event, <?= $subscription['id'] ?>)\">\r\n                            <?php include $imagePath . \"images/siteicons/svg/delete.php\"; ?>\r\n                            <?= translate('delete', $i18n) ?>\r\n                        </li>\r\n                        <li class=\"clone\" title=\"<?= translate('clone', $i18n) ?>\"\r\n                            onClick=\"cloneSubscription(event, <?= $subscription['id'] ?>)\">\r\n                            <?php include $imagePath . \"images/siteicons/svg/clone.php\"; ?>\r\n                            <?= translate('clone', $i18n) ?>\r\n                        </li>\r\n                        <?php\r\n                        if ($subscription['auto_renew'] != 1) {\r\n                            ?>\r\n                            <li class=\"renew\" title=\"<?= translate('renew', $i18n) ?>\"\r\n                                onClick=\"renewSubscription(event, <?= $subscription['id'] ?>)\">\r\n                                <?php include $imagePath . \"images/siteicons/svg/renew.php\"; ?>\r\n                                <?= translate('renew', $i18n) ?>\r\n                            </li>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                    </ul>\r\n                </div>\r\n                <div class=\"subscription-secondary\">\r\n                    <span\r\n                        class=\"name\"><?php include $imagePath . \"images/siteicons/svg/subscription.php\"; ?><?= $subscription['name'] ?></span>\r\n                    <span class=\"payer_user\"\r\n                        title=\"<?= translate('paid_by', $i18n) ?>\"><?php include $imagePath . \"images/siteicons/svg/payment.php\"; ?><?= $members[$subscription['payer_user_id']]['name'] ?></span>\r\n                    <span class=\"category\"\r\n                        title=\"<?= translate('category', $i18n) ?>\"><?php include $imagePath . \"images/siteicons/svg/category.php\"; ?><?= $categories[$subscription['category_id']]['name'] ?></span>\r\n                    <?php\r\n                    if ($subscription['url'] != \"\") {\r\n                        $url = $subscription['url'];\r\n                        if (!preg_match('/^https?:\\/\\//', $url)) {\r\n                            $url = \"https://\" . $url;\r\n                        }\r\n                        ?>\r\n                        <span class=\"url\" title=\"<?= translate('external_url', $i18n) ?>\"><a href=\"<?= $url ?>\" target=\"_blank\"\r\n                                rel=\"noreferrer\"><?php include $imagePath . \"images/siteicons/svg/web.php\"; ?></a></span>\r\n                        <?php\r\n                    }\r\n                    ?>\r\n                </div>\r\n                <?php\r\n                if ($subscription['notes'] != \"\") {\r\n                    ?>\r\n                    <div class=\"subscription-notes\">\r\n                        <span class=\"notes\">\r\n                            <?php include $imagePath . \"images/siteicons/svg/notes.php\"; ?>\r\n                            <?= $subscription['notes'] ?>\r\n                        </span>\r\n                    </div>\r\n                    <?php\r\n                }\r\n                ?>\r\n            </div>\r\n            <?php\r\n            if ($showSubscriptionProgress === 'true') {\r\n                $progress = $subscription['progress'] > 100 ? 100 : $subscription['progress'];\r\n                ?>\r\n                <div class=\"subscription-progress-container\">\r\n                    <span class=\"subscription-progress\" style=\"width: <?= $progress ?>%;\"></span>\r\n                </div>\r\n                <?php\r\n            }\r\n            ?>\r\n        </div>\r\n        <?php\r\n    }\r\n}\r\n\r\n$query = \"SELECT main_currency FROM user WHERE id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\nif ($row !== false) {\r\n    $mainCurrencyId = $row['main_currency'];\r\n} else {\r\n    $mainCurrencyId = $currencies[1]['id'];\r\n}\r\n\r\n?>"
  },
  {
    "path": "includes/oidc/handle_oidc_callback.php",
    "content": "<?php\r\n\r\nfunction generate_username_from_email($email)\r\n{\r\n    if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {\r\n        return null;\r\n    }\r\n    // Take the part before the @, remove non-alphanumeric characters, and lowercase\r\n    $username = strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', explode('@', $email)[0]));\r\n    return $username;\r\n}\r\n\r\n// get OIDC settings\r\n$stmt = $db->prepare('SELECT * FROM oauth_settings WHERE id = 1');\r\n$result = $stmt->execute();\r\n$oidcSettings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n$tokenUrl = $oidcSettings['token_url'];\r\n$redirectUri = $oidcSettings['redirect_url'];\r\n\r\n$postFields = [\r\n    'grant_type' => 'authorization_code',\r\n    'code' => $_GET['code'],\r\n    'redirect_uri' => $redirectUri,\r\n    'client_id' => $oidcSettings['client_id'],\r\n    'client_secret' => $oidcSettings['client_secret'],\r\n];\r\n\r\n$ch = curl_init($tokenUrl);\r\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postFields));\r\ncurl_setopt($ch, CURLOPT_POST, true);\r\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);\r\n$response = curl_exec($ch);\r\ncurl_close($ch);\r\n\r\n$tokenData = json_decode($response, true);\r\nif (!$tokenData || !isset($tokenData['access_token'])) {\r\n    die(\"OIDC token exchange failed.\");\r\n}\r\n\r\n$userInfoUrl = $oidcSettings['user_info_url'];\r\n\r\n$ch = curl_init($userInfoUrl);\r\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\ncurl_setopt($ch, CURLOPT_HTTPHEADER, [\r\n    'Authorization: Bearer ' . $tokenData['access_token']\r\n]);\r\n$response = curl_exec($ch);\r\ncurl_close($ch);\r\n\r\n$userInfo = json_decode($response, true);\r\nif (!$userInfo || !isset($userInfo[$oidcSettings['user_identifier_field']])) {\r\n    die(\"Failed to fetch OIDC user info.\");\r\n}\r\n\r\n$oidcSub = $userInfo[$oidcSettings['user_identifier_field']];\r\n\r\n// Check if sub matches an existing user\r\n$stmt = $db->prepare('SELECT * FROM user WHERE oidc_sub = :oidcSub');\r\n$stmt->bindValue(':oidcSub', $oidcSub, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n$userData = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($userData) {\r\n    // User exists, log the user in\r\n    require_once('oidc_login.php');\r\n\r\n} else {\r\n    // Might be an existing user with the same email\r\n    $email = $userInfo['email'] ?? null;\r\n\r\n    if (!$email) {\r\n        // Login failed, we have nothing to go on with, redirect to login page with error\r\n        header(\"Location: login.php?error=oidc_user_not_found\");\r\n        exit();\r\n    }\r\n\r\n    $stmt = $db->prepare('SELECT * FROM user WHERE email = :email');\r\n    $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n    $result = $stmt->execute();\r\n    $userData = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($userData) {\r\n        // Update existing user with OIDC sub\r\n        $stmt = $db->prepare('UPDATE user SET oidc_sub = :oidcSub WHERE id = :userId');\r\n        $stmt->bindValue(':oidcSub', $oidcSub, SQLITE3_TEXT);\r\n        $stmt->bindValue(':userId', $userData['id'], SQLITE3_INTEGER);\r\n        $stmt->execute();\r\n\r\n        // Log the user in\r\n        require_once('oidc_login.php');\r\n    } else {\r\n        // Check if auto-create is enabled\r\n        if ($oidcSettings['auto_create_user']) {\r\n            // Create a new user\r\n\r\n            //check if username is already taken\r\n            $usernameBase = $userInfo['preferred_username'] ?? generate_username_from_email($email);\r\n            $username = $usernameBase;\r\n            $attempt = 1;\r\n\r\n            while (true) {\r\n                $stmt = $db->prepare('SELECT COUNT(*) as count FROM user WHERE username = :username');\r\n                $stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n                $result = $stmt->execute();\r\n                $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                if ($row['count'] == 0) {\r\n                    break; // Username is available\r\n                }\r\n\r\n                $username = $usernameBase . $attempt;\r\n                $attempt++;\r\n            }\r\n\r\n            require_once('oidc_create_user.php');\r\n\r\n\r\n        } else {\r\n            // Login failed, redirect to login page with error\r\n            header(\"Location: login.php?error=oidc_user_not_found\");\r\n            exit();\r\n        }\r\n    }\r\n}\r\n\r\n\r\n?>"
  },
  {
    "path": "includes/oidc/oidc_create_user.php",
    "content": "<?php\r\n\r\n// Try to extract first and last name from \"name\"\r\n$fullName = $userInfo['name'] ?? '';\r\n$parts = explode(' ', trim($fullName), 2);\r\n$firstname = $parts[0] ?? '';\r\n$lastname = $parts[1] ?? '';\r\n\r\n// Defaults\r\n$language = 'en';\r\n$avatar = \"images/avatars/0.svg\";\r\n$budget = 0;\r\n$main_currency_id = 1; // Euro\r\n$password = bin2hex(random_bytes(16)); // 32-character random password\r\n$hashedPassword = password_hash($password, PASSWORD_DEFAULT);\r\n\r\n// Insert user\r\n$query = \"INSERT INTO user (username, email, oidc_sub, main_currency, avatar, language, budget, firstname, lastname, password)\r\n          VALUES (:username, :email, :oidc_sub, :main_currency, :avatar, :language, :budget, :firstname, :lastname, :password)\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n$stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n$stmt->bindValue(':oidc_sub', $oidcSub, SQLITE3_TEXT);\r\n$stmt->bindValue(':main_currency', $main_currency_id, SQLITE3_INTEGER);\r\n$stmt->bindValue(':avatar', $avatar, SQLITE3_TEXT);\r\n$stmt->bindValue(':language', $language, SQLITE3_TEXT);\r\n$stmt->bindValue(':budget', $budget, SQLITE3_INTEGER);\r\n$stmt->bindValue(':firstname', $firstname, SQLITE3_TEXT);\r\n$stmt->bindValue(':lastname', $lastname, SQLITE3_TEXT);\r\n$stmt->bindValue(':password', $hashedPassword, SQLITE3_TEXT);\r\n\r\nif (!$stmt->execute()) {\r\n    die(\"Failed to create user\");\r\n}\r\n\r\n// Get the user data into $userData\r\n$stmt = $db->prepare(\"SELECT * FROM user WHERE username = :username\");\r\n$stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n$result = $stmt->execute();\r\n$userData = $result->fetchArray(SQLITE3_ASSOC);\r\n$newUserId = $userData['id'];\r\n\r\n// Household\r\n$stmt = $db->prepare(\"INSERT INTO household (name, user_id) VALUES (:name, :user_id)\");\r\n$stmt->bindValue(':name', $username, SQLITE3_TEXT);\r\n$stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n\r\n// Categories\r\n$categories = [\r\n    'No category', 'Entertainment', 'Music', 'Utilities', 'Food & Beverages',\r\n    'Health & Wellbeing', 'Productivity', 'Banking', 'Transport', 'Education',\r\n    'Insurance', 'Gaming', 'News & Magazines', 'Software', 'Technology',\r\n    'Cloud Services', 'Charity & Donations'\r\n];\r\n\r\n$stmt = $db->prepare(\"INSERT INTO categories (name, \\\"order\\\", user_id) VALUES (:name, :order, :user_id)\");\r\nforeach ($categories as $index => $name) {\r\n    $stmt->bindValue(':name', $name, SQLITE3_TEXT);\r\n    $stmt->bindValue(':order', $index + 1, SQLITE3_INTEGER);\r\n    $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n}\r\n\r\n// Payment Methods\r\n$payment_methods = [\r\n    ['name' => 'PayPal', 'icon' => 'images/uploads/icons/paypal.png'],\r\n    ['name' => 'Credit Card', 'icon' => 'images/uploads/icons/creditcard.png'],\r\n    ['name' => 'Bank Transfer', 'icon' => 'images/uploads/icons/banktransfer.png'],\r\n    ['name' => 'Direct Debit', 'icon' => 'images/uploads/icons/directdebit.png'],\r\n    ['name' => 'Money', 'icon' => 'images/uploads/icons/money.png'],\r\n    ['name' => 'Google Pay', 'icon' => 'images/uploads/icons/googlepay.png'],\r\n    ['name' => 'Samsung Pay', 'icon' => 'images/uploads/icons/samsungpay.png'],\r\n    ['name' => 'Apple Pay', 'icon' => 'images/uploads/icons/applepay.png'],\r\n    ['name' => 'Crypto', 'icon' => 'images/uploads/icons/crypto.png'],\r\n    ['name' => 'Klarna', 'icon' => 'images/uploads/icons/klarna.png'],\r\n    ['name' => 'Amazon Pay', 'icon' => 'images/uploads/icons/amazonpay.png'],\r\n    ['name' => 'SEPA', 'icon' => 'images/uploads/icons/sepa.png'],\r\n    ['name' => 'Skrill', 'icon' => 'images/uploads/icons/skrill.png'],\r\n    ['name' => 'Sofort', 'icon' => 'images/uploads/icons/sofort.png'],\r\n    ['name' => 'Stripe', 'icon' => 'images/uploads/icons/stripe.png'],\r\n    ['name' => 'Affirm', 'icon' => 'images/uploads/icons/affirm.png'],\r\n    ['name' => 'AliPay', 'icon' => 'images/uploads/icons/alipay.png'],\r\n    ['name' => 'Elo', 'icon' => 'images/uploads/icons/elo.png'],\r\n    ['name' => 'Facebook Pay', 'icon' => 'images/uploads/icons/facebookpay.png'],\r\n    ['name' => 'GiroPay', 'icon' => 'images/uploads/icons/giropay.png'],\r\n    ['name' => 'iDeal', 'icon' => 'images/uploads/icons/ideal.png'],\r\n    ['name' => 'Union Pay', 'icon' => 'images/uploads/icons/unionpay.png'],\r\n    ['name' => 'Interac', 'icon' => 'images/uploads/icons/interac.png'],\r\n    ['name' => 'WeChat', 'icon' => 'images/uploads/icons/wechat.png'],\r\n    ['name' => 'Paysafe', 'icon' => 'images/uploads/icons/paysafe.png'],\r\n    ['name' => 'Poli', 'icon' => 'images/uploads/icons/poli.png'],\r\n    ['name' => 'Qiwi', 'icon' => 'images/uploads/icons/qiwi.png'],\r\n    ['name' => 'ShopPay', 'icon' => 'images/uploads/icons/shoppay.png'],\r\n    ['name' => 'Venmo', 'icon' => 'images/uploads/icons/venmo.png'],\r\n    ['name' => 'VeriFone', 'icon' => 'images/uploads/icons/verifone.png'],\r\n    ['name' => 'WebMoney', 'icon' => 'images/uploads/icons/webmoney.png'],\r\n];\r\n\r\n$stmt = $db->prepare(\"INSERT INTO payment_methods (name, icon, \\\"order\\\", user_id) VALUES (:name, :icon, :order, :user_id)\");\r\nforeach ($payment_methods as $index => $method) {\r\n    $stmt->bindValue(':name', $method['name'], SQLITE3_TEXT);\r\n    $stmt->bindValue(':icon', $method['icon'], SQLITE3_TEXT);\r\n    $stmt->bindValue(':order', $index + 1, SQLITE3_INTEGER);\r\n    $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n}\r\n\r\n// Currencies\r\n$currencies = [\r\n    ['name' => 'Euro', 'symbol' => '€', 'code' => 'EUR'],\r\n    ['name' => 'US Dollar', 'symbol' => '$', 'code' => 'USD'],\r\n    ['name' => 'Japanese Yen', 'symbol' => '¥', 'code' => 'JPY'],\r\n    ['name' => 'Bulgarian Lev', 'symbol' => 'лв', 'code' => 'BGN'],\r\n    ['name' => 'Czech Republic Koruna', 'symbol' => 'Kč', 'code' => 'CZK'],\r\n    ['name' => 'Danish Krone', 'symbol' => 'kr', 'code' => 'DKK'],\r\n    ['name' => 'British Pound Sterling', 'symbol' => '£', 'code' => 'GBP'],\r\n    ['name' => 'Hungarian Forint', 'symbol' => 'Ft', 'code' => 'HUF'],\r\n    ['name' => 'Polish Zloty', 'symbol' => 'zł', 'code' => 'PLN'],\r\n    ['name' => 'Romanian Leu', 'symbol' => 'lei', 'code' => 'RON'],\r\n    ['name' => 'Swedish Krona', 'symbol' => 'kr', 'code' => 'SEK'],\r\n    ['name' => 'Swiss Franc', 'symbol' => 'Fr', 'code' => 'CHF'],\r\n    ['name' => 'Icelandic Króna', 'symbol' => 'kr', 'code' => 'ISK'],\r\n    ['name' => 'Norwegian Krone', 'symbol' => 'kr', 'code' => 'NOK'],\r\n    ['name' => 'Russian Ruble', 'symbol' => '₽', 'code' => 'RUB'],\r\n    ['name' => 'Turkish Lira', 'symbol' => '₺', 'code' => 'TRY'],\r\n    ['name' => 'Australian Dollar', 'symbol' => '$', 'code' => 'AUD'],\r\n    ['name' => 'Brazilian Real', 'symbol' => 'R$', 'code' => 'BRL'],\r\n    ['name' => 'Canadian Dollar', 'symbol' => '$', 'code' => 'CAD'],\r\n    ['name' => 'Chinese Yuan', 'symbol' => '¥', 'code' => 'CNY'],\r\n    ['name' => 'Hong Kong Dollar', 'symbol' => 'HK$', 'code' => 'HKD'],\r\n    ['name' => 'Indonesian Rupiah', 'symbol' => 'Rp', 'code' => 'IDR'],\r\n    ['name' => 'Israeli New Sheqel', 'symbol' => '₪', 'code' => 'ILS'],\r\n    ['name' => 'Indian Rupee', 'symbol' => '₹', 'code' => 'INR'],\r\n    ['name' => 'South Korean Won', 'symbol' => '₩', 'code' => 'KRW'],\r\n    ['name' => 'Mexican Peso', 'symbol' => 'Mex$', 'code' => 'MXN'],\r\n    ['name' => 'Malaysian Ringgit', 'symbol' => 'RM', 'code' => 'MYR'],\r\n    ['name' => 'New Zealand Dollar', 'symbol' => 'NZ$', 'code' => 'NZD'],\r\n    ['name' => 'Philippine Peso', 'symbol' => '₱', 'code' => 'PHP'],\r\n    ['name' => 'Singapore Dollar', 'symbol' => 'S$', 'code' => 'SGD'],\r\n    ['name' => 'Thai Baht', 'symbol' => '฿', 'code' => 'THB'],\r\n    ['name' => 'South African Rand', 'symbol' => 'R', 'code' => 'ZAR'],\r\n    ['name' => 'Ukrainian Hryvnia', 'symbol' => '₴', 'code' => 'UAH'],\r\n    ['name' => 'New Taiwan Dollar', 'symbol' => 'NT$', 'code' => 'TWD'],\r\n];\r\n\r\n$stmt = $db->prepare(\"INSERT INTO currencies (name, symbol, code, rate, user_id) \r\n                      VALUES (:name, :symbol, :code, :rate, :user_id)\");\r\nforeach ($currencies as $currency) {\r\n    $stmt->bindValue(':name', $currency['name'], SQLITE3_TEXT);\r\n    $stmt->bindValue(':symbol', $currency['symbol'], SQLITE3_TEXT);\r\n    $stmt->bindValue(':code', $currency['code'], SQLITE3_TEXT);\r\n    $stmt->bindValue(':rate', 1.0, SQLITE3_FLOAT);\r\n    $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n}\r\n\r\n// Get actual Euro currency ID\r\n$stmt = $db->prepare(\"SELECT id FROM currencies WHERE code = 'EUR' AND user_id = :user_id\");\r\n$stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$currency = $result->fetchArray(SQLITE3_ASSOC);\r\nif ($currency) {\r\n    $stmt = $db->prepare(\"UPDATE user SET main_currency = :main_currency WHERE id = :user_id\");\r\n    $stmt->bindValue(':main_currency', $currency['id'], SQLITE3_INTEGER);\r\n    $stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n}\r\n\r\n$userData['main_currency'] = $currency['id'];\r\n\r\n// Insert settings\r\n$stmt = $db->prepare(\"INSERT INTO settings (dark_theme, monthly_price, convert_currency, remove_background, color_theme, hide_disabled, user_id, disabled_to_bottom, show_original_price, mobile_nav) \r\n                      VALUES (2, 0, 0, 0, 'blue', 0, :user_id, 0, 0, 0)\");\r\n$stmt->bindValue(':user_id', $newUserId, SQLITE3_INTEGER);\r\n$stmt->execute();\r\n\r\n// Log the user in\r\nrequire_once('oidc_login.php');"
  },
  {
    "path": "includes/oidc/oidc_login.php",
    "content": "<?php\r\n\r\nif (!isset($userData)) {\r\n    die(\"User data missing for OIDC login.\");\r\n}\r\n\r\n$userId = $userData['id'];\r\n$username = $userData['username'];\r\n$language = $userData['language'];\r\n$main_currency = $userData['main_currency'];\r\n\r\n$_SESSION['username'] = $username;\r\n$_SESSION['loggedin'] = true;\r\n$_SESSION['main_currency'] = $main_currency;\r\n$_SESSION['userId'] = $userId;\r\n$_SESSION['from_oidc'] = true; // Indicate this session is from OIDC login\r\n\r\n$cookieExpire = time() + (86400 * 30); // 30 days\r\n\r\n// generate remember token\r\n$token = bin2hex(random_bytes(32));\r\n$addLoginTokens = \"INSERT INTO login_tokens (user_id, token) VALUES (:userId, :token)\";\r\n$addLoginTokensStmt = $db->prepare($addLoginTokens);\r\n$addLoginTokensStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n$addLoginTokensStmt->bindParam(':token', $token, SQLITE3_TEXT);\r\n$addLoginTokensStmt->execute();\r\n\r\n$_SESSION['token'] = $token;\r\n$cookieValue = $username . \"|\" . $token . \"|\" . $main_currency;\r\nsetcookie('wallos_login', $cookieValue, [\r\n    'expires' => $cookieExpire,\r\n    'samesite' => 'Strict',\r\n    'httponly' => true,\r\n]);\r\n\r\n// Set language cookie\r\nsetcookie('language', $language, [\r\n    'expires' => $cookieExpire,\r\n    'samesite' => 'Strict'\r\n]);\r\n\r\n// Set sort order default\r\nif (!isset($_COOKIE['sortOrder'])) {\r\n    setcookie('sortOrder', 'next_payment', [\r\n        'expires' => $cookieExpire,\r\n        'samesite' => 'Strict'\r\n    ]);\r\n}\r\n\r\n// Set color theme\r\n$query = \"SELECT color_theme FROM settings WHERE user_id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$settings = $result->fetchArray(SQLITE3_ASSOC);\r\nsetcookie('colorTheme', $settings['color_theme'], [\r\n    'expires' => $cookieExpire,\r\n    'samesite' => 'Strict'\r\n]);\r\n\r\n// Done\r\n$db->close();\r\nheader(\"Location: .\");\r\nexit();\r\n"
  },
  {
    "path": "includes/sort_options.php",
    "content": "<div class=\"sort-options\" id=\"sort-options\">\r\n    <ul>\r\n        <li <?= $sortOrder == \"name\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('name')\" id=\"sort-name\">\r\n            <?= translate('name', $i18n) ?>\r\n        </li>\r\n        <li <?= $sortOrder == \"id\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('id')\" id=\"sort-id\">\r\n            <?= translate('last_added', $i18n) ?>\r\n        </li>\r\n        <li <?= $sortOrder == \"price\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('price')\" id=\"sort-price\">\r\n            <?= translate('price', $i18n) ?>\r\n        </li>\r\n        <li <?= $sortOrder == \"next_payment\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('next_payment')\"\r\n            id=\"sort-next_payment\"><?= translate('next_payment', $i18n) ?></li>\r\n        <li <?= $sortOrder == \"payer_user_id\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('payer_user_id')\"\r\n            id=\"sort-payer_user_id\"><?= translate('member', $i18n) ?></li>\r\n        <li <?= $sortOrder == \"category_id\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('category_id')\"\r\n            id=\"sort-category_id\"><?= translate('category', $i18n) ?></li>\r\n        <li <?= $sortOrder == \"payment_method_id\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('payment_method_id')\"\r\n            id=\"sort-payment_method_id\">\r\n            <?= translate('payment_method', $i18n) ?>\r\n        </li>\r\n        <?php\r\n        if (!isset($settings['hideDisabledSubscriptions']) || $settings['hideDisabledSubscriptions'] !== 'true') {\r\n            ?>\r\n            <li <?= $sortOrder == \"inactive\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('inactive')\"\r\n                id=\"sort-inactive\"><?= translate('state', $i18n) ?></li>\r\n            <?php\r\n        }\r\n        ?>\r\n        <li <?= $sortOrder == \"alphanumeric\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('alphanumeric')\"\r\n            id=\"sort-alphanumeric\"><?= translate('alphanumeric', $i18n) ?></li>\r\n        <li <?= $sortOrder == \"renewal_type\" ? 'class=\"selected\"' : \"\" ?> onClick=\"setSortOption('renewal_type')\"\r\n            id=\"sort-renewal_type\"><?= translate('renewal_type', $i18n) ?></li>\r\n    </ul>\r\n</div>"
  },
  {
    "path": "includes/ssrf_helper.php",
    "content": "<?php\r\n\r\n/**\r\n * Checks if an IP falls in the RFC 6598 Carrier-Grade NAT range (100.64.0.0/10).\r\n * PHP's FILTER_FLAG_NO_PRIV_RANGE does not cover this range.\r\n * Used by Tailscale and corporate CGNAT environments.\r\n */\r\nfunction is_cgnat_ip($ip) {\r\n    $long = ip2long($ip);\r\n    return $long !== false\r\n        && $long >= ip2long('100.64.0.0')\r\n        && $long <= ip2long('100.127.255.255');\r\n}\r\n\r\n/**\r\n * Validates a webhook URL against SSRF attacks and checks the admin allowlist.\r\n * If validation fails, it kills the script and outputs a JSON error response.\r\n * * @param string $url The destination URL to check\r\n * @param SQLite3 $db The database connection\r\n * @param array $i18n The translation array\r\n * @return array Returns an array with ['host', 'ip', 'port'] for cURL hardening\r\n */\r\nfunction validate_webhook_url_for_ssrf($url, $db, $i18n) {\r\n    $parsedUrl = parse_url($url);\r\n    \r\n    // Fallback if parse_url fails completely\r\n    if (!$parsedUrl || !isset($parsedUrl['host'])) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => translate(\"error\", $i18n)\r\n        ]));\r\n    }\r\n\r\n    $urlHost = $parsedUrl['host'];\r\n    $port = $parsedUrl['port'] ?? '';\r\n    $ip = gethostbyname($urlHost);\r\n\r\n    // CATCH DNS FAILURES\r\n    if ($ip === $urlHost && filter_var($urlHost, FILTER_VALIDATE_IP) === false) {\r\n        die(json_encode([\r\n            \"success\" => false,\r\n            \"message\" => \"Error: Could not resolve the hostname. Please check the URL or your server's DNS.\"\r\n        ]));\r\n    }\r\n\r\n    $hostWithPort = $port ? $urlHost . ':' . $port : $urlHost;\r\n    $ipWithPort = $port ? $ip . ':' . $port : $ip;\r\n\r\n    // Check if it's a private IP\r\n    $is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false || is_cgnat_ip($ip);\r\n\r\n    if ($is_private) {\r\n        $stmt = $db->prepare(\"SELECT local_webhook_notifications_allowlist FROM admin LIMIT 1\");\r\n        $result = $stmt->execute();\r\n        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n        \r\n        $allowlist_str = $row ? $row['local_webhook_notifications_allowlist'] : '';\r\n        $allowlist = array_filter(array_map('trim', explode(',', $allowlist_str)));\r\n        \r\n        if (!in_array($urlHost, $allowlist) && \r\n            !in_array($ip, $allowlist) && \r\n            !in_array($hostWithPort, $allowlist) && \r\n            !in_array($ipWithPort, $allowlist)) {\r\n            \r\n            die(json_encode([\r\n                \"success\" => false,\r\n                \"message\" => \"Security Block: The target IP/Port is private and not present in the Webhook Allowlist.\"\r\n            ]));\r\n        }\r\n    }\r\n\r\n    // Determine the exact port being targeted for cURL DNS rebinding protection\r\n    $targetPort = $port ?: (strtolower($parsedUrl['scheme'] ?? 'http') === 'https' ? 443 : 80);\r\n\r\n    return [\r\n        'host' => $urlHost,\r\n        'ip'   => $ip,\r\n        'port' => $targetPort\r\n    ];\r\n}\r\n\r\n/**\r\n * Non-fatal variant for use in cron jobs (sendnotifications.php).\r\n * Returns the same ['host', 'ip', 'port'] array on success, or false on failure.\r\n * Never calls die() — caller should use continue/skip on false.\r\n * Respects the admin allowlist for private IPs, just like the main function.\r\n *\r\n * @param string $url The destination URL to check\r\n * @param SQLite3 $db The database connection\r\n * @return array|false\r\n */\r\nfunction is_url_safe_for_ssrf($url, $db) {\r\n    $parsedUrl = parse_url($url);\r\n    if (!$parsedUrl || !isset($parsedUrl['host'])) return false;\r\n\r\n    $scheme = strtolower($parsedUrl['scheme'] ?? '');\r\n    if (!in_array($scheme, ['http', 'https'])) return false;\r\n\r\n    $urlHost = $parsedUrl['host'];\r\n    $port    = $parsedUrl['port'] ?? '';\r\n    $ip      = gethostbyname($urlHost);\r\n\r\n    // DNS failure\r\n    if ($ip === $urlHost && filter_var($urlHost, FILTER_VALIDATE_IP) === false) return false;\r\n\r\n    $hostWithPort = $port ? $urlHost . ':' . $port : $urlHost;\r\n    $ipWithPort   = $port ? $ip . ':' . $port : $ip;\r\n\r\n    $is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false\r\n               || is_cgnat_ip($ip);\r\n\r\n    if ($is_private) {\r\n        $stmt  = $db->prepare(\"SELECT local_webhook_notifications_allowlist FROM admin LIMIT 1\");\r\n        $result = $stmt->execute();\r\n        $row   = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n        $allowlist_str = $row ? $row['local_webhook_notifications_allowlist'] : '';\r\n        $allowlist     = array_filter(array_map('trim', explode(',', $allowlist_str)));\r\n\r\n        if (\r\n            !in_array($urlHost, $allowlist) &&\r\n            !in_array($ip, $allowlist) &&\r\n            !in_array($hostWithPort, $allowlist) &&\r\n            !in_array($ipWithPort, $allowlist)\r\n        ) {\r\n            return false; // private and not in allowlist — skip silently\r\n        }\r\n    }\r\n\r\n    $targetPort = $port ?: ($scheme === 'https' ? 443 : 80);\r\n\r\n    return [\r\n        'host' => $urlHost,\r\n        'ip'   => $ip,\r\n        'port' => $targetPort\r\n    ];\r\n}\r\n"
  },
  {
    "path": "includes/stats_calculations.php",
    "content": "<?php\r\n\r\nfunction getPricePerMonth($cycle, $frequency, $price)\r\n{\r\n    switch ($cycle) {\r\n        case 1:\r\n            $numberOfPaymentsPerMonth = (30 / $frequency);\r\n            return $price * $numberOfPaymentsPerMonth;\r\n        case 2:\r\n            $numberOfPaymentsPerMonth = (4.35 / $frequency);\r\n            return $price * $numberOfPaymentsPerMonth;\r\n        case 3:\r\n            $numberOfPaymentsPerMonth = (1 / $frequency);\r\n            return $price * $numberOfPaymentsPerMonth;\r\n        case 4:\r\n            $numberOfMonths = (12 * $frequency);\r\n            return $price / $numberOfMonths;\r\n    }\r\n}\r\n\r\nfunction getPriceConverted($price, $currency, $database, $userId)\r\n{\r\n    $query = \"SELECT rate FROM currencies WHERE id = :currency AND user_id = :userId\";\r\n    $stmt = $database->prepare($query);\r\n    $stmt->bindParam(':currency', $currency, SQLITE3_INTEGER);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $exchangeRate = $result->fetchArray(SQLITE3_ASSOC);\r\n    if ($exchangeRate === false) {\r\n        return $price;\r\n    } else {\r\n        $fromRate = $exchangeRate['rate'];\r\n        return $price / $fromRate;\r\n    }\r\n}\r\n\r\n// Get categories\r\n$categories = array();\r\n$query = \"SELECT * FROM categories WHERE user_id = :userId ORDER BY 'order' ASC\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n  $categoryId = $row['id'];\r\n  $categories[$categoryId] = $row;\r\n  $categories[$categoryId]['count'] = 0;\r\n  $categoryCost[$categoryId]['cost'] = 0;\r\n  $categoryCost[$categoryId]['name'] = $row['name'];\r\n}\r\n\r\n// Get payment methods\r\n$paymentMethods = array();\r\n$query = \"SELECT * FROM payment_methods WHERE user_id = :userId AND enabled = 1\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n  $paymentMethodId = $row['id'];\r\n  $paymentMethods[$paymentMethodId] = $row;\r\n  $paymentMethods[$paymentMethodId]['count'] = 0;\r\n  $paymentMethodsCount[$paymentMethodId]['count'] = 0;\r\n  $paymentMethodsCount[$paymentMethodId]['name'] = $row['name'];\r\n}\r\n\r\n//Get household members\r\n$members = array();\r\n$query = \"SELECT * FROM household WHERE user_id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n  $memberId = $row['id'];\r\n  $members[$memberId] = $row;\r\n  $members[$memberId]['count'] = 0;\r\n  $memberCost[$memberId]['cost'] = 0;\r\n  $memberCost[$memberId]['name'] = $row['name'];\r\n}\r\n\r\n$activeSubscriptions = 0;\r\n$inactiveSubscriptions = 0;\r\n// Calculate total monthly price\r\n$mostExpensiveSubscription = array();\r\n$mostExpensiveSubscription['price'] = 0;\r\n$amountDueThisMonth = 0;\r\n$totalCostPerMonth = 0;\r\n$totalSavingsPerMonth = 0;\r\n$totalCostsInReplacementsPerMonth = 0;\r\n\r\n$statsSubtitleParts = [];\r\n$query = \"SELECT name, price, logo, frequency, cycle, currency_id, next_payment, payer_user_id, category_id, payment_method_id, inactive, replacement_subscription_id FROM subscriptions\";\r\n$conditions = [];\r\n$params = [];\r\n\r\nif (isset($_GET['member'])) {\r\n    $conditions[] = \"payer_user_id = :member\";\r\n    $params[':member'] = $_GET['member'];\r\n    $statsSubtitleParts[] = $members[$_GET['member']]['name'];\r\n}\r\n\r\nif (isset($_GET['category'])) {\r\n    $conditions[] = \"category_id = :category\";\r\n    $params[':category'] = $_GET['category'];\r\n    $statsSubtitleParts[] = $categories[$_GET['category']]['name'] == \"No category\" ? translate(\"no_category\", $i18n) : $categories[$_GET['category']]['name'];\r\n}\r\n\r\nif (isset($_GET['payment'])) {\r\n    $conditions[] = \"payment_method_id = :payment\";\r\n    $params[':payment'] = $_GET['payment'];\r\n    $statsSubtitleParts[] = $paymentMethodsCount[$_GET['payment']]['name'];\r\n}\r\n\r\n$conditions[] = \"user_id = :userId\";\r\n$params[':userId'] = $userId;\r\n\r\nif (!empty($conditions)) {\r\n    $query .= \" WHERE \" . implode(' AND ', $conditions);\r\n}\r\n\r\n$stmt = $db->prepare($query);\r\n$statsSubtitle = !empty($statsSubtitleParts) ? '(' . implode(', ', $statsSubtitleParts) . ')' : \"\";\r\n\r\nforeach ($params as $key => $value) {\r\n    $stmt->bindValue($key, $value, SQLITE3_INTEGER);\r\n}\r\n\r\n$result = $stmt->execute();\r\n$usesMultipleCurrencies = false;\r\n\r\nif ($result) {\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $subscriptions[] = $row;\r\n    }\r\n    if (isset($subscriptions)) {\r\n        $replacementSubscriptions = array();\r\n\r\n        foreach ($subscriptions as $subscription) {\r\n            $name = $subscription['name'];\r\n            $price = $subscription['price'];\r\n            $logo = $subscription['logo'];\r\n            $frequency = $subscription['frequency'];\r\n            $cycle = $subscription['cycle'];\r\n            $currency = $subscription['currency_id'];\r\n            if ($currency != $userData['main_currency']) {\r\n                $usesMultipleCurrencies = true;\r\n            }\r\n            $next_payment = $subscription['next_payment'];\r\n            $payerId = $subscription['payer_user_id'];\r\n            $members[$payerId]['count'] += 1;\r\n            $categoryId = $subscription['category_id'];\r\n            $categories[$categoryId]['count'] += 1;\r\n            $paymentMethodId = $subscription['payment_method_id'];\r\n            $paymentMethods[$paymentMethodId]['count'] += 1;\r\n            $inactive = $subscription['inactive'];\r\n            $replacementSubscriptionId = $subscription['replacement_subscription_id'];\r\n            $originalSubscriptionPrice = getPriceConverted($price, $currency, $db, $userId);\r\n            $price = getPricePerMonth($cycle, $frequency, $originalSubscriptionPrice);\r\n\r\n            if ($inactive == 0) {\r\n                $activeSubscriptions++;\r\n                $totalCostPerMonth += $price;\r\n                $memberCost[$payerId]['cost'] += $price;\r\n                $categoryCost[$categoryId]['cost'] += $price;\r\n                $paymentMethodsCount[$paymentMethodId]['count'] += 1;\r\n                if ($price > $mostExpensiveSubscription['price']) {\r\n                    $mostExpensiveSubscription['price'] = $price;\r\n                    $mostExpensiveSubscription['name'] = $name;\r\n                    $mostExpensiveSubscription['logo'] = $logo;\r\n                }\r\n\r\n                // Calculate ammount due this month\r\n                $nextPaymentDate = DateTime::createFromFormat('Y-m-d', trim($next_payment));\r\n                $tomorrow = new DateTime('tomorrow');\r\n                $endOfMonth = new DateTime('last day of this month');\r\n\r\n                if ($nextPaymentDate >= $tomorrow && $nextPaymentDate <= $endOfMonth) {\r\n                    $timesToPay = 1;\r\n                    $daysInMonth = $endOfMonth->diff($tomorrow)->days + 1;\r\n                    $daysRemaining = $endOfMonth->diff($nextPaymentDate)->days + 1;\r\n                    if ($cycle == 1) {\r\n                        $timesToPay = $daysRemaining / $frequency;\r\n                    }\r\n                    if ($cycle == 2) {\r\n                        $weeksInMonth = ceil($daysInMonth / 7);\r\n                        $weeksRemaining = ceil($daysRemaining / 7);\r\n                        $timesToPay = $weeksRemaining / $frequency;\r\n                    }\r\n                    $amountDueThisMonth += $originalSubscriptionPrice * $timesToPay;\r\n                }\r\n            } else {\r\n                $inactiveSubscriptions++;\r\n                $totalSavingsPerMonth += $price;\r\n\r\n                // Check if it has a replacement subscription and if it was not already counted\r\n                if ($replacementSubscriptionId && !in_array($replacementSubscriptionId, $replacementSubscriptions)) {\r\n                    $query = \"SELECT price, currency_id, cycle, frequency FROM subscriptions WHERE id = :replacementSubscriptionId\";\r\n                    $stmt = $db->prepare($query);\r\n                    $stmt->bindValue(':replacementSubscriptionId', $replacementSubscriptionId, SQLITE3_INTEGER);\r\n                    $result = $stmt->execute();\r\n                    $replacementSubscription = $result->fetchArray(SQLITE3_ASSOC);\r\n                    if ($replacementSubscription) {\r\n                        $replacementSubscriptionPrice = getPriceConverted($replacementSubscription['price'], $replacementSubscription['currency_id'], $db, $userId);\r\n                        $replacementSubscriptionPrice = getPricePerMonth($replacementSubscription['cycle'], $replacementSubscription['frequency'], $replacementSubscriptionPrice);\r\n                        $totalCostsInReplacementsPerMonth += $replacementSubscriptionPrice;\r\n                    }\r\n                }\r\n\r\n                $replacementSubscriptions[] = $replacementSubscriptionId;\r\n            }\r\n\r\n        }\r\n\r\n        // Subtract the total cost of replacement subscriptions from the total savings\r\n        $totalSavingsPerMonth -= $totalCostsInReplacementsPerMonth;\r\n\r\n        // Calculate yearly price\r\n        $totalCostPerYear = $totalCostPerMonth * 12;\r\n\r\n        // Calculate average subscription monthly cost\r\n        if ($activeSubscriptions > 0) {\r\n            $averageSubscriptionCost = $totalCostPerMonth / $activeSubscriptions;\r\n        } else {\r\n            $totalCostPerYear = 0;\r\n            $averageSubscriptionCost = 0;\r\n        }\r\n    } else {\r\n        $totalCostPerYear = 0;\r\n        $averageSubscriptionCost = 0;\r\n    }\r\n}\r\n\r\n$showVsBudgetGraph = false;\r\n$vsBudgetDataPoints = [];\r\nif (isset($userData['budget']) && $userData['budget'] > 0) {\r\n    $budget = $userData['budget'];\r\n    $budgetLeft = $budget - $totalCostPerMonth;\r\n    $budgetLeft = $budgetLeft < 0 ? 0 : $budgetLeft;\r\n    $budgetUsed = ($totalCostPerMonth / $budget) * 100;\r\n    $budgetUsed = $budgetUsed > 100 ? 100 : $budgetUsed;\r\n    if ($totalCostPerMonth > $budget) {\r\n        $overBudgetAmount = $totalCostPerMonth - $budget;\r\n    }\r\n    $showVsBudgetGraph = true;\r\n    $vsBudgetDataPoints = [\r\n        [\r\n            \"label\" => translate('budget_remaining', $i18n),\r\n            \"y\" => $budgetLeft,\r\n        ],\r\n        [\r\n            \"label\" => translate('total_cost', $i18n),\r\n            \"y\" => $totalCostPerMonth,\r\n        ],\r\n    ];\r\n}\r\n\r\n$showCantConverErrorMessage = false;\r\nif ($usesMultipleCurrencies) {\r\n    $query = \"SELECT api_key FROM fixer WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    if ($result->fetchArray(SQLITE3_ASSOC) === false) {\r\n        $showCantConverErrorMessage = true;\r\n    }\r\n}\r\n\r\n$query = \"SELECT * FROM total_yearly_cost WHERE user_id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\n$totalMonthlyCostDataPoints = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $totalMonthlyCostDataPoints[] = [\r\n        \"label\" => html_entity_decode($row['date']),\r\n        \"y\" => round($row['cost'] / 12, 2),\r\n    ];\r\n}\r\n\r\n$showTotalMonthlyCostGraph = count($totalMonthlyCostDataPoints) > 1;\r\n\r\n?>"
  },
  {
    "path": "includes/validate_endpoint.php",
    "content": "<?php\r\n// All requests should be POST requests\r\n// CSRF Token must be included and match the token stored on the session\r\n// User must be logged in\r\n\r\nrequire_once __DIR__ . '/../libs/csrf.php';\r\n\r\nif ($_SERVER['REQUEST_METHOD'] !== 'POST') {\r\n    echo json_encode([\"success\" => false, \"message\" => \"Invalid request method\"]);\r\n    exit;\r\n}\r\n\r\n$csrf = $_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');\r\nif (!verify_csrf_token($csrf)) {\r\n    echo json_encode([\"success\" => false, \"message\" => \"Invalid CSRF token\"]);\r\n    exit;\r\n}\r\n\r\nif (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {\r\n    echo json_encode([\"success\" => false, \"message\" => translate('session_expired', $i18n)]);\r\n    exit;\r\n}"
  },
  {
    "path": "includes/validate_endpoint_admin.php",
    "content": "<?php\r\nrequire_once __DIR__ . '/validate_endpoint.php';\r\n// Check that user is an admin\r\nif ($userId !== 1) {\r\n    die(json_encode([\r\n        \"success\" => false,\r\n        \"message\" => translate('error', $i18n)\r\n    ]));\r\n}"
  },
  {
    "path": "includes/version.php",
    "content": "<?php\r\n$version = \"v4.7.2\";\r\n?>"
  },
  {
    "path": "index.php",
    "content": "<?php\r\n\r\nrequire_once 'includes/header.php';\r\nrequire_once 'includes/getdbkeys.php';\r\n\r\nfunction formatPrice($price, $currencyCode, $currencies)\r\n{\r\n    $formattedPrice = CurrencyFormatter::format($price, $currencyCode);\r\n    if (strstr($formattedPrice, $currencyCode)) {\r\n        $symbol = $currencyCode;\r\n\r\n        foreach ($currencies as $currency) {\r\n\r\n            if ($currency['code'] === $currencyCode) {\r\n                if ($currency['symbol'] != \"\") {\r\n                    $symbol = $currency['symbol'];\r\n                }\r\n                break;\r\n            }\r\n        }\r\n        $formattedPrice = str_replace($currencyCode, $symbol, $formattedPrice);\r\n    }\r\n\r\n    return $formattedPrice;\r\n}\r\n\r\nfunction formatDate($date, $lang = 'en')\r\n{\r\n    $currentYear = date('Y');\r\n    $dateYear = date('Y', strtotime($date));\r\n\r\n    // Determine the date format based on whether the year matches the current year\r\n    $dateFormat = ($currentYear == $dateYear) ? 'MMM d' : 'MMM yyyy';\r\n\r\n    // Validate the locale and fallback to 'en' if unsupported\r\n    if (!in_array($lang, ResourceBundle::getLocales(''))) {\r\n        $lang = 'en'; // Fallback to English\r\n    }\r\n\r\n    // Create an IntlDateFormatter instance for the specified language\r\n    $formatter = new IntlDateFormatter(\r\n        $lang,\r\n        IntlDateFormatter::SHORT,\r\n        IntlDateFormatter::NONE,\r\n        null,\r\n        null,\r\n        $dateFormat\r\n    );\r\n\r\n    // Format the date\r\n    $formattedDate = $formatter->format(new DateTime($date));\r\n\r\n    return $formattedDate;\r\n}\r\n\r\n// Get the first name of the user\r\n$stmt = $db->prepare(\"SELECT username, firstname FROM user WHERE id = :userId\");\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$user = $result->fetchArray(SQLITE3_ASSOC);\r\n$first_name = $user['firstname'] ?? $user['username'] ?? '';\r\n\r\n// Fetch the next 3 enabled subscriptions up for payment\r\n$stmt = $db->prepare(\"SELECT id, logo, name, price, currency_id, next_payment, inactive FROM subscriptions WHERE user_id = :userId AND next_payment >= date('now') AND inactive = 0 ORDER BY next_payment ASC LIMIT 3\");\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$upcomingSubscriptions = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $upcomingSubscriptions[] = $row;\r\n}\r\n\r\n// Fetch enabled subscriptions with manual renewal that are overdue\r\n$stmt = $db->prepare(\"SELECT id, logo, name, price, currency_id, next_payment, inactive, auto_renew FROM subscriptions WHERE user_id = :userId AND next_payment < date('now') AND auto_renew = 0 AND inactive = 0 ORDER BY next_payment ASC\");\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$overdueSubscriptions = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $overdueSubscriptions[] = $row;\r\n}\r\n$hasOverdueSubscriptions = !empty($overdueSubscriptions);\r\n\r\nrequire_once 'includes/stats_calculations.php';\r\n\r\n// Get AI Recommendations for user\r\n$stmt = $db->prepare(\"SELECT * FROM ai_recommendations WHERE user_id = :userId\");\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$aiRecommendations = [];\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $aiRecommendations[] = $row;\r\n}\r\n\r\n?>\r\n\r\n<section class=\"contain dashboard\">\r\n    <h1><?= translate('hello', $i18n) ?> <?= htmlspecialchars($first_name) ?></h1>\r\n\r\n    <?php\r\n    // If there are overdue subscriptions, display them\r\n    if ($hasOverdueSubscriptions) {\r\n        ?>\r\n        <div class=\"overdue-subscriptions\">\r\n            <h2><?= translate('overdue_renewals', $i18n) ?></h2>\r\n            <div class=\"dashboard-subscriptions-container\">\r\n                <div class=\"dashboard-subscriptions-list\">\r\n                    <?php\r\n\r\n                    foreach ($overdueSubscriptions as $subscription) {\r\n                        $subscriptionLogo = \"images/uploads/logos/\" . $subscription['logo'];\r\n                        $subscriptionName = htmlspecialchars($subscription['name']);\r\n                        $subscriptionPrice = $subscription['price'];\r\n                        $subscriptionCurrency = $subscription['currency_id'];\r\n                        $subscriptionNextPayment = $subscription['next_payment'];\r\n                        $subscriptionDisplayNextPayment = date('F j', strtotime($subscriptionNextPayment));\r\n                        $subscriptionDisplayPrice = formatPrice($subscriptionPrice, $currencies[$subscriptionCurrency]['code'], $currencies);\r\n\r\n                        ?>\r\n                        <div class=\"subscription-item\">\r\n                            <?php\r\n                            if (empty($subscription['logo'])) {\r\n                                ?>\r\n                                <p class=\"subscription-item-title\"><?= $subscriptionName ?></p>\r\n                                <?php\r\n                            } else {\r\n                                ?>\r\n                                <img src=\"<?= $subscriptionLogo ?>\" alt=\"<?= $subscriptionName ?> logo\"\r\n                                    class=\"subscription-item-logo\" title=\"<?= $subscriptionName ?>\">\r\n                                <?php\r\n                            }\r\n                            ?>\r\n                            <div class=\"subscription-item-info\">\r\n                                <p class=\"subscription-item-date\"> <?= formatDate($subscriptionDisplayNextPayment, $lang) ?>\r\n                                </p>\r\n                                <p class=\"subscription-item-price\"> <?= $subscriptionDisplayPrice ?></p>\r\n                            </div>\r\n                        </div>\r\n                        <?php\r\n                    }\r\n                    ?>\r\n                </div>\r\n            </div>\r\n        </div>\r\n        <?php\r\n    }\r\n    ?>\r\n\r\n    <div class=\"upcoming-subscriptions\">\r\n        <h2><?= translate('upcoming_payments', $i18n) ?></h2>\r\n        <div class=\"dashboard-subscriptions-container\">\r\n            <div class=\"dashboard-subscriptions-list\">\r\n                <?php\r\n                if (empty($upcomingSubscriptions)) {\r\n                    ?>\r\n                    <p><?= translate('no_upcoming_payments', $i18n) ?></p>\r\n                    <?php\r\n                } else {\r\n                    foreach ($upcomingSubscriptions as $subscription) {\r\n                        $subscriptionLogo = \"images/uploads/logos/\" . $subscription['logo'];\r\n                        $subscriptionName = htmlspecialchars($subscription['name']);\r\n                        $subscriptionPrice = $subscription['price'];\r\n                        $subscriptionCurrency = $subscription['currency_id'];\r\n                        $subscriptionNextPayment = $subscription['next_payment'];\r\n                        $subscriptionDisplayNextPayment = date('F j', strtotime($subscriptionNextPayment));\r\n                        $subscriptionDisplayPrice = formatPrice($subscriptionPrice, $currencies[$subscriptionCurrency]['code'], $currencies);\r\n\r\n                        ?>\r\n                        <div class=\"subscription-item\">\r\n                            <?php\r\n                            if (empty($subscription['logo'])) {\r\n                                ?>\r\n                                <p class=\"subscription-item-title\"><?= $subscriptionName ?></p>\r\n                                <?php\r\n                            } else {\r\n                                ?>\r\n                                <img src=\"<?= $subscriptionLogo ?>\" alt=\"<?= $subscriptionName ?> logo\"\r\n                                    class=\"subscription-item-logo\" title=\"<?= $subscriptionName ?>\">\r\n                                <?php\r\n                            }\r\n                            ?>\r\n                            <div class=\"subscription-item-info\">\r\n                                <p class=\"subscription-item-date\"> <?= formatDate($subscriptionDisplayNextPayment, $lang) ?></p>\r\n                                <p class=\"subscription-item-price\"> <?= $subscriptionDisplayPrice ?></p>\r\n                            </div>\r\n                        </div>\r\n                        <?php\r\n                    }\r\n                }\r\n                ?>\r\n            </div>\r\n        </div>\r\n\r\n        <?php if (!empty($aiRecommendations)) { ?>\r\n            <div class=\"ai-recommendations\">\r\n                <h2><?= translate('ai_recommendations', $i18n) ?></h2>\r\n                <div class=\"ai-recommendations-container\">\r\n                    <ul class=\"ai-recommendations-list\">\r\n                        <?php\r\n\r\n                        foreach ($aiRecommendations as $key => $recommendation) { ?>\r\n                            <li class=\"ai-recommendation-item\" data-id=\"<?= $recommendation['id'] ?>\">\r\n                                <div class=\"ai-recommendation-header\">\r\n                                    <h3>\r\n                                        <span><?= ($key + 1) . \". \" ?></span>\r\n                                        <?= htmlspecialchars($recommendation['title']) ?>\r\n                                    </h3>\r\n                                    <span class=\"item-arrow-down fa fa-caret-down\"></span>\r\n                                </div>\r\n                                <p class=\"collapsible\"><?= htmlspecialchars($recommendation['description']) ?></p>\r\n                                <p class=\"ai-recommendation-savings\">\r\n                                    <?= htmlspecialchars($recommendation['savings']) ?>\r\n                                    <span>\r\n                                        <a href=\"#\" class=\"delete-ai-recommendation\" title=\"<?= translate('delete', $i18n) ?>\">\r\n                                            <i class=\"fa fa-trash\"></i>\r\n                                        </a>\r\n                                    </span>\r\n                                </p>\r\n                            </li>\r\n                        <?php } ?>\r\n                    </ul>\r\n                </div>\r\n            </div>\r\n\r\n        <?php } ?>\r\n\r\n        <?php if (isset($amountDueThisMonth) || isset($budget) || isset($budgetUsed) || isset($budgetLeft) || isset($overBudgetAmount)) { ?>\r\n            <div class=\"budget-subscriptions\">\r\n                <h2><?= translate('your_budget', $i18n) ?></h2>\r\n                <div class=\"dashboard-subscriptions-container\">\r\n                    <div class=\"dashboard-subscriptions-list\">\r\n                        <?php if (isset($amountDueThisMonth)) { ?>\r\n                            <div class=\"subscription-item thin\">\r\n                                <p class=\"subscription-item-title\"><?= translate(\"amount_due\", $i18n) ?></p>\r\n                                <div class=\"subscription-item-info\">\r\n                                    <p class=\"subscription-item-value\">\r\n                                        <?= CurrencyFormatter::format($amountDueThisMonth, $currencies[$userData['main_currency']]['code']) ?>\r\n                                    </p>\r\n                                </div>\r\n                            </div>\r\n                        <?php } ?>\r\n                        <?php if (isset($budget) && $budget > 0) { ?>\r\n                            <div class=\"subscription-item thin\">\r\n                                <p class=\"subscription-item-title\"><?= translate(\"budget\", $i18n) ?></p>\r\n                                <div class=\"subscription-item-info\">\r\n                                    <p class=\"subscription-item-value\">\r\n                                        <?= formatPrice($budget, $currencies[$userData['main_currency']]['code'], $currencies) ?>\r\n                                    </p>\r\n                                </div>\r\n                            </div>\r\n                        <?php } ?>\r\n                        <?php if (isset($budgetUsed)) { ?>\r\n                            <div class=\"subscription-item thin\">\r\n                                <p class=\"subscription-item-title\"><?= translate(\"budget_used\", $i18n) ?></p>\r\n                                <div class=\"subscription-item-info\">\r\n                                    <p class=\"subscription-item-value\">\r\n                                        <?= number_format($budgetUsed, 2) ?>%\r\n                                    </p>\r\n                                </div>\r\n                            </div>\r\n                        <?php } ?>\r\n                        <?php if (isset($budgetLeft)) { ?>\r\n                            <div class=\"subscription-item thin\">\r\n                                <p class=\"subscription-item-title\"><?= translate(\"budget_remaining\", $i18n) ?></p>\r\n                                <div class=\"subscription-item-info\">\r\n                                    <p class=\"subscription-item-value\">\r\n                                        <?= formatPrice($budgetLeft, $currencies[$userData['main_currency']]['code'], $currencies) ?>\r\n                                    </p>\r\n                                </div>\r\n                            </div>\r\n                        <?php } ?>\r\n                        <?php if (isset($overBudgetAmount) && $overBudgetAmount > 0) { ?>\r\n                            <div class=\"subscription-item thin\">\r\n                                <p class=\"subscription-item-title\"><?= translate(\"over_budget\", $i18n) ?></p>\r\n                                <div class=\"subscription-item-info\">\r\n                                    <p class=\"subscription-item-value\">\r\n                                        <?= formatPrice($overBudgetAmount, $currencies[$userData['main_currency']]['code'], $currencies) ?>\r\n                                    </p>\r\n                                </div>\r\n                            </div>\r\n                        <?php } ?>\r\n                    </div>\r\n                </div>\r\n            </div>\r\n        <?php } ?>\r\n    </div>\r\n\r\n    <?php if (isset($activeSubscriptions) && $activeSubscriptions > 0) { ?>\r\n        <div class=\"current-subscriptions\">\r\n            <h2><?= translate('your_subscriptions', $i18n) ?></h2>\r\n            <div class=\"dashboard-subscriptions-container\">\r\n                <div class=\"dashboard-subscriptions-list\">\r\n                    <div class=\"subscription-item thin\">\r\n                        <p class=\"subscription-item-title\"><?= translate('active_subscriptions', $i18n) ?></p>\r\n                        <div class=\"subscription-item-info\">\r\n                            <p class=\"subscription-item-value\"><?= $activeSubscriptions ?></p>\r\n                        </div>\r\n                    </div>\r\n\r\n                    <?php if (isset($totalCostPerMonth)) { ?>\r\n                        <div class=\"subscription-item thin\">\r\n                            <p class=\"subscription-item-title\"><?= translate('monthly_cost', $i18n) ?></p>\r\n                            <div class=\"subscription-item-info\">\r\n                                <p class=\"subscription-item-value\">\r\n                                    <?= CurrencyFormatter::format($totalCostPerMonth, $currencies[$userData['main_currency']]['code']) ?>\r\n                                </p>\r\n                            </div>\r\n                        </div>\r\n                    <?php } ?>\r\n\r\n                    <?php if (isset($totalCostPerYear)) { ?>\r\n                        <div class=\"subscription-item thin\">\r\n                            <p class=\"subscription-item-title\"><?= translate('yearly_cost', $i18n) ?></p>\r\n                            <div class=\"subscription-item-info\">\r\n                                <p class=\"subscription-item-value\">\r\n                                    <?= CurrencyFormatter::format($totalCostPerYear, $currencies[$userData['main_currency']]['code']) ?>\r\n                                </p>\r\n                            </div>\r\n                        </div>\r\n                    <?php } ?>\r\n                </div>\r\n            </div>\r\n        </div>\r\n    <?php } ?>\r\n\r\n    <?php if (isset($inactiveSubscriptions) && $inactiveSubscriptions > 0) { ?>\r\n        <div class=\"savings-subscriptions\">\r\n            <h2><?= translate('your_savings', $i18n) ?></h2>\r\n            <div class=\"dashboard-subscriptions-container\">\r\n                <div class=\"dashboard-subscriptions-list\">\r\n                    <div class=\"subscription-item thin\">\r\n                        <p class=\"subscription-item-title\"><?= translate('inactive_subscriptions', $i18n) ?></p>\r\n                        <div class=\"subscription-item-info\">\r\n                            <p class=\"subscription-item-value\"><?= $inactiveSubscriptions ?></p>\r\n                        </div>\r\n                    </div>\r\n\r\n                    <?php if (isset($totalSavingsPerMonth) && $totalSavingsPerMonth > 0) { ?>\r\n                        <div class=\"subscription-item thin\">\r\n                            <p class=\"subscription-item-title\"><?= translate('monthly_savings', $i18n) ?></p>\r\n                            <div class=\"subscription-item-info\">\r\n                                <p class=\"subscription-item-value\">\r\n                                    <?= CurrencyFormatter::format($totalSavingsPerMonth, $currencies[$userData['main_currency']]['code']) ?>\r\n                                </p>\r\n                            </div>\r\n                        </div>\r\n\r\n                        <div class=\"subscription-item thin\">\r\n                            <p class=\"subscription-item-title\"><?= translate('yearly_savings', $i18n) ?></p>\r\n                            <div class=\"subscription-item-info\">\r\n                                <p class=\"subscription-item-value\">\r\n                                    <?= CurrencyFormatter::format($totalSavingsPerMonth * 12, $currencies[$userData['main_currency']]['code']) ?>\r\n                                </p>\r\n                            </div>\r\n                        </div>\r\n                    <?php } ?>\r\n                </div>\r\n            </div>\r\n        </div>\r\n    <?php } ?>\r\n\r\n</section>\r\n\r\n\r\n<script src=\"scripts/dashboard.js?<?= $version ?>\"></script>\r\n\r\n<?php\r\nrequire_once 'includes/footer.php';\r\n?>"
  },
  {
    "path": "libs/OTPHP/Factory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\nuse InvalidArgumentException;\nuse Psr\\Clock\\ClockInterface;\nuse Throwable;\nuse function assert;\nuse function count;\n\n/**\n * This class is used to load OTP object from a provisioning Uri.\n *\n * @see \\OTPHP\\Test\\FactoryTest\n */\nfinal class Factory implements FactoryInterface\n{\n    public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface\n    {\n        try {\n            $parsed_url = Url::fromString($uri);\n            $parsed_url->getScheme() === 'otpauth' || throw new InvalidArgumentException('Invalid scheme.');\n        } catch (Throwable $throwable) {\n            throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable);\n        }\n        if ($clock === null) {\n            trigger_deprecation(\n                'spomky-labs/otphp',\n                '11.3.0',\n                'The parameter \"$clock\" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of \"null\".'\n            );\n            $clock = new InternalClock();\n        }\n\n        $otp = self::createOTP($parsed_url, $clock);\n\n        self::populateOTP($otp, $parsed_url);\n\n        return $otp;\n    }\n\n    private static function populateParameters(OTPInterface $otp, Url $data): void\n    {\n        foreach ($data->getQuery() as $key => $value) {\n            $otp->setParameter($key, $value);\n        }\n    }\n\n    private static function populateOTP(OTPInterface $otp, Url $data): void\n    {\n        self::populateParameters($otp, $data);\n        $result = explode(':', rawurldecode(mb_substr($data->getPath(), 1)));\n\n        if (count($result) < 2) {\n            $otp->setIssuerIncludedAsParameter(false);\n\n            return;\n        }\n\n        if ($otp->getIssuer() !== null) {\n            $result[0] === $otp->getIssuer() || throw new InvalidArgumentException(\n                'Invalid OTP: invalid issuer in parameter'\n            );\n            $otp->setIssuerIncludedAsParameter(true);\n        }\n\n        assert($result[0] !== '');\n\n        $otp->setIssuer($result[0]);\n    }\n\n    private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface\n    {\n        switch ($parsed_url->getHost()) {\n            case 'totp':\n                $totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock);\n                $totp->setLabel(self::getLabel($parsed_url->getPath()));\n\n                return $totp;\n            case 'hotp':\n                $hotp = HOTP::createFromSecret($parsed_url->getSecret());\n                $hotp->setLabel(self::getLabel($parsed_url->getPath()));\n\n                return $hotp;\n            default:\n                throw new InvalidArgumentException(sprintf('Unsupported \"%s\" OTP type', $parsed_url->getHost()));\n        }\n    }\n\n    /**\n     * @param non-empty-string $data\n     * @return non-empty-string\n     */\n    private static function getLabel(string $data): string\n    {\n        $result = explode(':', rawurldecode(mb_substr($data, 1)));\n        $label = count($result) === 2 ? $result[1] : $result[0];\n        assert($label !== '');\n\n        return $label;\n    }\n}\n"
  },
  {
    "path": "libs/OTPHP/FactoryInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\ninterface FactoryInterface\n{\n    /**\n     * This method is the unique public method of the class. It can load a provisioning Uri and convert it into an OTP\n     * object.\n     *\n     * @param non-empty-string $uri\n     */\n    public static function loadFromProvisioningUri(string $uri): OTPInterface;\n}\n"
  },
  {
    "path": "libs/OTPHP/HOTP.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\nuse InvalidArgumentException;\nuse function is_int;\n\n/**\n * @see \\OTPHP\\Test\\HOTPTest\n */\nfinal class HOTP extends OTP implements HOTPInterface\n{\n    private const DEFAULT_WINDOW = 0;\n\n    public static function create(\n        null|string $secret = null,\n        int $counter = self::DEFAULT_COUNTER,\n        string $digest = self::DEFAULT_DIGEST,\n        int $digits = self::DEFAULT_DIGITS\n    ): self {\n        $htop = $secret !== null\n            ? self::createFromSecret($secret)\n            : self::generate()\n        ;\n        $htop->setCounter($counter);\n        $htop->setDigest($digest);\n        $htop->setDigits($digits);\n\n        return $htop;\n    }\n\n    public static function createFromSecret(string $secret): self\n    {\n        $htop = new self($secret);\n        $htop->setCounter(self::DEFAULT_COUNTER);\n        $htop->setDigest(self::DEFAULT_DIGEST);\n        $htop->setDigits(self::DEFAULT_DIGITS);\n\n        return $htop;\n    }\n\n    public static function generate(): self\n    {\n        return self::createFromSecret(self::generateSecret());\n    }\n\n    /**\n     * @return 0|positive-int\n     */\n    public function getCounter(): int\n    {\n        $value = $this->getParameter('counter');\n        (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid \"counter\" parameter.');\n\n        return $value;\n    }\n\n    public function getProvisioningUri(): string\n    {\n        return $this->generateURI('hotp', [\n            'counter' => $this->getCounter(),\n        ]);\n    }\n\n    /**\n     * If the counter is not provided, the OTP is verified at the actual counter.\n     *\n     * @param null|0|positive-int $counter\n     */\n    public function verify(string $otp, null|int $counter = null, null|int $window = null): bool\n    {\n        $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');\n\n        if ($counter === null) {\n            $counter = $this->getCounter();\n        } elseif ($counter < $this->getCounter()) {\n            return false;\n        }\n\n        return $this->verifyOtpWithWindow($otp, $counter, $window);\n    }\n\n    public function setCounter(int $counter): void\n    {\n        $this->setParameter('counter', $counter);\n    }\n\n    /**\n     * @return array<non-empty-string, callable>\n     */\n    protected function getParameterMap(): array\n    {\n        return [...parent::getParameterMap(), ...[\n            'counter' => static function (mixed $value): int {\n                $value = (int) $value;\n                $value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');\n\n                return $value;\n            },\n        ]];\n    }\n\n    private function updateCounter(int $counter): void\n    {\n        $this->setCounter($counter);\n    }\n\n    /**\n     * @param null|0|positive-int $window\n     */\n    private function getWindow(null|int $window): int\n    {\n        return abs($window ?? self::DEFAULT_WINDOW);\n    }\n\n    /**\n     * @param non-empty-string $otp\n     * @param 0|positive-int $counter\n     * @param null|0|positive-int $window\n     */\n    private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool\n    {\n        $window = $this->getWindow($window);\n\n        for ($i = $counter; $i <= $counter + $window; ++$i) {\n            if ($this->compareOTP($this->at($i), $otp)) {\n                $this->updateCounter($i + 1);\n\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "libs/OTPHP/HOTPInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\ninterface HOTPInterface extends OTPInterface\n{\n    public const DEFAULT_COUNTER = 0;\n\n    /**\n     * The initial counter (a positive integer).\n     */\n    public function getCounter(): int;\n\n    /**\n     * Create a new HOTP object.\n     *\n     * If the secret is null, a random 64 bytes secret will be generated.\n     *\n     * @param null|non-empty-string $secret\n     * @param 0|positive-int $counter\n     * @param non-empty-string $digest\n     * @param positive-int $digits\n     *\n     * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead\n     */\n    public static function create(\n        null|string $secret = null,\n        int $counter = 0,\n        string $digest = 'sha1',\n        int $digits = 6\n    ): self;\n\n    public function setCounter(int $counter): void;\n}\n"
  },
  {
    "path": "libs/OTPHP/InternalClock.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\nuse DateTimeImmutable;\nuse Psr\\Clock\\ClockInterface;\n\n/**\n * @internal\n */\nfinal class InternalClock implements ClockInterface\n{\n    public function now(): DateTimeImmutable\n    {\n        return new DateTimeImmutable();\n    }\n}\n"
  },
  {
    "path": "libs/OTPHP/OTP.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\nuse Exception;\nuse InvalidArgumentException;\nuse ParagonIE\\ConstantTime\\Base32;\nuse RuntimeException;\nuse function assert;\nuse function chr;\nuse function count;\nuse function is_string;\nuse const STR_PAD_LEFT;\n\nabstract class OTP implements OTPInterface\n{\n    use ParameterTrait;\n\n    private const DEFAULT_SECRET_SIZE = 64;\n\n    /**\n     * @param non-empty-string $secret\n     */\n    protected function __construct(string $secret)\n    {\n        $this->setSecret($secret);\n    }\n\n    public function getQrCodeUri(string $uri, string $placeholder): string\n    {\n        $provisioning_uri = urlencode($this->getProvisioningUri());\n\n        return str_replace($placeholder, $provisioning_uri, $uri);\n    }\n\n    /**\n     * @param 0|positive-int $input\n     */\n    public function at(int $input): string\n    {\n        return $this->generateOTP($input);\n    }\n\n    /**\n     * @return non-empty-string\n     */\n    final protected static function generateSecret(): string\n    {\n        return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));\n    }\n\n    /**\n     * The OTP at the specified input.\n     *\n     * @param 0|positive-int $input\n     *\n     * @return non-empty-string\n     */\n    protected function generateOTP(int $input): string\n    {\n        $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);\n        $unpacked = unpack('C*', $hash);\n        $unpacked !== false || throw new InvalidArgumentException('Invalid data.');\n        $hmac = array_values($unpacked);\n\n        $offset = ($hmac[count($hmac) - 1] & 0xF);\n        $code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);\n        $otp = $code % (10 ** $this->getDigits());\n\n        return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT);\n    }\n\n    /**\n     * @param array<non-empty-string, mixed> $options\n     */\n    protected function filterOptions(array &$options): void\n    {\n        foreach ([\n            'algorithm' => 'sha1',\n            'period' => 30,\n            'digits' => 6,\n        ] as $key => $default) {\n            if (isset($options[$key]) && $default === $options[$key]) {\n                unset($options[$key]);\n            }\n        }\n\n        ksort($options);\n    }\n\n    /**\n     * @param non-empty-string $type\n     * @param array<non-empty-string, mixed> $options\n     *\n     * @return non-empty-string\n     */\n    protected function generateURI(string $type, array $options): string\n    {\n        $label = $this->getLabel();\n        is_string($label) || throw new InvalidArgumentException('The label is not set.');\n        $this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.');\n        $options = [...$options, ...$this->getParameters()];\n        $this->filterOptions($options);\n        $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));\n\n        return sprintf(\n            'otpauth://%s/%s?%s',\n            $type,\n            rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),\n            $params\n        );\n    }\n\n    /**\n     * @param non-empty-string $safe\n     * @param non-empty-string $user\n     */\n    protected function compareOTP(string $safe, string $user): bool\n    {\n        return hash_equals($safe, $user);\n    }\n\n    /**\n     * @return non-empty-string\n     */\n    private function getDecodedSecret(): string\n    {\n        try {\n            $decoded = Base32::decodeUpper($this->getSecret());\n        } catch (Exception) {\n            throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');\n        }\n        assert($decoded !== '');\n\n        return $decoded;\n    }\n\n    private function intToByteString(int $int): string\n    {\n        $result = [];\n        while ($int !== 0) {\n            $result[] = chr($int & 0xFF);\n            $int >>= 8;\n        }\n\n        return str_pad(implode('', array_reverse($result)), 8, \"\\000\", STR_PAD_LEFT);\n    }\n}\n"
  },
  {
    "path": "libs/OTPHP/OTPInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\ninterface OTPInterface\n{\n    public const DEFAULT_DIGITS = 6;\n\n    public const DEFAULT_DIGEST = 'sha1';\n\n    /**\n     * Create a OTP object from an existing secret.\n     *\n     * @param non-empty-string $secret\n     */\n    public static function createFromSecret(string $secret): self;\n\n    /**\n     * Create a new OTP object. A random 64 bytes secret will be generated.\n     */\n    public static function generate(): self;\n\n    /**\n     * @param non-empty-string $secret\n     */\n    public function setSecret(string $secret): void;\n\n    public function setDigits(int $digits): void;\n\n    /**\n     * @param non-empty-string $digest\n     */\n    public function setDigest(string $digest): void;\n\n    /**\n     * Generate the OTP at the specified input.\n     *\n     * @param 0|positive-int $input\n     *\n     * @return non-empty-string Return the OTP at the specified timestamp\n     */\n    public function at(int $input): string;\n\n    /**\n     * Verify that the OTP is valid with the specified input. If no input is provided, the input is set to a default\n     * value or false is returned.\n     *\n     * @param non-empty-string $otp\n     * @param null|0|positive-int $input\n     * @param null|0|positive-int $window\n     */\n    public function verify(string $otp, null|int $input = null, null|int $window = null): bool;\n\n    /**\n     * @return non-empty-string The secret of the OTP\n     */\n    public function getSecret(): string;\n\n    /**\n     * @param non-empty-string $label The label of the OTP\n     */\n    public function setLabel(string $label): void;\n\n    /**\n     * @return non-empty-string|null The label of the OTP\n     */\n    public function getLabel(): null|string;\n\n    /**\n     * @return non-empty-string|null The issuer\n     */\n    public function getIssuer(): ?string;\n\n    /**\n     * @param non-empty-string $issuer\n     */\n    public function setIssuer(string $issuer): void;\n\n    /**\n     * @return bool If true, the issuer will be added as a parameter in the provisioning URI\n     */\n    public function isIssuerIncludedAsParameter(): bool;\n\n    public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void;\n\n    /**\n     * @return positive-int Number of digits in the OTP\n     */\n    public function getDigits(): int;\n\n    /**\n     * @return non-empty-string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512'\n     */\n    public function getDigest(): string;\n\n    /**\n     * @param non-empty-string $parameter\n     */\n    public function getParameter(string $parameter): mixed;\n\n    /**\n     * @param non-empty-string $parameter\n     */\n    public function hasParameter(string $parameter): bool;\n\n    /**\n     * @return array<non-empty-string, mixed>\n     */\n    public function getParameters(): array;\n\n    /**\n     * @param non-empty-string $parameter\n     */\n    public function setParameter(string $parameter, mixed $value): void;\n\n    /**\n     * Get the provisioning URI.\n     *\n     * @return non-empty-string\n     */\n    public function getProvisioningUri(): string;\n\n    /**\n     * Get the provisioning URI.\n     *\n     * @param non-empty-string $uri         The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method.\n     * @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI\n     */\n    public function getQrCodeUri(string $uri, string $placeholder): string;\n}\n"
  },
  {
    "path": "libs/OTPHP/ParameterTrait.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\nuse InvalidArgumentException;\nuse function array_key_exists;\nuse function assert;\nuse function in_array;\nuse function is_int;\nuse function is_string;\n\ntrait ParameterTrait\n{\n    /**\n     * @var array<non-empty-string, mixed>\n     */\n    private array $parameters = [];\n\n    /**\n     * @var non-empty-string|null\n     */\n    private null|string $issuer = null;\n\n    /**\n     * @var non-empty-string|null\n     */\n    private null|string $label = null;\n\n    private bool $issuer_included_as_parameter = true;\n\n    /**\n     * @return array<non-empty-string, mixed>\n     */\n    public function getParameters(): array\n    {\n        $parameters = $this->parameters;\n\n        if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {\n            $parameters['issuer'] = $this->getIssuer();\n        }\n\n        return $parameters;\n    }\n\n    public function getSecret(): string\n    {\n        $value = $this->getParameter('secret');\n        (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid \"secret\" parameter.');\n\n        return $value;\n    }\n\n    public function getLabel(): null|string\n    {\n        return $this->label;\n    }\n\n    public function setLabel(string $label): void\n    {\n        $this->setParameter('label', $label);\n    }\n\n    public function getIssuer(): null|string\n    {\n        return $this->issuer;\n    }\n\n    public function setIssuer(string $issuer): void\n    {\n        $this->setParameter('issuer', $issuer);\n    }\n\n    public function isIssuerIncludedAsParameter(): bool\n    {\n        return $this->issuer_included_as_parameter;\n    }\n\n    public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void\n    {\n        $this->issuer_included_as_parameter = $issuer_included_as_parameter;\n    }\n\n    public function getDigits(): int\n    {\n        $value = $this->getParameter('digits');\n        (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid \"digits\" parameter.');\n\n        return $value;\n    }\n\n    public function getDigest(): string\n    {\n        $value = $this->getParameter('algorithm');\n        (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid \"algorithm\" parameter.');\n\n        return $value;\n    }\n\n    public function hasParameter(string $parameter): bool\n    {\n        return array_key_exists($parameter, $this->parameters);\n    }\n\n    public function getParameter(string $parameter): mixed\n    {\n        if ($this->hasParameter($parameter)) {\n            return $this->getParameters()[$parameter];\n        }\n\n        throw new InvalidArgumentException(sprintf('Parameter \"%s\" does not exist', $parameter));\n    }\n\n    public function setParameter(string $parameter, mixed $value): void\n    {\n        $map = $this->getParameterMap();\n\n        if (array_key_exists($parameter, $map) === true) {\n            $callback = $map[$parameter];\n            $value = $callback($value);\n        }\n\n        if (property_exists($this, $parameter)) {\n            $this->{$parameter} = $value;\n        } else {\n            $this->parameters[$parameter] = $value;\n        }\n    }\n\n    public function setSecret(string $secret): void\n    {\n        $this->setParameter('secret', $secret);\n    }\n\n    public function setDigits(int $digits): void\n    {\n        $this->setParameter('digits', $digits);\n    }\n\n    public function setDigest(string $digest): void\n    {\n        $this->setParameter('algorithm', $digest);\n    }\n\n    /**\n     * @return array<non-empty-string, callable>\n     */\n    protected function getParameterMap(): array\n    {\n        return [\n            'label' => function (string $value): string {\n                assert($value !== '');\n                $this->hasColon($value) === false || throw new InvalidArgumentException(\n                    'Label must not contain a colon.'\n                );\n\n                return $value;\n            },\n            'secret' => static fn (string $value): string => mb_strtoupper(trim($value, '=')),\n            'algorithm' => static function (string $value): string {\n                $value = mb_strtolower($value);\n                in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(\n                    'The \"%s\" digest is not supported.',\n                    $value\n                ));\n\n                return $value;\n            },\n            'digits' => static function ($value): int {\n                $value > 0 || throw new InvalidArgumentException('Digits must be at least 1.');\n\n                return (int) $value;\n            },\n            'issuer' => function (string $value): string {\n                assert($value !== '');\n                $this->hasColon($value) === false || throw new InvalidArgumentException(\n                    'Issuer must not contain a colon.'\n                );\n\n                return $value;\n            },\n        ];\n    }\n\n    /**\n     * @param non-empty-string $value\n     */\n    private function hasColon(string $value): bool\n    {\n        $colons = [':', '%3A', '%3a'];\n        foreach ($colons as $colon) {\n            if (str_contains($value, $colon)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "libs/OTPHP/TOTP.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\nuse InvalidArgumentException;\nuse Psr\\Clock\\ClockInterface;\nuse function assert;\nuse function is_int;\n\n/**\n * @see \\OTPHP\\Test\\TOTPTest\n */\nfinal class TOTP extends OTP implements TOTPInterface\n{\n    private readonly ClockInterface $clock;\n\n    public function __construct(string $secret, ?ClockInterface $clock = null)\n    {\n        parent::__construct($secret);\n        if ($clock === null) {\n            trigger_deprecation(\n                'spomky-labs/otphp',\n                '11.3.0',\n                'The parameter \"$clock\" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of \"null\".'\n            );\n            $clock = new InternalClock();\n        }\n\n        $this->clock = $clock;\n    }\n\n    public static function create(\n        null|string $secret = null,\n        int $period = self::DEFAULT_PERIOD,\n        string $digest = self::DEFAULT_DIGEST,\n        int $digits = self::DEFAULT_DIGITS,\n        int $epoch = self::DEFAULT_EPOCH,\n        ?ClockInterface $clock = null\n    ): self {\n        $totp = $secret !== null\n            ? self::createFromSecret($secret, $clock)\n            : self::generate($clock)\n        ;\n        $totp->setPeriod($period);\n        $totp->setDigest($digest);\n        $totp->setDigits($digits);\n        $totp->setEpoch($epoch);\n\n        return $totp;\n    }\n\n    public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self\n    {\n        $totp = new self($secret, $clock);\n        $totp->setPeriod(self::DEFAULT_PERIOD);\n        $totp->setDigest(self::DEFAULT_DIGEST);\n        $totp->setDigits(self::DEFAULT_DIGITS);\n        $totp->setEpoch(self::DEFAULT_EPOCH);\n\n        return $totp;\n    }\n\n    public static function generate(?ClockInterface $clock = null): self\n    {\n        return self::createFromSecret(self::generateSecret(), $clock);\n    }\n\n    public function getPeriod(): int\n    {\n        $value = $this->getParameter('period');\n        (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid \"period\" parameter.');\n\n        return $value;\n    }\n\n    public function getEpoch(): int\n    {\n        $value = $this->getParameter('epoch');\n        (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid \"epoch\" parameter.');\n\n        return $value;\n    }\n\n    public function expiresIn(): int\n    {\n        $period = $this->getPeriod();\n\n        return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());\n    }\n\n    /**\n     * The OTP at the specified input.\n     *\n     * @param 0|positive-int $input\n     */\n    public function at(int $input): string\n    {\n        return $this->generateOTP($this->timecode($input));\n    }\n\n    public function now(): string\n    {\n        $timestamp = $this->clock->now()\n            ->getTimestamp();\n        assert($timestamp >= 0, 'The timestamp must return a positive integer.');\n\n        return $this->at($timestamp);\n    }\n\n    /**\n     * If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will\n     * allow time drift. The passed value is in seconds.\n     *\n     * @param 0|positive-int $timestamp\n     * @param null|0|positive-int $leeway\n     */\n    public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool\n    {\n        $timestamp ??= $this->clock->now()\n            ->getTimestamp();\n        $timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');\n\n        if ($leeway === null) {\n            return $this->compareOTP($this->at($timestamp), $otp);\n        }\n\n        $leeway = abs($leeway);\n        $leeway < $this->getPeriod() || throw new InvalidArgumentException(\n            'The leeway must be lower than the TOTP period'\n        );\n        $timestampMinusLeeway = $timestamp - $leeway;\n        $timestampMinusLeeway >= 0 || throw new InvalidArgumentException(\n            'The timestamp must be greater than or equal to the leeway.'\n        );\n\n        return $this->compareOTP($this->at($timestampMinusLeeway), $otp)\n            || $this->compareOTP($this->at($timestamp), $otp)\n            || $this->compareOTP($this->at($timestamp + $leeway), $otp);\n    }\n\n    public function getProvisioningUri(): string\n    {\n        $params = [];\n        if ($this->getPeriod() !== 30) {\n            $params['period'] = $this->getPeriod();\n        }\n\n        if ($this->getEpoch() !== 0) {\n            $params['epoch'] = $this->getEpoch();\n        }\n\n        return $this->generateURI('totp', $params);\n    }\n\n    public function setPeriod(int $period): void\n    {\n        $this->setParameter('period', $period);\n    }\n\n    public function setEpoch(int $epoch): void\n    {\n        $this->setParameter('epoch', $epoch);\n    }\n\n    /**\n     * @return array<non-empty-string, callable>\n     */\n    protected function getParameterMap(): array\n    {\n        return [\n            ...parent::getParameterMap(),\n            'period' => static function ($value): int {\n                (int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.');\n\n                return (int) $value;\n            },\n            'epoch' => static function ($value): int {\n                (int) $value >= 0 || throw new InvalidArgumentException(\n                    'Epoch must be greater than or equal to 0.'\n                );\n\n                return (int) $value;\n            },\n        ];\n    }\n\n    /**\n     * @param array<non-empty-string, mixed> $options\n     */\n    protected function filterOptions(array &$options): void\n    {\n        parent::filterOptions($options);\n\n        if (isset($options['epoch']) && $options['epoch'] === 0) {\n            unset($options['epoch']);\n        }\n\n        ksort($options);\n    }\n\n    /**\n     * @param 0|positive-int $timestamp\n     *\n     * @return 0|positive-int\n     */\n    private function timecode(int $timestamp): int\n    {\n        $timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());\n        assert($timecode >= 0);\n\n        return $timecode;\n    }\n}\n"
  },
  {
    "path": "libs/OTPHP/TOTPInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\ninterface TOTPInterface extends OTPInterface\n{\n    public const DEFAULT_PERIOD = 30;\n\n    public const DEFAULT_EPOCH = 0;\n\n    /**\n     * Create a new TOTP object.\n     *\n     * If the secret is null, a random 64 bytes secret will be generated.\n     *\n     * @param null|non-empty-string $secret\n     * @param positive-int $period\n     * @param non-empty-string $digest\n     * @param positive-int $digits\n     *\n     * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead\n     */\n    public static function create(\n        null|string $secret = null,\n        int $period = self::DEFAULT_PERIOD,\n        string $digest = self::DEFAULT_DIGEST,\n        int $digits = self::DEFAULT_DIGITS\n    ): self;\n\n    public function setPeriod(int $period): void;\n\n    public function setEpoch(int $epoch): void;\n\n    /**\n     * Return the TOTP at the current time.\n     *\n     * @return non-empty-string\n     */\n    public function now(): string;\n\n    /**\n     * Get the period of time for OTP generation (a non-null positive integer, in second).\n     */\n    public function getPeriod(): int;\n\n    public function expiresIn(): int;\n\n    public function getEpoch(): int;\n}\n"
  },
  {
    "path": "libs/OTPHP/Url.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace OTPHP;\n\nuse InvalidArgumentException;\nuse function array_key_exists;\nuse function is_string;\n\n/**\n * @internal\n */\nfinal class Url\n{\n    /**\n     * @param non-empty-string $scheme\n     * @param non-empty-string $host\n     * @param non-empty-string $path\n     * @param non-empty-string $secret\n     * @param array<non-empty-string, mixed> $query\n     */\n    public function __construct(\n        private readonly string $scheme,\n        private readonly string $host,\n        private readonly string $path,\n        private readonly string $secret,\n        private readonly array $query\n    ) {\n    }\n\n    /**\n     * @return non-empty-string\n     */\n    public function getScheme(): string\n    {\n        return $this->scheme;\n    }\n\n    /**\n     * @return non-empty-string\n     */\n    public function getHost(): string\n    {\n        return $this->host;\n    }\n\n    /**\n     * @return non-empty-string\n     */\n    public function getPath(): string\n    {\n        return $this->path;\n    }\n\n    /**\n     * @return non-empty-string\n     */\n    public function getSecret(): string\n    {\n        return $this->secret;\n    }\n\n    /**\n     * @return array<non-empty-string, mixed>\n     */\n    public function getQuery(): array\n    {\n        return $this->query;\n    }\n\n    /**\n     * @param non-empty-string $uri\n     */\n    public static function fromString(string $uri): self\n    {\n        $parsed_url = parse_url($uri);\n        $parsed_url !== false || throw new InvalidArgumentException('Invalid URI.');\n        foreach (['scheme', 'host', 'path', 'query'] as $key) {\n            array_key_exists($key, $parsed_url) || throw new InvalidArgumentException(\n                'Not a valid OTP provisioning URI'\n            );\n        }\n        $scheme = $parsed_url['scheme'] ?? null;\n        $host = $parsed_url['host'] ?? null;\n        $path = $parsed_url['path'] ?? null;\n        $query = $parsed_url['query'] ?? null;\n        $scheme === 'otpauth' || throw new InvalidArgumentException('Not a valid OTP provisioning URI');\n        is_string($host) || throw new InvalidArgumentException('Invalid URI.');\n        is_string($path) || throw new InvalidArgumentException('Invalid URI.');\n        is_string($query) || throw new InvalidArgumentException('Invalid URI.');\n        $parsedQuery = [];\n        parse_str($query, $parsedQuery);\n        array_key_exists('secret', $parsedQuery) || throw new InvalidArgumentException(\n            'Not a valid OTP provisioning URI'\n        );\n        $secret = $parsedQuery['secret'];\n        unset($parsedQuery['secret']);\n\n        return new self($scheme, $host, $path, $secret, $parsedQuery);\n    }\n}\n"
  },
  {
    "path": "libs/PHPMailer/Exception.php",
    "content": "<?php\n\n/**\n * PHPMailer Exception class.\n * PHP Version 5.5.\n *\n * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project\n *\n * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>\n * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>\n * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>\n * @author    Brent R. Matzelle (original founder)\n * @copyright 2012 - 2020 Marcus Bointon\n * @copyright 2010 - 2012 Jim Jagielski\n * @copyright 2004 - 2009 Andy Prevost\n * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License\n * @note      This program is distributed in the hope that it will be useful - WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n * FITNESS FOR A PARTICULAR PURPOSE.\n */\n\nnamespace PHPMailer\\PHPMailer;\n\n/**\n * PHPMailer exception handler.\n *\n * @author Marcus Bointon <phpmailer@synchromedia.co.uk>\n */\nclass Exception extends \\Exception\n{\n    /**\n     * Prettify error message output.\n     *\n     * @return string\n     */\n    public function errorMessage()\n    {\n        return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . \"</strong><br />\\n\";\n    }\n}\n"
  },
  {
    "path": "libs/PHPMailer/PHPMailer.php",
    "content": "<?php\n\n/**\n * PHPMailer - PHP email creation and transport class.\n * PHP Version 5.5.\n *\n * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project\n *\n * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>\n * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>\n * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>\n * @author    Brent R. Matzelle (original founder)\n * @copyright 2012 - 2020 Marcus Bointon\n * @copyright 2010 - 2012 Jim Jagielski\n * @copyright 2004 - 2009 Andy Prevost\n * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License\n * @note      This program is distributed in the hope that it will be useful - WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n * FITNESS FOR A PARTICULAR PURPOSE.\n */\n\nnamespace PHPMailer\\PHPMailer;\n\n/**\n * PHPMailer - PHP email creation and transport class.\n *\n * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>\n * @author Jim Jagielski (jimjag) <jimjag@gmail.com>\n * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>\n * @author Brent R. Matzelle (original founder)\n */\nclass PHPMailer\n{\n    const CHARSET_ASCII = 'us-ascii';\n    const CHARSET_ISO88591 = 'iso-8859-1';\n    const CHARSET_UTF8 = 'utf-8';\n\n    const CONTENT_TYPE_PLAINTEXT = 'text/plain';\n    const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';\n    const CONTENT_TYPE_TEXT_HTML = 'text/html';\n    const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';\n    const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';\n    const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';\n\n    const ENCODING_7BIT = '7bit';\n    const ENCODING_8BIT = '8bit';\n    const ENCODING_BASE64 = 'base64';\n    const ENCODING_BINARY = 'binary';\n    const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';\n\n    const ENCRYPTION_STARTTLS = 'tls';\n    const ENCRYPTION_SMTPS = 'ssl';\n\n    const ICAL_METHOD_REQUEST = 'REQUEST';\n    const ICAL_METHOD_PUBLISH = 'PUBLISH';\n    const ICAL_METHOD_REPLY = 'REPLY';\n    const ICAL_METHOD_ADD = 'ADD';\n    const ICAL_METHOD_CANCEL = 'CANCEL';\n    const ICAL_METHOD_REFRESH = 'REFRESH';\n    const ICAL_METHOD_COUNTER = 'COUNTER';\n    const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';\n\n    /**\n     * Email priority.\n     * Options: null (default), 1 = High, 3 = Normal, 5 = low.\n     * When null, the header is not set at all.\n     *\n     * @var int|null\n     */\n    public $Priority;\n\n    /**\n     * The character set of the message.\n     *\n     * @var string\n     */\n    public $CharSet = self::CHARSET_ISO88591;\n\n    /**\n     * The MIME Content-type of the message.\n     *\n     * @var string\n     */\n    public $ContentType = self::CONTENT_TYPE_PLAINTEXT;\n\n    /**\n     * The message encoding.\n     * Options: \"8bit\", \"7bit\", \"binary\", \"base64\", and \"quoted-printable\".\n     *\n     * @var string\n     */\n    public $Encoding = self::ENCODING_8BIT;\n\n    /**\n     * Holds the most recent mailer error message.\n     *\n     * @var string\n     */\n    public $ErrorInfo = '';\n\n    /**\n     * The From email address for the message.\n     *\n     * @var string\n     */\n    public $From = '';\n\n    /**\n     * The From name of the message.\n     *\n     * @var string\n     */\n    public $FromName = '';\n\n    /**\n     * The envelope sender of the message.\n     * This will usually be turned into a Return-Path header by the receiver,\n     * and is the address that bounces will be sent to.\n     * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.\n     *\n     * @var string\n     */\n    public $Sender = '';\n\n    /**\n     * The Subject of the message.\n     *\n     * @var string\n     */\n    public $Subject = '';\n\n    /**\n     * An HTML or plain text message body.\n     * If HTML then call isHTML(true).\n     *\n     * @var string\n     */\n    public $Body = '';\n\n    /**\n     * The plain-text message body.\n     * This body can be read by mail clients that do not have HTML email\n     * capability such as mutt & Eudora.\n     * Clients that can read HTML will view the normal Body.\n     *\n     * @var string\n     */\n    public $AltBody = '';\n\n    /**\n     * An iCal message part body.\n     * Only supported in simple alt or alt_inline message types\n     * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.\n     *\n     * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/\n     * @see http://kigkonsult.se/iCalcreator/\n     *\n     * @var string\n     */\n    public $Ical = '';\n\n    /**\n     * Value-array of \"method\" in Contenttype header \"text/calendar\"\n     *\n     * @var string[]\n     */\n    protected static $IcalMethods = [\n        self::ICAL_METHOD_REQUEST,\n        self::ICAL_METHOD_PUBLISH,\n        self::ICAL_METHOD_REPLY,\n        self::ICAL_METHOD_ADD,\n        self::ICAL_METHOD_CANCEL,\n        self::ICAL_METHOD_REFRESH,\n        self::ICAL_METHOD_COUNTER,\n        self::ICAL_METHOD_DECLINECOUNTER,\n    ];\n\n    /**\n     * The complete compiled MIME message body.\n     *\n     * @var string\n     */\n    protected $MIMEBody = '';\n\n    /**\n     * The complete compiled MIME message headers.\n     *\n     * @var string\n     */\n    protected $MIMEHeader = '';\n\n    /**\n     * Extra headers that createHeader() doesn't fold in.\n     *\n     * @var string\n     */\n    protected $mailHeader = '';\n\n    /**\n     * Word-wrap the message body to this number of chars.\n     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.\n     *\n     * @see static::STD_LINE_LENGTH\n     *\n     * @var int\n     */\n    public $WordWrap = 0;\n\n    /**\n     * Which method to use to send mail.\n     * Options: \"mail\", \"sendmail\", or \"smtp\".\n     *\n     * @var string\n     */\n    public $Mailer = 'mail';\n\n    /**\n     * The path to the sendmail program.\n     *\n     * @var string\n     */\n    public $Sendmail = '/usr/sbin/sendmail';\n\n    /**\n     * Whether mail() uses a fully sendmail-compatible MTA.\n     * One which supports sendmail's \"-oi -f\" options.\n     *\n     * @var bool\n     */\n    public $UseSendmailOptions = true;\n\n    /**\n     * The email address that a reading confirmation should be sent to, also known as read receipt.\n     *\n     * @var string\n     */\n    public $ConfirmReadingTo = '';\n\n    /**\n     * The hostname to use in the Message-ID header and as default HELO string.\n     * If empty, PHPMailer attempts to find one with, in order,\n     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value\n     * 'localhost.localdomain'.\n     *\n     * @see PHPMailer::$Helo\n     *\n     * @var string\n     */\n    public $Hostname = '';\n\n    /**\n     * An ID to be used in the Message-ID header.\n     * If empty, a unique id will be generated.\n     * You can set your own, but it must be in the format \"<id@domain>\",\n     * as defined in RFC5322 section 3.6.4 or it will be ignored.\n     *\n     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4\n     *\n     * @var string\n     */\n    public $MessageID = '';\n\n    /**\n     * The message Date to be used in the Date header.\n     * If empty, the current date will be added.\n     *\n     * @var string\n     */\n    public $MessageDate = '';\n\n    /**\n     * SMTP hosts.\n     * Either a single hostname or multiple semicolon-delimited hostnames.\n     * You can also specify a different port\n     * for each host by using this format: [hostname:port]\n     * (e.g. \"smtp1.example.com:25;smtp2.example.com\").\n     * You can also specify encryption type, for example:\n     * (e.g. \"tls://smtp1.example.com:587;ssl://smtp2.example.com:465\").\n     * Hosts will be tried in order.\n     *\n     * @var string\n     */\n    public $Host = 'localhost';\n\n    /**\n     * The default SMTP server port.\n     *\n     * @var int\n     */\n    public $Port = 25;\n\n    /**\n     * The SMTP HELO/EHLO name used for the SMTP connection.\n     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find\n     * one with the same method described above for $Hostname.\n     *\n     * @see PHPMailer::$Hostname\n     *\n     * @var string\n     */\n    public $Helo = '';\n\n    /**\n     * What kind of encryption to use on the SMTP connection.\n     * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.\n     *\n     * @var string\n     */\n    public $SMTPSecure = '';\n\n    /**\n     * Whether to enable TLS encryption automatically if a server supports it,\n     * even if `SMTPSecure` is not set to 'tls'.\n     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.\n     *\n     * @var bool\n     */\n    public $SMTPAutoTLS = true;\n\n    /**\n     * Whether to use SMTP authentication.\n     * Uses the Username and Password properties.\n     *\n     * @see PHPMailer::$Username\n     * @see PHPMailer::$Password\n     *\n     * @var bool\n     */\n    public $SMTPAuth = false;\n\n    /**\n     * Options array passed to stream_context_create when connecting via SMTP.\n     *\n     * @var array\n     */\n    public $SMTPOptions = [];\n\n    /**\n     * SMTP username.\n     *\n     * @var string\n     */\n    public $Username = '';\n\n    /**\n     * SMTP password.\n     *\n     * @var string\n     */\n    public $Password = '';\n\n    /**\n     * SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2.\n     * If not specified, the first one from that list that the server supports will be selected.\n     *\n     * @var string\n     */\n    public $AuthType = '';\n\n    /**\n     * An implementation of the PHPMailer OAuthTokenProvider interface.\n     *\n     * @var OAuthTokenProvider\n     */\n    protected $oauth;\n\n    /**\n     * The SMTP server timeout in seconds.\n     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.\n     *\n     * @var int\n     */\n    public $Timeout = 300;\n\n    /**\n     * Comma separated list of DSN notifications\n     * 'NEVER' under no circumstances a DSN must be returned to the sender.\n     *         If you use NEVER all other notifications will be ignored.\n     * 'SUCCESS' will notify you when your mail has arrived at its destination.\n     * 'FAILURE' will arrive if an error occurred during delivery.\n     * 'DELAY'   will notify you if there is an unusual delay in delivery, but the actual\n     *           delivery's outcome (success or failure) is not yet decided.\n     *\n     * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY\n     */\n    public $dsn = '';\n\n    /**\n     * SMTP class debug output mode.\n     * Debug output level.\n     * Options:\n     * @see SMTP::DEBUG_OFF: No output\n     * @see SMTP::DEBUG_CLIENT: Client messages\n     * @see SMTP::DEBUG_SERVER: Client and server messages\n     * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status\n     * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed\n     *\n     * @see SMTP::$do_debug\n     *\n     * @var int\n     */\n    public $SMTPDebug = 0;\n\n    /**\n     * How to handle debug output.\n     * Options:\n     * * `echo` Output plain-text as-is, appropriate for CLI\n     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output\n     * * `error_log` Output to error log as configured in php.ini\n     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.\n     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:\n     *\n     * ```php\n     * $mail->Debugoutput = function($str, $level) {echo \"debug level $level; message: $str\";};\n     * ```\n     *\n     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`\n     * level output is used:\n     *\n     * ```php\n     * $mail->Debugoutput = new myPsr3Logger;\n     * ```\n     *\n     * @see SMTP::$Debugoutput\n     *\n     * @var string|callable|\\Psr\\Log\\LoggerInterface\n     */\n    public $Debugoutput = 'echo';\n\n    /**\n     * Whether to keep the SMTP connection open after each message.\n     * If this is set to true then the connection will remain open after a send,\n     * and closing the connection will require an explicit call to smtpClose().\n     * It's a good idea to use this if you are sending multiple messages as it reduces overhead.\n     * See the mailing list example for how to use it.\n     *\n     * @var bool\n     */\n    public $SMTPKeepAlive = false;\n\n    /**\n     * Whether to split multiple to addresses into multiple messages\n     * or send them all in one message.\n     * Only supported in `mail` and `sendmail` transports, not in SMTP.\n     *\n     * @var bool\n     *\n     * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!\n     */\n    public $SingleTo = false;\n\n    /**\n     * Storage for addresses when SingleTo is enabled.\n     *\n     * @var array\n     */\n    protected $SingleToArray = [];\n\n    /**\n     * Whether to generate VERP addresses on send.\n     * Only applicable when sending via SMTP.\n     *\n     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path\n     * @see http://www.postfix.org/VERP_README.html Postfix VERP info\n     *\n     * @var bool\n     */\n    public $do_verp = false;\n\n    /**\n     * Whether to allow sending messages with an empty body.\n     *\n     * @var bool\n     */\n    public $AllowEmpty = false;\n\n    /**\n     * DKIM selector.\n     *\n     * @var string\n     */\n    public $DKIM_selector = '';\n\n    /**\n     * DKIM Identity.\n     * Usually the email address used as the source of the email.\n     *\n     * @var string\n     */\n    public $DKIM_identity = '';\n\n    /**\n     * DKIM passphrase.\n     * Used if your key is encrypted.\n     *\n     * @var string\n     */\n    public $DKIM_passphrase = '';\n\n    /**\n     * DKIM signing domain name.\n     *\n     * @example 'example.com'\n     *\n     * @var string\n     */\n    public $DKIM_domain = '';\n\n    /**\n     * DKIM Copy header field values for diagnostic use.\n     *\n     * @var bool\n     */\n    public $DKIM_copyHeaderFields = true;\n\n    /**\n     * DKIM Extra signing headers.\n     *\n     * @example ['List-Unsubscribe', 'List-Help']\n     *\n     * @var array\n     */\n    public $DKIM_extraHeaders = [];\n\n    /**\n     * DKIM private key file path.\n     *\n     * @var string\n     */\n    public $DKIM_private = '';\n\n    /**\n     * DKIM private key string.\n     *\n     * If set, takes precedence over `$DKIM_private`.\n     *\n     * @var string\n     */\n    public $DKIM_private_string = '';\n\n    /**\n     * Callback Action function name.\n     *\n     * The function that handles the result of the send email action.\n     * It is called out by send() for each email sent.\n     *\n     * Value can be any php callable: http://www.php.net/is_callable\n     *\n     * Parameters:\n     *   bool $result        result of the send action\n     *   array   $to            email addresses of the recipients\n     *   array   $cc            cc email addresses\n     *   array   $bcc           bcc email addresses\n     *   string  $subject       the subject\n     *   string  $body          the email body\n     *   string  $from          email address of sender\n     *   string  $extra         extra information of possible use\n     *                          \"smtp_transaction_id' => last smtp transaction id\n     *\n     * @var string\n     */\n    public $action_function = '';\n\n    /**\n     * What to put in the X-Mailer header.\n     * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.\n     *\n     * @var string|null\n     */\n    public $XMailer = '';\n\n    /**\n     * Which validator to use by default when validating email addresses.\n     * May be a callable to inject your own validator, but there are several built-in validators.\n     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.\n     *\n     * @see PHPMailer::validateAddress()\n     *\n     * @var string|callable\n     */\n    public static $validator = 'php';\n\n    /**\n     * An instance of the SMTP sender class.\n     *\n     * @var SMTP\n     */\n    protected $smtp;\n\n    /**\n     * The array of 'to' names and addresses.\n     *\n     * @var array\n     */\n    protected $to = [];\n\n    /**\n     * The array of 'cc' names and addresses.\n     *\n     * @var array\n     */\n    protected $cc = [];\n\n    /**\n     * The array of 'bcc' names and addresses.\n     *\n     * @var array\n     */\n    protected $bcc = [];\n\n    /**\n     * The array of reply-to names and addresses.\n     *\n     * @var array\n     */\n    protected $ReplyTo = [];\n\n    /**\n     * An array of all kinds of addresses.\n     * Includes all of $to, $cc, $bcc.\n     *\n     * @see PHPMailer::$to\n     * @see PHPMailer::$cc\n     * @see PHPMailer::$bcc\n     *\n     * @var array\n     */\n    protected $all_recipients = [];\n\n    /**\n     * An array of names and addresses queued for validation.\n     * In send(), valid and non duplicate entries are moved to $all_recipients\n     * and one of $to, $cc, or $bcc.\n     * This array is used only for addresses with IDN.\n     *\n     * @see PHPMailer::$to\n     * @see PHPMailer::$cc\n     * @see PHPMailer::$bcc\n     * @see PHPMailer::$all_recipients\n     *\n     * @var array\n     */\n    protected $RecipientsQueue = [];\n\n    /**\n     * An array of reply-to names and addresses queued for validation.\n     * In send(), valid and non duplicate entries are moved to $ReplyTo.\n     * This array is used only for addresses with IDN.\n     *\n     * @see PHPMailer::$ReplyTo\n     *\n     * @var array\n     */\n    protected $ReplyToQueue = [];\n\n    /**\n     * The array of attachments.\n     *\n     * @var array\n     */\n    protected $attachment = [];\n\n    /**\n     * The array of custom headers.\n     *\n     * @var array\n     */\n    protected $CustomHeader = [];\n\n    /**\n     * The most recent Message-ID (including angular brackets).\n     *\n     * @var string\n     */\n    protected $lastMessageID = '';\n\n    /**\n     * The message's MIME type.\n     *\n     * @var string\n     */\n    protected $message_type = '';\n\n    /**\n     * The array of MIME boundary strings.\n     *\n     * @var array\n     */\n    protected $boundary = [];\n\n    /**\n     * The array of available text strings for the current language.\n     *\n     * @var array\n     */\n    protected $language = [];\n\n    /**\n     * The number of errors encountered.\n     *\n     * @var int\n     */\n    protected $error_count = 0;\n\n    /**\n     * The S/MIME certificate file path.\n     *\n     * @var string\n     */\n    protected $sign_cert_file = '';\n\n    /**\n     * The S/MIME key file path.\n     *\n     * @var string\n     */\n    protected $sign_key_file = '';\n\n    /**\n     * The optional S/MIME extra certificates (\"CA Chain\") file path.\n     *\n     * @var string\n     */\n    protected $sign_extracerts_file = '';\n\n    /**\n     * The S/MIME password for the key.\n     * Used only if the key is encrypted.\n     *\n     * @var string\n     */\n    protected $sign_key_pass = '';\n\n    /**\n     * Whether to throw exceptions for errors.\n     *\n     * @var bool\n     */\n    protected $exceptions = false;\n\n    /**\n     * Unique ID used for message ID and boundaries.\n     *\n     * @var string\n     */\n    protected $uniqueid = '';\n\n    /**\n     * The PHPMailer Version number.\n     *\n     * @var string\n     */\n    const VERSION = '6.8.1';\n\n    /**\n     * Error severity: message only, continue processing.\n     *\n     * @var int\n     */\n    const STOP_MESSAGE = 0;\n\n    /**\n     * Error severity: message, likely ok to continue processing.\n     *\n     * @var int\n     */\n    const STOP_CONTINUE = 1;\n\n    /**\n     * Error severity: message, plus full stop, critical error reached.\n     *\n     * @var int\n     */\n    const STOP_CRITICAL = 2;\n\n    /**\n     * The SMTP standard CRLF line break.\n     * If you want to change line break format, change static::$LE, not this.\n     */\n    const CRLF = \"\\r\\n\";\n\n    /**\n     * \"Folding White Space\" a white space string used for line folding.\n     */\n    const FWS = ' ';\n\n    /**\n     * SMTP RFC standard line ending; Carriage Return, Line Feed.\n     *\n     * @var string\n     */\n    protected static $LE = self::CRLF;\n\n    /**\n     * The maximum line length supported by mail().\n     *\n     * Background: mail() will sometimes corrupt messages\n     * with headers longer than 65 chars, see #818.\n     *\n     * @var int\n     */\n    const MAIL_MAX_LINE_LENGTH = 63;\n\n    /**\n     * The maximum line length allowed by RFC 2822 section 2.1.1.\n     *\n     * @var int\n     */\n    const MAX_LINE_LENGTH = 998;\n\n    /**\n     * The lower maximum line length allowed by RFC 2822 section 2.1.1.\n     * This length does NOT include the line break\n     * 76 means that lines will be 77 or 78 chars depending on whether\n     * the line break format is LF or CRLF; both are valid.\n     *\n     * @var int\n     */\n    const STD_LINE_LENGTH = 76;\n\n    /**\n     * Constructor.\n     *\n     * @param bool $exceptions Should we throw external exceptions?\n     */\n    public function __construct($exceptions = null)\n    {\n        if (null !== $exceptions) {\n            $this->exceptions = (bool) $exceptions;\n        }\n        //Pick an appropriate debug output format automatically\n        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');\n    }\n\n    /**\n     * Destructor.\n     */\n    public function __destruct()\n    {\n        //Close any open SMTP connection nicely\n        $this->smtpClose();\n    }\n\n    /**\n     * Call mail() in a safe_mode-aware fashion.\n     * Also, unless sendmail_path points to sendmail (or something that\n     * claims to be sendmail), don't pass params (not a perfect fix,\n     * but it will do).\n     *\n     * @param string      $to      To\n     * @param string      $subject Subject\n     * @param string      $body    Message Body\n     * @param string      $header  Additional Header(s)\n     * @param string|null $params  Params\n     *\n     * @return bool\n     */\n    private function mailPassthru($to, $subject, $body, $header, $params)\n    {\n        //Check overloading of mail function to avoid double-encoding\n        if ((int)ini_get('mbstring.func_overload') & 1) {\n            $subject = $this->secureHeader($subject);\n        } else {\n            $subject = $this->encodeHeader($this->secureHeader($subject));\n        }\n        //Calling mail() with null params breaks\n        $this->edebug('Sending with mail()');\n        $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));\n        $this->edebug(\"Envelope sender: {$this->Sender}\");\n        $this->edebug(\"To: {$to}\");\n        $this->edebug(\"Subject: {$subject}\");\n        $this->edebug(\"Headers: {$header}\");\n        if (!$this->UseSendmailOptions || null === $params) {\n            $result = @mail($to, $subject, $body, $header);\n        } else {\n            $this->edebug(\"Additional params: {$params}\");\n            $result = @mail($to, $subject, $body, $header, $params);\n        }\n        $this->edebug('Result: ' . ($result ? 'true' : 'false'));\n        return $result;\n    }\n\n    /**\n     * Output debugging info via a user-defined method.\n     * Only generates output if debug output is enabled.\n     *\n     * @see PHPMailer::$Debugoutput\n     * @see PHPMailer::$SMTPDebug\n     *\n     * @param string $str\n     */\n    protected function edebug($str)\n    {\n        if ($this->SMTPDebug <= 0) {\n            return;\n        }\n        //Is this a PSR-3 logger?\n        if ($this->Debugoutput instanceof \\Psr\\Log\\LoggerInterface) {\n            $this->Debugoutput->debug($str);\n\n            return;\n        }\n        //Avoid clash with built-in function names\n        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {\n            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);\n\n            return;\n        }\n        switch ($this->Debugoutput) {\n            case 'error_log':\n                //Don't output, just log\n                /** @noinspection ForgottenDebugOutputInspection */\n                error_log($str);\n                break;\n            case 'html':\n                //Cleans up output a bit for a better looking, HTML-safe output\n                echo htmlentities(\n                    preg_replace('/[\\r\\n]+/', '', $str),\n                    ENT_QUOTES,\n                    'UTF-8'\n                ), \"<br>\\n\";\n                break;\n            case 'echo':\n            default:\n                //Normalize line breaks\n                $str = preg_replace('/\\r\\n|\\r/m', \"\\n\", $str);\n                echo gmdate('Y-m-d H:i:s'),\n                \"\\t\",\n                    //Trim trailing space\n                trim(\n                    //Indent for readability, except for trailing break\n                    str_replace(\n                        \"\\n\",\n                        \"\\n                   \\t                  \",\n                        trim($str)\n                    )\n                ),\n                \"\\n\";\n        }\n    }\n\n    /**\n     * Sets message type to HTML or plain.\n     *\n     * @param bool $isHtml True for HTML mode\n     */\n    public function isHTML($isHtml = true)\n    {\n        if ($isHtml) {\n            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;\n        } else {\n            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;\n        }\n    }\n\n    /**\n     * Send messages using SMTP.\n     */\n    public function isSMTP()\n    {\n        $this->Mailer = 'smtp';\n    }\n\n    /**\n     * Send messages using PHP's mail() function.\n     */\n    public function isMail()\n    {\n        $this->Mailer = 'mail';\n    }\n\n    /**\n     * Send messages using $Sendmail.\n     */\n    public function isSendmail()\n    {\n        $ini_sendmail_path = ini_get('sendmail_path');\n\n        if (false === stripos($ini_sendmail_path, 'sendmail')) {\n            $this->Sendmail = '/usr/sbin/sendmail';\n        } else {\n            $this->Sendmail = $ini_sendmail_path;\n        }\n        $this->Mailer = 'sendmail';\n    }\n\n    /**\n     * Send messages using qmail.\n     */\n    public function isQmail()\n    {\n        $ini_sendmail_path = ini_get('sendmail_path');\n\n        if (false === stripos($ini_sendmail_path, 'qmail')) {\n            $this->Sendmail = '/var/qmail/bin/qmail-inject';\n        } else {\n            $this->Sendmail = $ini_sendmail_path;\n        }\n        $this->Mailer = 'qmail';\n    }\n\n    /**\n     * Add a \"To\" address.\n     *\n     * @param string $address The email address to send to\n     * @param string $name\n     *\n     * @throws Exception\n     *\n     * @return bool true on success, false if address already used or invalid in some way\n     */\n    public function addAddress($address, $name = '')\n    {\n        return $this->addOrEnqueueAnAddress('to', $address, $name);\n    }\n\n    /**\n     * Add a \"CC\" address.\n     *\n     * @param string $address The email address to send to\n     * @param string $name\n     *\n     * @throws Exception\n     *\n     * @return bool true on success, false if address already used or invalid in some way\n     */\n    public function addCC($address, $name = '')\n    {\n        return $this->addOrEnqueueAnAddress('cc', $address, $name);\n    }\n\n    /**\n     * Add a \"BCC\" address.\n     *\n     * @param string $address The email address to send to\n     * @param string $name\n     *\n     * @throws Exception\n     *\n     * @return bool true on success, false if address already used or invalid in some way\n     */\n    public function addBCC($address, $name = '')\n    {\n        return $this->addOrEnqueueAnAddress('bcc', $address, $name);\n    }\n\n    /**\n     * Add a \"Reply-To\" address.\n     *\n     * @param string $address The email address to reply to\n     * @param string $name\n     *\n     * @throws Exception\n     *\n     * @return bool true on success, false if address already used or invalid in some way\n     */\n    public function addReplyTo($address, $name = '')\n    {\n        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);\n    }\n\n    /**\n     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer\n     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still\n     * be modified after calling this function), addition of such addresses is delayed until send().\n     * Addresses that have been added already return false, but do not throw exceptions.\n     *\n     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'\n     * @param string $address The email address\n     * @param string $name    An optional username associated with the address\n     *\n     * @throws Exception\n     *\n     * @return bool true on success, false if address already used or invalid in some way\n     */\n    protected function addOrEnqueueAnAddress($kind, $address, $name)\n    {\n        $pos = false;\n        if ($address !== null) {\n            $address = trim($address);\n            $pos = strrpos($address, '@');\n        }\n        if (false === $pos) {\n            //At-sign is missing.\n            $error_message = sprintf(\n                '%s (%s): %s',\n                $this->lang('invalid_address'),\n                $kind,\n                $address\n            );\n            $this->setError($error_message);\n            $this->edebug($error_message);\n            if ($this->exceptions) {\n                throw new Exception($error_message);\n            }\n\n            return false;\n        }\n        if ($name !== null && is_string($name)) {\n            $name = trim(preg_replace('/[\\r\\n]+/', '', $name)); //Strip breaks and trim\n        } else {\n            $name = '';\n        }\n        $params = [$kind, $address, $name];\n        //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.\n        //Domain is assumed to be whatever is after the last @ symbol in the address\n        if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {\n            if ('Reply-To' !== $kind) {\n                if (!array_key_exists($address, $this->RecipientsQueue)) {\n                    $this->RecipientsQueue[$address] = $params;\n\n                    return true;\n                }\n            } elseif (!array_key_exists($address, $this->ReplyToQueue)) {\n                $this->ReplyToQueue[$address] = $params;\n\n                return true;\n            }\n\n            return false;\n        }\n\n        //Immediately add standard addresses without IDN.\n        return call_user_func_array([$this, 'addAnAddress'], $params);\n    }\n\n    /**\n     * Set the boundaries to use for delimiting MIME parts.\n     * If you override this, ensure you set all 3 boundaries to unique values.\n     * The default boundaries include a \"=_\" sequence which cannot occur in quoted-printable bodies,\n     * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7\n     *\n     * @return void\n     */\n    public function setBoundaries()\n    {\n        $this->uniqueid = $this->generateId();\n        $this->boundary[1] = 'b1=_' . $this->uniqueid;\n        $this->boundary[2] = 'b2=_' . $this->uniqueid;\n        $this->boundary[3] = 'b3=_' . $this->uniqueid;\n    }\n\n    /**\n     * Add an address to one of the recipient arrays or to the ReplyTo array.\n     * Addresses that have been added already return false, but do not throw exceptions.\n     *\n     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'\n     * @param string $address The email address to send, resp. to reply to\n     * @param string $name\n     *\n     * @throws Exception\n     *\n     * @return bool true on success, false if address already used or invalid in some way\n     */\n    protected function addAnAddress($kind, $address, $name = '')\n    {\n        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {\n            $error_message = sprintf(\n                '%s: %s',\n                $this->lang('Invalid recipient kind'),\n                $kind\n            );\n            $this->setError($error_message);\n            $this->edebug($error_message);\n            if ($this->exceptions) {\n                throw new Exception($error_message);\n            }\n\n            return false;\n        }\n        if (!static::validateAddress($address)) {\n            $error_message = sprintf(\n                '%s (%s): %s',\n                $this->lang('invalid_address'),\n                $kind,\n                $address\n            );\n            $this->setError($error_message);\n            $this->edebug($error_message);\n            if ($this->exceptions) {\n                throw new Exception($error_message);\n            }\n\n            return false;\n        }\n        if ('Reply-To' !== $kind) {\n            if (!array_key_exists(strtolower($address), $this->all_recipients)) {\n                $this->{$kind}[] = [$address, $name];\n                $this->all_recipients[strtolower($address)] = true;\n\n                return true;\n            }\n        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {\n            $this->ReplyTo[strtolower($address)] = [$address, $name];\n\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses\n     * of the form \"display name <address>\" into an array of name/address pairs.\n     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.\n     * Note that quotes in the name part are removed.\n     *\n     * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation\n     *\n     * @param string $addrstr The address list string\n     * @param bool   $useimap Whether to use the IMAP extension to parse the list\n     * @param string $charset The charset to use when decoding the address list string.\n     *\n     * @return array\n     */\n    public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)\n    {\n        $addresses = [];\n        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {\n            //Use this built-in parser if it's available\n            $list = imap_rfc822_parse_adrlist($addrstr, '');\n            // Clear any potential IMAP errors to get rid of notices being thrown at end of script.\n            imap_errors();\n            foreach ($list as $address) {\n                if (\n                    '.SYNTAX-ERROR.' !== $address->host &&\n                    static::validateAddress($address->mailbox . '@' . $address->host)\n                ) {\n                    //Decode the name part if it's present and encoded\n                    if (\n                        property_exists($address, 'personal') &&\n                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled\n                        defined('MB_CASE_UPPER') &&\n                        preg_match('/^=\\?.*\\?=$/s', $address->personal)\n                    ) {\n                        $origCharset = mb_internal_encoding();\n                        mb_internal_encoding($charset);\n                        //Undo any RFC2047-encoded spaces-as-underscores\n                        $address->personal = str_replace('_', '=20', $address->personal);\n                        //Decode the name\n                        $address->personal = mb_decode_mimeheader($address->personal);\n                        mb_internal_encoding($origCharset);\n                    }\n\n                    $addresses[] = [\n                        'name' => (property_exists($address, 'personal') ? $address->personal : ''),\n                        'address' => $address->mailbox . '@' . $address->host,\n                    ];\n                }\n            }\n        } else {\n            //Use this simpler parser\n            $list = explode(',', $addrstr);\n            foreach ($list as $address) {\n                $address = trim($address);\n                //Is there a separate name part?\n                if (strpos($address, '<') === false) {\n                    //No separate name, just use the whole thing\n                    if (static::validateAddress($address)) {\n                        $addresses[] = [\n                            'name' => '',\n                            'address' => $address,\n                        ];\n                    }\n                } else {\n                    list($name, $email) = explode('<', $address);\n                    $email = trim(str_replace('>', '', $email));\n                    $name = trim($name);\n                    if (static::validateAddress($email)) {\n                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled\n                        //If this name is encoded, decode it\n                        if (defined('MB_CASE_UPPER') && preg_match('/^=\\?.*\\?=$/s', $name)) {\n                            $origCharset = mb_internal_encoding();\n                            mb_internal_encoding($charset);\n                            //Undo any RFC2047-encoded spaces-as-underscores\n                            $name = str_replace('_', '=20', $name);\n                            //Decode the name\n                            $name = mb_decode_mimeheader($name);\n                            mb_internal_encoding($origCharset);\n                        }\n                        $addresses[] = [\n                            //Remove any surrounding quotes and spaces from the name\n                            'name' => trim($name, '\\'\" '),\n                            'address' => $email,\n                        ];\n                    }\n                }\n            }\n        }\n\n        return $addresses;\n    }\n\n    /**\n     * Set the From and FromName properties.\n     *\n     * @param string $address\n     * @param string $name\n     * @param bool   $auto    Whether to also set the Sender address, defaults to true\n     *\n     * @throws Exception\n     *\n     * @return bool\n     */\n    public function setFrom($address, $name = '', $auto = true)\n    {\n        $address = trim((string)$address);\n        $name = trim(preg_replace('/[\\r\\n]+/', '', $name)); //Strip breaks and trim\n        //Don't validate now addresses with IDN. Will be done in send().\n        $pos = strrpos($address, '@');\n        if (\n            (false === $pos)\n            || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())\n            && !static::validateAddress($address))\n        ) {\n            $error_message = sprintf(\n                '%s (From): %s',\n                $this->lang('invalid_address'),\n                $address\n            );\n            $this->setError($error_message);\n            $this->edebug($error_message);\n            if ($this->exceptions) {\n                throw new Exception($error_message);\n            }\n\n            return false;\n        }\n        $this->From = $address;\n        $this->FromName = $name;\n        if ($auto && empty($this->Sender)) {\n            $this->Sender = $address;\n        }\n\n        return true;\n    }\n\n    /**\n     * Return the Message-ID header of the last email.\n     * Technically this is the value from the last time the headers were created,\n     * but it's also the message ID of the last sent message except in\n     * pathological cases.\n     *\n     * @return string\n     */\n    public function getLastMessageID()\n    {\n        return $this->lastMessageID;\n    }\n\n    /**\n     * Check that a string looks like an email address.\n     * Validation patterns supported:\n     * * `auto` Pick best pattern automatically;\n     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;\n     * * `pcre` Use old PCRE implementation;\n     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;\n     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.\n     * * `noregex` Don't use a regex: super fast, really dumb.\n     * Alternatively you may pass in a callable to inject your own validator, for example:\n     *\n     * ```php\n     * PHPMailer::validateAddress('user@example.com', function($address) {\n     *     return (strpos($address, '@') !== false);\n     * });\n     * ```\n     *\n     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.\n     *\n     * @param string          $address       The email address to check\n     * @param string|callable $patternselect Which pattern to use\n     *\n     * @return bool\n     */\n    public static function validateAddress($address, $patternselect = null)\n    {\n        if (null === $patternselect) {\n            $patternselect = static::$validator;\n        }\n        //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603\n        if (is_callable($patternselect) && !is_string($patternselect)) {\n            return call_user_func($patternselect, $address);\n        }\n        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321\n        if (strpos($address, \"\\n\") !== false || strpos($address, \"\\r\") !== false) {\n            return false;\n        }\n        switch ($patternselect) {\n            case 'pcre': //Kept for BC\n            case 'pcre8':\n                /*\n                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL\n                 * is based.\n                 * In addition to the addresses allowed by filter_var, also permits:\n                 *  * dotless domains: `a@b`\n                 *  * comments: `1234 @ local(blah) .machine .example`\n                 *  * quoted elements: `'\"test blah\"@example.org'`\n                 *  * numeric TLDs: `a@b.123`\n                 *  * unbracketed IPv4 literals: `a@192.168.0.1`\n                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'\n                 * Not all of these will necessarily work for sending!\n                 *\n                 * @see       http://squiloople.com/2009/12/20/email-address-validation/\n                 * @copyright 2009-2010 Michael Rushton\n                 * Feel free to use and redistribute this code. But please keep this copyright notice.\n                 */\n                return (bool) preg_match(\n                    '/^(?!(?>(?1)\"?(?>\\\\\\[ -~]|[^\"])\"?(?1)){255,})(?!(?>(?1)\"?(?>\\\\\\[ -~]|[^\"])\"?(?1)){65,}@)' .\n                    '((?>(?>(?>((?>(?>(?>\\x0D\\x0A)?[\\t ])+|(?>[\\t ]*\\x0D\\x0A)?[\\t ]+)?)(\\((?>(?2)' .\n                    '(?>[\\x01-\\x08\\x0B\\x0C\\x0E-\\'*-\\[\\]-\\x7F]|\\\\\\[\\x00-\\x7F]|(?3)))*(?2)\\)))+(?2))|(?2))?)' .\n                    '([!#-\\'*+\\/-9=?^-~-]+|\"(?>(?2)(?>[\\x01-\\x08\\x0B\\x0C\\x0E-!#-\\[\\]-\\x7F]|\\\\\\[\\x00-\\x7F]))*' .\n                    '(?2)\")(?>(?1)\\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .\n                    '(?>(?1)\\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .\n                    '|(?!(?:.*[a-f0-9][:\\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .\n                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .\n                    '|[1-9]?[0-9])(?>\\.(?9)){3}))\\])(?1)$/isD',\n                    $address\n                );\n            case 'html5':\n                /*\n                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.\n                 *\n                 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)\n                 */\n                return (bool) preg_match(\n                    '/^[a-zA-Z0-9.!#$%&\\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .\n                    '[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',\n                    $address\n                );\n            case 'php':\n            default:\n                return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;\n        }\n    }\n\n    /**\n     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the\n     * `intl` and `mbstring` PHP extensions.\n     *\n     * @return bool `true` if required functions for IDN support are present\n     */\n    public static function idnSupported()\n    {\n        return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');\n    }\n\n    /**\n     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.\n     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.\n     * This function silently returns unmodified address if:\n     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)\n     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)\n     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).\n     *\n     * @see PHPMailer::$CharSet\n     *\n     * @param string $address The email address to convert\n     *\n     * @return string The encoded address in ASCII form\n     */\n    public function punyencodeAddress($address)\n    {\n        //Verify we have required functions, CharSet, and at-sign.\n        $pos = strrpos($address, '@');\n        if (\n            !empty($this->CharSet) &&\n            false !== $pos &&\n            static::idnSupported()\n        ) {\n            $domain = substr($address, ++$pos);\n            //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.\n            if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {\n                //Convert the domain from whatever charset it's in to UTF-8\n                $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);\n                //Ignore IDE complaints about this line - method signature changed in PHP 5.4\n                $errorcode = 0;\n                if (defined('INTL_IDNA_VARIANT_UTS46')) {\n                    //Use the current punycode standard (appeared in PHP 7.2)\n                    $punycode = idn_to_ascii(\n                        $domain,\n                        \\IDNA_DEFAULT | \\IDNA_USE_STD3_RULES | \\IDNA_CHECK_BIDI |\n                            \\IDNA_CHECK_CONTEXTJ | \\IDNA_NONTRANSITIONAL_TO_ASCII,\n                        \\INTL_IDNA_VARIANT_UTS46\n                    );\n                } elseif (defined('INTL_IDNA_VARIANT_2003')) {\n                    //Fall back to this old, deprecated/removed encoding\n                    $punycode = idn_to_ascii($domain, $errorcode, \\INTL_IDNA_VARIANT_2003);\n                } else {\n                    //Fall back to a default we don't know about\n                    $punycode = idn_to_ascii($domain, $errorcode);\n                }\n                if (false !== $punycode) {\n                    return substr($address, 0, $pos) . $punycode;\n                }\n            }\n        }\n\n        return $address;\n    }\n\n    /**\n     * Create a message and send it.\n     * Uses the sending method specified by $Mailer.\n     *\n     * @throws Exception\n     *\n     * @return bool false on error - See the ErrorInfo property for details of the error\n     */\n    public function send()\n    {\n        try {\n            if (!$this->preSend()) {\n                return false;\n            }\n\n            return $this->postSend();\n        } catch (Exception $exc) {\n            $this->mailHeader = '';\n            $this->setError($exc->getMessage());\n            if ($this->exceptions) {\n                throw $exc;\n            }\n\n            return false;\n        }\n    }\n\n    /**\n     * Prepare a message for sending.\n     *\n     * @throws Exception\n     *\n     * @return bool\n     */\n    public function preSend()\n    {\n        if (\n            'smtp' === $this->Mailer\n            || ('mail' === $this->Mailer && (\\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))\n        ) {\n            //SMTP mandates RFC-compliant line endings\n            //and it's also used with mail() on Windows\n            static::setLE(self::CRLF);\n        } else {\n            //Maintain backward compatibility with legacy Linux command line mailers\n            static::setLE(PHP_EOL);\n        }\n        //Check for buggy PHP versions that add a header with an incorrect line break\n        if (\n            'mail' === $this->Mailer\n            && ((\\PHP_VERSION_ID >= 70000 && \\PHP_VERSION_ID < 70017)\n                || (\\PHP_VERSION_ID >= 70100 && \\PHP_VERSION_ID < 70103))\n            && ini_get('mail.add_x_header') === '1'\n            && stripos(PHP_OS, 'WIN') === 0\n        ) {\n            trigger_error($this->lang('buggy_php'), E_USER_WARNING);\n        }\n\n        try {\n            $this->error_count = 0; //Reset errors\n            $this->mailHeader = '';\n\n            //Dequeue recipient and Reply-To addresses with IDN\n            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {\n                $params[1] = $this->punyencodeAddress($params[1]);\n                call_user_func_array([$this, 'addAnAddress'], $params);\n            }\n            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {\n                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);\n            }\n\n            //Validate From, Sender, and ConfirmReadingTo addresses\n            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {\n                if ($this->{$address_kind} === null) {\n                    $this->{$address_kind} = '';\n                    continue;\n                }\n                $this->{$address_kind} = trim($this->{$address_kind});\n                if (empty($this->{$address_kind})) {\n                    continue;\n                }\n                $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind});\n                if (!static::validateAddress($this->{$address_kind})) {\n                    $error_message = sprintf(\n                        '%s (%s): %s',\n                        $this->lang('invalid_address'),\n                        $address_kind,\n                        $this->{$address_kind}\n                    );\n                    $this->setError($error_message);\n                    $this->edebug($error_message);\n                    if ($this->exceptions) {\n                        throw new Exception($error_message);\n                    }\n\n                    return false;\n                }\n            }\n\n            //Set whether the message is multipart/alternative\n            if ($this->alternativeExists()) {\n                $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;\n            }\n\n            $this->setMessageType();\n            //Refuse to send an empty message unless we are specifically allowing it\n            if (!$this->AllowEmpty && empty($this->Body)) {\n                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);\n            }\n\n            //Trim subject consistently\n            $this->Subject = trim($this->Subject);\n            //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)\n            $this->MIMEHeader = '';\n            $this->MIMEBody = $this->createBody();\n            //createBody may have added some headers, so retain them\n            $tempheaders = $this->MIMEHeader;\n            $this->MIMEHeader = $this->createHeader();\n            $this->MIMEHeader .= $tempheaders;\n\n            //To capture the complete message when using mail(), create\n            //an extra header list which createHeader() doesn't fold in\n            if ('mail' === $this->Mailer) {\n                if (count($this->to) > 0) {\n                    $this->mailHeader .= $this->addrAppend('To', $this->to);\n                } else {\n                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');\n                }\n                $this->mailHeader .= $this->headerLine(\n                    'Subject',\n                    $this->encodeHeader($this->secureHeader($this->Subject))\n                );\n            }\n\n            //Sign with DKIM if enabled\n            if (\n                !empty($this->DKIM_domain)\n                && !empty($this->DKIM_selector)\n                && (!empty($this->DKIM_private_string)\n                    || (!empty($this->DKIM_private)\n                        && static::isPermittedPath($this->DKIM_private)\n                        && file_exists($this->DKIM_private)\n                    )\n                )\n            ) {\n                $header_dkim = $this->DKIM_Add(\n                    $this->MIMEHeader . $this->mailHeader,\n                    $this->encodeHeader($this->secureHeader($this->Subject)),\n                    $this->MIMEBody\n                );\n                $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .\n                    static::normalizeBreaks($header_dkim) . static::$LE;\n            }\n\n            return true;\n        } catch (Exception $exc) {\n            $this->setError($exc->getMessage());\n            if ($this->exceptions) {\n                throw $exc;\n            }\n\n            return false;\n        }\n    }\n\n    /**\n     * Actually send a message via the selected mechanism.\n     *\n     * @throws Exception\n     *\n     * @return bool\n     */\n    public function postSend()\n    {\n        try {\n            //Choose the mailer and send through it\n            switch ($this->Mailer) {\n                case 'sendmail':\n                case 'qmail':\n                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);\n                case 'smtp':\n                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);\n                case 'mail':\n                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);\n                default:\n                    $sendMethod = $this->Mailer . 'Send';\n                    if (method_exists($this, $sendMethod)) {\n                        return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody);\n                    }\n\n                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);\n            }\n        } catch (Exception $exc) {\n            $this->setError($exc->getMessage());\n            $this->edebug($exc->getMessage());\n            if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) {\n                $this->smtp->reset();\n            }\n            if ($this->exceptions) {\n                throw $exc;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Send mail using the $Sendmail program.\n     *\n     * @see PHPMailer::$Sendmail\n     *\n     * @param string $header The message headers\n     * @param string $body   The message body\n     *\n     * @throws Exception\n     *\n     * @return bool\n     */\n    protected function sendmailSend($header, $body)\n    {\n        if ($this->Mailer === 'qmail') {\n            $this->edebug('Sending with qmail');\n        } else {\n            $this->edebug('Sending with sendmail');\n        }\n        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;\n        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver\n        //A space after `-f` is optional, but there is a long history of its presence\n        //causing problems, so we don't use one\n        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html\n        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html\n        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html\n        //Example problem: https://www.drupal.org/node/1057954\n\n        //PHP 5.6 workaround\n        $sendmail_from_value = ini_get('sendmail_from');\n        if (empty($this->Sender) && !empty($sendmail_from_value)) {\n            //PHP config has a sender address we can use\n            $this->Sender = ini_get('sendmail_from');\n        }\n        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.\n        if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {\n            if ($this->Mailer === 'qmail') {\n                $sendmailFmt = '%s -f%s';\n            } else {\n                $sendmailFmt = '%s -oi -f%s -t';\n            }\n        } else {\n            //allow sendmail to choose a default envelope sender. It may\n            //seem preferable to force it to use the From header as with\n            //SMTP, but that introduces new problems (see\n            //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and\n            //it has historically worked this way.\n            $sendmailFmt = '%s -oi -t';\n        }\n\n        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);\n        $this->edebug('Sendmail path: ' . $this->Sendmail);\n        $this->edebug('Sendmail command: ' . $sendmail);\n        $this->edebug('Envelope sender: ' . $this->Sender);\n        $this->edebug(\"Headers: {$header}\");\n\n        if ($this->SingleTo) {\n            foreach ($this->SingleToArray as $toAddr) {\n                $mail = @popen($sendmail, 'w');\n                if (!$mail) {\n                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);\n                }\n                $this->edebug(\"To: {$toAddr}\");\n                fwrite($mail, 'To: ' . $toAddr . \"\\n\");\n                fwrite($mail, $header);\n                fwrite($mail, $body);\n                $result = pclose($mail);\n                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);\n                $this->doCallback(\n                    ($result === 0),\n                    [[$addrinfo['address'], $addrinfo['name']]],\n                    $this->cc,\n                    $this->bcc,\n                    $this->Subject,\n                    $body,\n                    $this->From,\n                    []\n                );\n                $this->edebug(\"Result: \" . ($result === 0 ? 'true' : 'false'));\n                if (0 !== $result) {\n                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);\n                }\n            }\n        } else {\n            $mail = @popen($sendmail, 'w');\n            if (!$mail) {\n                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);\n            }\n            fwrite($mail, $header);\n            fwrite($mail, $body);\n            $result = pclose($mail);\n            $this->doCallback(\n                ($result === 0),\n                $this->to,\n                $this->cc,\n                $this->bcc,\n                $this->Subject,\n                $body,\n                $this->From,\n                []\n            );\n            $this->edebug(\"Result: \" . ($result === 0 ? 'true' : 'false'));\n            if (0 !== $result) {\n                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.\n     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.\n     *\n     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report\n     *\n     * @param string $string The string to be validated\n     *\n     * @return bool\n     */\n    protected static function isShellSafe($string)\n    {\n        //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg,\n        //but some hosting providers disable it, creating a security problem that we don't want to have to deal with,\n        //so we don't.\n        if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) {\n            return false;\n        }\n\n        if (\n            escapeshellcmd($string) !== $string\n            || !in_array(escapeshellarg($string), [\"'$string'\", \"\\\"$string\\\"\"])\n        ) {\n            return false;\n        }\n\n        $length = strlen($string);\n\n        for ($i = 0; $i < $length; ++$i) {\n            $c = $string[$i];\n\n            //All other characters have a special meaning in at least one common shell, including = and +.\n            //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.\n            //Note that this does permit non-Latin alphanumeric characters based on the current locale.\n            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Check whether a file path is of a permitted type.\n     * Used to reject URLs and phar files from functions that access local file paths,\n     * such as addAttachment.\n     *\n     * @param string $path A relative or absolute path to a file\n     *\n     * @return bool\n     */\n    protected static function isPermittedPath($path)\n    {\n        //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1\n        return !preg_match('#^[a-z][a-z\\d+.-]*://#i', $path);\n    }\n\n    /**\n     * Check whether a file path is safe, accessible, and readable.\n     *\n     * @param string $path A relative or absolute path to a file\n     *\n     * @return bool\n     */\n    protected static function fileIsAccessible($path)\n    {\n        if (!static::isPermittedPath($path)) {\n            return false;\n        }\n        $readable = is_file($path);\n        //If not a UNC path (expected to start with \\\\), check read permission, see #2069\n        if (strpos($path, '\\\\\\\\') !== 0) {\n            $readable = $readable && is_readable($path);\n        }\n        return  $readable;\n    }\n\n    /**\n     * Send mail using the PHP mail() function.\n     *\n     * @see http://www.php.net/manual/en/book.mail.php\n     *\n     * @param string $header The message headers\n     * @param string $body   The message body\n     *\n     * @throws Exception\n     *\n     * @return bool\n     */\n    protected function mailSend($header, $body)\n    {\n        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;\n\n        $toArr = [];\n        foreach ($this->to as $toaddr) {\n            $toArr[] = $this->addrFormat($toaddr);\n        }\n        $to = trim(implode(', ', $toArr));\n\n        //If there are no To-addresses (e.g. when sending only to BCC-addresses)\n        //the following should be added to get a correct DKIM-signature.\n        //Compare with $this->preSend()\n        if ($to === '') {\n            $to = 'undisclosed-recipients:;';\n        }\n\n        $params = null;\n        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver\n        //A space after `-f` is optional, but there is a long history of its presence\n        //causing problems, so we don't use one\n        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html\n        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html\n        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html\n        //Example problem: https://www.drupal.org/node/1057954\n        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.\n\n        //PHP 5.6 workaround\n        $sendmail_from_value = ini_get('sendmail_from');\n        if (empty($this->Sender) && !empty($sendmail_from_value)) {\n            //PHP config has a sender address we can use\n            $this->Sender = ini_get('sendmail_from');\n        }\n        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {\n            if (self::isShellSafe($this->Sender)) {\n                $params = sprintf('-f%s', $this->Sender);\n            }\n            $old_from = ini_get('sendmail_from');\n            ini_set('sendmail_from', $this->Sender);\n        }\n        $result = false;\n        if ($this->SingleTo && count($toArr) > 1) {\n            foreach ($toArr as $toAddr) {\n                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);\n                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);\n                $this->doCallback(\n                    $result,\n                    [[$addrinfo['address'], $addrinfo['name']]],\n                    $this->cc,\n                    $this->bcc,\n                    $this->Subject,\n                    $body,\n                    $this->From,\n                    []\n                );\n            }\n        } else {\n            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);\n            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);\n        }\n        if (isset($old_from)) {\n            ini_set('sendmail_from', $old_from);\n        }\n        if (!$result) {\n            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);\n        }\n\n        return true;\n    }\n\n    /**\n     * Get an instance to use for SMTP operations.\n     * Override this function to load your own SMTP implementation,\n     * or set one with setSMTPInstance.\n     *\n     * @return SMTP\n     */\n    public function getSMTPInstance()\n    {\n        if (!is_object($this->smtp)) {\n            $this->smtp = new SMTP();\n        }\n\n        return $this->smtp;\n    }\n\n    /**\n     * Provide an instance to use for SMTP operations.\n     *\n     * @return SMTP\n     */\n    public function setSMTPInstance(SMTP $smtp)\n    {\n        $this->smtp = $smtp;\n\n        return $this->smtp;\n    }\n\n    /**\n     * Send mail via SMTP.\n     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.\n     *\n     * @see PHPMailer::setSMTPInstance() to use a different class.\n     *\n     * @uses \\PHPMailer\\PHPMailer\\SMTP\n     *\n     * @param string $header The message headers\n     * @param string $body   The message body\n     *\n     * @throws Exception\n     *\n     * @return bool\n     */\n    protected function smtpSend($header, $body)\n    {\n        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;\n        $bad_rcpt = [];\n        if (!$this->smtpConnect($this->SMTPOptions)) {\n            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);\n        }\n        //Sender already validated in preSend()\n        if ('' === $this->Sender) {\n            $smtp_from = $this->From;\n        } else {\n            $smtp_from = $this->Sender;\n        }\n        if (!$this->smtp->mail($smtp_from)) {\n            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));\n            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);\n        }\n\n        $callbacks = [];\n        //Attempt to send to all recipients\n        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {\n            foreach ($togroup as $to) {\n                if (!$this->smtp->recipient($to[0], $this->dsn)) {\n                    $error = $this->smtp->getError();\n                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];\n                    $isSent = false;\n                } else {\n                    $isSent = true;\n                }\n\n                $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];\n            }\n        }\n\n        //Only send the DATA command if we have viable recipients\n        if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {\n            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);\n        }\n\n        $smtp_transaction_id = $this->smtp->getLastTransactionID();\n\n        if ($this->SMTPKeepAlive) {\n            $this->smtp->reset();\n        } else {\n            $this->smtp->quit();\n            $this->smtp->close();\n        }\n\n        foreach ($callbacks as $cb) {\n            $this->doCallback(\n                $cb['issent'],\n                [[$cb['to'], $cb['name']]],\n                [],\n                [],\n                $this->Subject,\n                $body,\n                $this->From,\n                ['smtp_transaction_id' => $smtp_transaction_id]\n            );\n        }\n\n        //Create error message for any bad addresses\n        if (count($bad_rcpt) > 0) {\n            $errstr = '';\n            foreach ($bad_rcpt as $bad) {\n                $errstr .= $bad['to'] . ': ' . $bad['error'];\n            }\n            throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);\n        }\n\n        return true;\n    }\n\n    /**\n     * Initiate a connection to an SMTP server.\n     * Returns false if the operation failed.\n     *\n     * @param array $options An array of options compatible with stream_context_create()\n     *\n     * @throws Exception\n     *\n     * @uses \\PHPMailer\\PHPMailer\\SMTP\n     *\n     * @return bool\n     */\n    public function smtpConnect($options = null)\n    {\n        if (null === $this->smtp) {\n            $this->smtp = $this->getSMTPInstance();\n        }\n\n        //If no options are provided, use whatever is set in the instance\n        if (null === $options) {\n            $options = $this->SMTPOptions;\n        }\n\n        //Already connected?\n        if ($this->smtp->connected()) {\n            return true;\n        }\n\n        $this->smtp->setTimeout($this->Timeout);\n        $this->smtp->setDebugLevel($this->SMTPDebug);\n        $this->smtp->setDebugOutput($this->Debugoutput);\n        $this->smtp->setVerp($this->do_verp);\n        if ($this->Host === null) {\n            $this->Host = 'localhost';\n        }\n        $hosts = explode(';', $this->Host);\n        $lastexception = null;\n\n        foreach ($hosts as $hostentry) {\n            $hostinfo = [];\n            if (\n                !preg_match(\n                    '/^(?:(ssl|tls):\\/\\/)?(.+?)(?::(\\d+))?$/',\n                    trim($hostentry),\n                    $hostinfo\n                )\n            ) {\n                $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));\n                //Not a valid host entry\n                continue;\n            }\n            //$hostinfo[1]: optional ssl or tls prefix\n            //$hostinfo[2]: the hostname\n            //$hostinfo[3]: optional port number\n            //The host string prefix can temporarily override the current setting for SMTPSecure\n            //If it's not specified, the default value is used\n\n            //Check the host name is a valid name or IP address before trying to use it\n            if (!static::isValidHost($hostinfo[2])) {\n                $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);\n                continue;\n            }\n            $prefix = '';\n            $secure = $this->SMTPSecure;\n            $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);\n            if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {\n                $prefix = 'ssl://';\n                $tls = false; //Can't have SSL and TLS at the same time\n                $secure = static::ENCRYPTION_SMTPS;\n            } elseif ('tls' === $hostinfo[1]) {\n                $tls = true;\n                //TLS doesn't use a prefix\n                $secure = static::ENCRYPTION_STARTTLS;\n            }\n            //Do we need the OpenSSL extension?\n            $sslext = defined('OPENSSL_ALGO_SHA256');\n            if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {\n                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled\n                if (!$sslext) {\n                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);\n                }\n            }\n            $host = $hostinfo[2];\n            $port = $this->Port;\n            if (\n                array_key_exists(3, $hostinfo) &&\n                is_numeric($hostinfo[3]) &&\n                $hostinfo[3] > 0 &&\n                $hostinfo[3] < 65536\n            ) {\n                $port = (int) $hostinfo[3];\n            }\n            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {\n                try {\n                    if ($this->Helo) {\n                        $hello = $this->Helo;\n                    } else {\n                        $hello = $this->serverHostname();\n                    }\n                    $this->smtp->hello($hello);\n                    //Automatically enable TLS encryption if:\n                    //* it's not disabled\n                    //* we are not connecting to localhost\n                    //* we have openssl extension\n                    //* we are not already using SSL\n                    //* the server offers STARTTLS\n                    if (\n                        $this->SMTPAutoTLS &&\n                        $this->Host !== 'localhost' &&\n                        $sslext &&\n                        $secure !== 'ssl' &&\n                        $this->smtp->getServerExt('STARTTLS')\n                    ) {\n                        $tls = true;\n                    }\n                    if ($tls) {\n                        if (!$this->smtp->startTLS()) {\n                            $message = $this->getSmtpErrorMessage('connect_host');\n                            throw new Exception($message);\n                        }\n                        //We must resend EHLO after TLS negotiation\n                        $this->smtp->hello($hello);\n                    }\n                    if (\n                        $this->SMTPAuth && !$this->smtp->authenticate(\n                            $this->Username,\n                            $this->Password,\n                            $this->AuthType,\n                            $this->oauth\n                        )\n                    ) {\n                        throw new Exception($this->lang('authenticate'));\n                    }\n\n                    return true;\n                } catch (Exception $exc) {\n                    $lastexception = $exc;\n                    $this->edebug($exc->getMessage());\n                    //We must have connected, but then failed TLS or Auth, so close connection nicely\n                    $this->smtp->quit();\n                }\n            }\n        }\n        //If we get here, all connection attempts have failed, so close connection hard\n        $this->smtp->close();\n        //As we've caught all exceptions, just report whatever the last one was\n        if ($this->exceptions && null !== $lastexception) {\n            throw $lastexception;\n        }\n        if ($this->exceptions) {\n            // no exception was thrown, likely $this->smtp->connect() failed\n            $message = $this->getSmtpErrorMessage('connect_host');\n            throw new Exception($message);\n        }\n\n        return false;\n    }\n\n    /**\n     * Close the active SMTP session if one exists.\n     */\n    public function smtpClose()\n    {\n        if ((null !== $this->smtp) && $this->smtp->connected()) {\n            $this->smtp->quit();\n            $this->smtp->close();\n        }\n    }\n\n    /**\n     * Set the language for error messages.\n     * The default language is English.\n     *\n     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is \"fr\")\n     *                          Optionally, the language code can be enhanced with a 4-character\n     *                          script annotation and/or a 2-character country annotation.\n     * @param string $lang_path Path to the language file directory, with trailing separator (slash)\n     *                          Do not set this from user input!\n     *\n     * @return bool Returns true if the requested language was loaded, false otherwise.\n     */\n    public function setLanguage($langcode = 'en', $lang_path = '')\n    {\n        //Backwards compatibility for renamed language codes\n        $renamed_langcodes = [\n            'br' => 'pt_br',\n            'cz' => 'cs',\n            'dk' => 'da',\n            'no' => 'nb',\n            'se' => 'sv',\n            'rs' => 'sr',\n            'tg' => 'tl',\n            'am' => 'hy',\n        ];\n\n        if (array_key_exists($langcode, $renamed_langcodes)) {\n            $langcode = $renamed_langcodes[$langcode];\n        }\n\n        //Define full set of translatable strings in English\n        $PHPMAILER_LANG = [\n            'authenticate' => 'SMTP Error: Could not authenticate.',\n            'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' .\n                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .\n                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',\n            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',\n            'data_not_accepted' => 'SMTP Error: data not accepted.',\n            'empty_message' => 'Message body empty',\n            'encoding' => 'Unknown encoding: ',\n            'execute' => 'Could not execute: ',\n            'extension_missing' => 'Extension missing: ',\n            'file_access' => 'Could not access file: ',\n            'file_open' => 'File Error: Could not open file: ',\n            'from_failed' => 'The following From address failed: ',\n            'instantiate' => 'Could not instantiate mail function.',\n            'invalid_address' => 'Invalid address: ',\n            'invalid_header' => 'Invalid header name or value',\n            'invalid_hostentry' => 'Invalid hostentry: ',\n            'invalid_host' => 'Invalid host: ',\n            'mailer_not_supported' => ' mailer is not supported.',\n            'provide_address' => 'You must provide at least one recipient email address.',\n            'recipients_failed' => 'SMTP Error: The following recipients failed: ',\n            'signing' => 'Signing Error: ',\n            'smtp_code' => 'SMTP code: ',\n            'smtp_code_ex' => 'Additional SMTP info: ',\n            'smtp_connect_failed' => 'SMTP connect() failed.',\n            'smtp_detail' => 'Detail: ',\n            'smtp_error' => 'SMTP server error: ',\n            'variable_set' => 'Cannot set or reset variable: ',\n        ];\n        if (empty($lang_path)) {\n            //Calculate an absolute path so it can work if CWD is not here\n            $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;\n        }\n\n        //Validate $langcode\n        $foundlang = true;\n        $langcode  = strtolower($langcode);\n        if (\n            !preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches)\n            && $langcode !== 'en'\n        ) {\n            $foundlang = false;\n            $langcode = 'en';\n        }\n\n        //There is no English translation file\n        if ('en' !== $langcode) {\n            $langcodes = [];\n            if (!empty($matches['script']) && !empty($matches['country'])) {\n                $langcodes[] = $matches['lang'] . $matches['script'] . $matches['country'];\n            }\n            if (!empty($matches['country'])) {\n                $langcodes[] = $matches['lang'] . $matches['country'];\n            }\n            if (!empty($matches['script'])) {\n                $langcodes[] = $matches['lang'] . $matches['script'];\n            }\n            $langcodes[] = $matches['lang'];\n\n            //Try and find a readable language file for the requested language.\n            $foundFile = false;\n            foreach ($langcodes as $code) {\n                $lang_file = $lang_path . 'phpmailer.lang-' . $code . '.php';\n                if (static::fileIsAccessible($lang_file)) {\n                    $foundFile = true;\n                    break;\n                }\n            }\n\n            if ($foundFile === false) {\n                $foundlang = false;\n            } else {\n                $lines = file($lang_file);\n                foreach ($lines as $line) {\n                    //Translation file lines look like this:\n                    //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';\n                    //These files are parsed as text and not PHP so as to avoid the possibility of code injection\n                    //See https://blog.stevenlevithan.com/archives/match-quoted-string\n                    $matches = [];\n                    if (\n                        preg_match(\n                            '/^\\$PHPMAILER_LANG\\[\\'([a-z\\d_]+)\\'\\]\\s*=\\s*([\"\\'])(.+)*?\\2;/',\n                            $line,\n                            $matches\n                        ) &&\n                        //Ignore unknown translation keys\n                        array_key_exists($matches[1], $PHPMAILER_LANG)\n                    ) {\n                        //Overwrite language-specific strings so we'll never have missing translation keys.\n                        $PHPMAILER_LANG[$matches[1]] = (string)$matches[3];\n                    }\n                }\n            }\n        }\n        $this->language = $PHPMAILER_LANG;\n\n        return $foundlang; //Returns false if language not found\n    }\n\n    /**\n     * Get the array of strings for the current language.\n     *\n     * @return array\n     */\n    public function getTranslations()\n    {\n        if (empty($this->language)) {\n            $this->setLanguage(); // Set the default language.\n        }\n\n        return $this->language;\n    }\n\n    /**\n     * Create recipient headers.\n     *\n     * @param string $type\n     * @param array  $addr An array of recipients,\n     *                     where each recipient is a 2-element indexed array with element 0 containing an address\n     *                     and element 1 containing a name, like:\n     *                     [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']]\n     *\n     * @return string\n     */\n    public function addrAppend($type, $addr)\n    {\n        $addresses = [];\n        foreach ($addr as $address) {\n            $addresses[] = $this->addrFormat($address);\n        }\n\n        return $type . ': ' . implode(', ', $addresses) . static::$LE;\n    }\n\n    /**\n     * Format an address for use in a message header.\n     *\n     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like\n     *                    ['joe@example.com', 'Joe User']\n     *\n     * @return string\n     */\n    public function addrFormat($addr)\n    {\n        if (!isset($addr[1]) || ($addr[1] === '')) { //No name provided\n            return $this->secureHeader($addr[0]);\n        }\n\n        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .\n            ' <' . $this->secureHeader($addr[0]) . '>';\n    }\n\n    /**\n     * Word-wrap message.\n     * For use with mailers that do not automatically perform wrapping\n     * and for quoted-printable encoded messages.\n     * Original written by philippe.\n     *\n     * @param string $message The message to wrap\n     * @param int    $length  The line length to wrap to\n     * @param bool   $qp_mode Whether to run in Quoted-Printable mode\n     *\n     * @return string\n     */\n    public function wrapText($message, $length, $qp_mode = false)\n    {\n        if ($qp_mode) {\n            $soft_break = sprintf(' =%s', static::$LE);\n        } else {\n            $soft_break = static::$LE;\n        }\n        //If utf-8 encoding is used, we will need to make sure we don't\n        //split multibyte characters when we wrap\n        $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);\n        $lelen = strlen(static::$LE);\n        $crlflen = strlen(static::$LE);\n\n        $message = static::normalizeBreaks($message);\n        //Remove a trailing line break\n        if (substr($message, -$lelen) === static::$LE) {\n            $message = substr($message, 0, -$lelen);\n        }\n\n        //Split message into lines\n        $lines = explode(static::$LE, $message);\n        //Message will be rebuilt in here\n        $message = '';\n        foreach ($lines as $line) {\n            $words = explode(' ', $line);\n            $buf = '';\n            $firstword = true;\n            foreach ($words as $word) {\n                if ($qp_mode && (strlen($word) > $length)) {\n                    $space_left = $length - strlen($buf) - $crlflen;\n                    if (!$firstword) {\n                        if ($space_left > 20) {\n                            $len = $space_left;\n                            if ($is_utf8) {\n                                $len = $this->utf8CharBoundary($word, $len);\n                            } elseif ('=' === substr($word, $len - 1, 1)) {\n                                --$len;\n                            } elseif ('=' === substr($word, $len - 2, 1)) {\n                                $len -= 2;\n                            }\n                            $part = substr($word, 0, $len);\n                            $word = substr($word, $len);\n                            $buf .= ' ' . $part;\n                            $message .= $buf . sprintf('=%s', static::$LE);\n                        } else {\n                            $message .= $buf . $soft_break;\n                        }\n                        $buf = '';\n                    }\n                    while ($word !== '') {\n                        if ($length <= 0) {\n                            break;\n                        }\n                        $len = $length;\n                        if ($is_utf8) {\n                            $len = $this->utf8CharBoundary($word, $len);\n                        } elseif ('=' === substr($word, $len - 1, 1)) {\n                            --$len;\n                        } elseif ('=' === substr($word, $len - 2, 1)) {\n                            $len -= 2;\n                        }\n                        $part = substr($word, 0, $len);\n                        $word = (string) substr($word, $len);\n\n                        if ($word !== '') {\n                            $message .= $part . sprintf('=%s', static::$LE);\n                        } else {\n                            $buf = $part;\n                        }\n                    }\n                } else {\n                    $buf_o = $buf;\n                    if (!$firstword) {\n                        $buf .= ' ';\n                    }\n                    $buf .= $word;\n\n                    if ('' !== $buf_o && strlen($buf) > $length) {\n                        $message .= $buf_o . $soft_break;\n                        $buf = $word;\n                    }\n                }\n                $firstword = false;\n            }\n            $message .= $buf . static::$LE;\n        }\n\n        return $message;\n    }\n\n    /**\n     * Find the last character boundary prior to $maxLength in a utf-8\n     * quoted-printable encoded string.\n     * Original written by Colin Brown.\n     *\n     * @param string $encodedText utf-8 QP text\n     * @param int    $maxLength   Find the last character boundary prior to this length\n     *\n     * @return int\n     */\n    public function utf8CharBoundary($encodedText, $maxLength)\n    {\n        $foundSplitPos = false;\n        $lookBack = 3;\n        while (!$foundSplitPos) {\n            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);\n            $encodedCharPos = strpos($lastChunk, '=');\n            if (false !== $encodedCharPos) {\n                //Found start of encoded character byte within $lookBack block.\n                //Check the encoded byte value (the 2 chars after the '=')\n                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);\n                $dec = hexdec($hex);\n                if ($dec < 128) {\n                    //Single byte character.\n                    //If the encoded char was found at pos 0, it will fit\n                    //otherwise reduce maxLength to start of the encoded char\n                    if ($encodedCharPos > 0) {\n                        $maxLength -= $lookBack - $encodedCharPos;\n                    }\n                    $foundSplitPos = true;\n                } elseif ($dec >= 192) {\n                    //First byte of a multi byte character\n                    //Reduce maxLength to split at start of character\n                    $maxLength -= $lookBack - $encodedCharPos;\n                    $foundSplitPos = true;\n                } elseif ($dec < 192) {\n                    //Middle byte of a multi byte character, look further back\n                    $lookBack += 3;\n                }\n            } else {\n                //No encoded character found\n                $foundSplitPos = true;\n            }\n        }\n\n        return $maxLength;\n    }\n\n    /**\n     * Apply word wrapping to the message body.\n     * Wraps the message body to the number of chars set in the WordWrap property.\n     * You should only do this to plain-text bodies as wrapping HTML tags may break them.\n     * This is called automatically by createBody(), so you don't need to call it yourself.\n     */\n    public function setWordWrap()\n    {\n        if ($this->WordWrap < 1) {\n            return;\n        }\n\n        switch ($this->message_type) {\n            case 'alt':\n            case 'alt_inline':\n            case 'alt_attach':\n            case 'alt_inline_attach':\n                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);\n                break;\n            default:\n                $this->Body = $this->wrapText($this->Body, $this->WordWrap);\n                break;\n        }\n    }\n\n    /**\n     * Assemble message headers.\n     *\n     * @return string The assembled headers\n     */\n    public function createHeader()\n    {\n        $result = '';\n\n        $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);\n\n        //The To header is created automatically by mail(), so needs to be omitted here\n        if ('mail' !== $this->Mailer) {\n            if ($this->SingleTo) {\n                foreach ($this->to as $toaddr) {\n                    $this->SingleToArray[] = $this->addrFormat($toaddr);\n                }\n            } elseif (count($this->to) > 0) {\n                $result .= $this->addrAppend('To', $this->to);\n            } elseif (count($this->cc) === 0) {\n                $result .= $this->headerLine('To', 'undisclosed-recipients:;');\n            }\n        }\n        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);\n\n        //sendmail and mail() extract Cc from the header before sending\n        if (count($this->cc) > 0) {\n            $result .= $this->addrAppend('Cc', $this->cc);\n        }\n\n        //sendmail and mail() extract Bcc from the header before sending\n        if (\n            (\n                'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer\n            )\n            && count($this->bcc) > 0\n        ) {\n            $result .= $this->addrAppend('Bcc', $this->bcc);\n        }\n\n        if (count($this->ReplyTo) > 0) {\n            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);\n        }\n\n        //mail() sets the subject itself\n        if ('mail' !== $this->Mailer) {\n            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));\n        }\n\n        //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4\n        //https://tools.ietf.org/html/rfc5322#section-3.6.4\n        if (\n            '' !== $this->MessageID &&\n            preg_match(\n                '/^<((([a-z\\d!#$%&\\'*+\\/=?^_`{|}~-]+(\\.[a-z\\d!#$%&\\'*+\\/=?^_`{|}~-]+)*)' .\n                '|(\"(([\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]|[\\x21\\x23-\\x5B\\x5D-\\x7E])' .\n                '|(\\\\[\\x01-\\x09\\x0B\\x0C\\x0E-\\x7F]))*\"))@(([a-z\\d!#$%&\\'*+\\/=?^_`{|}~-]+' .\n                '(\\.[a-z\\d!#$%&\\'*+\\/=?^_`{|}~-]+)*)|(\\[(([\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]' .\n                '|[\\x21-\\x5A\\x5E-\\x7E])|(\\\\[\\x01-\\x09\\x0B\\x0C\\x0E-\\x7F]))*\\])))>$/Di',\n                $this->MessageID\n            )\n        ) {\n            $this->lastMessageID = $this->MessageID;\n        } else {\n            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());\n        }\n        $result .= $this->headerLine('Message-ID', $this->lastMessageID);\n        if (null !== $this->Priority) {\n            $result .= $this->headerLine('X-Priority', $this->Priority);\n        }\n        if ('' === $this->XMailer) {\n            //Empty string for default X-Mailer header\n            $result .= $this->headerLine(\n                'X-Mailer',\n                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'\n            );\n        } elseif (is_string($this->XMailer) && trim($this->XMailer) !== '') {\n            //Some string\n            $result .= $this->headerLine('X-Mailer', trim($this->XMailer));\n        } //Other values result in no X-Mailer header\n\n        if ('' !== $this->ConfirmReadingTo) {\n            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');\n        }\n\n        //Add custom headers\n        foreach ($this->CustomHeader as $header) {\n            $result .= $this->headerLine(\n                trim($header[0]),\n                $this->encodeHeader(trim($header[1]))\n            );\n        }\n        if (!$this->sign_key_file) {\n            $result .= $this->headerLine('MIME-Version', '1.0');\n            $result .= $this->getMailMIME();\n        }\n\n        return $result;\n    }\n\n    /**\n     * Get the message MIME type headers.\n     *\n     * @return string\n     */\n    public function getMailMIME()\n    {\n        $result = '';\n        $ismultipart = true;\n        switch ($this->message_type) {\n            case 'inline':\n                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');\n                $result .= $this->textLine(' boundary=\"' . $this->boundary[1] . '\"');\n                break;\n            case 'attach':\n            case 'inline_attach':\n            case 'alt_attach':\n            case 'alt_inline_attach':\n                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');\n                $result .= $this->textLine(' boundary=\"' . $this->boundary[1] . '\"');\n                break;\n            case 'alt':\n            case 'alt_inline':\n                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');\n                $result .= $this->textLine(' boundary=\"' . $this->boundary[1] . '\"');\n                break;\n            default:\n                //Catches case 'plain': and case '':\n                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);\n                $ismultipart = false;\n                break;\n        }\n        //RFC1341 part 5 says 7bit is assumed if not specified\n        if (static::ENCODING_7BIT !== $this->Encoding) {\n            //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE\n            if ($ismultipart) {\n                if (static::ENCODING_8BIT === $this->Encoding) {\n                    $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);\n                }\n                //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible\n            } else {\n                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Returns the whole MIME message.\n     * Includes complete headers and body.\n     * Only valid post preSend().\n     *\n     * @see PHPMailer::preSend()\n     *\n     * @return string\n     */\n    public function getSentMIMEMessage()\n    {\n        return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .\n            static::$LE . static::$LE . $this->MIMEBody;\n    }\n\n    /**\n     * Create a unique ID to use for boundaries.\n     *\n     * @return string\n     */\n    protected function generateId()\n    {\n        $len = 32; //32 bytes = 256 bits\n        $bytes = '';\n        if (function_exists('random_bytes')) {\n            try {\n                $bytes = random_bytes($len);\n            } catch (\\Exception $e) {\n                //Do nothing\n            }\n        } elseif (function_exists('openssl_random_pseudo_bytes')) {\n            /** @noinspection CryptographicallySecureRandomnessInspection */\n            $bytes = openssl_random_pseudo_bytes($len);\n        }\n        if ($bytes === '') {\n            //We failed to produce a proper random string, so make do.\n            //Use a hash to force the length to the same as the other methods\n            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);\n        }\n\n        //We don't care about messing up base64 format here, just want a random string\n        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));\n    }\n\n    /**\n     * Assemble the message body.\n     * Returns an empty string on failure.\n     *\n     * @throws Exception\n     *\n     * @return string The assembled message body\n     */\n    public function createBody()\n    {\n        $body = '';\n        //Create unique IDs and preset boundaries\n        $this->setBoundaries();\n\n        if ($this->sign_key_file) {\n            $body .= $this->getMailMIME() . static::$LE;\n        }\n\n        $this->setWordWrap();\n\n        $bodyEncoding = $this->Encoding;\n        $bodyCharSet = $this->CharSet;\n        //Can we do a 7-bit downgrade?\n        if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {\n            $bodyEncoding = static::ENCODING_7BIT;\n            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit\n            $bodyCharSet = static::CHARSET_ASCII;\n        }\n        //If lines are too long, and we're not already using an encoding that will shorten them,\n        //change to quoted-printable transfer encoding for the body part only\n        if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {\n            $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;\n        }\n\n        $altBodyEncoding = $this->Encoding;\n        $altBodyCharSet = $this->CharSet;\n        //Can we do a 7-bit downgrade?\n        if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {\n            $altBodyEncoding = static::ENCODING_7BIT;\n            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit\n            $altBodyCharSet = static::CHARSET_ASCII;\n        }\n        //If lines are too long, and we're not already using an encoding that will shorten them,\n        //change to quoted-printable transfer encoding for the alt body part only\n        if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {\n            $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;\n        }\n        //Use this as a preamble in all multipart message types\n        $mimepre = '';\n        switch ($this->message_type) {\n            case 'inline':\n                $body .= $mimepre;\n                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);\n                $body .= $this->encodeString($this->Body, $bodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->attachAll('inline', $this->boundary[1]);\n                break;\n            case 'attach':\n                $body .= $mimepre;\n                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);\n                $body .= $this->encodeString($this->Body, $bodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->attachAll('attachment', $this->boundary[1]);\n                break;\n            case 'inline_attach':\n                $body .= $mimepre;\n                $body .= $this->textLine('--' . $this->boundary[1]);\n                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');\n                $body .= $this->textLine(' boundary=\"' . $this->boundary[2] . '\";');\n                $body .= $this->textLine(' type=\"' . static::CONTENT_TYPE_TEXT_HTML . '\"');\n                $body .= static::$LE;\n                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);\n                $body .= $this->encodeString($this->Body, $bodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->attachAll('inline', $this->boundary[2]);\n                $body .= static::$LE;\n                $body .= $this->attachAll('attachment', $this->boundary[1]);\n                break;\n            case 'alt':\n                $body .= $mimepre;\n                $body .= $this->getBoundary(\n                    $this->boundary[1],\n                    $altBodyCharSet,\n                    static::CONTENT_TYPE_PLAINTEXT,\n                    $altBodyEncoding\n                );\n                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->getBoundary(\n                    $this->boundary[1],\n                    $bodyCharSet,\n                    static::CONTENT_TYPE_TEXT_HTML,\n                    $bodyEncoding\n                );\n                $body .= $this->encodeString($this->Body, $bodyEncoding);\n                $body .= static::$LE;\n                if (!empty($this->Ical)) {\n                    $method = static::ICAL_METHOD_REQUEST;\n                    foreach (static::$IcalMethods as $imethod) {\n                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {\n                            $method = $imethod;\n                            break;\n                        }\n                    }\n                    $body .= $this->getBoundary(\n                        $this->boundary[1],\n                        '',\n                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,\n                        ''\n                    );\n                    $body .= $this->encodeString($this->Ical, $this->Encoding);\n                    $body .= static::$LE;\n                }\n                $body .= $this->endBoundary($this->boundary[1]);\n                break;\n            case 'alt_inline':\n                $body .= $mimepre;\n                $body .= $this->getBoundary(\n                    $this->boundary[1],\n                    $altBodyCharSet,\n                    static::CONTENT_TYPE_PLAINTEXT,\n                    $altBodyEncoding\n                );\n                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->textLine('--' . $this->boundary[1]);\n                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');\n                $body .= $this->textLine(' boundary=\"' . $this->boundary[2] . '\";');\n                $body .= $this->textLine(' type=\"' . static::CONTENT_TYPE_TEXT_HTML . '\"');\n                $body .= static::$LE;\n                $body .= $this->getBoundary(\n                    $this->boundary[2],\n                    $bodyCharSet,\n                    static::CONTENT_TYPE_TEXT_HTML,\n                    $bodyEncoding\n                );\n                $body .= $this->encodeString($this->Body, $bodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->attachAll('inline', $this->boundary[2]);\n                $body .= static::$LE;\n                $body .= $this->endBoundary($this->boundary[1]);\n                break;\n            case 'alt_attach':\n                $body .= $mimepre;\n                $body .= $this->textLine('--' . $this->boundary[1]);\n                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');\n                $body .= $this->textLine(' boundary=\"' . $this->boundary[2] . '\"');\n                $body .= static::$LE;\n                $body .= $this->getBoundary(\n                    $this->boundary[2],\n                    $altBodyCharSet,\n                    static::CONTENT_TYPE_PLAINTEXT,\n                    $altBodyEncoding\n                );\n                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->getBoundary(\n                    $this->boundary[2],\n                    $bodyCharSet,\n                    static::CONTENT_TYPE_TEXT_HTML,\n                    $bodyEncoding\n                );\n                $body .= $this->encodeString($this->Body, $bodyEncoding);\n                $body .= static::$LE;\n                if (!empty($this->Ical)) {\n                    $method = static::ICAL_METHOD_REQUEST;\n                    foreach (static::$IcalMethods as $imethod) {\n                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {\n                            $method = $imethod;\n                            break;\n                        }\n                    }\n                    $body .= $this->getBoundary(\n                        $this->boundary[2],\n                        '',\n                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,\n                        ''\n                    );\n                    $body .= $this->encodeString($this->Ical, $this->Encoding);\n                }\n                $body .= $this->endBoundary($this->boundary[2]);\n                $body .= static::$LE;\n                $body .= $this->attachAll('attachment', $this->boundary[1]);\n                break;\n            case 'alt_inline_attach':\n                $body .= $mimepre;\n                $body .= $this->textLine('--' . $this->boundary[1]);\n                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');\n                $body .= $this->textLine(' boundary=\"' . $this->boundary[2] . '\"');\n                $body .= static::$LE;\n                $body .= $this->getBoundary(\n                    $this->boundary[2],\n                    $altBodyCharSet,\n                    static::CONTENT_TYPE_PLAINTEXT,\n                    $altBodyEncoding\n                );\n                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->textLine('--' . $this->boundary[2]);\n                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');\n                $body .= $this->textLine(' boundary=\"' . $this->boundary[3] . '\";');\n                $body .= $this->textLine(' type=\"' . static::CONTENT_TYPE_TEXT_HTML . '\"');\n                $body .= static::$LE;\n                $body .= $this->getBoundary(\n                    $this->boundary[3],\n                    $bodyCharSet,\n                    static::CONTENT_TYPE_TEXT_HTML,\n                    $bodyEncoding\n                );\n                $body .= $this->encodeString($this->Body, $bodyEncoding);\n                $body .= static::$LE;\n                $body .= $this->attachAll('inline', $this->boundary[3]);\n                $body .= static::$LE;\n                $body .= $this->endBoundary($this->boundary[2]);\n                $body .= static::$LE;\n                $body .= $this->attachAll('attachment', $this->boundary[1]);\n                break;\n            default:\n                //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types\n                //Reset the `Encoding` property in case we changed it for line length reasons\n                $this->Encoding = $bodyEncoding;\n                $body .= $this->encodeString($this->Body, $this->Encoding);\n                break;\n        }\n\n        if ($this->isError()) {\n            $body = '';\n            if ($this->exceptions) {\n                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);\n            }\n        } elseif ($this->sign_key_file) {\n            try {\n                if (!defined('PKCS7_TEXT')) {\n                    throw new Exception($this->lang('extension_missing') . 'openssl');\n                }\n\n                $file = tempnam(sys_get_temp_dir(), 'srcsign');\n                $signed = tempnam(sys_get_temp_dir(), 'mailsign');\n                file_put_contents($file, $body);\n\n                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197\n                if (empty($this->sign_extracerts_file)) {\n                    $sign = @openssl_pkcs7_sign(\n                        $file,\n                        $signed,\n                        'file://' . realpath($this->sign_cert_file),\n                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],\n                        []\n                    );\n                } else {\n                    $sign = @openssl_pkcs7_sign(\n                        $file,\n                        $signed,\n                        'file://' . realpath($this->sign_cert_file),\n                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],\n                        [],\n                        PKCS7_DETACHED,\n                        $this->sign_extracerts_file\n                    );\n                }\n\n                @unlink($file);\n                if ($sign) {\n                    $body = file_get_contents($signed);\n                    @unlink($signed);\n                    //The message returned by openssl contains both headers and body, so need to split them up\n                    $parts = explode(\"\\n\\n\", $body, 2);\n                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;\n                    $body = $parts[1];\n                } else {\n                    @unlink($signed);\n                    throw new Exception($this->lang('signing') . openssl_error_string());\n                }\n            } catch (Exception $exc) {\n                $body = '';\n                if ($this->exceptions) {\n                    throw $exc;\n                }\n            }\n        }\n\n        return $body;\n    }\n\n    /**\n     * Get the boundaries that this message will use\n     * @return array\n     */\n    public function getBoundaries()\n    {\n        if (empty($this->boundary)) {\n            $this->setBoundaries();\n        }\n        return $this->boundary;\n    }\n\n    /**\n     * Return the start of a message boundary.\n     *\n     * @param string $boundary\n     * @param string $charSet\n     * @param string $contentType\n     * @param string $encoding\n     *\n     * @return string\n     */\n    protected function getBoundary($boundary, $charSet, $contentType, $encoding)\n    {\n        $result = '';\n        if ('' === $charSet) {\n            $charSet = $this->CharSet;\n        }\n        if ('' === $contentType) {\n            $contentType = $this->ContentType;\n        }\n        if ('' === $encoding) {\n            $encoding = $this->Encoding;\n        }\n        $result .= $this->textLine('--' . $boundary);\n        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);\n        $result .= static::$LE;\n        //RFC1341 part 5 says 7bit is assumed if not specified\n        if (static::ENCODING_7BIT !== $encoding) {\n            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);\n        }\n        $result .= static::$LE;\n\n        return $result;\n    }\n\n    /**\n     * Return the end of a message boundary.\n     *\n     * @param string $boundary\n     *\n     * @return string\n     */\n    protected function endBoundary($boundary)\n    {\n        return static::$LE . '--' . $boundary . '--' . static::$LE;\n    }\n\n    /**\n     * Set the message type.\n     * PHPMailer only supports some preset message types, not arbitrary MIME structures.\n     */\n    protected function setMessageType()\n    {\n        $type = [];\n        if ($this->alternativeExists()) {\n            $type[] = 'alt';\n        }\n        if ($this->inlineImageExists()) {\n            $type[] = 'inline';\n        }\n        if ($this->attachmentExists()) {\n            $type[] = 'attach';\n        }\n        $this->message_type = implode('_', $type);\n        if ('' === $this->message_type) {\n            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text\n            $this->message_type = 'plain';\n        }\n    }\n\n    /**\n     * Format a header line.\n     *\n     * @param string     $name\n     * @param string|int $value\n     *\n     * @return string\n     */\n    public function headerLine($name, $value)\n    {\n        return $name . ': ' . $value . static::$LE;\n    }\n\n    /**\n     * Return a formatted mail line.\n     *\n     * @param string $value\n     *\n     * @return string\n     */\n    public function textLine($value)\n    {\n        return $value . static::$LE;\n    }\n\n    /**\n     * Add an attachment from a path on the filesystem.\n     * Never use a user-supplied path to a file!\n     * Returns false if the file could not be found or read.\n     * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.\n     * If you need to do that, fetch the resource yourself and pass it in via a local file or string.\n     *\n     * @param string $path        Path to the attachment\n     * @param string $name        Overrides the attachment name\n     * @param string $encoding    File encoding (see $Encoding)\n     * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified\n     * @param string $disposition Disposition to use\n     *\n     * @throws Exception\n     *\n     * @return bool\n     */\n    public function addAttachment(\n        $path,\n        $name = '',\n        $encoding = self::ENCODING_BASE64,\n        $type = '',\n        $disposition = 'attachment'\n    ) {\n        try {\n            if (!static::fileIsAccessible($path)) {\n                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);\n            }\n\n            //If a MIME type is not specified, try to work it out from the file name\n            if ('' === $type) {\n                $type = static::filenameToType($path);\n            }\n\n            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);\n            if ('' === $name) {\n                $name = $filename;\n            }\n            if (!$this->validateEncoding($encoding)) {\n                throw new Exception($this->lang('encoding') . $encoding);\n            }\n\n            $this->attachment[] = [\n                0 => $path,\n                1 => $filename,\n                2 => $name,\n                3 => $encoding,\n                4 => $type,\n                5 => false, //isStringAttachment\n                6 => $disposition,\n                7 => $name,\n            ];\n        } catch (Exception $exc) {\n            $this->setError($exc->getMessage());\n            $this->edebug($exc->getMessage());\n            if ($this->exceptions) {\n                throw $exc;\n            }\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Return the array of attachments.\n     *\n     * @return array\n     */\n    public function getAttachments()\n    {\n        return $this->attachment;\n    }\n\n    /**\n     * Attach all file, string, and binary attachments to the message.\n     * Returns an empty string on failure.\n     *\n     * @param string $disposition_type\n     * @param string $boundary\n     *\n     * @throws Exception\n     *\n     * @return string\n     */\n    protected function attachAll($disposition_type, $boundary)\n    {\n        //Return text of body\n        $mime = [];\n        $cidUniq = [];\n        $incl = [];\n\n        //Add all attachments\n        foreach ($this->attachment as $attachment) {\n            //Check if it is a valid disposition_filter\n            if ($attachment[6] === $disposition_type) {\n                //Check for string attachment\n                $string = '';\n                $path = '';\n                $bString = $attachment[5];\n                if ($bString) {\n                    $string = $attachment[0];\n                } else {\n                    $path = $attachment[0];\n                }\n\n                $inclhash = hash('sha256', serialize($attachment));\n                if (in_array($inclhash, $incl, true)) {\n                    continue;\n                }\n                $incl[] = $inclhash;\n                $name = $attachment[2];\n                $encoding = $attachment[3];\n                $type = $attachment[4];\n                $disposition = $attachment[6];\n                $cid = $attachment[7];\n                if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {\n                    continue;\n                }\n                $cidUniq[$cid] = true;\n\n                $mime[] = sprintf('--%s%s', $boundary, static::$LE);\n                //Only include a filename property if we have one\n                if (!empty($name)) {\n                    $mime[] = sprintf(\n                        'Content-Type: %s; name=%s%s',\n                        $type,\n                        static::quotedString($this->encodeHeader($this->secureHeader($name))),\n                        static::$LE\n                    );\n                } else {\n                    $mime[] = sprintf(\n                        'Content-Type: %s%s',\n                        $type,\n                        static::$LE\n                    );\n                }\n                //RFC1341 part 5 says 7bit is assumed if not specified\n                if (static::ENCODING_7BIT !== $encoding) {\n                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);\n                }\n\n                //Only set Content-IDs on inline attachments\n                if ((string) $cid !== '' && $disposition === 'inline') {\n                    $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;\n                }\n\n                //Allow for bypassing the Content-Disposition header\n                if (!empty($disposition)) {\n                    $encoded_name = $this->encodeHeader($this->secureHeader($name));\n                    if (!empty($encoded_name)) {\n                        $mime[] = sprintf(\n                            'Content-Disposition: %s; filename=%s%s',\n                            $disposition,\n                            static::quotedString($encoded_name),\n                            static::$LE . static::$LE\n                        );\n                    } else {\n                        $mime[] = sprintf(\n                            'Content-Disposition: %s%s',\n                            $disposition,\n                            static::$LE . static::$LE\n                        );\n                    }\n                } else {\n                    $mime[] = static::$LE;\n                }\n\n                //Encode as string attachment\n                if ($bString) {\n                    $mime[] = $this->encodeString($string, $encoding);\n                } else {\n                    $mime[] = $this->encodeFile($path, $encoding);\n                }\n                if ($this->isError()) {\n                    return '';\n                }\n                $mime[] = static::$LE;\n            }\n        }\n\n        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);\n\n        return implode('', $mime);\n    }\n\n    /**\n     * Encode a file attachment in requested format.\n     * Returns an empty string on failure.\n     *\n     * @param string $path     The full path to the file\n     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'\n     *\n     * @return string\n     */\n    protected function encodeFile($path, $encoding = self::ENCODING_BASE64)\n    {\n        try {\n            if (!static::fileIsAccessible($path)) {\n                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);\n            }\n            $file_buffer = file_get_contents($path);\n            if (false === $file_buffer) {\n                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);\n            }\n            $file_buffer = $this->encodeString($file_buffer, $encoding);\n\n            return $file_buffer;\n        } catch (Exception $exc) {\n            $this->setError($exc->getMessage());\n            $this->edebug($exc->getMessage());\n            if ($this->exceptions) {\n                throw $exc;\n            }\n\n            return '';\n        }\n    }\n\n    /**\n     * Encode a string in requested format.\n     * Returns an empty string on failure.\n     *\n     * @param string $str      The text to encode\n     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'\n     *\n     * @throws Exception\n     *\n     * @return string\n     */\n    public function encodeString($str, $encoding = self::ENCODING_BASE64)\n    {\n        $encoded = '';\n        switch (strtolower($encoding)) {\n            case static::ENCODING_BASE64:\n                $encoded = chunk_split(\n                    base64_encode($str),\n                    static::STD_LINE_LENGTH,\n                    static::$LE\n                );\n                break;\n            case static::ENCODING_7BIT:\n            case static::ENCODING_8BIT:\n                $encoded = static::normalizeBreaks($str);\n                //Make sure it ends with a line break\n                if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {\n                    $encoded .= static::$LE;\n                }\n                break;\n            case static::ENCODING_BINARY:\n                $encoded = $str;\n                break;\n            case static::ENCODING_QUOTED_PRINTABLE:\n                $encoded = $this->encodeQP($str);\n                break;\n            default:\n                $this->setError($this->lang('encoding') . $encoding);\n                if ($this->exceptions) {\n                    throw new Exception($this->lang('encoding') . $encoding);\n                }\n                break;\n        }\n\n        return $encoded;\n    }\n\n    /**\n     * Encode a header value (not including its label) optimally.\n     * Picks shortest of Q, B, or none. Result includes folding if needed.\n     * See RFC822 definitions for phrase, comment and text positions.\n     *\n     * @param string $str      The header value to encode\n     * @param string $position What context the string will be used in\n     *\n     * @return string\n     */\n    public function encodeHeader($str, $position = 'text')\n    {\n        $matchcount = 0;\n        switch (strtolower($position)) {\n            case 'phrase':\n                if (!preg_match('/[\\200-\\377]/', $str)) {\n                    //Can't use addslashes as we don't know the value of magic_quotes_sybase\n                    $encoded = addcslashes($str, \"\\0..\\37\\177\\\\\\\"\");\n                    if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\\'*+\\/=?^_`{|}~ -]/', $str)) {\n                        return $encoded;\n                    }\n\n                    return \"\\\"$encoded\\\"\";\n                }\n                $matchcount = preg_match_all('/[^\\040\\041\\043-\\133\\135-\\176]/', $str, $matches);\n                break;\n            /* @noinspection PhpMissingBreakStatementInspection */\n            case 'comment':\n                $matchcount = preg_match_all('/[()\"]/', $str, $matches);\n            //fallthrough\n            case 'text':\n            default:\n                $matchcount += preg_match_all('/[\\000-\\010\\013\\014\\016-\\037\\177-\\377]/', $str, $matches);\n                break;\n        }\n\n        if ($this->has8bitChars($str)) {\n            $charset = $this->CharSet;\n        } else {\n            $charset = static::CHARSET_ASCII;\n        }\n\n        //Q/B encoding adds 8 chars and the charset (\"` =?<charset>?[QB]?<content>?=`\").\n        $overhead = 8 + strlen($charset);\n\n        if ('mail' === $this->Mailer) {\n            $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;\n        } else {\n            $maxlen = static::MAX_LINE_LENGTH - $overhead;\n        }\n\n        //Select the encoding that produces the shortest output and/or prevents corruption.\n        if ($matchcount > strlen($str) / 3) {\n            //More than 1/3 of the content needs encoding, use B-encode.\n            $encoding = 'B';\n        } elseif ($matchcount > 0) {\n            //Less than 1/3 of the content needs encoding, use Q-encode.\n            $encoding = 'Q';\n        } elseif (strlen($str) > $maxlen) {\n            //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.\n            $encoding = 'Q';\n        } else {\n            //No reformatting needed\n            $encoding = false;\n        }\n\n        switch ($encoding) {\n            case 'B':\n                if ($this->hasMultiBytes($str)) {\n                    //Use a custom function which correctly encodes and wraps long\n                    //multibyte strings without breaking lines within a character\n                    $encoded = $this->base64EncodeWrapMB($str, \"\\n\");\n                } else {\n                    $encoded = base64_encode($str);\n                    $maxlen -= $maxlen % 4;\n                    $encoded = trim(chunk_split($encoded, $maxlen, \"\\n\"));\n                }\n                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . \"?$encoding?\\\\1?=\", $encoded);\n                break;\n            case 'Q':\n                $encoded = $this->encodeQ($str, $position);\n                $encoded = $this->wrapText($encoded, $maxlen, true);\n                $encoded = str_replace('=' . static::$LE, \"\\n\", trim($encoded));\n                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . \"?$encoding?\\\\1?=\", $encoded);\n                break;\n            default:\n                return $str;\n        }\n\n        return trim(static::normalizeBreaks($encoded));\n    }\n\n    /**\n     * Check if a string contains multi-byte characters.\n     *\n     * @param string $str multi-byte text to wrap encode\n     *\n     * @return bool\n     */\n    public function hasMultiBytes($str)\n    {\n        if (function_exists('mb_strlen')) {\n            return strlen($str) > mb_strlen($str, $this->CharSet);\n        }\n\n        //Assume no multibytes (we can't handle without mbstring functions anyway)\n        return false;\n    }\n\n    /**\n     * Does a string contain any 8-bit chars (in any charset)?\n     *\n     * @param string $text\n     *\n     * @return bool\n     */\n    public function has8bitChars($text)\n    {\n        return (bool) preg_match('/[\\x80-\\xFF]/', $text);\n    }\n\n    /**\n     * Encode and wrap long multibyte strings for mail headers\n     * without breaking lines within a character.\n     * Adapted from a function by paravoid.\n     *\n     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283\n     *\n     * @param string $str       multi-byte text to wrap encode\n     * @param string $linebreak string to use as linefeed/end-of-line\n     *\n     * @return string\n     */\n    public function base64EncodeWrapMB($str, $linebreak = null)\n    {\n        $start = '=?' . $this->CharSet . '?B?';\n        $end = '?=';\n        $encoded = '';\n        if (null === $linebreak) {\n            $linebreak = static::$LE;\n        }\n\n        $mb_length = mb_strlen($str, $this->CharSet);\n        //Each line must have length <= 75, including $start and $end\n        $length = 75 - strlen($start) - strlen($end);\n        //Average multi-byte ratio\n        $ratio = $mb_length / strlen($str);\n        //Base64 has a 4:3 ratio\n        $avgLength = floor($length * $ratio * .75);\n\n        $offset = 0;\n        for ($i = 0; $i < $mb_length; $i += $offset) {\n            $lookBack = 0;\n            do {\n                $offset = $avgLength - $lookBack;\n                $chunk = mb_substr($str, $i, $offset, $this->CharSet);\n                $chunk = base64_encode($chunk);\n                ++$lookBack;\n            } while (strlen($chunk) > $length);\n            $encoded .= $chunk . $linebreak;\n        }\n\n        //Chomp the last linefeed\n        return substr($encoded, 0, -strlen($linebreak));\n    }\n\n    /**\n     * Encode a string in quoted-printable format.\n     * According to RFC2045 section 6.7.\n     *\n     * @param string $string The text to encode\n     *\n     * @return string\n     */\n    public function encodeQP($string)\n    {\n        return static::normalizeBreaks(quoted_printable_encode($string));\n    }\n\n    /**\n     * Encode a string using Q encoding.\n     *\n     * @see http://tools.ietf.org/html/rfc2047#section-4.2\n     *\n     * @param string $str      the text to encode\n     * @param string $position Where the text is going to be used, see the RFC for what that means\n     *\n     * @return string\n     */\n    public function encodeQ($str, $position = 'text')\n    {\n        //There should not be any EOL in the string\n        $pattern = '';\n        $encoded = str_replace([\"\\r\", \"\\n\"], '', $str);\n        switch (strtolower($position)) {\n            case 'phrase':\n                //RFC 2047 section 5.3\n                $pattern = '^A-Za-z0-9!*+\\/ -';\n                break;\n            /*\n             * RFC 2047 section 5.2.\n             * Build $pattern without including delimiters and []\n             */\n            /* @noinspection PhpMissingBreakStatementInspection */\n            case 'comment':\n                $pattern = '\\(\\)\"';\n            /* Intentional fall through */\n            case 'text':\n            default:\n                //RFC 2047 section 5.1\n                //Replace every high ascii, control, =, ? and _ characters\n                $pattern = '\\000-\\011\\013\\014\\016-\\037\\075\\077\\137\\177-\\377' . $pattern;\n                break;\n        }\n        $matches = [];\n        if (preg_match_all(\"/[{$pattern}]/\", $encoded, $matches)) {\n            //If the string contains an '=', make sure it's the first thing we replace\n            //so as to avoid double-encoding\n            $eqkey = array_search('=', $matches[0], true);\n            if (false !== $eqkey) {\n                unset($matches[0][$eqkey]);\n                array_unshift($matches[0], '=');\n            }\n            foreach (array_unique($matches[0]) as $char) {\n                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);\n            }\n        }\n        //Replace spaces with _ (more readable than =20)\n        //RFC 2047 section 4.2(2)\n        return str_replace(' ', '_', $encoded);\n    }\n\n    /**\n     * Add a string or binary attachment (non-filesystem).\n     * This method can be used to attach ascii or binary data,\n     * such as a BLOB record from a database.\n     *\n     * @param string $string      String attachment data\n     * @param string $filename    Name of the attachment\n     * @param string $encoding    File encoding (see $Encoding)\n     * @param string $type        File extension (MIME) type\n     * @param string $disposition Disposition to use\n     *\n     * @throws Exception\n     *\n     * @return bool True on successfully adding an attachment\n     */\n    public function addStringAttachment(\n        $string,\n        $filename,\n        $encoding = self::ENCODING_BASE64,\n        $type = '',\n        $disposition = 'attachment'\n    ) {\n        try {\n            //If a MIME type is not specified, try to work it out from the file name\n            if ('' === $type) {\n                $type = static::filenameToType($filename);\n            }\n\n            if (!$this->validateEncoding($encoding)) {\n                throw new Exception($this->lang('encoding') . $encoding);\n            }\n\n            //Append to $attachment array\n            $this->attachment[] = [\n                0 => $string,\n                1 => $filename,\n                2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),\n                3 => $encoding,\n                4 => $type,\n                5 => true, //isStringAttachment\n                6 => $disposition,\n                7 => 0,\n            ];\n        } catch (Exception $exc) {\n            $this->setError($exc->getMessage());\n            $this->edebug($exc->getMessage());\n            if ($this->exceptions) {\n                throw $exc;\n            }\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Add an embedded (inline) attachment from a file.\n     * This can include images, sounds, and just about any other document type.\n     * These differ from 'regular' attachments in that they are intended to be\n     * displayed inline with the message, not just attached for download.\n     * This is used in HTML messages that embed the images\n     * the HTML refers to using the `$cid` value in `img` tags, for example `<img src=\"cid:mylogo\">`.\n     * Never use a user-supplied path to a file!\n     *\n     * @param string $path        Path to the attachment\n     * @param string $cid         Content ID of the attachment; Use this to reference\n     *                            the content when using an embedded image in HTML\n     * @param string $name        Overrides the attachment filename\n     * @param string $encoding    File encoding (see $Encoding) defaults to `base64`\n     * @param string $type        File MIME type (by default mapped from the `$path` filename's extension)\n     * @param string $disposition Disposition to use: `inline` (default) or `attachment`\n     *                            (unlikely you want this – {@see `addAttachment()`} instead)\n     *\n     * @return bool True on successfully adding an attachment\n     * @throws Exception\n     *\n     */\n    public function addEmbeddedImage(\n        $path,\n        $cid,\n        $name = '',\n        $encoding = self::ENCODING_BASE64,\n        $type = '',\n        $disposition = 'inline'\n    ) {\n        try {\n            if (!static::fileIsAccessible($path)) {\n                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);\n            }\n\n            //If a MIME type is not specified, try to work it out from the file name\n            if ('' === $type) {\n                $type = static::filenameToType($path);\n            }\n\n            if (!$this->validateEncoding($encoding)) {\n                throw new Exception($this->lang('encoding') . $encoding);\n            }\n\n            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);\n            if ('' === $name) {\n                $name = $filename;\n            }\n\n            //Append to $attachment array\n            $this->attachment[] = [\n                0 => $path,\n                1 => $filename,\n                2 => $name,\n                3 => $encoding,\n                4 => $type,\n                5 => false, //isStringAttachment\n                6 => $disposition,\n                7 => $cid,\n            ];\n        } catch (Exception $exc) {\n            $this->setError($exc->getMessage());\n            $this->edebug($exc->getMessage());\n            if ($this->exceptions) {\n                throw $exc;\n            }\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Add an embedded stringified attachment.\n     * This can include images, sounds, and just about any other document type.\n     * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.\n     *\n     * @param string $string      The attachment binary data\n     * @param string $cid         Content ID of the attachment; Use this to reference\n     *                            the content when using an embedded image in HTML\n     * @param string $name        A filename for the attachment. If this contains an extension,\n     *                            PHPMailer will attempt to set a MIME type for the attachment.\n     *                            For example 'file.jpg' would get an 'image/jpeg' MIME type.\n     * @param string $encoding    File encoding (see $Encoding), defaults to 'base64'\n     * @param string $type        MIME type - will be used in preference to any automatically derived type\n     * @param string $disposition Disposition to use\n     *\n     * @throws Exception\n     *\n     * @return bool True on successfully adding an attachment\n     */\n    public function addStringEmbeddedImage(\n        $string,\n        $cid,\n        $name = '',\n        $encoding = self::ENCODING_BASE64,\n        $type = '',\n        $disposition = 'inline'\n    ) {\n        try {\n            //If a MIME type is not specified, try to work it out from the name\n            if ('' === $type && !empty($name)) {\n                $type = static::filenameToType($name);\n            }\n\n            if (!$this->validateEncoding($encoding)) {\n                throw new Exception($this->lang('encoding') . $encoding);\n            }\n\n            //Append to $attachment array\n            $this->attachment[] = [\n                0 => $string,\n                1 => $name,\n                2 => $name,\n                3 => $encoding,\n                4 => $type,\n                5 => true, //isStringAttachment\n                6 => $disposition,\n                7 => $cid,\n            ];\n        } catch (Exception $exc) {\n            $this->setError($exc->getMessage());\n            $this->edebug($exc->getMessage());\n            if ($this->exceptions) {\n                throw $exc;\n            }\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Validate encodings.\n     *\n     * @param string $encoding\n     *\n     * @return bool\n     */\n    protected function validateEncoding($encoding)\n    {\n        return in_array(\n            $encoding,\n            [\n                self::ENCODING_7BIT,\n                self::ENCODING_QUOTED_PRINTABLE,\n                self::ENCODING_BASE64,\n                self::ENCODING_8BIT,\n                self::ENCODING_BINARY,\n            ],\n            true\n        );\n    }\n\n    /**\n     * Check if an embedded attachment is present with this cid.\n     *\n     * @param string $cid\n     *\n     * @return bool\n     */\n    protected function cidExists($cid)\n    {\n        foreach ($this->attachment as $attachment) {\n            if ('inline' === $attachment[6] && $cid === $attachment[7]) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Check if an inline attachment is present.\n     *\n     * @return bool\n     */\n    public function inlineImageExists()\n    {\n        foreach ($this->attachment as $attachment) {\n            if ('inline' === $attachment[6]) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Check if an attachment (non-inline) is present.\n     *\n     * @return bool\n     */\n    public function attachmentExists()\n    {\n        foreach ($this->attachment as $attachment) {\n            if ('attachment' === $attachment[6]) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * Check if this message has an alternative body set.\n     *\n     * @return bool\n     */\n    public function alternativeExists()\n    {\n        return !empty($this->AltBody);\n    }\n\n    /**\n     * Clear queued addresses of given kind.\n     *\n     * @param string $kind 'to', 'cc', or 'bcc'\n     */\n    public function clearQueuedAddresses($kind)\n    {\n        $this->RecipientsQueue = array_filter(\n            $this->RecipientsQueue,\n            static function ($params) use ($kind) {\n                return $params[0] !== $kind;\n            }\n        );\n    }\n\n    /**\n     * Clear all To recipients.\n     */\n    public function clearAddresses()\n    {\n        foreach ($this->to as $to) {\n            unset($this->all_recipients[strtolower($to[0])]);\n        }\n        $this->to = [];\n        $this->clearQueuedAddresses('to');\n    }\n\n    /**\n     * Clear all CC recipients.\n     */\n    public function clearCCs()\n    {\n        foreach ($this->cc as $cc) {\n            unset($this->all_recipients[strtolower($cc[0])]);\n        }\n        $this->cc = [];\n        $this->clearQueuedAddresses('cc');\n    }\n\n    /**\n     * Clear all BCC recipients.\n     */\n    public function clearBCCs()\n    {\n        foreach ($this->bcc as $bcc) {\n            unset($this->all_recipients[strtolower($bcc[0])]);\n        }\n        $this->bcc = [];\n        $this->clearQueuedAddresses('bcc');\n    }\n\n    /**\n     * Clear all ReplyTo recipients.\n     */\n    public function clearReplyTos()\n    {\n        $this->ReplyTo = [];\n        $this->ReplyToQueue = [];\n    }\n\n    /**\n     * Clear all recipient types.\n     */\n    public function clearAllRecipients()\n    {\n        $this->to = [];\n        $this->cc = [];\n        $this->bcc = [];\n        $this->all_recipients = [];\n        $this->RecipientsQueue = [];\n    }\n\n    /**\n     * Clear all filesystem, string, and binary attachments.\n     */\n    public function clearAttachments()\n    {\n        $this->attachment = [];\n    }\n\n    /**\n     * Clear all custom headers.\n     */\n    public function clearCustomHeaders()\n    {\n        $this->CustomHeader = [];\n    }\n\n    /**\n     * Add an error message to the error container.\n     *\n     * @param string $msg\n     */\n    protected function setError($msg)\n    {\n        ++$this->error_count;\n        if ('smtp' === $this->Mailer && null !== $this->smtp) {\n            $lasterror = $this->smtp->getError();\n            if (!empty($lasterror['error'])) {\n                $msg .= $this->lang('smtp_error') . $lasterror['error'];\n                if (!empty($lasterror['detail'])) {\n                    $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail'];\n                }\n                if (!empty($lasterror['smtp_code'])) {\n                    $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code'];\n                }\n                if (!empty($lasterror['smtp_code_ex'])) {\n                    $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex'];\n                }\n            }\n        }\n        $this->ErrorInfo = $msg;\n    }\n\n    /**\n     * Return an RFC 822 formatted date.\n     *\n     * @return string\n     */\n    public static function rfcDate()\n    {\n        //Set the time zone to whatever the default is to avoid 500 errors\n        //Will default to UTC if it's not set properly in php.ini\n        date_default_timezone_set(@date_default_timezone_get());\n\n        return date('D, j M Y H:i:s O');\n    }\n\n    /**\n     * Get the server hostname.\n     * Returns 'localhost.localdomain' if unknown.\n     *\n     * @return string\n     */\n    protected function serverHostname()\n    {\n        $result = '';\n        if (!empty($this->Hostname)) {\n            $result = $this->Hostname;\n        } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {\n            $result = $_SERVER['SERVER_NAME'];\n        } elseif (function_exists('gethostname') && gethostname() !== false) {\n            $result = gethostname();\n        } elseif (php_uname('n') !== false) {\n            $result = php_uname('n');\n        }\n        if (!static::isValidHost($result)) {\n            return 'localhost.localdomain';\n        }\n\n        return $result;\n    }\n\n    /**\n     * Validate whether a string contains a valid value to use as a hostname or IP address.\n     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.\n     *\n     * @param string $host The host name or IP address to check\n     *\n     * @return bool\n     */\n    public static function isValidHost($host)\n    {\n        //Simple syntax limits\n        if (\n            empty($host)\n            || !is_string($host)\n            || strlen($host) > 256\n            || !preg_match('/^([a-zA-Z\\d.-]*|\\[[a-fA-F\\d:]+\\])$/', $host)\n        ) {\n            return false;\n        }\n        //Looks like a bracketed IPv6 address\n        if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {\n            return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;\n        }\n        //If removing all the dots results in a numeric string, it must be an IPv4 address.\n        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names\n        if (is_numeric(str_replace('.', '', $host))) {\n            //Is it a valid IPv4 address?\n            return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;\n        }\n        //Is it a syntactically valid hostname (when embeded in a URL)?\n        return filter_var('http://' . $host, FILTER_VALIDATE_URL) !== false;\n    }\n\n    /**\n     * Get an error message in the current language.\n     *\n     * @param string $key\n     *\n     * @return string\n     */\n    protected function lang($key)\n    {\n        if (count($this->language) < 1) {\n            $this->setLanguage(); //Set the default language\n        }\n\n        if (array_key_exists($key, $this->language)) {\n            if ('smtp_connect_failed' === $key) {\n                //Include a link to troubleshooting docs on SMTP connection failure.\n                //This is by far the biggest cause of support questions\n                //but it's usually not PHPMailer's fault.\n                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';\n            }\n\n            return $this->language[$key];\n        }\n\n        //Return the key as a fallback\n        return $key;\n    }\n\n    /**\n     * Build an error message starting with a generic one and adding details if possible.\n     *\n     * @param string $base_key\n     * @return string\n     */\n    private function getSmtpErrorMessage($base_key)\n    {\n        $message = $this->lang($base_key);\n        $error = $this->smtp->getError();\n        if (!empty($error['error'])) {\n            $message .= ' ' . $error['error'];\n            if (!empty($error['detail'])) {\n                $message .= ' ' . $error['detail'];\n            }\n        }\n\n        return $message;\n    }\n\n    /**\n     * Check if an error occurred.\n     *\n     * @return bool True if an error did occur\n     */\n    public function isError()\n    {\n        return $this->error_count > 0;\n    }\n\n    /**\n     * Add a custom header.\n     * $name value can be overloaded to contain\n     * both header name and value (name:value).\n     *\n     * @param string      $name  Custom header name\n     * @param string|null $value Header value\n     *\n     * @return bool True if a header was set successfully\n     * @throws Exception\n     */\n    public function addCustomHeader($name, $value = null)\n    {\n        if (null === $value && strpos($name, ':') !== false) {\n            //Value passed in as name:value\n            list($name, $value) = explode(':', $name, 2);\n        }\n        $name = trim($name);\n        $value = (null === $value) ? '' : trim($value);\n        //Ensure name is not empty, and that neither name nor value contain line breaks\n        if (empty($name) || strpbrk($name . $value, \"\\r\\n\") !== false) {\n            if ($this->exceptions) {\n                throw new Exception($this->lang('invalid_header'));\n            }\n\n            return false;\n        }\n        $this->CustomHeader[] = [$name, $value];\n\n        return true;\n    }\n\n    /**\n     * Returns all custom headers.\n     *\n     * @return array\n     */\n    public function getCustomHeaders()\n    {\n        return $this->CustomHeader;\n    }\n\n    /**\n     * Create a message body from an HTML string.\n     * Automatically inlines images and creates a plain-text version by converting the HTML,\n     * overwriting any existing values in Body and AltBody.\n     * Do not source $message content from user input!\n     * $basedir is prepended when handling relative URLs, e.g. <img src=\"/images/a.png\"> and must not be empty\n     * will look for an image file in $basedir/images/a.png and convert it to inline.\n     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)\n     * Converts data-uri images into embedded attachments.\n     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.\n     *\n     * @param string        $message  HTML message string\n     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images\n     * @param bool|callable $advanced Whether to use the internal HTML to text converter\n     *                                or your own custom converter\n     * @return string The transformed message body\n     *\n     * @throws Exception\n     *\n     * @see PHPMailer::html2text()\n     */\n    public function msgHTML($message, $basedir = '', $advanced = false)\n    {\n        preg_match_all('/(?<!-)(src|background)=[\"\\'](.*)[\"\\']/Ui', $message, $images);\n        if (array_key_exists(2, $images)) {\n            if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {\n                //Ensure $basedir has a trailing /\n                $basedir .= '/';\n            }\n            foreach ($images[2] as $imgindex => $url) {\n                //Convert data URIs into embedded images\n                //e.g. \"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\"\n                $match = [];\n                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {\n                    if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {\n                        $data = base64_decode($match[3]);\n                    } elseif ('' === $match[2]) {\n                        $data = rawurldecode($match[3]);\n                    } else {\n                        //Not recognised so leave it alone\n                        continue;\n                    }\n                    //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places\n                    //will only be embedded once, even if it used a different encoding\n                    $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2\n\n                    if (!$this->cidExists($cid)) {\n                        $this->addStringEmbeddedImage(\n                            $data,\n                            $cid,\n                            'embed' . $imgindex,\n                            static::ENCODING_BASE64,\n                            $match[1]\n                        );\n                    }\n                    $message = str_replace(\n                        $images[0][$imgindex],\n                        $images[1][$imgindex] . '=\"cid:' . $cid . '\"',\n                        $message\n                    );\n                    continue;\n                }\n                if (\n                    //Only process relative URLs if a basedir is provided (i.e. no absolute local paths)\n                    !empty($basedir)\n                    //Ignore URLs containing parent dir traversal (..)\n                    && (strpos($url, '..') === false)\n                    //Do not change urls that are already inline images\n                    && 0 !== strpos($url, 'cid:')\n                    //Do not change absolute URLs, including anonymous protocol\n                    && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)\n                ) {\n                    $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);\n                    $directory = dirname($url);\n                    if ('.' === $directory) {\n                        $directory = '';\n                    }\n                    //RFC2392 S 2\n                    $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';\n                    if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {\n                        $basedir .= '/';\n                    }\n                    if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {\n                        $directory .= '/';\n                    }\n                    if (\n                        $this->addEmbeddedImage(\n                            $basedir . $directory . $filename,\n                            $cid,\n                            $filename,\n                            static::ENCODING_BASE64,\n                            static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))\n                        )\n                    ) {\n                        $message = preg_replace(\n                            '/' . $images[1][$imgindex] . '=[\"\\']' . preg_quote($url, '/') . '[\"\\']/Ui',\n                            $images[1][$imgindex] . '=\"cid:' . $cid . '\"',\n                            $message\n                        );\n                    }\n                }\n            }\n        }\n        $this->isHTML();\n        //Convert all message body line breaks to LE, makes quoted-printable encoding work much better\n        $this->Body = static::normalizeBreaks($message);\n        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));\n        if (!$this->alternativeExists()) {\n            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'\n                . static::$LE;\n        }\n\n        return $this->Body;\n    }\n\n    /**\n     * Convert an HTML string into plain text.\n     * This is used by msgHTML().\n     * Note - older versions of this function used a bundled advanced converter\n     * which was removed for license reasons in #232.\n     * Example usage:\n     *\n     * ```php\n     * //Use default conversion\n     * $plain = $mail->html2text($html);\n     * //Use your own custom converter\n     * $plain = $mail->html2text($html, function($html) {\n     *     $converter = new MyHtml2text($html);\n     *     return $converter->get_text();\n     * });\n     * ```\n     *\n     * @param string        $html     The HTML text to convert\n     * @param bool|callable $advanced Any boolean value to use the internal converter,\n     *                                or provide your own callable for custom conversion.\n     *                                *Never* pass user-supplied data into this parameter\n     *\n     * @return string\n     */\n    public function html2text($html, $advanced = false)\n    {\n        if (is_callable($advanced)) {\n            return call_user_func($advanced, $html);\n        }\n\n        return html_entity_decode(\n            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\\/\\\\1>/si', '', $html))),\n            ENT_QUOTES,\n            $this->CharSet\n        );\n    }\n\n    /**\n     * Get the MIME type for a file extension.\n     *\n     * @param string $ext File extension\n     *\n     * @return string MIME type of file\n     */\n    public static function _mime_types($ext = '')\n    {\n        $mimes = [\n            'xl' => 'application/excel',\n            'js' => 'application/javascript',\n            'hqx' => 'application/mac-binhex40',\n            'cpt' => 'application/mac-compactpro',\n            'bin' => 'application/macbinary',\n            'doc' => 'application/msword',\n            'word' => 'application/msword',\n            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',\n            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',\n            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',\n            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',\n            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',\n            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',\n            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',\n            'class' => 'application/octet-stream',\n            'dll' => 'application/octet-stream',\n            'dms' => 'application/octet-stream',\n            'exe' => 'application/octet-stream',\n            'lha' => 'application/octet-stream',\n            'lzh' => 'application/octet-stream',\n            'psd' => 'application/octet-stream',\n            'sea' => 'application/octet-stream',\n            'so' => 'application/octet-stream',\n            'oda' => 'application/oda',\n            'pdf' => 'application/pdf',\n            'ai' => 'application/postscript',\n            'eps' => 'application/postscript',\n            'ps' => 'application/postscript',\n            'smi' => 'application/smil',\n            'smil' => 'application/smil',\n            'mif' => 'application/vnd.mif',\n            'xls' => 'application/vnd.ms-excel',\n            'ppt' => 'application/vnd.ms-powerpoint',\n            'wbxml' => 'application/vnd.wap.wbxml',\n            'wmlc' => 'application/vnd.wap.wmlc',\n            'dcr' => 'application/x-director',\n            'dir' => 'application/x-director',\n            'dxr' => 'application/x-director',\n            'dvi' => 'application/x-dvi',\n            'gtar' => 'application/x-gtar',\n            'php3' => 'application/x-httpd-php',\n            'php4' => 'application/x-httpd-php',\n            'php' => 'application/x-httpd-php',\n            'phtml' => 'application/x-httpd-php',\n            'phps' => 'application/x-httpd-php-source',\n            'swf' => 'application/x-shockwave-flash',\n            'sit' => 'application/x-stuffit',\n            'tar' => 'application/x-tar',\n            'tgz' => 'application/x-tar',\n            'xht' => 'application/xhtml+xml',\n            'xhtml' => 'application/xhtml+xml',\n            'zip' => 'application/zip',\n            'mid' => 'audio/midi',\n            'midi' => 'audio/midi',\n            'mp2' => 'audio/mpeg',\n            'mp3' => 'audio/mpeg',\n            'm4a' => 'audio/mp4',\n            'mpga' => 'audio/mpeg',\n            'aif' => 'audio/x-aiff',\n            'aifc' => 'audio/x-aiff',\n            'aiff' => 'audio/x-aiff',\n            'ram' => 'audio/x-pn-realaudio',\n            'rm' => 'audio/x-pn-realaudio',\n            'rpm' => 'audio/x-pn-realaudio-plugin',\n            'ra' => 'audio/x-realaudio',\n            'wav' => 'audio/x-wav',\n            'mka' => 'audio/x-matroska',\n            'bmp' => 'image/bmp',\n            'gif' => 'image/gif',\n            'jpeg' => 'image/jpeg',\n            'jpe' => 'image/jpeg',\n            'jpg' => 'image/jpeg',\n            'png' => 'image/png',\n            'tiff' => 'image/tiff',\n            'tif' => 'image/tiff',\n            'webp' => 'image/webp',\n            'avif' => 'image/avif',\n            'heif' => 'image/heif',\n            'heifs' => 'image/heif-sequence',\n            'heic' => 'image/heic',\n            'heics' => 'image/heic-sequence',\n            'eml' => 'message/rfc822',\n            'css' => 'text/css',\n            'html' => 'text/html',\n            'htm' => 'text/html',\n            'shtml' => 'text/html',\n            'log' => 'text/plain',\n            'text' => 'text/plain',\n            'txt' => 'text/plain',\n            'rtx' => 'text/richtext',\n            'rtf' => 'text/rtf',\n            'vcf' => 'text/vcard',\n            'vcard' => 'text/vcard',\n            'ics' => 'text/calendar',\n            'xml' => 'text/xml',\n            'xsl' => 'text/xml',\n            'csv' => 'text/csv',\n            'wmv' => 'video/x-ms-wmv',\n            'mpeg' => 'video/mpeg',\n            'mpe' => 'video/mpeg',\n            'mpg' => 'video/mpeg',\n            'mp4' => 'video/mp4',\n            'm4v' => 'video/mp4',\n            'mov' => 'video/quicktime',\n            'qt' => 'video/quicktime',\n            'rv' => 'video/vnd.rn-realvideo',\n            'avi' => 'video/x-msvideo',\n            'movie' => 'video/x-sgi-movie',\n            'webm' => 'video/webm',\n            'mkv' => 'video/x-matroska',\n        ];\n        $ext = strtolower($ext);\n        if (array_key_exists($ext, $mimes)) {\n            return $mimes[$ext];\n        }\n\n        return 'application/octet-stream';\n    }\n\n    /**\n     * Map a file name to a MIME type.\n     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.\n     *\n     * @param string $filename A file name or full path, does not need to exist as a file\n     *\n     * @return string\n     */\n    public static function filenameToType($filename)\n    {\n        //In case the path is a URL, strip any query string before getting extension\n        $qpos = strpos($filename, '?');\n        if (false !== $qpos) {\n            $filename = substr($filename, 0, $qpos);\n        }\n        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);\n\n        return static::_mime_types($ext);\n    }\n\n    /**\n     * Multi-byte-safe pathinfo replacement.\n     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.\n     *\n     * @see http://www.php.net/manual/en/function.pathinfo.php#107461\n     *\n     * @param string     $path    A filename or path, does not need to exist as a file\n     * @param int|string $options Either a PATHINFO_* constant,\n     *                            or a string name to return only the specified piece\n     *\n     * @return string|array\n     */\n    public static function mb_pathinfo($path, $options = null)\n    {\n        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];\n        $pathinfo = [];\n        if (preg_match('#^(.*?)[\\\\\\\\/]*(([^/\\\\\\\\]*?)(\\.([^.\\\\\\\\/]+?)|))[\\\\\\\\/.]*$#m', $path, $pathinfo)) {\n            if (array_key_exists(1, $pathinfo)) {\n                $ret['dirname'] = $pathinfo[1];\n            }\n            if (array_key_exists(2, $pathinfo)) {\n                $ret['basename'] = $pathinfo[2];\n            }\n            if (array_key_exists(5, $pathinfo)) {\n                $ret['extension'] = $pathinfo[5];\n            }\n            if (array_key_exists(3, $pathinfo)) {\n                $ret['filename'] = $pathinfo[3];\n            }\n        }\n        switch ($options) {\n            case PATHINFO_DIRNAME:\n            case 'dirname':\n                return $ret['dirname'];\n            case PATHINFO_BASENAME:\n            case 'basename':\n                return $ret['basename'];\n            case PATHINFO_EXTENSION:\n            case 'extension':\n                return $ret['extension'];\n            case PATHINFO_FILENAME:\n            case 'filename':\n                return $ret['filename'];\n            default:\n                return $ret;\n        }\n    }\n\n    /**\n     * Set or reset instance properties.\n     * You should avoid this function - it's more verbose, less efficient, more error-prone and\n     * harder to debug than setting properties directly.\n     * Usage Example:\n     * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`\n     *   is the same as:\n     * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.\n     *\n     * @param string $name  The property name to set\n     * @param mixed  $value The value to set the property to\n     *\n     * @return bool\n     */\n    public function set($name, $value = '')\n    {\n        if (property_exists($this, $name)) {\n            $this->{$name} = $value;\n\n            return true;\n        }\n        $this->setError($this->lang('variable_set') . $name);\n\n        return false;\n    }\n\n    /**\n     * Strip newlines to prevent header injection.\n     *\n     * @param string $str\n     *\n     * @return string\n     */\n    public function secureHeader($str)\n    {\n        return trim(str_replace([\"\\r\", \"\\n\"], '', $str));\n    }\n\n    /**\n     * Normalize line breaks in a string.\n     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.\n     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.\n     *\n     * @param string $text\n     * @param string $breaktype What kind of line break to use; defaults to static::$LE\n     *\n     * @return string\n     */\n    public static function normalizeBreaks($text, $breaktype = null)\n    {\n        if (null === $breaktype) {\n            $breaktype = static::$LE;\n        }\n        //Normalise to \\n\n        $text = str_replace([self::CRLF, \"\\r\"], \"\\n\", $text);\n        //Now convert LE as needed\n        if (\"\\n\" !== $breaktype) {\n            $text = str_replace(\"\\n\", $breaktype, $text);\n        }\n\n        return $text;\n    }\n\n    /**\n     * Remove trailing whitespace from a string.\n     *\n     * @param string $text\n     *\n     * @return string The text to remove whitespace from\n     */\n    public static function stripTrailingWSP($text)\n    {\n        return rtrim($text, \" \\r\\n\\t\");\n    }\n\n    /**\n     * Strip trailing line breaks from a string.\n     *\n     * @param string $text\n     *\n     * @return string The text to remove breaks from\n     */\n    public static function stripTrailingBreaks($text)\n    {\n        return rtrim($text, \"\\r\\n\");\n    }\n\n    /**\n     * Return the current line break format string.\n     *\n     * @return string\n     */\n    public static function getLE()\n    {\n        return static::$LE;\n    }\n\n    /**\n     * Set the line break format string, e.g. \"\\r\\n\".\n     *\n     * @param string $le\n     */\n    protected static function setLE($le)\n    {\n        static::$LE = $le;\n    }\n\n    /**\n     * Set the public and private key files and password for S/MIME signing.\n     *\n     * @param string $cert_filename\n     * @param string $key_filename\n     * @param string $key_pass            Password for private key\n     * @param string $extracerts_filename Optional path to chain certificate\n     */\n    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')\n    {\n        $this->sign_cert_file = $cert_filename;\n        $this->sign_key_file = $key_filename;\n        $this->sign_key_pass = $key_pass;\n        $this->sign_extracerts_file = $extracerts_filename;\n    }\n\n    /**\n     * Quoted-Printable-encode a DKIM header.\n     *\n     * @param string $txt\n     *\n     * @return string\n     */\n    public function DKIM_QP($txt)\n    {\n        $line = '';\n        $len = strlen($txt);\n        for ($i = 0; $i < $len; ++$i) {\n            $ord = ord($txt[$i]);\n            if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {\n                $line .= $txt[$i];\n            } else {\n                $line .= '=' . sprintf('%02X', $ord);\n            }\n        }\n\n        return $line;\n    }\n\n    /**\n     * Generate a DKIM signature.\n     *\n     * @param string $signHeader\n     *\n     * @throws Exception\n     *\n     * @return string The DKIM signature value\n     */\n    public function DKIM_Sign($signHeader)\n    {\n        if (!defined('PKCS7_TEXT')) {\n            if ($this->exceptions) {\n                throw new Exception($this->lang('extension_missing') . 'openssl');\n            }\n\n            return '';\n        }\n        $privKeyStr = !empty($this->DKIM_private_string) ?\n            $this->DKIM_private_string :\n            file_get_contents($this->DKIM_private);\n        if ('' !== $this->DKIM_passphrase) {\n            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);\n        } else {\n            $privKey = openssl_pkey_get_private($privKeyStr);\n        }\n        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {\n            if (\\PHP_MAJOR_VERSION < 8) {\n                openssl_pkey_free($privKey);\n            }\n\n            return base64_encode($signature);\n        }\n        if (\\PHP_MAJOR_VERSION < 8) {\n            openssl_pkey_free($privKey);\n        }\n\n        return '';\n    }\n\n    /**\n     * Generate a DKIM canonicalization header.\n     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.\n     * Canonicalized headers should *always* use CRLF, regardless of mailer setting.\n     *\n     * @see https://tools.ietf.org/html/rfc6376#section-3.4.2\n     *\n     * @param string $signHeader Header\n     *\n     * @return string\n     */\n    public function DKIM_HeaderC($signHeader)\n    {\n        //Normalize breaks to CRLF (regardless of the mailer)\n        $signHeader = static::normalizeBreaks($signHeader, self::CRLF);\n        //Unfold header lines\n        //Note PCRE \\s is too broad a definition of whitespace; RFC5322 defines it as `[ \\t]`\n        //@see https://tools.ietf.org/html/rfc5322#section-2.2\n        //That means this may break if you do something daft like put vertical tabs in your headers.\n        $signHeader = preg_replace('/\\r\\n[ \\t]+/', ' ', $signHeader);\n        //Break headers out into an array\n        $lines = explode(self::CRLF, $signHeader);\n        foreach ($lines as $key => $line) {\n            //If the header is missing a :, skip it as it's invalid\n            //This is likely to happen because the explode() above will also split\n            //on the trailing LE, leaving an empty line\n            if (strpos($line, ':') === false) {\n                continue;\n            }\n            list($heading, $value) = explode(':', $line, 2);\n            //Lower-case header name\n            $heading = strtolower($heading);\n            //Collapse white space within the value, also convert WSP to space\n            $value = preg_replace('/[ \\t]+/', ' ', $value);\n            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value\n            //But then says to delete space before and after the colon.\n            //Net result is the same as trimming both ends of the value.\n            //By elimination, the same applies to the field name\n            $lines[$key] = trim($heading, \" \\t\") . ':' . trim($value, \" \\t\");\n        }\n\n        return implode(self::CRLF, $lines);\n    }\n\n    /**\n     * Generate a DKIM canonicalization body.\n     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.\n     * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.\n     *\n     * @see https://tools.ietf.org/html/rfc6376#section-3.4.3\n     *\n     * @param string $body Message Body\n     *\n     * @return string\n     */\n    public function DKIM_BodyC($body)\n    {\n        if (empty($body)) {\n            return self::CRLF;\n        }\n        //Normalize line endings to CRLF\n        $body = static::normalizeBreaks($body, self::CRLF);\n\n        //Reduce multiple trailing line breaks to a single one\n        return static::stripTrailingBreaks($body) . self::CRLF;\n    }\n\n    /**\n     * Create the DKIM header and body in a new message header.\n     *\n     * @param string $headers_line Header lines\n     * @param string $subject      Subject\n     * @param string $body         Body\n     *\n     * @throws Exception\n     *\n     * @return string\n     */\n    public function DKIM_Add($headers_line, $subject, $body)\n    {\n        $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms\n        $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body\n        $DKIMquery = 'dns/txt'; //Query method\n        $DKIMtime = time();\n        //Always sign these headers without being asked\n        //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1\n        $autoSignHeaders = [\n            'from',\n            'to',\n            'cc',\n            'date',\n            'subject',\n            'reply-to',\n            'message-id',\n            'content-type',\n            'mime-version',\n            'x-mailer',\n        ];\n        if (stripos($headers_line, 'Subject') === false) {\n            $headers_line .= 'Subject: ' . $subject . static::$LE;\n        }\n        $headerLines = explode(static::$LE, $headers_line);\n        $currentHeaderLabel = '';\n        $currentHeaderValue = '';\n        $parsedHeaders = [];\n        $headerLineIndex = 0;\n        $headerLineCount = count($headerLines);\n        foreach ($headerLines as $headerLine) {\n            $matches = [];\n            if (preg_match('/^([^ \\t]*?)(?::[ \\t]*)(.*)$/', $headerLine, $matches)) {\n                if ($currentHeaderLabel !== '') {\n                    //We were previously in another header; This is the start of a new header, so save the previous one\n                    $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];\n                }\n                $currentHeaderLabel = $matches[1];\n                $currentHeaderValue = $matches[2];\n            } elseif (preg_match('/^[ \\t]+(.*)$/', $headerLine, $matches)) {\n                //This is a folded continuation of the current header, so unfold it\n                $currentHeaderValue .= ' ' . $matches[1];\n            }\n            ++$headerLineIndex;\n            if ($headerLineIndex >= $headerLineCount) {\n                //This was the last line, so finish off this header\n                $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];\n            }\n        }\n        $copiedHeaders = [];\n        $headersToSignKeys = [];\n        $headersToSign = [];\n        foreach ($parsedHeaders as $header) {\n            //Is this header one that must be included in the DKIM signature?\n            if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {\n                $headersToSignKeys[] = $header['label'];\n                $headersToSign[] = $header['label'] . ': ' . $header['value'];\n                if ($this->DKIM_copyHeaderFields) {\n                    $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC\n                        str_replace('|', '=7C', $this->DKIM_QP($header['value']));\n                }\n                continue;\n            }\n            //Is this an extra custom header we've been asked to sign?\n            if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {\n                //Find its value in custom headers\n                foreach ($this->CustomHeader as $customHeader) {\n                    if ($customHeader[0] === $header['label']) {\n                        $headersToSignKeys[] = $header['label'];\n                        $headersToSign[] = $header['label'] . ': ' . $header['value'];\n                        if ($this->DKIM_copyHeaderFields) {\n                            $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC\n                                str_replace('|', '=7C', $this->DKIM_QP($header['value']));\n                        }\n                        //Skip straight to the next header\n                        continue 2;\n                    }\n                }\n            }\n        }\n        $copiedHeaderFields = '';\n        if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {\n            //Assemble a DKIM 'z' tag\n            $copiedHeaderFields = ' z=';\n            $first = true;\n            foreach ($copiedHeaders as $copiedHeader) {\n                if (!$first) {\n                    $copiedHeaderFields .= static::$LE . ' |';\n                }\n                //Fold long values\n                if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {\n                    $copiedHeaderFields .= substr(\n                        chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS),\n                        0,\n                        -strlen(static::$LE . self::FWS)\n                    );\n                } else {\n                    $copiedHeaderFields .= $copiedHeader;\n                }\n                $first = false;\n            }\n            $copiedHeaderFields .= ';' . static::$LE;\n        }\n        $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;\n        $headerValues = implode(static::$LE, $headersToSign);\n        $body = $this->DKIM_BodyC($body);\n        //Base64 of packed binary SHA-256 hash of body\n        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));\n        $ident = '';\n        if ('' !== $this->DKIM_identity) {\n            $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;\n        }\n        //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag\n        //which is appended after calculating the signature\n        //https://tools.ietf.org/html/rfc6376#section-3.5\n        $dkimSignatureHeader = 'DKIM-Signature: v=1;' .\n            ' d=' . $this->DKIM_domain . ';' .\n            ' s=' . $this->DKIM_selector . ';' . static::$LE .\n            ' a=' . $DKIMsignatureType . ';' .\n            ' q=' . $DKIMquery . ';' .\n            ' t=' . $DKIMtime . ';' .\n            ' c=' . $DKIMcanonicalization . ';' . static::$LE .\n            $headerKeys .\n            $ident .\n            $copiedHeaderFields .\n            ' bh=' . $DKIMb64 . ';' . static::$LE .\n            ' b=';\n        //Canonicalize the set of headers\n        $canonicalizedHeaders = $this->DKIM_HeaderC(\n            $headerValues . static::$LE . $dkimSignatureHeader\n        );\n        $signature = $this->DKIM_Sign($canonicalizedHeaders);\n        $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS));\n\n        return static::normalizeBreaks($dkimSignatureHeader . $signature);\n    }\n\n    /**\n     * Detect if a string contains a line longer than the maximum line length\n     * allowed by RFC 2822 section 2.1.1.\n     *\n     * @param string $str\n     *\n     * @return bool\n     */\n    public static function hasLineLongerThanMax($str)\n    {\n        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);\n    }\n\n    /**\n     * If a string contains any \"special\" characters, double-quote the name,\n     * and escape any double quotes with a backslash.\n     *\n     * @param string $str\n     *\n     * @return string\n     *\n     * @see RFC822 3.4.1\n     */\n    public static function quotedString($str)\n    {\n        if (preg_match('/[ ()<>@,;:\"\\/\\[\\]?=]/', $str)) {\n            //If the string contains any of these chars, it must be double-quoted\n            //and any double quotes must be escaped with a backslash\n            return '\"' . str_replace('\"', '\\\\\"', $str) . '\"';\n        }\n\n        //Return the string untouched, it doesn't need quoting\n        return $str;\n    }\n\n    /**\n     * Allows for public read access to 'to' property.\n     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.\n     *\n     * @return array\n     */\n    public function getToAddresses()\n    {\n        return $this->to;\n    }\n\n    /**\n     * Allows for public read access to 'cc' property.\n     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.\n     *\n     * @return array\n     */\n    public function getCcAddresses()\n    {\n        return $this->cc;\n    }\n\n    /**\n     * Allows for public read access to 'bcc' property.\n     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.\n     *\n     * @return array\n     */\n    public function getBccAddresses()\n    {\n        return $this->bcc;\n    }\n\n    /**\n     * Allows for public read access to 'ReplyTo' property.\n     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.\n     *\n     * @return array\n     */\n    public function getReplyToAddresses()\n    {\n        return $this->ReplyTo;\n    }\n\n    /**\n     * Allows for public read access to 'all_recipients' property.\n     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.\n     *\n     * @return array\n     */\n    public function getAllRecipientAddresses()\n    {\n        return $this->all_recipients;\n    }\n\n    /**\n     * Perform a callback.\n     *\n     * @param bool   $isSent\n     * @param array  $to\n     * @param array  $cc\n     * @param array  $bcc\n     * @param string $subject\n     * @param string $body\n     * @param string $from\n     * @param array  $extra\n     */\n    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)\n    {\n        if (!empty($this->action_function) && is_callable($this->action_function)) {\n            call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);\n        }\n    }\n\n    /**\n     * Get the OAuthTokenProvider instance.\n     *\n     * @return OAuthTokenProvider\n     */\n    public function getOAuth()\n    {\n        return $this->oauth;\n    }\n\n    /**\n     * Set an OAuthTokenProvider instance.\n     */\n    public function setOAuth(OAuthTokenProvider $oauth)\n    {\n        $this->oauth = $oauth;\n    }\n}\n"
  },
  {
    "path": "libs/PHPMailer/SMTP.php",
    "content": "<?php\n\n/**\n * PHPMailer RFC821 SMTP email transport class.\n * PHP Version 5.5.\n *\n * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project\n *\n * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>\n * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>\n * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>\n * @author    Brent R. Matzelle (original founder)\n * @copyright 2012 - 2020 Marcus Bointon\n * @copyright 2010 - 2012 Jim Jagielski\n * @copyright 2004 - 2009 Andy Prevost\n * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License\n * @note      This program is distributed in the hope that it will be useful - WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n * FITNESS FOR A PARTICULAR PURPOSE.\n */\n\nnamespace PHPMailer\\PHPMailer;\n\n/**\n * PHPMailer RFC821 SMTP email transport class.\n * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.\n *\n * @author Chris Ryan\n * @author Marcus Bointon <phpmailer@synchromedia.co.uk>\n */\nclass SMTP\n{\n    /**\n     * The PHPMailer SMTP version number.\n     *\n     * @var string\n     */\n    const VERSION = '6.8.1';\n\n    /**\n     * SMTP line break constant.\n     *\n     * @var string\n     */\n    const LE = \"\\r\\n\";\n\n    /**\n     * The SMTP port to use if one is not specified.\n     *\n     * @var int\n     */\n    const DEFAULT_PORT = 25;\n\n    /**\n     * The SMTPs port to use if one is not specified.\n     *\n     * @var int\n     */\n    const DEFAULT_SECURE_PORT = 465;\n\n    /**\n     * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,\n     * *excluding* a trailing CRLF break.\n     *\n     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6\n     *\n     * @var int\n     */\n    const MAX_LINE_LENGTH = 998;\n\n    /**\n     * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,\n     * *including* a trailing CRLF line break.\n     *\n     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5\n     *\n     * @var int\n     */\n    const MAX_REPLY_LENGTH = 512;\n\n    /**\n     * Debug level for no output.\n     *\n     * @var int\n     */\n    const DEBUG_OFF = 0;\n\n    /**\n     * Debug level to show client -> server messages.\n     *\n     * @var int\n     */\n    const DEBUG_CLIENT = 1;\n\n    /**\n     * Debug level to show client -> server and server -> client messages.\n     *\n     * @var int\n     */\n    const DEBUG_SERVER = 2;\n\n    /**\n     * Debug level to show connection status, client -> server and server -> client messages.\n     *\n     * @var int\n     */\n    const DEBUG_CONNECTION = 3;\n\n    /**\n     * Debug level to show all messages.\n     *\n     * @var int\n     */\n    const DEBUG_LOWLEVEL = 4;\n\n    /**\n     * Debug output level.\n     * Options:\n     * * self::DEBUG_OFF (`0`) No debug output, default\n     * * self::DEBUG_CLIENT (`1`) Client commands\n     * * self::DEBUG_SERVER (`2`) Client commands and server responses\n     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status\n     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.\n     *\n     * @var int\n     */\n    public $do_debug = self::DEBUG_OFF;\n\n    /**\n     * How to handle debug output.\n     * Options:\n     * * `echo` Output plain-text as-is, appropriate for CLI\n     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output\n     * * `error_log` Output to error log as configured in php.ini\n     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:\n     *\n     * ```php\n     * $smtp->Debugoutput = function($str, $level) {echo \"debug level $level; message: $str\";};\n     * ```\n     *\n     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`\n     * level output is used:\n     *\n     * ```php\n     * $mail->Debugoutput = new myPsr3Logger;\n     * ```\n     *\n     * @var string|callable|\\Psr\\Log\\LoggerInterface\n     */\n    public $Debugoutput = 'echo';\n\n    /**\n     * Whether to use VERP.\n     *\n     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path\n     * @see http://www.postfix.org/VERP_README.html Info on VERP\n     *\n     * @var bool\n     */\n    public $do_verp = false;\n\n    /**\n     * The timeout value for connection, in seconds.\n     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.\n     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.\n     *\n     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2\n     *\n     * @var int\n     */\n    public $Timeout = 300;\n\n    /**\n     * How long to wait for commands to complete, in seconds.\n     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.\n     *\n     * @var int\n     */\n    public $Timelimit = 300;\n\n    /**\n     * Patterns to extract an SMTP transaction id from reply to a DATA command.\n     * The first capture group in each regex will be used as the ID.\n     * MS ESMTP returns the message ID, which may not be correct for internal tracking.\n     *\n     * @var string[]\n     */\n    protected $smtp_transaction_id_patterns = [\n        'exim' => '/[\\d]{3} OK id=(.*)/',\n        'sendmail' => '/[\\d]{3} 2.0.0 (.*) Message/',\n        'postfix' => '/[\\d]{3} 2.0.0 Ok: queued as (.*)/',\n        'Microsoft_ESMTP' => '/[0-9]{3} 2.[\\d].0 (.*)@(?:.*) Queued mail for delivery/',\n        'Amazon_SES' => '/[\\d]{3} Ok (.*)/',\n        'SendGrid' => '/[\\d]{3} Ok: queued as (.*)/',\n        'CampaignMonitor' => '/[\\d]{3} 2.0.0 OK:([a-zA-Z\\d]{48})/',\n        'Haraka' => '/[\\d]{3} Message Queued \\((.*)\\)/',\n        'ZoneMTA' => '/[\\d]{3} Message queued as (.*)/',\n        'Mailjet' => '/[\\d]{3} OK queued as (.*)/',\n    ];\n\n    /**\n     * The last transaction ID issued in response to a DATA command,\n     * if one was detected.\n     *\n     * @var string|bool|null\n     */\n    protected $last_smtp_transaction_id;\n\n    /**\n     * The socket for the server connection.\n     *\n     * @var ?resource\n     */\n    protected $smtp_conn;\n\n    /**\n     * Error information, if any, for the last SMTP command.\n     *\n     * @var array\n     */\n    protected $error = [\n        'error' => '',\n        'detail' => '',\n        'smtp_code' => '',\n        'smtp_code_ex' => '',\n    ];\n\n    /**\n     * The reply the server sent to us for HELO.\n     * If null, no HELO string has yet been received.\n     *\n     * @var string|null\n     */\n    protected $helo_rply;\n\n    /**\n     * The set of SMTP extensions sent in reply to EHLO command.\n     * Indexes of the array are extension names.\n     * Value at index 'HELO' or 'EHLO' (according to command that was sent)\n     * represents the server name. In case of HELO it is the only element of the array.\n     * Other values can be boolean TRUE or an array containing extension options.\n     * If null, no HELO/EHLO string has yet been received.\n     *\n     * @var array|null\n     */\n    protected $server_caps;\n\n    /**\n     * The most recent reply received from the server.\n     *\n     * @var string\n     */\n    protected $last_reply = '';\n\n    /**\n     * Output debugging info via a user-selected method.\n     *\n     * @param string $str   Debug string to output\n     * @param int    $level The debug level of this message; see DEBUG_* constants\n     *\n     * @see SMTP::$Debugoutput\n     * @see SMTP::$do_debug\n     */\n    protected function edebug($str, $level = 0)\n    {\n        if ($level > $this->do_debug) {\n            return;\n        }\n        //Is this a PSR-3 logger?\n        if ($this->Debugoutput instanceof \\Psr\\Log\\LoggerInterface) {\n            $this->Debugoutput->debug($str);\n\n            return;\n        }\n        //Avoid clash with built-in function names\n        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {\n            call_user_func($this->Debugoutput, $str, $level);\n\n            return;\n        }\n        switch ($this->Debugoutput) {\n            case 'error_log':\n                //Don't output, just log\n                error_log($str);\n                break;\n            case 'html':\n                //Cleans up output a bit for a better looking, HTML-safe output\n                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(\n                    preg_replace('/[\\r\\n]+/', '', $str),\n                    ENT_QUOTES,\n                    'UTF-8'\n                ), \"<br>\\n\";\n                break;\n            case 'echo':\n            default:\n                //Normalize line breaks\n                $str = preg_replace('/\\r\\n|\\r/m', \"\\n\", $str);\n                echo gmdate('Y-m-d H:i:s'),\n                \"\\t\",\n                    //Trim trailing space\n                trim(\n                    //Indent for readability, except for trailing break\n                    str_replace(\n                        \"\\n\",\n                        \"\\n                   \\t                  \",\n                        trim($str)\n                    )\n                ),\n                \"\\n\";\n        }\n    }\n\n    /**\n     * Connect to an SMTP server.\n     *\n     * @param string $host    SMTP server IP or host name\n     * @param int    $port    The port number to connect to\n     * @param int    $timeout How long to wait for the connection to open\n     * @param array  $options An array of options for stream_context_create()\n     *\n     * @return bool\n     */\n    public function connect($host, $port = null, $timeout = 30, $options = [])\n    {\n        //Clear errors to avoid confusion\n        $this->setError('');\n        //Make sure we are __not__ connected\n        if ($this->connected()) {\n            //Already connected, generate error\n            $this->setError('Already connected to a server');\n\n            return false;\n        }\n        if (empty($port)) {\n            $port = self::DEFAULT_PORT;\n        }\n        //Connect to the SMTP server\n        $this->edebug(\n            \"Connection: opening to $host:$port, timeout=$timeout, options=\" .\n            (count($options) > 0 ? var_export($options, true) : 'array()'),\n            self::DEBUG_CONNECTION\n        );\n\n        $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);\n\n        if ($this->smtp_conn === false) {\n            //Error info already set inside `getSMTPConnection()`\n            return false;\n        }\n\n        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);\n\n        //Get any announcement\n        $this->last_reply = $this->get_lines();\n        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);\n        $responseCode = (int)substr($this->last_reply, 0, 3);\n        if ($responseCode === 220) {\n            return true;\n        }\n        //Anything other than a 220 response means something went wrong\n        //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error\n        //https://tools.ietf.org/html/rfc5321#section-3.1\n        if ($responseCode === 554) {\n            $this->quit();\n        }\n        //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)\n        $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);\n        $this->close();\n        return false;\n    }\n\n    /**\n     * Create connection to the SMTP server.\n     *\n     * @param string $host    SMTP server IP or host name\n     * @param int    $port    The port number to connect to\n     * @param int    $timeout How long to wait for the connection to open\n     * @param array  $options An array of options for stream_context_create()\n     *\n     * @return false|resource\n     */\n    protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])\n    {\n        static $streamok;\n        //This is enabled by default since 5.0.0 but some providers disable it\n        //Check this once and cache the result\n        if (null === $streamok) {\n            $streamok = function_exists('stream_socket_client');\n        }\n\n        $errno = 0;\n        $errstr = '';\n        if ($streamok) {\n            $socket_context = stream_context_create($options);\n            set_error_handler([$this, 'errorHandler']);\n            $connection = stream_socket_client(\n                $host . ':' . $port,\n                $errno,\n                $errstr,\n                $timeout,\n                STREAM_CLIENT_CONNECT,\n                $socket_context\n            );\n        } else {\n            //Fall back to fsockopen which should work in more places, but is missing some features\n            $this->edebug(\n                'Connection: stream_socket_client not available, falling back to fsockopen',\n                self::DEBUG_CONNECTION\n            );\n            set_error_handler([$this, 'errorHandler']);\n            $connection = fsockopen(\n                $host,\n                $port,\n                $errno,\n                $errstr,\n                $timeout\n            );\n        }\n        restore_error_handler();\n\n        //Verify we connected properly\n        if (!is_resource($connection)) {\n            $this->setError(\n                'Failed to connect to server',\n                '',\n                (string) $errno,\n                $errstr\n            );\n            $this->edebug(\n                'SMTP ERROR: ' . $this->error['error']\n                . \": $errstr ($errno)\",\n                self::DEBUG_CLIENT\n            );\n\n            return false;\n        }\n\n        //SMTP server can take longer to respond, give longer timeout for first read\n        //Windows does not have support for this timeout function\n        if (strpos(PHP_OS, 'WIN') !== 0) {\n            $max = (int)ini_get('max_execution_time');\n            //Don't bother if unlimited, or if set_time_limit is disabled\n            if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {\n                @set_time_limit($timeout);\n            }\n            stream_set_timeout($connection, $timeout, 0);\n        }\n\n        return $connection;\n    }\n\n    /**\n     * Initiate a TLS (encrypted) session.\n     *\n     * @return bool\n     */\n    public function startTLS()\n    {\n        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {\n            return false;\n        }\n\n        //Allow the best TLS version(s) we can\n        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;\n\n        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT\n        //so add them back in manually if we can\n        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {\n            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;\n            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;\n        }\n\n        //Begin encrypted connection\n        set_error_handler([$this, 'errorHandler']);\n        $crypto_ok = stream_socket_enable_crypto(\n            $this->smtp_conn,\n            true,\n            $crypto_method\n        );\n        restore_error_handler();\n\n        return (bool) $crypto_ok;\n    }\n\n    /**\n     * Perform SMTP authentication.\n     * Must be run after hello().\n     *\n     * @see    hello()\n     *\n     * @param string $username The user name\n     * @param string $password The password\n     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)\n     * @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication\n     *\n     * @return bool True if successfully authenticated\n     */\n    public function authenticate(\n        $username,\n        $password,\n        $authtype = null,\n        $OAuth = null\n    ) {\n        if (!$this->server_caps) {\n            $this->setError('Authentication is not allowed before HELO/EHLO');\n\n            return false;\n        }\n\n        if (array_key_exists('EHLO', $this->server_caps)) {\n            //SMTP extensions are available; try to find a proper authentication method\n            if (!array_key_exists('AUTH', $this->server_caps)) {\n                $this->setError('Authentication is not allowed at this stage');\n                //'at this stage' means that auth may be allowed after the stage changes\n                //e.g. after STARTTLS\n\n                return false;\n            }\n\n            $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);\n            $this->edebug(\n                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),\n                self::DEBUG_LOWLEVEL\n            );\n\n            //If we have requested a specific auth type, check the server supports it before trying others\n            if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {\n                $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);\n                $authtype = null;\n            }\n\n            if (empty($authtype)) {\n                //If no auth mechanism is specified, attempt to use these, in this order\n                //Try CRAM-MD5 first as it's more secure than the others\n                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {\n                    if (in_array($method, $this->server_caps['AUTH'], true)) {\n                        $authtype = $method;\n                        break;\n                    }\n                }\n                if (empty($authtype)) {\n                    $this->setError('No supported authentication methods found');\n\n                    return false;\n                }\n                $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);\n            }\n\n            if (!in_array($authtype, $this->server_caps['AUTH'], true)) {\n                $this->setError(\"The requested authentication method \\\"$authtype\\\" is not supported by the server\");\n\n                return false;\n            }\n        } elseif (empty($authtype)) {\n            $authtype = 'LOGIN';\n        }\n        switch ($authtype) {\n            case 'PLAIN':\n                //Start authentication\n                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {\n                    return false;\n                }\n                //Send encoded username and password\n                if (\n                    //Format from https://tools.ietf.org/html/rfc4616#section-2\n                    //We skip the first field (it's forgery), so the string starts with a null byte\n                    !$this->sendCommand(\n                        'User & Password',\n                        base64_encode(\"\\0\" . $username . \"\\0\" . $password),\n                        235\n                    )\n                ) {\n                    return false;\n                }\n                break;\n            case 'LOGIN':\n                //Start authentication\n                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {\n                    return false;\n                }\n                if (!$this->sendCommand('Username', base64_encode($username), 334)) {\n                    return false;\n                }\n                if (!$this->sendCommand('Password', base64_encode($password), 235)) {\n                    return false;\n                }\n                break;\n            case 'CRAM-MD5':\n                //Start authentication\n                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {\n                    return false;\n                }\n                //Get the challenge\n                $challenge = base64_decode(substr($this->last_reply, 4));\n\n                //Build the response\n                $response = $username . ' ' . $this->hmac($challenge, $password);\n\n                //send encoded credentials\n                return $this->sendCommand('Username', base64_encode($response), 235);\n            case 'XOAUTH2':\n                //The OAuth instance must be set up prior to requesting auth.\n                if (null === $OAuth) {\n                    return false;\n                }\n                $oauth = $OAuth->getOauth64();\n\n                //Start authentication\n                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {\n                    return false;\n                }\n                break;\n            default:\n                $this->setError(\"Authentication method \\\"$authtype\\\" is not supported\");\n\n                return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Calculate an MD5 HMAC hash.\n     * Works like hash_hmac('md5', $data, $key)\n     * in case that function is not available.\n     *\n     * @param string $data The data to hash\n     * @param string $key  The key to hash with\n     *\n     * @return string\n     */\n    protected function hmac($data, $key)\n    {\n        if (function_exists('hash_hmac')) {\n            return hash_hmac('md5', $data, $key);\n        }\n\n        //The following borrowed from\n        //http://php.net/manual/en/function.mhash.php#27225\n\n        //RFC 2104 HMAC implementation for php.\n        //Creates an md5 HMAC.\n        //Eliminates the need to install mhash to compute a HMAC\n        //by Lance Rushing\n\n        $bytelen = 64; //byte length for md5\n        if (strlen($key) > $bytelen) {\n            $key = pack('H*', md5($key));\n        }\n        $key = str_pad($key, $bytelen, chr(0x00));\n        $ipad = str_pad('', $bytelen, chr(0x36));\n        $opad = str_pad('', $bytelen, chr(0x5c));\n        $k_ipad = $key ^ $ipad;\n        $k_opad = $key ^ $opad;\n\n        return md5($k_opad . pack('H*', md5($k_ipad . $data)));\n    }\n\n    /**\n     * Check connection state.\n     *\n     * @return bool True if connected\n     */\n    public function connected()\n    {\n        if (is_resource($this->smtp_conn)) {\n            $sock_status = stream_get_meta_data($this->smtp_conn);\n            if ($sock_status['eof']) {\n                //The socket is valid but we are not connected\n                $this->edebug(\n                    'SMTP NOTICE: EOF caught while checking if connected',\n                    self::DEBUG_CLIENT\n                );\n                $this->close();\n\n                return false;\n            }\n\n            return true; //everything looks good\n        }\n\n        return false;\n    }\n\n    /**\n     * Close the socket and clean up the state of the class.\n     * Don't use this function without first trying to use QUIT.\n     *\n     * @see quit()\n     */\n    public function close()\n    {\n        $this->server_caps = null;\n        $this->helo_rply = null;\n        if (is_resource($this->smtp_conn)) {\n            //Close the connection and cleanup\n            fclose($this->smtp_conn);\n            $this->smtp_conn = null; //Makes for cleaner serialization\n            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);\n        }\n    }\n\n    /**\n     * Send an SMTP DATA command.\n     * Issues a data command and sends the msg_data to the server,\n     * finalizing the mail transaction. $msg_data is the message\n     * that is to be sent with the headers. Each header needs to be\n     * on a single line followed by a <CRLF> with the message headers\n     * and the message body being separated by an additional <CRLF>.\n     * Implements RFC 821: DATA <CRLF>.\n     *\n     * @param string $msg_data Message data to send\n     *\n     * @return bool\n     */\n    public function data($msg_data)\n    {\n        //This will use the standard timelimit\n        if (!$this->sendCommand('DATA', 'DATA', 354)) {\n            return false;\n        }\n\n        /* The server is ready to accept data!\n         * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)\n         * so we will break the data up into lines by \\r and/or \\n then if needed we will break each of those into\n         * smaller lines to fit within the limit.\n         * We will also look for lines that start with a '.' and prepend an additional '.'.\n         * NOTE: this does not count towards line-length limit.\n         */\n\n        //Normalize line breaks before exploding\n        $lines = explode(\"\\n\", str_replace([\"\\r\\n\", \"\\r\"], \"\\n\", $msg_data));\n\n        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field\n         * of the first line (':' separated) does not contain a space then it _should_ be a header, and we will\n         * process all lines before a blank line as headers.\n         */\n\n        $field = substr($lines[0], 0, strpos($lines[0], ':'));\n        $in_headers = false;\n        if (!empty($field) && strpos($field, ' ') === false) {\n            $in_headers = true;\n        }\n\n        foreach ($lines as $line) {\n            $lines_out = [];\n            if ($in_headers && $line === '') {\n                $in_headers = false;\n            }\n            //Break this line up into several smaller lines if it's too long\n            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),\n            while (isset($line[self::MAX_LINE_LENGTH])) {\n                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on\n                //so as to avoid breaking in the middle of a word\n                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');\n                //Deliberately matches both false and 0\n                if (!$pos) {\n                    //No nice break found, add a hard break\n                    $pos = self::MAX_LINE_LENGTH - 1;\n                    $lines_out[] = substr($line, 0, $pos);\n                    $line = substr($line, $pos);\n                } else {\n                    //Break at the found point\n                    $lines_out[] = substr($line, 0, $pos);\n                    //Move along by the amount we dealt with\n                    $line = substr($line, $pos + 1);\n                }\n                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1\n                if ($in_headers) {\n                    $line = \"\\t\" . $line;\n                }\n            }\n            $lines_out[] = $line;\n\n            //Send the lines to the server\n            foreach ($lines_out as $line_out) {\n                //Dot-stuffing as per RFC5321 section 4.5.2\n                //https://tools.ietf.org/html/rfc5321#section-4.5.2\n                if (!empty($line_out) && $line_out[0] === '.') {\n                    $line_out = '.' . $line_out;\n                }\n                $this->client_send($line_out . static::LE, 'DATA');\n            }\n        }\n\n        //Message data has been sent, complete the command\n        //Increase timelimit for end of DATA command\n        $savetimelimit = $this->Timelimit;\n        $this->Timelimit *= 2;\n        $result = $this->sendCommand('DATA END', '.', 250);\n        $this->recordLastTransactionID();\n        //Restore timelimit\n        $this->Timelimit = $savetimelimit;\n\n        return $result;\n    }\n\n    /**\n     * Send an SMTP HELO or EHLO command.\n     * Used to identify the sending server to the receiving server.\n     * This makes sure that client and server are in a known state.\n     * Implements RFC 821: HELO <SP> <domain> <CRLF>\n     * and RFC 2821 EHLO.\n     *\n     * @param string $host The host name or IP to connect to\n     *\n     * @return bool\n     */\n    public function hello($host = '')\n    {\n        //Try extended hello first (RFC 2821)\n        if ($this->sendHello('EHLO', $host)) {\n            return true;\n        }\n\n        //Some servers shut down the SMTP service here (RFC 5321)\n        if (substr($this->helo_rply, 0, 3) == '421') {\n            return false;\n        }\n\n        return $this->sendHello('HELO', $host);\n    }\n\n    /**\n     * Send an SMTP HELO or EHLO command.\n     * Low-level implementation used by hello().\n     *\n     * @param string $hello The HELO string\n     * @param string $host  The hostname to say we are\n     *\n     * @return bool\n     *\n     * @see hello()\n     */\n    protected function sendHello($hello, $host)\n    {\n        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);\n        $this->helo_rply = $this->last_reply;\n        if ($noerror) {\n            $this->parseHelloFields($hello);\n        } else {\n            $this->server_caps = null;\n        }\n\n        return $noerror;\n    }\n\n    /**\n     * Parse a reply to HELO/EHLO command to discover server extensions.\n     * In case of HELO, the only parameter that can be discovered is a server name.\n     *\n     * @param string $type `HELO` or `EHLO`\n     */\n    protected function parseHelloFields($type)\n    {\n        $this->server_caps = [];\n        $lines = explode(\"\\n\", $this->helo_rply);\n\n        foreach ($lines as $n => $s) {\n            //First 4 chars contain response code followed by - or space\n            $s = trim(substr($s, 4));\n            if (empty($s)) {\n                continue;\n            }\n            $fields = explode(' ', $s);\n            if (!empty($fields)) {\n                if (!$n) {\n                    $name = $type;\n                    $fields = $fields[0];\n                } else {\n                    $name = array_shift($fields);\n                    switch ($name) {\n                        case 'SIZE':\n                            $fields = ($fields ? $fields[0] : 0);\n                            break;\n                        case 'AUTH':\n                            if (!is_array($fields)) {\n                                $fields = [];\n                            }\n                            break;\n                        default:\n                            $fields = true;\n                    }\n                }\n                $this->server_caps[$name] = $fields;\n            }\n        }\n    }\n\n    /**\n     * Send an SMTP MAIL command.\n     * Starts a mail transaction from the email address specified in\n     * $from. Returns true if successful or false otherwise. If True\n     * the mail transaction is started and then one or more recipient\n     * commands may be called followed by a data command.\n     * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.\n     *\n     * @param string $from Source address of this message\n     *\n     * @return bool\n     */\n    public function mail($from)\n    {\n        $useVerp = ($this->do_verp ? ' XVERP' : '');\n\n        return $this->sendCommand(\n            'MAIL FROM',\n            'MAIL FROM:<' . $from . '>' . $useVerp,\n            250\n        );\n    }\n\n    /**\n     * Send an SMTP QUIT command.\n     * Closes the socket if there is no error or the $close_on_error argument is true.\n     * Implements from RFC 821: QUIT <CRLF>.\n     *\n     * @param bool $close_on_error Should the connection close if an error occurs?\n     *\n     * @return bool\n     */\n    public function quit($close_on_error = true)\n    {\n        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);\n        $err = $this->error; //Save any error\n        if ($noerror || $close_on_error) {\n            $this->close();\n            $this->error = $err; //Restore any error from the quit command\n        }\n\n        return $noerror;\n    }\n\n    /**\n     * Send an SMTP RCPT command.\n     * Sets the TO argument to $toaddr.\n     * Returns true if the recipient was accepted false if it was rejected.\n     * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.\n     *\n     * @param string $address The address the message is being sent to\n     * @param string $dsn     Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE\n     *                        or DELAY. If you specify NEVER all other notifications are ignored.\n     *\n     * @return bool\n     */\n    public function recipient($address, $dsn = '')\n    {\n        if (empty($dsn)) {\n            $rcpt = 'RCPT TO:<' . $address . '>';\n        } else {\n            $dsn = strtoupper($dsn);\n            $notify = [];\n\n            if (strpos($dsn, 'NEVER') !== false) {\n                $notify[] = 'NEVER';\n            } else {\n                foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {\n                    if (strpos($dsn, $value) !== false) {\n                        $notify[] = $value;\n                    }\n                }\n            }\n\n            $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);\n        }\n\n        return $this->sendCommand(\n            'RCPT TO',\n            $rcpt,\n            [250, 251]\n        );\n    }\n\n    /**\n     * Send an SMTP RSET command.\n     * Abort any transaction that is currently in progress.\n     * Implements RFC 821: RSET <CRLF>.\n     *\n     * @return bool True on success\n     */\n    public function reset()\n    {\n        return $this->sendCommand('RSET', 'RSET', 250);\n    }\n\n    /**\n     * Send a command to an SMTP server and check its return code.\n     *\n     * @param string    $command       The command name - not sent to the server\n     * @param string    $commandstring The actual command to send\n     * @param int|array $expect        One or more expected integer success codes\n     *\n     * @return bool True on success\n     */\n    protected function sendCommand($command, $commandstring, $expect)\n    {\n        if (!$this->connected()) {\n            $this->setError(\"Called $command without being connected\");\n\n            return false;\n        }\n        //Reject line breaks in all commands\n        if ((strpos($commandstring, \"\\n\") !== false) || (strpos($commandstring, \"\\r\") !== false)) {\n            $this->setError(\"Command '$command' contained line breaks\");\n\n            return false;\n        }\n        $this->client_send($commandstring . static::LE, $command);\n\n        $this->last_reply = $this->get_lines();\n        //Fetch SMTP code and possible error code explanation\n        $matches = [];\n        if (preg_match('/^([\\d]{3})[ -](?:([\\d]\\\\.[\\d]\\\\.[\\d]{1,2}) )?/', $this->last_reply, $matches)) {\n            $code = (int) $matches[1];\n            $code_ex = (count($matches) > 2 ? $matches[2] : null);\n            //Cut off error code from each response line\n            $detail = preg_replace(\n                \"/{$code}[ -]\" .\n                ($code_ex ? str_replace('.', '\\\\.', $code_ex) . ' ' : '') . '/m',\n                '',\n                $this->last_reply\n            );\n        } else {\n            //Fall back to simple parsing if regex fails\n            $code = (int) substr($this->last_reply, 0, 3);\n            $code_ex = null;\n            $detail = substr($this->last_reply, 4);\n        }\n\n        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);\n\n        if (!in_array($code, (array) $expect, true)) {\n            $this->setError(\n                \"$command command failed\",\n                $detail,\n                $code,\n                $code_ex\n            );\n            $this->edebug(\n                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,\n                self::DEBUG_CLIENT\n            );\n\n            return false;\n        }\n\n        //Don't clear the error store when using keepalive\n        if ($command !== 'RSET') {\n            $this->setError('');\n        }\n\n        return true;\n    }\n\n    /**\n     * Send an SMTP SAML command.\n     * Starts a mail transaction from the email address specified in $from.\n     * Returns true if successful or false otherwise. If True\n     * the mail transaction is started and then one or more recipient\n     * commands may be called followed by a data command. This command\n     * will send the message to the users terminal if they are logged\n     * in and send them an email.\n     * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.\n     *\n     * @param string $from The address the message is from\n     *\n     * @return bool\n     */\n    public function sendAndMail($from)\n    {\n        return $this->sendCommand('SAML', \"SAML FROM:$from\", 250);\n    }\n\n    /**\n     * Send an SMTP VRFY command.\n     *\n     * @param string $name The name to verify\n     *\n     * @return bool\n     */\n    public function verify($name)\n    {\n        return $this->sendCommand('VRFY', \"VRFY $name\", [250, 251]);\n    }\n\n    /**\n     * Send an SMTP NOOP command.\n     * Used to keep keep-alives alive, doesn't actually do anything.\n     *\n     * @return bool\n     */\n    public function noop()\n    {\n        return $this->sendCommand('NOOP', 'NOOP', 250);\n    }\n\n    /**\n     * Send an SMTP TURN command.\n     * This is an optional command for SMTP that this class does not support.\n     * This method is here to make the RFC821 Definition complete for this class\n     * and _may_ be implemented in future.\n     * Implements from RFC 821: TURN <CRLF>.\n     *\n     * @return bool\n     */\n    public function turn()\n    {\n        $this->setError('The SMTP TURN command is not implemented');\n        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);\n\n        return false;\n    }\n\n    /**\n     * Send raw data to the server.\n     *\n     * @param string $data    The data to send\n     * @param string $command Optionally, the command this is part of, used only for controlling debug output\n     *\n     * @return int|bool The number of bytes sent to the server or false on error\n     */\n    public function client_send($data, $command = '')\n    {\n        //If SMTP transcripts are left enabled, or debug output is posted online\n        //it can leak credentials, so hide credentials in all but lowest level\n        if (\n            self::DEBUG_LOWLEVEL > $this->do_debug &&\n            in_array($command, ['User & Password', 'Username', 'Password'], true)\n        ) {\n            $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);\n        } else {\n            $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);\n        }\n        set_error_handler([$this, 'errorHandler']);\n        $result = fwrite($this->smtp_conn, $data);\n        restore_error_handler();\n\n        return $result;\n    }\n\n    /**\n     * Get the latest error.\n     *\n     * @return array\n     */\n    public function getError()\n    {\n        return $this->error;\n    }\n\n    /**\n     * Get SMTP extensions available on the server.\n     *\n     * @return array|null\n     */\n    public function getServerExtList()\n    {\n        return $this->server_caps;\n    }\n\n    /**\n     * Get metadata about the SMTP server from its HELO/EHLO response.\n     * The method works in three ways, dependent on argument value and current state:\n     *   1. HELO/EHLO has not been sent - returns null and populates $this->error.\n     *   2. HELO has been sent -\n     *     $name == 'HELO': returns server name\n     *     $name == 'EHLO': returns boolean false\n     *     $name == any other string: returns null and populates $this->error\n     *   3. EHLO has been sent -\n     *     $name == 'HELO'|'EHLO': returns the server name\n     *     $name == any other string: if extension $name exists, returns True\n     *       or its options (e.g. AUTH mechanisms supported). Otherwise returns False.\n     *\n     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'\n     *\n     * @return string|bool|null\n     */\n    public function getServerExt($name)\n    {\n        if (!$this->server_caps) {\n            $this->setError('No HELO/EHLO was sent');\n\n            return null;\n        }\n\n        if (!array_key_exists($name, $this->server_caps)) {\n            if ('HELO' === $name) {\n                return $this->server_caps['EHLO'];\n            }\n            if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {\n                return false;\n            }\n            $this->setError('HELO handshake was used; No information about server extensions available');\n\n            return null;\n        }\n\n        return $this->server_caps[$name];\n    }\n\n    /**\n     * Get the last reply from the server.\n     *\n     * @return string\n     */\n    public function getLastReply()\n    {\n        return $this->last_reply;\n    }\n\n    /**\n     * Read the SMTP server's response.\n     * Either before eof or socket timeout occurs on the operation.\n     * With SMTP we can tell if we have more lines to read if the\n     * 4th character is '-' symbol. If it is a space then we don't\n     * need to read anything else.\n     *\n     * @return string\n     */\n    protected function get_lines()\n    {\n        //If the connection is bad, give up straight away\n        if (!is_resource($this->smtp_conn)) {\n            return '';\n        }\n        $data = '';\n        $endtime = 0;\n        stream_set_timeout($this->smtp_conn, $this->Timeout);\n        if ($this->Timelimit > 0) {\n            $endtime = time() + $this->Timelimit;\n        }\n        $selR = [$this->smtp_conn];\n        $selW = null;\n        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {\n            //Must pass vars in here as params are by reference\n            //solution for signals inspired by https://github.com/symfony/symfony/pull/6540\n            set_error_handler([$this, 'errorHandler']);\n            $n = stream_select($selR, $selW, $selW, $this->Timelimit);\n            restore_error_handler();\n\n            if ($n === false) {\n                $message = $this->getError()['detail'];\n\n                $this->edebug(\n                    'SMTP -> get_lines(): select failed (' . $message . ')',\n                    self::DEBUG_LOWLEVEL\n                );\n\n                //stream_select returns false when the `select` system call is interrupted\n                //by an incoming signal, try the select again\n                if (stripos($message, 'interrupted system call') !== false) {\n                    $this->edebug(\n                        'SMTP -> get_lines(): retrying stream_select',\n                        self::DEBUG_LOWLEVEL\n                    );\n                    $this->setError('');\n                    continue;\n                }\n\n                break;\n            }\n\n            if (!$n) {\n                $this->edebug(\n                    'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',\n                    self::DEBUG_LOWLEVEL\n                );\n                break;\n            }\n\n            //Deliberate noise suppression - errors are handled afterwards\n            $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);\n            $this->edebug('SMTP INBOUND: \"' . trim($str) . '\"', self::DEBUG_LOWLEVEL);\n            $data .= $str;\n            //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),\n            //or 4th character is a space or a line break char, we are done reading, break the loop.\n            //String array access is a significant micro-optimisation over strlen\n            if (!isset($str[3]) || $str[3] === ' ' || $str[3] === \"\\r\" || $str[3] === \"\\n\") {\n                break;\n            }\n            //Timed-out? Log and break\n            $info = stream_get_meta_data($this->smtp_conn);\n            if ($info['timed_out']) {\n                $this->edebug(\n                    'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',\n                    self::DEBUG_LOWLEVEL\n                );\n                break;\n            }\n            //Now check if reads took too long\n            if ($endtime && time() > $endtime) {\n                $this->edebug(\n                    'SMTP -> get_lines(): timelimit reached (' .\n                    $this->Timelimit . ' sec)',\n                    self::DEBUG_LOWLEVEL\n                );\n                break;\n            }\n        }\n\n        return $data;\n    }\n\n    /**\n     * Enable or disable VERP address generation.\n     *\n     * @param bool $enabled\n     */\n    public function setVerp($enabled = false)\n    {\n        $this->do_verp = $enabled;\n    }\n\n    /**\n     * Get VERP address generation mode.\n     *\n     * @return bool\n     */\n    public function getVerp()\n    {\n        return $this->do_verp;\n    }\n\n    /**\n     * Set error messages and codes.\n     *\n     * @param string $message      The error message\n     * @param string $detail       Further detail on the error\n     * @param string $smtp_code    An associated SMTP error code\n     * @param string $smtp_code_ex Extended SMTP code\n     */\n    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')\n    {\n        $this->error = [\n            'error' => $message,\n            'detail' => $detail,\n            'smtp_code' => $smtp_code,\n            'smtp_code_ex' => $smtp_code_ex,\n        ];\n    }\n\n    /**\n     * Set debug output method.\n     *\n     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it\n     */\n    public function setDebugOutput($method = 'echo')\n    {\n        $this->Debugoutput = $method;\n    }\n\n    /**\n     * Get debug output method.\n     *\n     * @return string\n     */\n    public function getDebugOutput()\n    {\n        return $this->Debugoutput;\n    }\n\n    /**\n     * Set debug output level.\n     *\n     * @param int $level\n     */\n    public function setDebugLevel($level = 0)\n    {\n        $this->do_debug = $level;\n    }\n\n    /**\n     * Get debug output level.\n     *\n     * @return int\n     */\n    public function getDebugLevel()\n    {\n        return $this->do_debug;\n    }\n\n    /**\n     * Set SMTP timeout.\n     *\n     * @param int $timeout The timeout duration in seconds\n     */\n    public function setTimeout($timeout = 0)\n    {\n        $this->Timeout = $timeout;\n    }\n\n    /**\n     * Get SMTP timeout.\n     *\n     * @return int\n     */\n    public function getTimeout()\n    {\n        return $this->Timeout;\n    }\n\n    /**\n     * Reports an error number and string.\n     *\n     * @param int    $errno   The error number returned by PHP\n     * @param string $errmsg  The error message returned by PHP\n     * @param string $errfile The file the error occurred in\n     * @param int    $errline The line number the error occurred on\n     */\n    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)\n    {\n        $notice = 'Connection failed.';\n        $this->setError(\n            $notice,\n            $errmsg,\n            (string) $errno\n        );\n        $this->edebug(\n            \"$notice Error #$errno: $errmsg [$errfile line $errline]\",\n            self::DEBUG_CONNECTION\n        );\n    }\n\n    /**\n     * Extract and return the ID of the last SMTP transaction based on\n     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.\n     * Relies on the host providing the ID in response to a DATA command.\n     * If no reply has been received yet, it will return null.\n     * If no pattern was matched, it will return false.\n     *\n     * @return bool|string|null\n     */\n    protected function recordLastTransactionID()\n    {\n        $reply = $this->getLastReply();\n\n        if (empty($reply)) {\n            $this->last_smtp_transaction_id = null;\n        } else {\n            $this->last_smtp_transaction_id = false;\n            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {\n                $matches = [];\n                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {\n                    $this->last_smtp_transaction_id = trim($matches[1]);\n                    break;\n                }\n            }\n        }\n\n        return $this->last_smtp_transaction_id;\n    }\n\n    /**\n     * Get the queue/transaction ID of the last SMTP transaction\n     * If no reply has been received yet, it will return null.\n     * If no pattern was matched, it will return false.\n     *\n     * @return bool|string|null\n     *\n     * @see recordLastTransactionID()\n     */\n    public function getLastTransactionID()\n    {\n        return $this->last_smtp_transaction_id;\n    }\n}\n"
  },
  {
    "path": "libs/Psr/Clock/ClockInterface.php",
    "content": "<?php\r\n\r\nnamespace Psr\\Clock;\r\n\r\nuse DateTimeImmutable;\r\n\r\ninterface ClockInterface\r\n{\r\n    /**\r\n     * Returns the current time as a DateTimeImmutable Object\r\n     */\r\n    public function now(): DateTimeImmutable;\r\n}"
  },
  {
    "path": "libs/constant_time_encoding/Base32.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\nuse InvalidArgumentException;\nuse RangeException;\nuse TypeError;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Base32\n * [A-Z][2-7]\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class Base32 implements EncoderInterface\n{\n    /**\n     * Decode a Base32-encoded string into raw binary\n     *\n     * @param string $encodedString\n     * @param bool $strictPadding\n     * @return string\n     */\n    public static function decode(\n        #[\\SensitiveParameter]\n        string $encodedString,\n        bool $strictPadding = false\n    ): string {\n        return static::doDecode($encodedString, false, $strictPadding);\n    }\n\n    /**\n     * Decode an uppercase Base32-encoded string into raw binary\n     *\n     * @param string $src\n     * @param bool $strictPadding\n     * @return string\n     */\n    public static function decodeUpper(\n        #[\\SensitiveParameter]\n        string $src,\n        bool $strictPadding = false\n    ): string {\n        return static::doDecode($src, true, $strictPadding);\n    }\n\n    /**\n     * Encode into Base32 (RFC 4648)\n     *\n     * @param string $binString\n     * @return string\n     * @throws TypeError\n     */\n    public static function encode(\n        #[\\SensitiveParameter]\n        string $binString\n    ): string {\n        return static::doEncode($binString, false, true);\n    }\n\n    /**\n     * Encode into Base32 (RFC 4648)\n     *\n     * @param string $src\n     * @return string\n     * @throws TypeError\n     */\n    public static function encodeUnpadded(\n        #[\\SensitiveParameter]\n        string $src\n    ): string {\n        return static::doEncode($src, false, false);\n    }\n\n    /**\n     * Encode into uppercase Base32 (RFC 4648)\n     *\n     * @param string $src\n     * @return string\n     * @throws TypeError\n     */\n    public static function encodeUpper(\n        #[\\SensitiveParameter]\n        string $src\n    ): string {\n        return static::doEncode($src, true, true);\n    }\n\n    /**\n     * Encode into uppercase Base32 (RFC 4648)\n     *\n     * @param string $src\n     * @return string\n     * @throws TypeError\n     */\n    public static function encodeUpperUnpadded(\n        #[\\SensitiveParameter]\n        string $src\n    ): string {\n        return static::doEncode($src, true, false);\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 5-bit integers\n     * into 8-bit integers.\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode5Bits(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 96 && $src < 123) $ret += $src - 97 + 1; // -64\n        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96);\n\n        // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23\n        $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 5-bit integers\n     * into 8-bit integers.\n     *\n     * Uppercase variant.\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode5BitsUpper(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64\n        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);\n\n        // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23\n        $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 5-bit integers.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode5Bits(int $src): string\n    {\n        $diff = 0x61;\n\n        // if ($src > 25) $ret -= 72;\n        $diff -= ((25 - $src) >> 8) & 73;\n\n        return \\pack('C', $src + $diff);\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 5-bit integers.\n     *\n     * Uppercase variant.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode5BitsUpper(int $src): string\n    {\n        $diff = 0x41;\n\n        // if ($src > 25) $ret -= 40;\n        $diff -= ((25 - $src) >> 8) & 41;\n\n        return \\pack('C', $src + $diff);\n    }\n\n    /**\n     * @param string $encodedString\n     * @param bool $upper\n     * @return string\n     */\n    public static function decodeNoPadding(\n        #[\\SensitiveParameter]\n        string $encodedString,\n        bool $upper = false\n    ): string {\n        $srcLen = Binary::safeStrlen($encodedString);\n        if ($srcLen === 0) {\n            return '';\n        }\n        if (($srcLen & 7) === 0) {\n            for ($j = 0; $j < 7 && $j < $srcLen; ++$j) {\n                if ($encodedString[$srcLen - $j - 1] === '=') {\n                    throw new InvalidArgumentException(\n                        \"decodeNoPadding() doesn't tolerate padding\"\n                    );\n                }\n            }\n        }\n        return static::doDecode(\n            $encodedString,\n            $upper,\n            true\n        );\n    }\n\n    /**\n     * Base32 decoding\n     *\n     * @param string $src\n     * @param bool $upper\n     * @param bool $strictPadding\n     * @return string\n     *\n     * @throws TypeError\n     */\n    protected static function doDecode(\n        #[\\SensitiveParameter]\n        string $src,\n        bool $upper = false,\n        bool $strictPadding = false\n    ): string {\n        // We do this to reduce code duplication:\n        $method = $upper\n            ? 'decode5BitsUpper'\n            : 'decode5Bits';\n\n        // Remove padding\n        $srcLen = Binary::safeStrlen($src);\n        if ($srcLen === 0) {\n            return '';\n        }\n        if ($strictPadding) {\n            if (($srcLen & 7) === 0) {\n                for ($j = 0; $j < 7; ++$j) {\n                    if ($src[$srcLen - 1] === '=') {\n                        $srcLen--;\n                    } else {\n                        break;\n                    }\n                }\n            }\n            if (($srcLen & 7) === 1) {\n                throw new RangeException(\n                    'Incorrect padding'\n                );\n            }\n        } else {\n            $src = \\rtrim($src, '=');\n            $srcLen = Binary::safeStrlen($src);\n        }\n\n        $err = 0;\n        $dest = '';\n        // Main loop (no padding):\n        for ($i = 0; $i + 8 <= $srcLen; $i += 8) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($src, $i, 8));\n            /** @var int $c0 */\n            $c0 = static::$method($chunk[1]);\n            /** @var int $c1 */\n            $c1 = static::$method($chunk[2]);\n            /** @var int $c2 */\n            $c2 = static::$method($chunk[3]);\n            /** @var int $c3 */\n            $c3 = static::$method($chunk[4]);\n            /** @var int $c4 */\n            $c4 = static::$method($chunk[5]);\n            /** @var int $c5 */\n            $c5 = static::$method($chunk[6]);\n            /** @var int $c6 */\n            $c6 = static::$method($chunk[7]);\n            /** @var int $c7 */\n            $c7 = static::$method($chunk[8]);\n\n            $dest .= \\pack(\n                'CCCCC',\n                (($c0 << 3) | ($c1 >> 2)             ) & 0xff,\n                (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,\n                (($c3 << 4) | ($c4 >> 1)             ) & 0xff,\n                (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff,\n                (($c6 << 5) | ($c7     )             ) & 0xff\n            );\n            $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8;\n        }\n        // The last chunk, which may have padding:\n        if ($i < $srcLen) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));\n            /** @var int $c0 */\n            $c0 = static::$method($chunk[1]);\n\n            if ($i + 6 < $srcLen) {\n                /** @var int $c1 */\n                $c1 = static::$method($chunk[2]);\n                /** @var int $c2 */\n                $c2 = static::$method($chunk[3]);\n                /** @var int $c3 */\n                $c3 = static::$method($chunk[4]);\n                /** @var int $c4 */\n                $c4 = static::$method($chunk[5]);\n                /** @var int $c5 */\n                $c5 = static::$method($chunk[6]);\n                /** @var int $c6 */\n                $c6 = static::$method($chunk[7]);\n\n                $dest .= \\pack(\n                    'CCCC',\n                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,\n                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,\n                    (($c3 << 4) | ($c4 >> 1)             ) & 0xff,\n                    (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff\n                );\n                $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8;\n                if ($strictPadding) {\n                    $err |= ($c6 << 5) & 0xff;\n                }\n            } elseif ($i + 5 < $srcLen) {\n                /** @var int $c1 */\n                $c1 = static::$method($chunk[2]);\n                /** @var int $c2 */\n                $c2 = static::$method($chunk[3]);\n                /** @var int $c3 */\n                $c3 = static::$method($chunk[4]);\n                /** @var int $c4 */\n                $c4 = static::$method($chunk[5]);\n                /** @var int $c5 */\n                $c5 = static::$method($chunk[6]);\n\n                $dest .= \\pack(\n                    'CCCC',\n                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,\n                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,\n                    (($c3 << 4) | ($c4 >> 1)             ) & 0xff,\n                    (($c4 << 7) | ($c5 << 2)             ) & 0xff\n                );\n                $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8;\n            } elseif ($i + 4 < $srcLen) {\n                /** @var int $c1 */\n                $c1 = static::$method($chunk[2]);\n                /** @var int $c2 */\n                $c2 = static::$method($chunk[3]);\n                /** @var int $c3 */\n                $c3 = static::$method($chunk[4]);\n                /** @var int $c4 */\n                $c4 = static::$method($chunk[5]);\n\n                $dest .= \\pack(\n                    'CCC',\n                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,\n                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,\n                    (($c3 << 4) | ($c4 >> 1)             ) & 0xff\n                );\n                $err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8;\n                if ($strictPadding) {\n                    $err |= ($c4 << 7) & 0xff;\n                }\n            } elseif ($i + 3 < $srcLen) {\n                /** @var int $c1 */\n                $c1 = static::$method($chunk[2]);\n                /** @var int $c2 */\n                $c2 = static::$method($chunk[3]);\n                /** @var int $c3 */\n                $c3 = static::$method($chunk[4]);\n\n                $dest .= \\pack(\n                    'CC',\n                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,\n                    (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff\n                );\n                $err |= ($c0 | $c1 | $c2 | $c3) >> 8;\n                if ($strictPadding) {\n                    $err |= ($c3 << 4) & 0xff;\n                }\n            } elseif ($i + 2 < $srcLen) {\n                /** @var int $c1 */\n                $c1 = static::$method($chunk[2]);\n                /** @var int $c2 */\n                $c2 = static::$method($chunk[3]);\n\n                $dest .= \\pack(\n                    'CC',\n                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff,\n                    (($c1 << 6) | ($c2 << 1)             ) & 0xff\n                );\n                $err |= ($c0 | $c1 | $c2) >> 8;\n                if ($strictPadding) {\n                    $err |= ($c2 << 6) & 0xff;\n                }\n            } elseif ($i + 1 < $srcLen) {\n                /** @var int $c1 */\n                $c1 = static::$method($chunk[2]);\n\n                $dest .= \\pack(\n                    'C',\n                    (($c0 << 3) | ($c1 >> 2)             ) & 0xff\n                );\n                $err |= ($c0 | $c1) >> 8;\n                if ($strictPadding) {\n                    $err |= ($c1 << 6) & 0xff;\n                }\n            } else {\n                $dest .= \\pack(\n                    'C',\n                    (($c0 << 3)                          ) & 0xff\n                );\n                $err |= ($c0) >> 8;\n            }\n        }\n        $check = ($err === 0);\n        if (!$check) {\n            throw new RangeException(\n                'Base32::doDecode() only expects characters in the correct base32 alphabet'\n            );\n        }\n        return $dest;\n    }\n\n    /**\n     * Base32 Encoding\n     *\n     * @param string $src\n     * @param bool $upper\n     * @param bool $pad\n     * @return string\n     * @throws TypeError\n     */\n    protected static function doEncode(\n        #[\\SensitiveParameter]\n        string $src,\n        bool $upper = false,\n        bool $pad = true\n    ): string {\n        // We do this to reduce code duplication:\n        $method = $upper\n            ? 'encode5BitsUpper'\n            : 'encode5Bits';\n        \n        $dest = '';\n        $srcLen = Binary::safeStrlen($src);\n\n        // Main loop (no padding):\n        for ($i = 0; $i + 5 <= $srcLen; $i += 5) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($src, $i, 5));\n            $b0 = $chunk[1];\n            $b1 = $chunk[2];\n            $b2 = $chunk[3];\n            $b3 = $chunk[4];\n            $b4 = $chunk[5];\n            $dest .=\n                static::$method(              ($b0 >> 3)  & 31) .\n                static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .\n                static::$method((($b1 >> 1)             ) & 31) .\n                static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .\n                static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .\n                static::$method((($b3 >> 2)             ) & 31) .\n                static::$method((($b3 << 3) | ($b4 >> 5)) & 31) .\n                static::$method(  $b4                     & 31);\n        }\n        // The last chunk, which may have padding:\n        if ($i < $srcLen) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));\n            $b0 = $chunk[1];\n            if ($i + 3 < $srcLen) {\n                $b1 = $chunk[2];\n                $b2 = $chunk[3];\n                $b3 = $chunk[4];\n                $dest .=\n                    static::$method(              ($b0 >> 3)  & 31) .\n                    static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .\n                    static::$method((($b1 >> 1)             ) & 31) .\n                    static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .\n                    static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .\n                    static::$method((($b3 >> 2)             ) & 31) .\n                    static::$method((($b3 << 3)             ) & 31);\n                if ($pad) {\n                    $dest .= '=';\n                }\n            } elseif ($i + 2 < $srcLen) {\n                $b1 = $chunk[2];\n                $b2 = $chunk[3];\n                $dest .=\n                    static::$method(              ($b0 >> 3)  & 31) .\n                    static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .\n                    static::$method((($b1 >> 1)             ) & 31) .\n                    static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .\n                    static::$method((($b2 << 1)             ) & 31);\n                if ($pad) {\n                    $dest .= '===';\n                }\n            } elseif ($i + 1 < $srcLen) {\n                $b1 = $chunk[2];\n                $dest .=\n                    static::$method(              ($b0 >> 3)  & 31) .\n                    static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .\n                    static::$method((($b1 >> 1)             ) & 31) .\n                    static::$method((($b1 << 4)             ) & 31);\n                if ($pad) {\n                    $dest .= '====';\n                }\n            } else {\n                $dest .=\n                    static::$method(              ($b0 >> 3)  & 31) .\n                    static::$method( ($b0 << 2)               & 31);\n                if ($pad) {\n                    $dest .= '======';\n                }\n            }\n        }\n        return $dest;\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/Base32Hex.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Base32Hex\n * [0-9][A-V]\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class Base32Hex extends Base32\n{\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 5-bit integers\n     * into 8-bit integers.\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode5Bits(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47\n        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);\n\n        // if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86\n        $ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86);\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 5-bit integers\n     * into 8-bit integers.\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode5BitsUpper(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47\n        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);\n\n        // if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54\n        $ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54);\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 5-bit integers.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode5Bits(int $src): string\n    {\n        $src += 0x30;\n\n        // if ($src > 0x39) $src += 0x61 - 0x3a; // 39\n        $src += ((0x39 - $src) >> 8) & 39;\n\n        return \\pack('C', $src);\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 5-bit integers.\n     *\n     * Uppercase variant.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode5BitsUpper(int $src): string\n    {\n        $src += 0x30;\n\n        // if ($src > 0x39) $src += 0x41 - 0x3a; // 7\n        $src += ((0x39 - $src) >> 8) & 7;\n\n        return \\pack('C', $src);\n    }\n}"
  },
  {
    "path": "libs/constant_time_encoding/Base64.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\nuse InvalidArgumentException;\nuse RangeException;\nuse TypeError;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Base64\n * [A-Z][a-z][0-9]+/\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class Base64 implements EncoderInterface\n{\n    /**\n     * Encode into Base64\n     *\n     * Base64 character set \"[A-Z][a-z][0-9]+/\"\n     *\n     * @param string $binString\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function encode(\n        #[\\SensitiveParameter]\n        string $binString\n    ): string {\n        return static::doEncode($binString, true);\n    }\n\n    /**\n     * Encode into Base64, no = padding\n     *\n     * Base64 character set \"[A-Z][a-z][0-9]+/\"\n     *\n     * @param string $src\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function encodeUnpadded(\n        #[\\SensitiveParameter]\n        string $src\n    ): string {\n        return static::doEncode($src, false);\n    }\n\n    /**\n     * @param string $src\n     * @param bool $pad   Include = padding?\n     * @return string\n     *\n     * @throws TypeError\n     */\n    protected static function doEncode(\n        #[\\SensitiveParameter]\n        string $src,\n        bool $pad = true\n    ): string {\n        $dest = '';\n        $srcLen = Binary::safeStrlen($src);\n        // Main loop (no padding):\n        for ($i = 0; $i + 3 <= $srcLen; $i += 3) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($src, $i, 3));\n            $b0 = $chunk[1];\n            $b1 = $chunk[2];\n            $b2 = $chunk[3];\n\n            $dest .=\n                static::encode6Bits(               $b0 >> 2       ) .\n                static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .\n                static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .\n                static::encode6Bits(  $b2                     & 63);\n        }\n        // The last chunk, which may have padding:\n        if ($i < $srcLen) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));\n            $b0 = $chunk[1];\n            if ($i + 1 < $srcLen) {\n                $b1 = $chunk[2];\n                $dest .=\n                    static::encode6Bits($b0 >> 2) .\n                    static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .\n                    static::encode6Bits(($b1 << 2) & 63);\n                if ($pad) {\n                    $dest .= '=';\n                }\n            } else {\n                $dest .=\n                    static::encode6Bits( $b0 >> 2) .\n                    static::encode6Bits(($b0 << 4) & 63);\n                if ($pad) {\n                    $dest .= '==';\n                }\n            }\n        }\n        return $dest;\n    }\n\n    /**\n     * decode from base64 into binary\n     *\n     * Base64 character set \"./[A-Z][a-z][0-9]\"\n     *\n     * @param string $encodedString\n     * @param bool $strictPadding\n     * @return string\n     *\n     * @throws RangeException\n     * @throws TypeError\n     */\n    public static function decode(\n        #[\\SensitiveParameter]\n        string $encodedString,\n        bool $strictPadding = false\n    ): string {\n        // Remove padding\n        $srcLen = Binary::safeStrlen($encodedString);\n        if ($srcLen === 0) {\n            return '';\n        }\n\n        if ($strictPadding) {\n            if (($srcLen & 3) === 0) {\n                if ($encodedString[$srcLen - 1] === '=') {\n                    $srcLen--;\n                    if ($encodedString[$srcLen - 1] === '=') {\n                        $srcLen--;\n                    }\n                }\n            }\n            if (($srcLen & 3) === 1) {\n                throw new RangeException(\n                    'Incorrect padding'\n                );\n            }\n            if ($encodedString[$srcLen - 1] === '=') {\n                throw new RangeException(\n                    'Incorrect padding'\n                );\n            }\n        } else {\n            $encodedString = \\rtrim($encodedString, '=');\n            $srcLen = Binary::safeStrlen($encodedString);\n        }\n\n        $err = 0;\n        $dest = '';\n        // Main loop (no padding):\n        for ($i = 0; $i + 4 <= $srcLen; $i += 4) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($encodedString, $i, 4));\n            $c0 = static::decode6Bits($chunk[1]);\n            $c1 = static::decode6Bits($chunk[2]);\n            $c2 = static::decode6Bits($chunk[3]);\n            $c3 = static::decode6Bits($chunk[4]);\n\n            $dest .= \\pack(\n                'CCC',\n                ((($c0 << 2) | ($c1 >> 4)) & 0xff),\n                ((($c1 << 4) | ($c2 >> 2)) & 0xff),\n                ((($c2 << 6) |  $c3      ) & 0xff)\n            );\n            $err |= ($c0 | $c1 | $c2 | $c3) >> 8;\n        }\n        // The last chunk, which may have padding:\n        if ($i < $srcLen) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i));\n            $c0 = static::decode6Bits($chunk[1]);\n\n            if ($i + 2 < $srcLen) {\n                $c1 = static::decode6Bits($chunk[2]);\n                $c2 = static::decode6Bits($chunk[3]);\n                $dest .= \\pack(\n                    'CC',\n                    ((($c0 << 2) | ($c1 >> 4)) & 0xff),\n                    ((($c1 << 4) | ($c2 >> 2)) & 0xff)\n                );\n                $err |= ($c0 | $c1 | $c2) >> 8;\n                if ($strictPadding) {\n                    $err |= ($c2 << 6) & 0xff;\n                }\n            } elseif ($i + 1 < $srcLen) {\n                $c1 = static::decode6Bits($chunk[2]);\n                $dest .= \\pack(\n                    'C',\n                    ((($c0 << 2) | ($c1 >> 4)) & 0xff)\n                );\n                $err |= ($c0 | $c1) >> 8;\n                if ($strictPadding) {\n                    $err |= ($c1 << 4) & 0xff;\n                }\n            } elseif ($strictPadding) {\n                $err |= 1;\n            }\n        }\n        $check = ($err === 0);\n        if (!$check) {\n            throw new RangeException(\n                'Base64::decode() only expects characters in the correct base64 alphabet'\n            );\n        }\n        return $dest;\n    }\n\n    /**\n     * @param string $encodedString\n     * @return string\n     */\n    public static function decodeNoPadding(\n        #[\\SensitiveParameter]\n        string $encodedString\n    ): string {\n        $srcLen = Binary::safeStrlen($encodedString);\n        if ($srcLen === 0) {\n            return '';\n        }\n        if (($srcLen & 3) === 0) {\n            // If $strLen is not zero, and it is divisible by 4, then it's at least 4.\n            if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') {\n                throw new InvalidArgumentException(\n                    \"decodeNoPadding() doesn't tolerate padding\"\n                );\n            }\n        }\n        return static::decode(\n            $encodedString,\n            true\n        );\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 6-bit integers\n     * into 8-bit integers.\n     *\n     * Base64 character set:\n     * [A-Z]      [a-z]      [0-9]      +     /\n     * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode6Bits(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64\n        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);\n\n        // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70\n        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);\n\n        // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5\n        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);\n\n        // if ($src == 0x2b) $ret += 62 + 1;\n        $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63;\n\n        // if ($src == 0x2f) ret += 63 + 1;\n        $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64;\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 6-bit integers.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode6Bits(int $src): string\n    {\n        $diff = 0x41;\n\n        // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6\n        $diff += ((25 - $src) >> 8) & 6;\n\n        // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75\n        $diff -= ((51 - $src) >> 8) & 75;\n\n        // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15\n        $diff -= ((61 - $src) >> 8) & 15;\n\n        // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3\n        $diff += ((62 - $src) >> 8) & 3;\n\n        return \\pack('C', $src + $diff);\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/Base64DotSlash.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Base64DotSlash\n * ./[A-Z][a-z][0-9]\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class Base64DotSlash extends Base64\n{\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 6-bit integers\n     * into 8-bit integers.\n     *\n     * Base64 character set:\n     * ./         [A-Z]      [a-z]     [0-9]\n     * 0x2e-0x2f, 0x41-0x5a, 0x61-0x7a, 0x30-0x39\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode6Bits(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45\n        $ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45);\n\n        // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62\n        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62);\n\n        // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68\n        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68);\n\n        // if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7\n        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7);\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 6-bit integers.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode6Bits(int $src): string\n    {\n        $src += 0x2e;\n\n        // if ($src > 0x2f) $src += 0x41 - 0x30; // 17\n        $src += ((0x2f - $src) >> 8) & 17;\n\n        // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6\n        $src += ((0x5a - $src) >> 8) & 6;\n\n        // if ($src > 0x7a) $src += 0x30 - 0x7b; // -75\n        $src -= ((0x7a - $src) >> 8) & 75;\n\n        return \\pack('C', $src);\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/Base64DotSlashOrdered.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Base64DotSlashOrdered\n * ./[0-9][A-Z][a-z]\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class Base64DotSlashOrdered extends Base64\n{\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 6-bit integers\n     * into 8-bit integers.\n     *\n     * Base64 character set:\n     * [.-9]      [A-Z]      [a-z]\n     * 0x2e-0x39, 0x41-0x5a, 0x61-0x7a\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode6Bits(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45\n        $ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45);\n\n        // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52\n        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52);\n\n        // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58\n        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58);\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 6-bit integers.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode6Bits(int $src): string\n    {\n        $src += 0x2e;\n\n        // if ($src > 0x39) $src += 0x41 - 0x3a; // 7\n        $src += ((0x39 - $src) >> 8) & 7;\n\n        // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6\n        $src += ((0x5a - $src) >> 8) & 6;\n\n        return \\pack('C', $src);\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/Base64UrlSafe.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Base64UrlSafe\n * [A-Z][a-z][0-9]\\-_\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class Base64UrlSafe extends Base64\n{\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 6-bit integers\n     * into 8-bit integers.\n     *\n     * Base64 character set:\n     * [A-Z]      [a-z]      [0-9]      -     _\n     * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2d, 0x5f\n     *\n     * @param int $src\n     * @return int\n     */\n    protected static function decode6Bits(int $src): int\n    {\n        $ret = -1;\n\n        // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64\n        $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);\n\n        // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70\n        $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);\n\n        // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5\n        $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);\n\n        // if ($src == 0x2c) $ret += 62 + 1;\n        $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63;\n\n        // if ($src == 0x5f) ret += 63 + 1;\n        $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64;\n\n        return $ret;\n    }\n\n    /**\n     * Uses bitwise operators instead of table-lookups to turn 8-bit integers\n     * into 6-bit integers.\n     *\n     * @param int $src\n     * @return string\n     */\n    protected static function encode6Bits(int $src): string\n    {\n        $diff = 0x41;\n\n        // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6\n        $diff += ((25 - $src) >> 8) & 6;\n\n        // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75\n        $diff -= ((51 - $src) >> 8) & 75;\n\n        // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13\n        $diff -= ((61 - $src) >> 8) & 13;\n\n        // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3\n        $diff += ((62 - $src) >> 8) & 49;\n\n        return \\pack('C', $src + $diff);\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/Binary.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\nuse TypeError;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Binary\n *\n * Binary string operators that don't choke on\n * mbstring.func_overload\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class Binary\n{\n    /**\n     * Safe string length\n     *\n     * @ref mbstring.func_overload\n     *\n     * @param string $str\n     * @return int\n     */\n    public static function safeStrlen(\n        #[\\SensitiveParameter]\n        string $str\n    ): int {\n        if (\\function_exists('mb_strlen')) {\n            // mb_strlen in PHP 7.x can return false.\n            /** @psalm-suppress RedundantCast */\n            return (int) \\mb_strlen($str, '8bit');\n        } else {\n            return \\strlen($str);\n        }\n    }\n\n    /**\n     * Safe substring\n     *\n     * @ref mbstring.func_overload\n     *\n     * @staticvar boolean $exists\n     * @param string $str\n     * @param int $start\n     * @param ?int $length\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function safeSubstr(\n        #[\\SensitiveParameter]\n        string $str,\n        int $start = 0,\n        ?int $length = null\n    ): string {\n        if ($length === 0) {\n            return '';\n        }\n        if (\\function_exists('mb_substr')) {\n            return \\mb_substr($str, $start, $length, '8bit');\n        }\n        // Unlike mb_substr(), substr() doesn't accept NULL for length\n        if ($length !== null) {\n            return \\substr($str, $start, $length);\n        } else {\n            return \\substr($str, $start);\n        }\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/EncoderInterface.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Interface EncoderInterface\n * @package ParagonIE\\ConstantTime\n */\ninterface EncoderInterface\n{\n    /**\n     * Convert a binary string into a hexadecimal string without cache-timing\n     * leaks\n     *\n     * @param string $binString (raw binary)\n     * @return string\n     */\n    public static function encode(string $binString): string;\n\n    /**\n     * Convert a binary string into a hexadecimal string without cache-timing\n     * leaks\n     *\n     * @param string $encodedString\n     * @param bool $strictPadding Error on invalid padding\n     * @return string (raw binary)\n     */\n    public static function decode(string $encodedString, bool $strictPadding = false): string;\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/Encoding.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\nuse TypeError;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Encoding\n * @package ParagonIE\\ConstantTime\n */\nabstract class Encoding\n{\n    /**\n     * RFC 4648 Base32 encoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32Encode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::encode($str);\n    }\n\n    /**\n     * RFC 4648 Base32 encoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32EncodeUpper(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::encodeUpper($str);\n    }\n\n    /**\n     * RFC 4648 Base32 decoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32Decode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::decode($str);\n    }\n\n    /**\n     * RFC 4648 Base32 decoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32DecodeUpper(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::decodeUpper($str);\n    }\n\n    /**\n     * RFC 4648 Base32 encoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32HexEncode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32Hex::encode($str);\n    }\n\n    /**\n     * RFC 4648 Base32Hex encoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32HexEncodeUpper(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32Hex::encodeUpper($str);\n    }\n\n    /**\n     * RFC 4648 Base32Hex decoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32HexDecode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32Hex::decode($str);\n    }\n\n    /**\n     * RFC 4648 Base32Hex decoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base32HexDecodeUpper(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32Hex::decodeUpper($str);\n    }\n\n    /**\n     * RFC 4648 Base64 encoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base64Encode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64::encode($str);\n    }\n\n    /**\n     * RFC 4648 Base64 decoding\n     *\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base64Decode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64::decode($str);\n    }\n\n    /**\n     * Encode into Base64\n     *\n     * Base64 character set \"./[A-Z][a-z][0-9]\"\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base64EncodeDotSlash(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64DotSlash::encode($str);\n    }\n\n    /**\n     * Decode from base64 to raw binary\n     *\n     * Base64 character set \"./[A-Z][a-z][0-9]\"\n     *\n     * @param string $str\n     * @return string\n     * @throws \\RangeException\n     * @throws TypeError\n     */\n    public static function base64DecodeDotSlash(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64DotSlash::decode($str);\n    }\n\n    /**\n     * Encode into Base64\n     *\n     * Base64 character set \"[.-9][A-Z][a-z]\" or \"./[0-9][A-Z][a-z]\"\n     * @param string $str\n     * @return string\n     * @throws TypeError\n     */\n    public static function base64EncodeDotSlashOrdered(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64DotSlashOrdered::encode($str);\n    }\n\n    /**\n     * Decode from base64 to raw binary\n     *\n     * Base64 character set \"[.-9][A-Z][a-z]\" or \"./[0-9][A-Z][a-z]\"\n     *\n     * @param string $str\n     * @return string\n     * @throws \\RangeException\n     * @throws TypeError\n     */\n    public static function base64DecodeDotSlashOrdered(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64DotSlashOrdered::decode($str);\n    }\n\n    /**\n     * Convert a binary string into a hexadecimal string without cache-timing\n     * leaks\n     *\n     * @param string $bin_string (raw binary)\n     * @return string\n     * @throws TypeError\n     */\n    public static function hexEncode(\n        #[\\SensitiveParameter]\n        string $bin_string\n    ): string {\n        return Hex::encode($bin_string);\n    }\n\n    /**\n     * Convert a hexadecimal string into a binary string without cache-timing\n     * leaks\n     *\n     * @param string $hex_string\n     * @return string (raw binary)\n     * @throws \\RangeException\n     */\n    public static function hexDecode(\n        #[\\SensitiveParameter]\n        string $hex_string\n    ): string {\n        return Hex::decode($hex_string);\n    }\n\n    /**\n     * Convert a binary string into a hexadecimal string without cache-timing\n     * leaks\n     *\n     * @param string $bin_string (raw binary)\n     * @return string\n     * @throws TypeError\n     */\n    public static function hexEncodeUpper(\n        #[\\SensitiveParameter]\n        string $bin_string\n    ): string {\n        return Hex::encodeUpper($bin_string);\n    }\n\n    /**\n     * Convert a binary string into a hexadecimal string without cache-timing\n     * leaks\n     *\n     * @param string $bin_string (raw binary)\n     * @return string\n     */\n    public static function hexDecodeUpper(\n        #[\\SensitiveParameter]\n        string $bin_string\n    ): string {\n        return Hex::decode($bin_string);\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/Hex.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\nuse RangeException;\nuse TypeError;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class Hex\n * @package ParagonIE\\ConstantTime\n */\nabstract class Hex implements EncoderInterface\n{\n    /**\n     * Convert a binary string into a hexadecimal string without cache-timing\n     * leaks\n     *\n     * @param string $binString (raw binary)\n     * @return string\n     * @throws TypeError\n     */\n    public static function encode(\n        #[\\SensitiveParameter]\n        string $binString\n    ): string {\n        $hex = '';\n        $len = Binary::safeStrlen($binString);\n        for ($i = 0; $i < $len; ++$i) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C', $binString[$i]);\n            $c = $chunk[1] & 0xf;\n            $b = $chunk[1] >> 4;\n\n            $hex .= \\pack(\n                'CC',\n                (87 + $b + ((($b - 10) >> 8) & ~38)),\n                (87 + $c + ((($c - 10) >> 8) & ~38))\n            );\n        }\n        return $hex;\n    }\n\n    /**\n     * Convert a binary string into a hexadecimal string without cache-timing\n     * leaks, returning uppercase letters (as per RFC 4648)\n     *\n     * @param string $binString (raw binary)\n     * @return string\n     * @throws TypeError\n     */\n    public static function encodeUpper(\n        #[\\SensitiveParameter]\n        string $binString\n    ): string {\n        $hex = '';\n        $len = Binary::safeStrlen($binString);\n\n        for ($i = 0; $i < $len; ++$i) {\n            /** @var array<int, int> $chunk */\n            $chunk = \\unpack('C', $binString[$i]);\n            $c = $chunk[1] & 0xf;\n            $b = $chunk[1] >> 4;\n\n            $hex .= \\pack(\n                'CC',\n                (55 + $b + ((($b - 10) >> 8) & ~6)),\n                (55 + $c + ((($c - 10) >> 8) & ~6))\n            );\n        }\n        return $hex;\n    }\n\n    /**\n     * Convert a hexadecimal string into a binary string without cache-timing\n     * leaks\n     *\n     * @param string $encodedString\n     * @param bool $strictPadding\n     * @return string (raw binary)\n     * @throws RangeException\n     */\n    public static function decode(\n        #[\\SensitiveParameter]\n        string $encodedString,\n        bool $strictPadding = false\n    ): string {\n        $hex_pos = 0;\n        $bin = '';\n        $c_acc = 0;\n        $hex_len = Binary::safeStrlen($encodedString);\n        $state = 0;\n        if (($hex_len & 1) !== 0) {\n            if ($strictPadding) {\n                throw new RangeException(\n                    'Expected an even number of hexadecimal characters'\n                );\n            } else {\n                $encodedString = '0' . $encodedString;\n                ++$hex_len;\n            }\n        }\n\n        /** @var array<int, int> $chunk */\n        $chunk = \\unpack('C*', $encodedString);\n        while ($hex_pos < $hex_len) {\n            ++$hex_pos;\n            $c = $chunk[$hex_pos];\n            $c_num = $c ^ 48;\n            $c_num0 = ($c_num - 10) >> 8;\n            $c_alpha = ($c & ~32) - 55;\n            $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;\n\n            if (($c_num0 | $c_alpha0) === 0) {\n                throw new RangeException(\n                    'Expected hexadecimal character'\n                );\n            }\n            $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);\n            if ($state === 0) {\n                $c_acc = $c_val * 16;\n            } else {\n                $bin .= \\pack('C', $c_acc | $c_val);\n            }\n            $state ^= 1;\n        }\n        return $bin;\n    }\n}\n"
  },
  {
    "path": "libs/constant_time_encoding/RFC4648.php",
    "content": "<?php\ndeclare(strict_types=1);\nnamespace ParagonIE\\ConstantTime;\n\nuse TypeError;\n\n/**\n *  Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.\n *  Copyright (c) 2014 Steve \"Sc00bz\" Thomas (steve at tobtu dot com)\n *\n *  Permission is hereby granted, free of charge, to any person obtaining a copy\n *  of this software and associated documentation files (the \"Software\"), to deal\n *  in the Software without restriction, including without limitation the rights\n *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n *  copies of the Software, and to permit persons to whom the Software is\n *  furnished to do so, subject to the following conditions:\n *\n *  The above copyright notice and this permission notice shall be included in all\n *  copies or substantial portions of the Software.\n *\n *  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n *  SOFTWARE.\n */\n\n/**\n * Class RFC4648\n *\n * This class conforms strictly to the RFC\n *\n * @package ParagonIE\\ConstantTime\n */\nabstract class RFC4648\n{\n    /**\n     * RFC 4648 Base64 encoding\n     *\n     * \"foo\" -> \"Zm9v\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base64Encode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64::encode($str);\n    }\n\n    /**\n     * RFC 4648 Base64 decoding\n     *\n     * \"Zm9v\" -> \"foo\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base64Decode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64::decode($str, true);\n    }\n\n    /**\n     * RFC 4648 Base64 (URL Safe) encoding\n     *\n     * \"foo\" -> \"Zm9v\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base64UrlSafeEncode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64UrlSafe::encode($str);\n    }\n\n    /**\n     * RFC 4648 Base64 (URL Safe) decoding\n     *\n     * \"Zm9v\" -> \"foo\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base64UrlSafeDecode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base64UrlSafe::decode($str, true);\n    }\n\n    /**\n     * RFC 4648 Base32 encoding\n     *\n     * \"foo\" -> \"MZXW6===\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base32Encode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::encodeUpper($str);\n    }\n\n    /**\n     * RFC 4648 Base32 encoding\n     *\n     * \"MZXW6===\" -> \"foo\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base32Decode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::decodeUpper($str, true);\n    }\n\n    /**\n     * RFC 4648 Base32-Hex encoding\n     *\n     * \"foo\" -> \"CPNMU===\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base32HexEncode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::encodeUpper($str);\n    }\n\n    /**\n     * RFC 4648 Base32-Hex decoding\n     *\n     * \"CPNMU===\" -> \"foo\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base32HexDecode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Base32::decodeUpper($str, true);\n    }\n\n    /**\n     * RFC 4648 Base16 decoding\n     *\n     * \"foo\" -> \"666F6F\"\n     *\n     * @param string $str\n     * @return string\n     *\n     * @throws TypeError\n     */\n    public static function base16Encode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Hex::encodeUpper($str);\n    }\n\n    /**\n     * RFC 4648 Base16 decoding\n     *\n     * \"666F6F\" -> \"foo\"\n     *\n     * @param string $str\n     * @return string\n     */\n    public static function base16Decode(\n        #[\\SensitiveParameter]\n        string $str\n    ): string {\n        return Hex::decode($str, true);\n    }\n}\n"
  },
  {
    "path": "libs/csrf.php",
    "content": "<?php\r\n\r\nif (session_status() !== PHP_SESSION_ACTIVE) {\r\n    session_start();\r\n}\r\n\r\nfunction generate_csrf_token(): string {\r\n    if (empty($_SESSION['csrf_token'])) {\r\n        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));\r\n    }\r\n    return $_SESSION['csrf_token'];\r\n}\r\n\r\nfunction verify_csrf_token(?string $token): bool {\r\n    if (empty($_SESSION['csrf_token']) || empty($token)) return false;\r\n    // Use hash_equals to avoid timing attacks\r\n    return hash_equals($_SESSION['csrf_token'], $token);\r\n}"
  },
  {
    "path": "login.php",
    "content": "<?php\r\nrequire_once 'includes/connect.php';\r\nrequire_once 'includes/checkuser.php';\r\n\r\nrequire_once 'includes/i18n/languages.php';\r\nrequire_once 'includes/i18n/getlang.php';\r\nrequire_once 'includes/i18n/' . $lang . '.php';\r\n\r\nrequire_once 'includes/version.php';\r\n\r\nif ($userCount == 0) {\r\n    header(\"Location: registration.php\");\r\n    exit();\r\n}\r\n\r\nsession_start();\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    $db->close();\r\n    header(\"Location: .\");\r\n    exit();\r\n}\r\n\r\n$demoMode = getenv('DEMO_MODE');\r\n\r\n$cookieExpire = time() + (30 * 24 * 60 * 60);\r\n\r\n// Check if login is disabled\r\n$adminQuery = \"SELECT login_disabled FROM admin\";\r\n$adminResult = $db->query($adminQuery);\r\n$adminRow = $adminResult->fetchArray(SQLITE3_ASSOC);\r\nif ($adminRow['login_disabled'] == 1) {\r\n\r\n    $query = \"SELECT id, username, main_currency, language FROM user WHERE id = :id\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':id', 1, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($row === false) {\r\n        // Something is wrong with admin user. Reenable login\r\n        $updateQuery = \"UPDATE admin SET login_disabled = 0\";\r\n        $updateStmt = $db->prepare($updateQuery);\r\n        $updateStmt->execute();\r\n\r\n        $db->close();\r\n        header(\"Location: login.php\");\r\n    } else {\r\n        $userId = $row['id'];\r\n        $main_currency = $row['main_currency'];\r\n        $username = $row['username'];\r\n        $language = $row['language'];\r\n\r\n        $_SESSION['username'] = $username;\r\n        $_SESSION['loggedin'] = true;\r\n        $_SESSION['main_currency'] = $main_currency;\r\n        $_SESSION['userId'] = $userId;\r\n        setcookie('language', $language, [\r\n            'expires' => $cookieExpire,\r\n            'samesite' => 'Strict'\r\n        ]);\r\n\r\n        if (!isset($_COOKIE['sortOrder'])) {\r\n            setcookie('sortOrder', 'next_payment', [\r\n                'expires' => $cookieExpire,\r\n                'samesite' => 'Strict'\r\n            ]);\r\n        }\r\n\r\n        $query = \"SELECT color_theme FROM settings\";\r\n        $stmt = $db->prepare($query);\r\n        $result = $stmt->execute();\r\n        $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n        setcookie('colorTheme', $settings['color_theme'], [\r\n            'expires' => $cookieExpire,\r\n            'samesite' => 'Strict',\r\n        ]);\r\n\r\n        $cookieValue = $username . \"|\" . \"abc123ABC\" . \"|\" . $main_currency;\r\n        setcookie('wallos_login', $cookieValue, [\r\n            'expires' => $cookieExpire,\r\n            'samesite' => 'Strict',\r\n            'httponly' => true,\r\n        ]);\r\n\r\n        $db->close();\r\n        header(\"Location: .\");\r\n    }\r\n}\r\n\r\nif (isset($_SESSION['totp_user_id'])) {\r\n    unset($_SESSION['totp_user_id']);\r\n}\r\n\r\nif (isset($_SESSION['token'])) {\r\n    unset($_SESSION['token']);\r\n}\r\n\r\n\r\n$theme = \"light\";\r\n$updateThemeSettings = false;\r\nif (isset($_COOKIE['theme'])) {\r\n    $theme = $_COOKIE['theme'];\r\n} else {\r\n    $updateThemeSettings = true;\r\n}\r\n\r\n$colorTheme = \"blue\";\r\nif (isset($_COOKIE['colorTheme'])) {\r\n    $colorTheme = $_COOKIE['colorTheme'];\r\n}\r\n\r\n// Check if OIDC is Enabled\r\n$password_login_disabled = false;\r\n$oidcEnabled = false;\r\n$oidcQuery = \"SELECT oidc_oauth_enabled FROM admin\";\r\n$oidcResult = $db->query($oidcQuery);\r\n$oidcRow = $oidcResult->fetchArray(SQLITE3_ASSOC);\r\nif ($oidcRow) {\r\n    $oidcEnabled = $oidcRow['oidc_oauth_enabled'] == 1;\r\n    if ($oidcEnabled) {\r\n        // Fetch OIDC settings\r\n        $oidcSettingsQuery = \"SELECT * FROM oauth_settings WHERE id = 1\";\r\n        $oidcSettingsResult = $db->query($oidcSettingsQuery);\r\n        $oidcSettings = $oidcSettingsResult->fetchArray(SQLITE3_ASSOC);\r\n        if (!$oidcSettings) {\r\n            $oidcEnabled = false;\r\n        } else {\r\n            $oidc_name = $oidcSettings['name'] ?? '';\r\n            $password_login_disabled = $oidcSettings['password_login_disabled'] == 1;\r\n\r\n            // Generate a CSRF-protecting state string\r\n            if (session_status() === PHP_SESSION_NONE) {\r\n                session_start();\r\n            }\r\n            $state = bin2hex(random_bytes(16));\r\n            $_SESSION['oidc_state'] = $state;\r\n\r\n            // Build the OIDC authorization URL\r\n            $params = http_build_query([\r\n                'response_type' => 'code',\r\n                'client_id' => $oidcSettings['client_id'],\r\n                'redirect_uri' => $oidcSettings['redirect_url'],\r\n                'scope' => $oidcSettings['scopes'],\r\n                'state' => $state,\r\n            ]);\r\n\r\n            $oidc_auth_url = rtrim($oidcSettings['authorization_url'], '?') . '?' . $params;\r\n        }\r\n    }\r\n}\r\n\r\n$loginFailed = false;\r\n$hasSuccessMessage = (isset($_GET['validated']) && $_GET['validated'] == \"true\") || (isset($_GET['registered']) && $_GET['registered'] == true) ? true : false;\r\n$userEmailWaitingVerification = false;\r\nif (isset($_POST['username']) && isset($_POST['password'])) {\r\n    $username = $_POST['username'];\r\n    $password = $_POST['password'];\r\n    $rememberMe = isset($_POST['remember']) ? true : false;\r\n\r\n    $query = \"SELECT id, password, main_currency, language FROM user WHERE username = :username\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($row) {\r\n        $hashedPasswordFromDb = $row['password'];\r\n        $userId = $row['id'];\r\n        $main_currency = $row['main_currency'];\r\n        $language = $row['language'];\r\n        if (password_verify($password, $hashedPasswordFromDb)) {\r\n\r\n            // Check if the user is in the email_verification table\r\n            $query = \"SELECT 1 FROM email_verification WHERE user_id = :userId\";\r\n            $stmt = $db->prepare($query);\r\n            $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $stmt->execute();\r\n            $verificationMissing = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n            // Check if the user has 2fa enabled\r\n            $query = \"SELECT totp_enabled FROM user WHERE id = :userId\";\r\n            $stmt = $db->prepare($query);\r\n            $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $stmt->execute();\r\n            $totpEnabled = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n            if ($verificationMissing) {\r\n                $userEmailWaitingVerification = true;\r\n                $loginFailed = true;\r\n            } else {\r\n                if ($totpEnabled['totp_enabled'] == 1) {\r\n                    $_SESSION['totp_user_id'] = $userId;\r\n                    if ($rememberMe) {\r\n                        $_SESSION['pending_remember_me'] = true; // defer cookie until TOTP done\r\n                    }\r\n                    $db->close();\r\n                    header(\"Location: totp.php\");\r\n                    exit();\r\n                }\r\n\r\n                // No TOTP — safe to create remember-me token now\r\n                if ($rememberMe) {\r\n                    $token = bin2hex(random_bytes(32));\r\n                    $addLoginTokens = \"INSERT INTO login_tokens (user_id, token) VALUES (:userId, :token)\";\r\n                    $addLoginTokensStmt = $db->prepare($addLoginTokens);\r\n                    $addLoginTokensStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                    $addLoginTokensStmt->bindParam(':token', $token, SQLITE3_TEXT);\r\n                    $addLoginTokensStmt->execute();\r\n                    $_SESSION['token'] = $token;\r\n                    $cookieValue = $username . \"|\" . $token . \"|\" . $main_currency;\r\n                    setcookie('wallos_login', $cookieValue, [\r\n                        'expires' => $cookieExpire,\r\n                        'samesite' => 'Strict',\r\n                        'httponly' => true,\r\n                    ]);\r\n                }\r\n\r\n                $_SESSION['username'] = $username;\r\n                $_SESSION['loggedin'] = true;\r\n                $_SESSION['main_currency'] = $main_currency;\r\n                $_SESSION['userId'] = $userId;\r\n                setcookie('language', $language, [\r\n                    'expires' => $cookieExpire,\r\n                    'samesite' => 'Strict'\r\n                ]);\r\n\r\n                if (!isset($_COOKIE['sortOrder'])) {\r\n                    setcookie('sortOrder', 'next_payment', [\r\n                        'expires' => $cookieExpire,\r\n                        'samesite' => 'Strict'\r\n                    ]);\r\n                }\r\n\r\n                $query = \"SELECT color_theme FROM settings WHERE user_id = :userId\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                $result = $stmt->execute();\r\n                $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n                setcookie('colorTheme', $settings['color_theme'], [\r\n                    'expires' => $cookieExpire,\r\n                    'samesite' => 'Strict'\r\n                ]);\r\n\r\n                $db->close();\r\n                header(\"Location: .\");\r\n                exit();\r\n            }\r\n\r\n        } else {\r\n            $loginFailed = true;\r\n        }\r\n    } else {\r\n        $loginFailed = true;\r\n    }\r\n}\r\n\r\n//Check if registration is open\r\n$registrations = false;\r\n$resetPasswordEnabled = false;\r\nif (!$password_login_disabled) {\r\n    $adminQuery = \"SELECT registrations_open, max_users, server_url, smtp_address FROM admin\";\r\n    $adminResult = $db->query($adminQuery);\r\n    $adminRow = $adminResult->fetchArray(SQLITE3_ASSOC);\r\n    $registrationsOpen = $adminRow['registrations_open'];\r\n    $maxUsers = $adminRow['max_users'];\r\n\r\n    if ($registrationsOpen == 1 && $maxUsers == 0) {\r\n        $registrations = true;\r\n    } else if ($registrationsOpen == 1 && $maxUsers > 0) {\r\n        $userCountQuery = \"SELECT COUNT(id) as userCount FROM user\";\r\n        $userCountResult = $db->query($userCountQuery);\r\n        $userCountRow = $userCountResult->fetchArray(SQLITE3_ASSOC);\r\n        $userCount = $userCountRow['userCount'];\r\n        if ($userCount < $maxUsers) {\r\n            $registrations = true;\r\n        }\r\n    }\r\n\r\n    if ($adminRow['smtp_address'] != \"\" && $adminRow['server_url'] != \"\") {\r\n        $resetPasswordEnabled = true;\r\n    }\r\n}\r\n\r\n\r\nif (isset($_GET['error']) && $_GET['error'] == \"oidc_user_not_found\") {\r\n    $loginFailed = true;\r\n}\r\n\r\n?>\r\n<!DOCTYPE html>\r\n<html dir=\"<?= $languages[$lang]['dir'] ?>\">\r\n\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\r\n    <meta name=\"theme-color\" content=\"<?= $theme == \"light\" ? \"#FFFFFF\" : \"#222222\" ?>\" id=\"theme-color\" />\r\n    <meta name=\"apple-mobile-web-app-title\" content=\"Wallos\">\r\n    <title>Wallos - Subscription Tracker</title>\r\n    <link rel=\"icon\" type=\"image/png\" href=\"images/icon/favicon.ico\" sizes=\"16x16\">\r\n    <link rel=\"apple-touch-icon\" href=\"images/icon/apple-touch-icon.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"images/icon/apple-touch-icon-152.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"images/icon/apple-touch-icon-180.png\">\r\n    <link rel=\"manifest\" href=\"manifest.json\">\r\n    <link rel=\"stylesheet\" href=\"styles/theme.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/login.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/themes/red.css?<?= $version ?>\" id=\"red-theme\" <?= $colorTheme != \"red\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/green.css?<?= $version ?>\" id=\"green-theme\" <?= $colorTheme != \"green\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/yellow.css?<?= $version ?>\" id=\"yellow-theme\" <?= $colorTheme != \"yellow\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/purple.css?<?= $version ?>\" id=\"purple-theme\" <?= $colorTheme != \"purple\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/font-awesome.min.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/barlow.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/login-dark-theme.css?<?= $version ?>\" id=\"dark-theme\" <?= $theme == \"light\" ? \"disabled\" : \"\" ?>>\r\n    <script type=\"text/javascript\">\r\n        window.update_theme_settings = \"<?= $updateThemeSettings ?>\";\r\n        window.color_theme = \"<?= $colorTheme ?>\";\r\n    </script>\r\n    <script type=\"text/javascript\" src=\"scripts/login.js?<?= $version ?>\"></script>\r\n</head>\r\n\r\n<body class=\"<?= $languages[$lang]['dir'] ?>\">\r\n    <div class=\"content\">\r\n        <section class=\"container\">\r\n            <header>\r\n                <div class=\"logo-image\" title=\"Wallos - Subscription Tracker\">\r\n                    <?php include \"images/siteicons/svg/logo.php\"; ?>\r\n                </div>\r\n                <p>\r\n                    <?= translate('please_login', $i18n) ?>\r\n                </p>\r\n            </header>\r\n            <form action=\"login.php\" method=\"post\">\r\n                <?php if (!$password_login_disabled) { ?>\r\n                    <div class=\"form-group\">\r\n                        <label for=\"username\"><?= translate('username', $i18n) ?>:</label>\r\n                        <input type=\"text\" id=\"username\" name=\"username\" autocomplete=\"username\" required>\r\n                    </div>\r\n                    <div class=\"form-group\">\r\n                        <label for=\"password\"><?= translate('password', $i18n) ?>:</label>\r\n                        <input type=\"password\" id=\"password\" name=\"password\" autocomplete=\"current-password\" required>\r\n                    </div>\r\n                    <?php\r\n                    if (!$demoMode) {\r\n                        ?>\r\n                        <div class=\"form-group-inline\">\r\n                            <input type=\"checkbox\" id=\"remember\" name=\"remember\">\r\n                            <label for=\"remember\"><?= translate('stay_logged_in', $i18n) ?></label>\r\n                        </div>\r\n                        <?php\r\n                    }\r\n                    ?>\r\n                    <div class=\"form-group\">\r\n                        <input type=\"submit\" value=\"<?= translate('login', $i18n) ?>\">\r\n                    </div>\r\n                <?php } ?>\r\n                <div class=\"form-group\">\r\n                    <?php\r\n                    if ($oidcEnabled) {\r\n                        if (!$password_login_disabled) {\r\n                            ?>\r\n                            <span class=\"or-separator\"><?= translate('or', $i18n) ?></span>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                        <a class=\"button secondary-button\" href=\"<?= htmlspecialchars($oidc_auth_url) ?>\">\r\n                            <?= translate('login_with', $i18n) ?>     <?= htmlspecialchars($oidc_name) ?>\r\n                        </a>\r\n                        <?php\r\n                    }\r\n                    ?>\r\n                </div>\r\n                <?php\r\n                if ($loginFailed) {\r\n                    ?>\r\n                    <ul class=\"error-box\">\r\n                        <?php\r\n                        if ($userEmailWaitingVerification) {\r\n                            ?>\r\n                            <li><i\r\n                                    class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('user_email_waiting_verification', $i18n) ?>\r\n                            </li>\r\n                            <?php\r\n                        } else {\r\n                            ?>\r\n                            <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('login_failed', $i18n) ?></li>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                    </ul>\r\n                    <?php\r\n                }\r\n                if ($hasSuccessMessage) {\r\n                    ?>\r\n                    <ul class=\"success-box\">\r\n                        <?php\r\n                        if (isset($_GET['validated']) && $_GET['validated'] == \"true\") {\r\n                            ?>\r\n                            <li><i class=\"fa-solid fa-check\"></i><?= translate('email_verified', $i18n) ?></li>\r\n                            <?php\r\n                        } else if (isset($_GET['registered']) && $_GET['registered']) {\r\n                            ?>\r\n                                <li><i class=\"fa-solid fa-check\"></i><?= translate('registration_successful', $i18n) ?></li>\r\n                                <?php\r\n                                if (isset($_GET['requireValidation']) && $_GET['requireValidation'] == true) {\r\n                                    ?>\r\n                                    <li><?= translate('user_email_waiting_verification', $i18n) ?></li>\r\n                                <?php\r\n                                }\r\n                        }\r\n                        ?>\r\n                    </ul>\r\n                    <?php\r\n                }\r\n\r\n                if ($resetPasswordEnabled) {\r\n                    ?>\r\n                    <div class=\"login-form-link\">\r\n                        <a href=\"passwordreset.php\"><?= translate('forgot_password', $i18n) ?></a>\r\n                    </div>\r\n                    <?php\r\n                }\r\n                ?>\r\n                <?php\r\n                if ($registrations) {\r\n                    ?>\r\n                    <div class=\"separator\">\r\n                        <input type=\"button\" class=\"secondary-button\" onclick=\"openRegitrationPage()\"\r\n                            value=\"<?= translate('register', $i18n) ?>\"></input>\r\n                    </div>\r\n                    <?php\r\n                }\r\n                ?>\r\n            </form>\r\n        </section>\r\n    </div>\r\n    <script type=\"text/javascript\">\r\n        function openRegitrationPage() {\r\n            window.location.href = \"registration.php\";\r\n        }\r\n    </script>\r\n</body>\r\n\r\n</html>"
  },
  {
    "path": "logos.php",
    "content": "<!DOCTYPE html>\r\n<html>\r\n\r\n<head>\r\n    <title>Subscription Logos</title>\r\n    <script type=\"text/javascript\">\r\n        document.addEventListener(\"DOMContentLoaded\", function () {\r\n            const searchForm = document.getElementById(\"search-form\");\r\n            const imageResults = document.getElementById(\"image-results\");\r\n\r\n            searchForm.addEventListener(\"submit\", function (e) {\r\n                e.preventDefault();\r\n\r\n                const searchTerm = document.getElementById(\"search\").value.trim();\r\n                if (searchTerm === \"\") {\r\n                    alert(\"Please enter a search term.\");\r\n                    return;\r\n                }\r\n\r\n                // Use the proxy to perform a Google image search\r\n                const proxyUrl = `endpoints/logos/search.php?search=${searchTerm}`;\r\n\r\n                // Send an AJAX request to the proxy\r\n                fetch(proxyUrl)\r\n                    .then(response => response.json())\r\n                    .then(data => {\r\n                        if (data.imageUrls) {\r\n                            // Display the image sources from the PHP response.\r\n                            displayImageResults(data.imageUrls);\r\n                        } else if (data.error) {\r\n                            console.error(data.error);\r\n                        }\r\n                    })\r\n                    .catch(error => {\r\n                        console.error(\"Error fetching image results:\", error);\r\n                    });\r\n            });\r\n\r\n            function displayImageResults(imageSources) {\r\n                // Clear previous results\r\n                imageResults.innerHTML = \"\";\r\n\r\n                // Display the image sources as image elements\r\n                imageSources.forEach(src => {\r\n                    const img = document.createElement(\"img\");\r\n                    img.src = src;\r\n                    imageResults.appendChild(img);\r\n                });\r\n            }\r\n        });\r\n    </script>\r\n</head>\r\n\r\n<body>\r\n    <form id=\"search-form\">\r\n        <input type=\"text\" name=\"search\" id=\"search\" autocomplete=\"off\">\r\n        <input type=\"submit\" value=\"Search\">\r\n    </form>\r\n    <div id=\"image-results\">\r\n        <!-- Image results will be displayed here -->\r\n    </div>\r\n</body>\r\n\r\n</html>"
  },
  {
    "path": "logout.php",
    "content": "<?php\r\nrequire_once 'includes/connect.php';\r\nsession_start();\r\n\r\n$logoutOIDC = false;\r\n\r\n// Check if user is logged in with OIDC\r\nif (isset($_SESSION['from_oidc']) && $_SESSION['from_oidc'] === true) {\r\n    $logoutOIDC = true;\r\n    // get OIDC settings\r\n    $stmt = $db->prepare('SELECT * FROM oauth_settings WHERE id = 1');\r\n    $result = $stmt->execute();\r\n    $oidcSettings = $result->fetchArray(SQLITE3_ASSOC);\r\n    $logoutUrl = $oidcSettings['logout_url'] ?? '';\r\n}\r\n\r\n// get token from cookie to remove from DB\r\nif (isset($_SESSION['token'])) {\r\n    $token = $_SESSION['token'];\r\n    $sql = \"DELETE FROM login_tokens WHERE token = :token AND user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindParam(':token', $token, SQLITE3_TEXT);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $stmt->execute();\r\n}\r\n$_SESSION = array();\r\nsession_destroy();\r\n$cookieExpire = time() - 3600;\r\nsetcookie('wallos_login', '', $cookieExpire);\r\n$db->close();\r\n\r\nif ($logoutOIDC && !empty($logoutUrl)) {\r\n    $returnTo = urlencode($oidcSettings['redirect_url'] ?? '');\r\n    header(\"Location: $logoutUrl?post_logout_redirect_uri=$returnTo\");\r\n    exit();\r\n}\r\n\r\n?>\r\n<!DOCTYPE html>\r\n<html>\r\n<head>\r\n<script>\r\n  async function clearAndRedirect() {\r\n    if ('caches' in window) {\r\n      await caches.delete('pages-cache-v1');\r\n    }\r\n    sessionStorage.removeItem('sw_prefetched');\r\n    window.location.href = '.';\r\n  }\r\n  clearAndRedirect();\r\n</script>\r\n</head>\r\n<body></body>\r\n</html>\r\n<?php\r\nexit();"
  },
  {
    "path": "manifest.json",
    "content": "{\r\n  \"short_name\": \"Wallos\",\r\n  \"name\": \"Wallos - Subscription Tracker\",\r\n  \"icons\": [\r\n    {\r\n      \"src\": \"images/icon/android-chrome-192x192.png\",\r\n      \"type\": \"image/png\",\r\n      \"sizes\": \"192x192\"\r\n    },\r\n    {\r\n      \"src\": \"images/icon/android-chrome-512x512.png\",\r\n      \"type\": \"image/png\",\r\n      \"sizes\": \"512x512\"\r\n    },\r\n    {\r\n      \"src\": \"images/icon/maskable_icon_x192.png\",\r\n      \"type\": \"image/png\",\r\n      \"sizes\": \"192x192\",\r\n      \"purpose\": \"maskable\"\r\n    },\r\n    {\r\n      \"src\": \"images/icon/maskable_icon_x512.png\",\r\n      \"type\": \"image/png\",\r\n      \"sizes\": \"512x512\",\r\n      \"purpose\": \"maskable\"\r\n    }\r\n  ],\r\n  \"start_url\": \"/\",\r\n  \"id\": \"com.wallos.app\",\r\n  \"shortcuts\": [\r\n    {\r\n      \"name\": \"Dashboard\",\r\n      \"short_name\": \"Dashboard\",\r\n      \"description\": \"View your dashboard\",\r\n      \"url\": \"index.php\",\r\n      \"icons\": [\r\n        {\r\n          \"src\": \"images/siteicons/pwa/dashboard.png\",\r\n          \"sizes\": \"96x96\"\r\n        }\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"Subscriptions\",\r\n      \"short_name\": \"Subscriptions\",\r\n      \"description\": \"View your subscriptions\",\r\n      \"url\": \"subscriptions.php\",\r\n      \"icons\": [\r\n        {\r\n          \"src\": \"images/siteicons/pwa/subscriptions.png\",\r\n          \"sizes\": \"96x96\"\r\n        }\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"Calendar\",\r\n      \"short_name\": \"Calendar\",\r\n      \"description\": \"View your calendar\",\r\n      \"url\": \"calendar.php\",\r\n      \"icons\": [\r\n        {\r\n          \"src\": \"images/siteicons/pwa/calendar.png\",\r\n          \"sizes\": \"96x96\"\r\n        }\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"Stats\",\r\n      \"short_name\": \"Stats\",\r\n      \"description\": \"View your statistics\",\r\n      \"url\": \"stats.php\",\r\n      \"icons\": [\r\n        {\r\n          \"src\": \"images/siteicons/pwa/stats.png\",\r\n          \"sizes\": \"96x96\"\r\n        }\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"Settings\",\r\n      \"short_name\": \"Settings\",\r\n      \"description\": \"Change your settings\",\r\n      \"url\": \"settings.php\",\r\n      \"icons\": [\r\n        {\r\n          \"src\": \"images/siteicons/pwa/settings.png\",\r\n          \"sizes\": \"96x96\"\r\n        }\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"About\",\r\n      \"short_name\": \"About\",\r\n      \"description\": \"More info about Wallos\",\r\n      \"url\": \"about.php\",\r\n      \"icons\": [\r\n        {\r\n          \"src\": \"images/siteicons/pwa/about.png\",\r\n          \"sizes\": \"96x96\"\r\n        }\r\n      ]\r\n    }\r\n  ],\r\n  \"screenshots\": [\r\n    {\r\n      \"src\": \"images/screenshots/desktop.png\",\r\n      \"sizes\": \"1000x750\",\r\n      \"type\": \"image/png\",\r\n      \"form_factor\": \"wide\"\r\n    },\r\n    {\r\n      \"src\": \"images/screenshots/mobile.png\",\r\n      \"sizes\": \"600x1000\",\r\n      \"type\": \"image/png\"\r\n    }\r\n  ],\r\n  \"background_color\": \"#FFFFFF\",\r\n  \"display\": \"standalone\",\r\n  \"scope\": \"/\",\r\n  \"theme_color\": \"#FFFFFF\",\r\n  \"description\": \"Wallos is a personal subscription tracker that helps you keep track of your subscriptions and save money.\",\r\n  \"orientation\": \"portrait-primary\",\r\n  \"display_override\": [\r\n    \"window-controls-overlay\"\r\n  ]\r\n}"
  },
  {
    "path": "migrations/000001.php",
    "content": "<?php\n\n$db->exec('CREATE TABLE IF NOT EXISTS migrations (\n    id INTEGER PRIMARY KEY,\n    migration TEXT NOT NULL,\n    migrated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n)');\n"
  },
  {
    "path": "migrations/000002.php",
    "content": "<?php\n// This migration adds an \"enabled\" column to the payment_methods table and sets all values to 1.\n// It allows the user to disable payment methods without deleting them.\n\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('payment_methods') where name='enabled'\");\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\n\nif ($columnRequired) {\n    $db->exec('ALTER TABLE payment_methods ADD COLUMN enabled BOOLEAN DEFAULT 1');\n    $db->exec('UPDATE payment_methods SET enabled = 1');\n}\n"
  },
  {
    "path": "migrations/000003.php",
    "content": "<?php\r\n// This migration adds a \"from_email\" column to the notifications table.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('notifications') where name='from_email'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE notifications ADD COLUMN from_email VARCHAR(255);');\r\n}\r\n"
  },
  {
    "path": "migrations/000004.php",
    "content": "<?php\r\n    // This migration adds a URL column to the subscriptions table.\r\n\r\n    $columnQuery = $db->query(\"SELECT * FROM pragma_table_info('subscriptions') where name='url'\");\r\n    $columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\n    if ($columnRequired) {\r\n        $db->exec('ALTER TABLE subscriptions ADD COLUMN url VARCHAR(255);');\r\n    }\r\n\r\n?>"
  },
  {
    "path": "migrations/000005.php",
    "content": "<?php\r\n// This migration adds a \"language\" column to the user table and sets all values to english.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('user') where name='language'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE user ADD COLUMN language TEXT DEFAULT \"en\"');\r\n    $db->exec('UPDATE user SET language = \"en\"');\r\n}\r\n"
  },
  {
    "path": "migrations/000006.php",
    "content": "<?php\r\n// This migration adds a \"provider\" column to the fixer table and sets all values to 0.\r\n// It allows the user to chose a different provider for their fixer api keys.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('fixer') where name='provider'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE fixer ADD COLUMN provider INT DEFAULT 0');\r\n    $db->exec('UPDATE fixer SET provider = 0');\r\n}"
  },
  {
    "path": "migrations/000007.php",
    "content": "<?php\r\n// This migration adds a new table to store the display and experimental settings\r\n// This settings will now be persisted across sessions and devices\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS settings (\r\n    dark_theme BOOLEAN DEFAULT 0,\r\n    monthly_price BOOLEAN DEFAULT 0,\r\n    convert_currency BOOLEAN DEFAULT 0,\r\n    remove_background BOOLEAN DEFAULT 0\r\n)');\r\n\r\n\r\n$db->exec('INSERT INTO settings (dark_theme, monthly_price, convert_currency, remove_background) VALUES (0, 0, 0, 0)');\r\n\r\n"
  },
  {
    "path": "migrations/000008.php",
    "content": "<?php\r\n// This migration adds a \"activated\" column to the subscriptions table and sets all values to true.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('subscriptions') WHERE name='inactive'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE subscriptions ADD COLUMN inactive BOOLEAN DEFAULT false');\r\n    $db->exec('UPDATE subscriptions SET inactive = false');\r\n}\r\n"
  },
  {
    "path": "migrations/000009.php",
    "content": "<?php\r\n// This migration adds an \"email\" column to the members table.\r\n// It allows the household member to receive notifications when their subscriptions are about to expire.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('household') where name='email'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE household ADD COLUMN email TEXT DEFAULT \"\"');\r\n}"
  },
  {
    "path": "migrations/000010.php",
    "content": "<?php\r\n    // This migration adds a \"order\" column to the categories table so that they can be sorted and initializes all values to their id.\r\n\r\n    $columnQuery = $db->query(\"SELECT * FROM pragma_table_info('categories') WHERE name='order'\");\r\n    $columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\n    if ($columnRequired) {\r\n        $db->exec('ALTER TABLE categories ADD COLUMN `order` INTEGER DEFAULT 0');\r\n        $db->exec('UPDATE categories SET `order` = id');\r\n    }\r\n\r\n\r\n?>"
  },
  {
    "path": "migrations/000011.php",
    "content": "<?php\r\n    // This migration adds a \"order\" column to the payment_methods table so that they can be sorted and initializes all values to their id.\r\n\r\n    $columnQuery = $db->query(\"SELECT * FROM pragma_table_info('payment_methods') WHERE name='order'\");\r\n    $columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\n    if ($columnRequired) {\r\n        $db->exec('ALTER TABLE payment_methods ADD COLUMN `order` INTEGER DEFAULT 0');\r\n        $db->exec('UPDATE payment_methods SET `order` = id');\r\n    }\r\n\r\n\r\n?>"
  },
  {
    "path": "migrations/000012.php",
    "content": "<?php\r\n    // This migration adds a \"encryption\" column to the notifications table so that the encryption type can be stored.\r\n\r\n    $columnQuery = $db->query(\"SELECT * FROM pragma_table_info('notifications') WHERE name='encryption'\");\r\n    $columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\n    if ($columnRequired) {\r\n        $db->exec('ALTER TABLE notifications ADD COLUMN `encryption` TEXT DEFAULT \"tls\"');\r\n        $db->exec('UPDATE notifications SET `encryption` = \"tls\"');\r\n    }\r\n?>"
  },
  {
    "path": "migrations/000013.php",
    "content": "<?php\r\n\r\n/**\r\n * This migration script updates the avatar field of the user table to use the new avatar path.\r\n */\r\n\r\n$sql = \"SELECT avatar FROM user\";\r\n$stmt = $db->prepare($sql);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($row) {\r\n    $avatar = $row['avatar'];\r\n\r\n    if (strlen($avatar) < 2) {\r\n        $avatarFullPath = \"images/avatars/\" . $avatar . \".svg\";\r\n        $sql = \"UPDATE user SET avatar = :avatarFullPath\";\r\n        $stmt = $db->prepare($sql);\r\n        $stmt->bindValue(':avatarFullPath', $avatarFullPath, SQLITE3_TEXT);\r\n        $stmt->execute();\r\n    }\r\n}\r\n\r\n?>"
  },
  {
    "path": "migrations/000014.php",
    "content": "<?php\r\n// This migration adds a \"color_theme\" column to the settings table and sets it to blue as default.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('settings') where name='color_theme'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec(\"ALTER TABLE settings ADD COLUMN color_theme TEXT DEFAULT 'blue'\");\r\n    $db->exec('UPDATE settings SET `color_theme` = \"blue\"');\r\n}\r\n\r\n// This migrations adds custom_colors table to the database, so the user can set custom accent colors to the application\r\n\r\n$customColorsTableQuery = $db->query(\"SELECT * FROM sqlite_master WHERE type='table' AND name='custom_colors'\");\r\n$customColorsTableRequired = $customColorsTableQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($customColorsTableRequired) {\r\n    $db->exec(\"CREATE TABLE custom_colors (\r\n        main_color TEXT NOT NULL,\r\n        accent_color TEXT NOT NULL,\r\n        hover_color TEXT NOT NULL\r\n    )\");\r\n}\r\n\r\n"
  },
  {
    "path": "migrations/000015.php",
    "content": "<?php\r\n// This migration adds a \"hide_disabled\" column to the settings table and sets to false as default.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('settings') where name='hide_disabled'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec(\"ALTER TABLE settings ADD COLUMN hide_disabled BOOLEAN DEFAULT 0\");\r\n    $db->exec('UPDATE settings SET `hide_disabled` = 0');\r\n}"
  },
  {
    "path": "migrations/000016.php",
    "content": "<?php\r\n\r\n/* \r\n* This migration adds tables to store the date about the new notification methods (telegram, webhooks and gotify)\r\n* Existing values on the notifications table will be split and migrated to the new tables.\r\n*/\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS telegram_notifications (\r\n    enabled BOOLEAN DEFAULT 0,\r\n    bot_token TEXT DEFAULT \"\",\r\n    chat_id TEXT DEFAULT \"\"\r\n)');\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS webhook_notifications (\r\n    enabled BOOLEAN DEFAULT 0,\r\n    headers TEXT DEFAULT \"\",\r\n    url TEXT DEFAULT \"\",\r\n    request_method TEXT DEFAULT \"POST\",\r\n    payload TEXT DEFAULT \"\",\r\n    iterator TEXT DEFAULT \"\"\r\n)');\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS gotify_notifications (\r\n    enabled BOOLEAN DEFAULT 0,\r\n    url TEXT DEFAULT \"\",\r\n    token TEXT DEFAULT \"\"\r\n)');\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS email_notifications (\r\n    enabled BOOLEAN DEFAULT 0,\r\n    smtp_address TEXT DEFAULT \"\",\r\n    smtp_port INTEGER DEFAULT 587,\r\n    smtp_username TEXT DEFAULT \"\",\r\n    smtp_password TEXT DEFAULT \"\",\r\n    from_email TEXT DEFAULT \"\",\r\n    encryption TEXT DEFAULT \"tls\"\r\n)');\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS notification_settings (\r\n    days INTEGER DEFAULT 0\r\n)');\r\n\r\n// Check if old email notifications table has data and migrate it\r\n$result = $db->query('SELECT COUNT(*) as count FROM notifications');\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($row['count'] > 0) {\r\n    // Copy data from notifications to email_notifications\r\n    $db->exec('INSERT INTO email_notifications (enabled, smtp_address, smtp_port, smtp_username, smtp_password, from_email, encryption)\r\n               SELECT enabled, smtp_address, smtp_port, smtp_username, smtp_password, from_email, encryption FROM notifications');\r\n\r\n    // Copy data from notifications to notification_settings\r\n    $db->exec('INSERT INTO notification_settings (days)\r\n               SELECT days FROM notifications');\r\n\r\n    if ($db->changes() > 0) {\r\n        $db->exec('DROP TABLE IF EXISTS notifications');\r\n    }\r\n} else {\r\n    $db->exec('DROP TABLE IF EXISTS notifications');\r\n}\r\n\r\n?>"
  },
  {
    "path": "migrations/000017.php",
    "content": "<?php\r\n\r\n/* \r\n* This migration adds tables to store the date about the new notification methods (pushover and discord)\r\n*/\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS pushover_notifications (\r\n    enabled BOOLEAN DEFAULT 0,\r\n    user_key TEXT DEFAULT \"\",\r\n    token TEXT DEFAULT \"\"\r\n)');\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS discord_notifications (\r\n    enabled BOOLEAN DEFAULT 0,\r\n    webhook_url TEXT DEFAULT \"\",\r\n    bot_username TEXT DEFAULT \"\",\r\n    bot_avatar_url TEXT DEFAULT \"\"  \r\n)');"
  },
  {
    "path": "migrations/000018.php",
    "content": "<?php\r\n\r\n/*\r\nThis migration adds a column to the users table to store a monthly budget that will be used to calculate statistics\r\n*/\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('users') where name='budget'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE user ADD COLUMN budget INTEGER DEFAULT 0');\r\n}\r\n\r\n?>"
  },
  {
    "path": "migrations/000019.php",
    "content": "<?php\r\n/*\r\nThis migration adds a column to the subscriptuons table to store individual choice for how many days before the subscription is up for payment to notify the user\r\nThe default value of 0 means global settings will be used\r\n*/\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('subscriptions') where name='notify_days_before'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE subscriptions ADD COLUMN notify_days_before INTEGER DEFAULT 0');\r\n}\r\n\r\n?>"
  },
  {
    "path": "migrations/000020.php",
    "content": "<?php\r\n/*\r\n/ This migration adds user_id foreign key to all the relevant tables, to allow for multiple users.\r\n/ It also creates the admin table to store the admin settings.\r\n*/\r\n\r\n$tablesToUpdate = ['payment_methods', 'subscriptions', 'categories', 'currencies', 'fixer', 'household', 'settings', 'custom_colors', 'notification_settings', 'telegram_notifications', 'webhook_notifications', 'gotify_notifications', 'email_notifications', 'pushover_notifications', 'discord_notifications', 'last_exchange_update'];\r\nforeach ($tablesToUpdate as $table) {\r\n    $columnQuery = $db->query(\"SELECT * FROM pragma_table_info('$table') WHERE name='user_id'\");\r\n    $columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\n    if ($columnRequired) {\r\n        $db->exec(\"ALTER TABLE $table ADD COLUMN user_id INTEGER DEFAULT 1\");\r\n    }\r\n}\r\n\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS admin (\r\n    id INTEGER PRIMARY KEY,\r\n    registrations_open BOOLEAN DEFAULT 0,\r\n    max_users INTEGER DEFAULT 0,\r\n    require_email_verification BOOLEAN DEFAULT 0,\r\n    server_url TEXT,\r\n    smtp_address TEXT,\r\n    smtp_port INTEGER DEFAULT 587,\r\n    smtp_username TEXT,\r\n    smtp_password TEXT,\r\n    from_email TEXT,\r\n    encryption TEXT DEFAULT \"tls\"\r\n)');\r\n\r\n$db->exec('INSERT INTO admin (id, registrations_open, require_email_verification, server_url, max_users, smtp_address, smtp_port, smtp_username, smtp_password, from_email, encryption) VALUES (1, 0, 0, \"\", 0, \"\", 587, \"\", \"\", \"\", \"tls\")');\r\n\r\n$updateQuery = \"UPDATE payment_methods SET icon = 'images/uploads/icons/' || icon WHERE id < 32 AND icon NOT LIKE '%/images/uploads/icons%'\";\r\n$db->exec($updateQuery);\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS email_verification (\r\n    id INTEGER PRIMARY KEY,\r\n    user_id INTEGER,\r\n    email TEXT,\r\n    token TEXT,\r\n    email_sent BOOLEAN DEFAULT 0)');\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS password_resets (\r\n    id INTEGER PRIMARY KEY,\r\n    user_id INTEGER,\r\n    email TEXT,\r\n    token TEXT,\r\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\r\n    email_sent BOOLEAN DEFAULT 0)');\r\n\r\n?>"
  },
  {
    "path": "migrations/000021.php",
    "content": "<?php\r\n\r\n/* \r\n* This migration adds tables to store the data about a new notification method Ntfy\r\n*/\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS ntfy_notifications (\r\n    enabled BOOLEAN DEFAULT 0,\r\n    host TEXT DEFAULT \"\",\r\n    topic TEXT DEFAULT \"\",\r\n    headers TEXT DEFAULT \"\",\r\n    user_id INTEGER,\r\n    FOREIGN KEY (user_id) REFERENCES user(id)\r\n)');"
  },
  {
    "path": "migrations/000022.php",
    "content": "<?php\r\n\r\n/*\r\nThis migration adds a column to the admin table to enable the option to disable login\r\n*/\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('admin') where name='login_disabled'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE admin ADD COLUMN login_disabled BOOLEAN DEFAULT 0');\r\n}\r\n\r\n?>"
  },
  {
    "path": "migrations/000023.php",
    "content": "<?php\r\n\r\n/* \r\n* This migration adds a table to store custom css styles per user\r\n*/\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS custom_css_style (\r\n    css TEXT DEFAULT \"\",\r\n    user_id INTEGER,\r\n    FOREIGN KEY (user_id) REFERENCES user(id)\r\n)');"
  },
  {
    "path": "migrations/000024.php",
    "content": "<?php\r\n// This migration adds a \"cancellation_date\" column to the subscriptions table.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('subscriptions') where name='cancellation_date'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE subscriptions ADD COLUMN cancellation_date DATE;');\r\n}"
  },
  {
    "path": "migrations/000025.php",
    "content": "<?php\r\n// This migration adds a \"disabled_to_bottom\" column to the settings table.\r\n// This magration also adds a latest_version and update_notification columns to the admin table.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('settings') where name='disabled_to_bottom'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE settings ADD COLUMN disabled_to_bottom BOOLEAN DEFAULT 0');\r\n}\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('admin') where name='latest_version'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec(\"ALTER TABLE admin ADD COLUMN latest_version TEXT DEFAULT 'v2.21.1'\");\r\n}\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('admin') where name='update_notification'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE admin ADD COLUMN update_notification BOOLEAN DEFAULT 0');\r\n}"
  },
  {
    "path": "migrations/000026.php",
    "content": "<?php\n// This migration adds a \"other_emails\" column to the email_notifications table.\n// It also adds a \"show_original_price\" column to the settings table.\n\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('email_notifications') where name='other_emails'\");\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\n\nif ($columnRequired) {\n    $db->exec('ALTER TABLE email_notifications ADD COLUMN other_emails TEXT DEFAULT \"\";');\n}\n\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('settings') where name='show_original_price'\");\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\n\nif ($columnRequired) {\n    $db->exec('ALTER TABLE settings ADD COLUMN show_original_price BOOLEAN DEFAULT 0');\n}"
  },
  {
    "path": "migrations/000027.php",
    "content": "<?php\r\n\r\n// this migration adds a \"totp_enabled\" column to the user table\r\n// it also adds a \"totp\" table to the database\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('user') where name='totp_enabled'\");\r\n\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE user ADD COLUMN totp_enabled BOOLEAN DEFAULT 0');\r\n}\r\n\r\n$db->exec('CREATE TABLE IF NOT EXISTS totp (\r\n    user_id INTEGER NOT NULL,\r\n    totp_secret TEXT NOT NULL,\r\n    backup_codes TEXT NOT NULL,\r\n    last_totp_used INTEGER DEFAULT 0,\r\n    FOREIGN KEY(user_id) REFERENCES user(id)\r\n)');"
  },
  {
    "path": "migrations/000028.php",
    "content": "<?php\r\n\r\n// This migration adds a \"mobile_nav\" column to the settings table\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('settings') where name='mobile_nav'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE settings ADD COLUMN mobile_nav BOOLEAN DEFAULT 0');\r\n}"
  },
  {
    "path": "migrations/000029.php",
    "content": "<?php\r\n\r\n// This migration adds a \"api_key\" column to the user table\r\n// It also generates an API key for each user\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('user') where name='api_key'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE user ADD COLUMN api_key TEXT');\r\n}\r\n\r\n$users = $db->query('SELECT * FROM user');\r\nwhile ($user = $users->fetchArray(SQLITE3_ASSOC)) {\r\n    if (empty($user['api_key'])) {\r\n        $apiKey = bin2hex(random_bytes(32));\r\n        $db->exec('UPDATE user SET api_key = \"' . $apiKey . '\" WHERE id = ' . $user['id']);\r\n    }\r\n}\r\n"
  },
  {
    "path": "migrations/000030.php",
    "content": "<?php\r\n// This migration adds a \"ignore_ssl\" column to the webhook_notifications, ntfy_notifications, and gotify_notifications tables\r\n// This is to allow users to ignore SSL certificate errors when sending notifications \r\n// This is useful for self-signed certificates or other cases where the SSL certificate is not valid\r\n\r\n// Add the ignore_ssl column to the webhook_notifications table\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('webhook_notifications') where name='ignore_ssl'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE webhook_notifications ADD COLUMN ignore_ssl INTEGER DEFAULT 0');\r\n}\r\n\r\n// Add the ignore_ssl column to the ntfy_notifications table\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('ntfy_notifications') where name='ignore_ssl'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE ntfy_notifications ADD COLUMN ignore_ssl INTEGER DEFAULT 0');\r\n}\r\n\r\n// Add the ignore_ssl column to the gotify_notifications table\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('gotify_notifications') where name='ignore_ssl'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE gotify_notifications ADD COLUMN ignore_ssl INTEGER DEFAULT 0');\r\n}\r\n\r\n\r\n\r\n"
  },
  {
    "path": "migrations/000031.php",
    "content": "<?php\r\n// This migration adds a \"replacement_subscription_id\" column to the subscriptions table\r\n// to allow users to track savings by replacing one subscription with another\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('subscriptions') where name='replacement_subscription_id'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE subscriptions ADD COLUMN replacement_subscription_id INTEGER DEFAULT NULL');\r\n}\r\n\r\n\r\n?>"
  },
  {
    "path": "migrations/000032.php",
    "content": "<?php\r\n// This migration adds a total_yearly_cost table to store the total yearly cost of all subscriptions over time\r\n// This migration adds a start_date column to the subscriptions table to store the start date of the subscription\r\n// This migration adds a auto_renew column to the subscriptions table to store if the subscription renews automatically or needs manual renewal\r\n\r\n$tableQuery = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='total_yearly_cost'\");\r\n$tableRequired = $tableQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($tableRequired) {\r\n    $db->exec('CREATE TABLE total_yearly_cost (\r\n        id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n        user_id INTEGER NOT NULL,\r\n        date INTEGER NOT NULL,\r\n        cost REAL NOT NULL,\r\n        currency TEXT NOT NULL\r\n    )');\r\n}\r\n\r\n$columnQuery = $db->query(\"PRAGMA table_info(subscriptions)\");\r\n$columns = [];\r\nwhile ($column = $columnQuery->fetchArray(SQLITE3_ASSOC)) {\r\n    $columns[] = $column['name'];\r\n}\r\n\r\nif (!in_array('start_date', $columns)) {\r\n    $db->exec('ALTER TABLE subscriptions ADD COLUMN start_date INTEGER DEFAULT NULL');\r\n}\r\n\r\nif (!in_array('auto_renew', $columns)) {\r\n    $db->exec('ALTER TABLE subscriptions ADD COLUMN auto_renew INTEGER DEFAULT 1');\r\n}\r\n\r\n?>"
  },
  {
    "path": "migrations/000033.php",
    "content": "<?php\r\n// This migration adds a \"show_subscription_progress\" column to the settings table and sets to false as default.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('settings') where name='show_subscription_progress'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec(\"ALTER TABLE settings ADD COLUMN show_subscription_progress BOOLEAN DEFAULT 0\");\r\n    $db->exec('UPDATE settings SET `show_subscription_progress` = 0');\r\n}"
  },
  {
    "path": "migrations/000034.php",
    "content": "<?php\r\n// This migration changes all the notify_days_before from 0 to -1 to to support the added \"on the day\" option\r\n\r\n$db->exec('UPDATE subscriptions SET `notify_days_before` = -1 WHERE `notify_days_before` = 0');"
  },
  {
    "path": "migrations/000035.php",
    "content": "<?php\r\n// This migration clears the total_yearly_cost table as the calculation up to this point was incorrect\r\n\r\n$db->exec('DELETE FROM total_yearly_cost');\r\n"
  },
  {
    "path": "migrations/000036.php",
    "content": "<?php\r\n\r\n// This migration adds a new column to the webhook_notifications table to store the cancelation payload \r\n// Also removes the iterator column as it is not used anymore.\r\n// The cancelation payload will be used to send cancelation notifications to the webhook\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('webhook_notifications') where name='cancelation_payload'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec(\"ALTER TABLE webhook_notifications ADD COLUMN cancelation_payload TEXT DEFAULT ''\");\r\n}\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('webhook_notifications') where name='iterator'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) !== false;\r\nif ($columnRequired) {\r\n    $db->exec(\"ALTER TABLE webhook_notifications DROP COLUMN iterator\");\r\n}\r\n"
  },
  {
    "path": "migrations/000037.php",
    "content": "<?php\r\n// This migration adds \"firstname\" and \"lastname\" columns to the user table\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('user') where name='firstname'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE user ADD COLUMN firstname TEXT DEFAULT \"\"');\r\n}\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('user') where name='lastname'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE user ADD COLUMN lastname TEXT DEFAULT \"\"');\r\n}\r\n"
  },
  {
    "path": "migrations/000038.php",
    "content": "<?php\r\n// This migration adds a \"oidc_oauth_enabled\" colum to the \"admin\" table\r\n// It also adds a \"oidc_sub\" column to the \"user\" table \r\n// It also adds a \"oauth_settings\" table to store OAuth settings.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('admin') where name='oidc_oauth_enabled'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE admin ADD COLUMN oidc_oauth_enabled INTEGER DEFAULT 0');\r\n}\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('user') where name='oidc_sub'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\nif ($columnRequired) {\r\n    $db->exec('ALTER TABLE user ADD COLUMN oidc_sub TEXT');\r\n}\r\n\r\n\r\n$tableQuery = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='oauth_settings'\");\r\n$tableExists = $tableQuery->fetchArray(SQLITE3_ASSOC);\r\n\r\nif (!$tableExists) {\r\n    $db->exec(\"CREATE TABLE oauth_settings (\r\n        id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n        name TEXT NOT NULL,\r\n        client_id TEXT NOT NULL,\r\n        client_secret TEXT NOT NULL,\r\n        authorization_url TEXT NOT NULL,\r\n        token_url TEXT NOT NULL,\r\n        user_info_url TEXT NOT NULL,\r\n        redirect_url TEXT NOT NULL,\r\n        logout_url TEXT,\r\n        user_identifier_field TEXT NOT NULL DEFAULT 'sub',\r\n        scopes TEXT NOT NULL DEFAULT 'openid email profile',\r\n        auth_style TEXT DEFAULT 'auto',\r\n        auto_create_user INTEGER DEFAULT 0,\r\n        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\r\n        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\r\n    )\");\r\n}"
  },
  {
    "path": "migrations/000039.php",
    "content": "<?php\r\n// This migration adds a \"password_login_disabled\" column to the \"oauth_settings\" table\r\n// This migration also adds a \"ai_settings\" table to store AI settings.\r\n// This migration also adds a \"ai_recommendations\" table to store AI recommendations.\r\n\r\n$columnQuery = $db->query(\"SELECT * FROM pragma_table_info('oauth_settings') WHERE name='password_login_disabled'\");\r\n$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;\r\n\r\nif ($columnRequired) {\r\n    $db->exec(\"ALTER TABLE oauth_settings ADD COLUMN password_login_disabled INTEGER DEFAULT 0\");\r\n} \r\n\r\n// Check if ai_settings table exists, if not, create it\r\n$tableQuery = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='ai_settings'\");\r\n$tableExists = $tableQuery->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($tableExists === false) {\r\n    $db->exec(\"\r\n        CREATE TABLE ai_settings (\r\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n            user_id INTEGER NOT NULL,\r\n            type TEXT NOT NULL,\r\n            enabled BOOLEAN NOT NULL DEFAULT 0,\r\n            api_key TEXT,\r\n            model TEXT NOT NULL,\r\n            url TEXT,\r\n            run_schedule TEXT NOT NULL DEFAULT 'manual',\r\n            last_successful_run DATETIME,\r\n            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\r\n            updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\r\n        );\r\n    \");\r\n}\r\n\r\n// Check if ai_recommendations table exists, if not, create it\r\n$tableQuery = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='ai_recommendations'\");\r\n$tableExists = $tableQuery->fetchArray(SQLITE3_ASSOC);\r\n\r\nif ($tableExists === false) {\r\n    $db->exec(\"\r\n        CREATE TABLE ai_recommendations (\r\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n            user_id INTEGER NOT NULL,\r\n            type TEXT NOT NULL,\r\n            title TEXT NOT NULL,\r\n            description TEXT NOT NULL,\r\n            savings TEXT NOT NULL DEFAULT '',\r\n            created_at DATETIME DEFAULT CURRENT_TIMESTAMP\r\n        );\r\n    \");\r\n}"
  },
  {
    "path": "migrations/000040.php",
    "content": "<?php\r\n// This migration adds a pushplus_notifications table to store PushPlus notification settings.\r\n\r\n$tableQuery = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='pushplus_notifications'\");\r\n$tableExists = $tableQuery->fetchArray(SQLITE3_ASSOC);\r\nif ($tableExists === false) {\r\n    $db->exec(\"\r\n        CREATE TABLE pushplus_notifications (\r\n            enabled INTEGER NOT NULL DEFAULT 0,\r\n            token TEXT,\r\n            user_id INTEGER\r\n        );\r\n    \");\r\n}"
  },
  {
    "path": "migrations/000041.php",
    "content": "<?php\r\n// This migration adds a mattermost_notifications table to store Mattermost notification settings.\r\n\r\n$tableQuery = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='mattermost_notifications'\");\r\n$tableExists = $tableQuery->fetchArray(SQLITE3_ASSOC);\r\nif ($tableExists === false) {\r\n    $db->exec(\"\r\n        CREATE TABLE mattermost_notifications (\r\n            enabled INTEGER NOT NULL DEFAULT 0,\r\n            user_id INTEGER,\r\n            webhook_url TEXT DEFAULT '',\r\n            bot_username TEXT DEFAULT '',\r\n            bot_icon_emoji TEXT DEFAULT ''\r\n        );\r\n    \");\r\n}"
  },
  {
    "path": "migrations/000042.php",
    "content": "<?php\n\n/* \n* This migration adds a table to store Serverchan notification settings\n*/\n\n$tableQuery = $db->query(\"SELECT name FROM sqlite_master WHERE type='table' AND name='serverchan_notifications'\");\n$tableExists = $tableQuery->fetchArray(SQLITE3_ASSOC);\n\nif (!$tableExists) {\n    $db->exec('CREATE TABLE serverchan_notifications (\n        enabled BOOLEAN DEFAULT 0,\n        sendkey TEXT DEFAULT \"\",\n        user_id INTEGER,\n        FOREIGN KEY (user_id) REFERENCES user(id)\n    )');\n}\n\n?>"
  },
  {
    "path": "migrations/000043.php",
    "content": "<?php\n\n/* * This migration adds a column to the admin table to store a comma-separated \n* allowlist of hostnames and IPs that can be used in webhook notifications. \n* This prevents SSRF attacks on internal services.\n*/\n\n// Check if the column already exists to prevent errors on multiple runs\n$query = $db->query(\"PRAGMA table_info(admin)\");\n$columnExists = false;\n\nwhile ($row = $query->fetchArray(SQLITE3_ASSOC)) {\n    if ($row['name'] === 'local_webhook_notifications_allowlist') {\n        $columnExists = true;\n        break;\n    }\n}\n\nif (!$columnExists) {\n    // Add the column with an empty string as the default\n    $db->exec(\"ALTER TABLE admin ADD COLUMN local_webhook_notifications_allowlist TEXT DEFAULT ''\");\n}\n\n?>"
  },
  {
    "path": "migrations/000044.php",
    "content": "<?php\n\n/* * This migration creates the uploaded_avatars table to isolate custom avatars\n * by user_id, preventing IDOR deletion vulnerabilities. It also migrates existing\n * avatars based on whether the instance is single-tenant or multi-tenant.\n */\n\n// Check if the table already exists to prevent duplicate migration runs\n$tableCheck = $db->querySingle(\"SELECT name FROM sqlite_master WHERE type='table' AND name='uploaded_avatars'\");\n\nif (!$tableCheck) {\n    // Create the uploaded_avatars table\n    $db->exec(\"\n        CREATE TABLE IF NOT EXISTS uploaded_avatars (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            user_id INTEGER NOT NULL,\n            path TEXT NOT NULL\n        )\n    \");\n\n    // Check if solo user or multiple users\n    $userCount = $db->querySingle(\"SELECT COUNT(*) FROM user\");\n\n    if ($userCount === 1) {\n        // SOLO USER MIGRATION\n        $userId = $db->querySingle(\"SELECT id FROM user LIMIT 1\");\n        \n        $avatarDir = '../../images/uploads/logos/avatars';\n        \n        if (is_dir($avatarDir)) {\n            $files = scandir($avatarDir);\n            \n            $stmt = $db->prepare(\"INSERT INTO uploaded_avatars (user_id, path) VALUES (:user_id, :path)\");\n            \n            foreach ($files as $file) {\n                // Skip directories and hidden files (like .gitkeep or .htaccess)\n                if ($file !== '.' && $file !== '..' && is_file($avatarDir . '/' . $file)) {\n                    // Store the path exactly as the app expects it in the database\n                    $relativePath = 'images/uploads/logos/avatars/' . $file;\n                    \n                    $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\n                    $stmt->bindValue(':path', $relativePath, SQLITE3_TEXT);\n                    $stmt->execute();\n                }\n            }\n        }\n    } elseif ($userCount > 1) {\n        // MULTI-USER MIGRATION\n        $results = $db->query(\"SELECT id, avatar FROM user\");\n        \n        $stmt = $db->prepare(\"INSERT INTO uploaded_avatars (user_id, path) VALUES (:user_id, :path)\");\n        \n        while ($row = $results->fetchArray(SQLITE3_ASSOC)) {\n            $userId = $row['id'];\n            $avatarPath = $row['avatar'];\n            \n            if (strpos($avatarPath, 'images/uploads/logos/avatars/') === 0) {\n                $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\n                $stmt->bindValue(':path', $avatarPath, SQLITE3_TEXT);\n                $stmt->execute();\n            }\n        }\n    }\n}\n\n?>"
  },
  {
    "path": "nginx.conf",
    "content": "user  nginx;\r\nworker_processes  1;\r\n\r\nerror_log  /var/log/nginx/error.log warn;\r\npid        /var/run/nginx.pid;\r\n\r\nevents {\r\n    worker_connections  1024;\r\n}\r\n\r\nhttp {\r\n    include       /etc/nginx/mime.types;\r\n    default_type  application/octet-stream;\r\n\r\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\r\n                      '$status $body_bytes_sent \"$http_referer\" '\r\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\r\n\r\n    access_log  /var/log/nginx/access.log  main;\r\n\r\n    sendfile        on;\r\n    keepalive_timeout  65;\r\n\r\n    server {\r\n        listen       [::]:80 ipv6only=off;\r\n        server_name  localhost;\r\n\r\n        location / {\r\n            root   /var/www/html;\r\n            index  index.php;\r\n        }\r\n\r\n        location ~ \\.php$ {\r\n            root           /var/www/html;\r\n            fastcgi_pass   127.0.0.1:9000;\r\n            fastcgi_index  index.php;\r\n            fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;\r\n            include        fastcgi_params;\r\n        }\r\n\r\n        location ~ \\.db$ {\r\n            deny all;\r\n            return 403;\r\n        }\r\n\r\n        location ~* images/uploads/logos/.*\\.php$ {\r\n            deny all;\r\n            return 403;\r\n        }\r\n\r\n        location ~* \\.tmp/.*\\.php$ {\r\n            deny all;\r\n            return 403;\r\n        }\r\n    }\r\n\r\n    include /etc/nginx/conf.d/*.conf;\r\n}"
  },
  {
    "path": "nginx.default.conf",
    "content": "server {\r\n    listen [::]:80 ipv6only=off;\r\n    server_name your_domain_or_ip; # Change to your domain or IP\r\n\r\n    root /var/www/html; # Change to your web root directory\r\n\r\n    location / {\r\n        try_files $uri $uri/ /index.php?$args;\r\n    }\r\n\r\n    location ~ \\.php$ {\r\n        include fastcgi_params;\r\n        fastcgi_pass unix:/var/run/php-fpm.sock; # Adjust the path if necessary\r\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\r\n    }\r\n\r\n    # Additional configuration if needed\r\n}"
  },
  {
    "path": "passwordreset.php",
    "content": "<?php\r\n\r\nrequire_once 'includes/connect.php';\r\nrequire_once 'includes/checkuser.php';\r\n\r\nrequire_once 'includes/i18n/languages.php';\r\nrequire_once 'includes/i18n/getlang.php';\r\nrequire_once 'includes/i18n/' . $lang . '.php';\r\n\r\nrequire_once 'includes/version.php';\r\n\r\nsession_start();\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    $db->close();\r\n    header(\"Location: .\");\r\n    exit();\r\n}\r\n\r\n$requestMode = true;\r\n$resetMode = false;\r\n\r\n$theme = \"light\";\r\nif (isset($_COOKIE['theme'])) {\r\n    $theme = $_COOKIE['theme'];\r\n}\r\n\r\n$colorTheme = \"blue\";\r\nif (isset($_COOKIE['colorTheme'])) {\r\n    $colorTheme = $_COOKIE['colorTheme'];\r\n}\r\n\r\n$settings = $db->querySingle(\"SELECT * FROM admin\", true);\r\nif ($settings['smtp_address'] == \"\" || $settings['server_url'] == \"\") {\r\n    header(\"Location: .\");\r\n    exit();\r\n} else {\r\n    $resetPasswordEnabled = true;\r\n}\r\n\r\n$hasSuccessMessage = false;\r\n$hasErrorMessage = false;\r\n$passwordsMismatch = false;\r\n$hideForm = false;\r\n\r\nif (isset($_POST['email']) && $_POST['email'] != \"\" && isset($_GET['submit']) && $_GET['submit'] && !(isset($_GET['token'])) && !(isset($_POST['token']))) {\r\n    $requestMode = true;\r\n    $resetMode = false;\r\n    $email = $_POST['email'];\r\n\r\n    $stmt = $db->prepare(\"SELECT * FROM user WHERE email = :email\");\r\n    $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n    $user = $stmt->execute()->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($user) {\r\n        $stmt = $db->prepare(\"DELETE FROM password_resets WHERE email = :email\");\r\n        $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n        $stmt->execute();\r\n\r\n        $token = bin2hex(random_bytes(32));\r\n\r\n        $stmt = $db->prepare(\"INSERT INTO password_resets (user_id, email, token) VALUES (:user_id, :email, :token)\");\r\n        $stmt->bindValue(':user_id', $user['id'], SQLITE3_INTEGER);\r\n        $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n        $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n        $stmt->execute();\r\n    }\r\n    $hasSuccessMessage = true;\r\n}\r\n\r\nif (isset($_GET['token']) && $_GET['token'] != \"\" && isset($_GET['email']) && $_GET['email'] != \"\") {\r\n    $requestMode = false;\r\n    $resetMode = true;\r\n    $token = $_GET['token'];\r\n    $email = $_GET['email'];\r\n    $matchCount = \"SELECT COUNT(*) FROM password_resets WHERE token = :token AND email = :email AND created_at > datetime('now', '-1 hour')\";\r\n    $stmt = $db->prepare($matchCount);\r\n    $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n    $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n    $count = $stmt->execute()->fetchArray(SQLITE3_NUM);\r\n    if ($count[0] == 0) {\r\n        $hasErrorMessage = true;\r\n        $hideForm = true;\r\n    }\r\n}\r\n\r\nif (isset($_POST['password']) && $_POST['password'] != \"\" && isset($_POST['confirm_password']) && $_POST['confirm_password'] != \"\" && isset($_GET['submit']) && $_GET['submit']) {\r\n    $requestMode = false;\r\n    $resetMode = true;\r\n    $password = $_POST['password'];\r\n    $confirmPassword = $_POST['confirm_password'];\r\n    $token = $_POST['token'];\r\n    $email = $_POST['email'];\r\n    $resetQuery = \"SELECT * FROM password_resets WHERE token = :token AND email = :email AND created_at > datetime('now', '-1 hour')\";\r\n    $stmt = $db->prepare($resetQuery);\r\n    $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n    $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n    $reset = $stmt->execute()->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($reset) {\r\n        $stmt = $db->prepare(\"SELECT * FROM user WHERE email = :email\");\r\n        $stmt->bindValue(':email', $reset['email'], SQLITE3_TEXT);\r\n        $result = $stmt->execute();\r\n        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n        \r\n        if ($password == $confirmPassword) {\r\n            $passwordHash = password_hash($password, PASSWORD_DEFAULT);\r\n            $stmt = $db->prepare(\"UPDATE user SET password = :password WHERE id = :id\");\r\n            $stmt->bindValue(':password', $passwordHash, SQLITE3_TEXT);\r\n            $stmt->bindValue(':id', $user['id'], SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n\r\n            $stmt = $db->prepare(\"DELETE FROM password_resets WHERE token = :token\");\r\n            $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n            $stmt->execute();\r\n            $hasSuccessMessage = true;\r\n            $hideForm = true;\r\n        } else {\r\n            $hasErrorMessage = true;\r\n            $passwordsMismatch = true;\r\n        }\r\n    } else {\r\n        $hasSuccessMessage = false;\r\n        $hasErrorMessage = true;\r\n    }\r\n}\r\n\r\n?>\r\n<!DOCTYPE html>\r\n<html dir=\"<?= $languages[$lang]['dir'] ?>\">\r\n\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\r\n    <meta name=\"theme-color\" content=\"<?= $theme == \"light\" ? \"#FFFFFF\" : \"#222222\" ?>\" />\r\n    <meta name=\"apple-mobile-web-app-title\" content=\"Wallos\">\r\n    <title>Wallos - Subscription Tracker</title>\r\n    <link rel=\"icon\" type=\"image/png\" href=\"images/icon/favicon.ico\" sizes=\"16x16\">\r\n    <link rel=\"apple-touch-icon\" href=\"images/icon/apple-touch-icon.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"images/icon/apple-touch-icon-152.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"images/icon/apple-touch-icon-180.png\">\r\n    <link rel=\"manifest\" href=\"manifest.json\">\r\n    <link rel=\"stylesheet\" href=\"styles/theme.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/login.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/themes/red.css?<?= $version ?>\" id=\"red-theme\" <?= $colorTheme != \"red\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/green.css?<?= $version ?>\" id=\"green-theme\" <?= $colorTheme != \"green\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/yellow.css?<?= $version ?>\" id=\"yellow-theme\" <?= $colorTheme != \"yellow\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/purple.css?<?= $version ?>\" id=\"purple-theme\" <?= $colorTheme != \"purple\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/font-awesome.min.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/barlow.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/login-dark-theme.css?<?= $version ?>\" id=\"dark-theme\" <?= $theme == \"light\" ? \"disabled\" : \"\" ?>>\r\n</head>\r\n\r\n<body class=\"<?= $languages[$lang]['dir'] ?>\">\r\n    <div class=\"content\">\r\n        <section class=\"container\">\r\n            <header>\r\n                <div class=\"logo-image\" title=\"Wallos - Subscription Tracker\">\r\n                    <?php include \"images/siteicons/svg/logo.php\"; ?>\r\n                </div>\r\n                <p>\r\n                    <?= translate('reset_password', $i18n) ?>\r\n                </p>\r\n            </header>\r\n            <form action=\"passwordreset.php?submit=true\" method=\"post\">\r\n                <?php\r\n                if ($requestMode) {\r\n                    if (!$hideForm) {\r\n                        ?>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"email\"><?= translate('email', $i18n) ?>:</label>\r\n                            <input type=\"text\" id=\"email\" name=\"email\" autocomplete=\"email\" required>\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <input type=\"submit\" value=\"<?= translate('reset_password', $i18n) ?>\">\r\n                        </div>\r\n                        <?php\r\n                    }\r\n                    if ($hasSuccessMessage) {\r\n                        ?>\r\n                        <ul class=\"success-box\">\r\n                            <li><i class=\"fa-solid fa-check\"></i><?= translate('reset_sent_check_email', $i18n) ?></li>\r\n                        </ul>\r\n                        <?php\r\n                    }\r\n                    if ($hasErrorMessage) {\r\n                        ?>\r\n                        <ul class=\"error-box\">\r\n                            <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('error', $i18n) ?></li>\r\n                        </ul>\r\n                        <?php\r\n                    }\r\n                }\r\n                if ($resetMode) {\r\n                    if (!$hideForm) {\r\n                        ?>\r\n                        <div class=\"form-group\">\r\n                            <input type=\"hidden\" name=\"token\" value=\"<?= htmlspecialchars($token ?? '', ENT_QUOTES, 'UTF-8') ?>\">\r\n                            <input type=\"hidden\" name=\"email\" value=\"<?= htmlspecialchars($email ?? '', ENT_QUOTES, 'UTF-8') ?>\">\r\n                            <label for=\"password\"><?= translate('password', $i18n) ?>:</label>\r\n                            <input type=\"password\" id=\"password\" name=\"password\" autocomplete=\"new-password\" required>\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"confirm_password\"><?= translate('confirm_password', $i18n) ?>:</label>\r\n                            <input type=\"password\" id=\"confirm_password\" name=\"confirm_password\" autocomplete=\"new-password\" required>\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <input type=\"submit\" value=\"<?= translate('reset_password', $i18n) ?>\">\r\n                        </div>\r\n                        <?php\r\n                    }\r\n                    if ($hasErrorMessage) {\r\n                        if ($passwordsMismatch) {\r\n                            ?>\r\n                            <ul class=\"error-box\">\r\n                                <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('passwords_dont_match', $i18n) ?>\r\n                                </li>\r\n                            </ul>\r\n                            <?php\r\n                        } else {\r\n                            ?>\r\n                            <ul class=\"error-box\">\r\n                                <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('error', $i18n) ?></li>\r\n                            </ul>\r\n                            <?php\r\n                        }\r\n                    }\r\n                    if ($hasSuccessMessage) {\r\n                        ?>\r\n                        <ul class=\"success-box\">\r\n                            <li><i class=\"fa-solid fa-check\"></i><?= translate('password_reset_successful', $i18n) ?></li>\r\n                        </ul>\r\n                        <?php\r\n                    }\r\n                }\r\n                ?>\r\n                <div class=\"login-form-link\">\r\n                    <a href=\"login.php\"><?= translate('login', $i18n) ?></a>\r\n                </div>\r\n            </form>\r\n        </section>\r\n    </div>\r\n    <script type=\"text/javascript\">\r\n        function openRegitrationPage() {\r\n            window.location.href = \"registration.php\";\r\n        }\r\n    </script>\r\n</body>\r\n\r\n</html>"
  },
  {
    "path": "profile.php",
    "content": "<?php\r\nrequire_once 'includes/header.php';\r\n\r\n// Fetch the avatars belonging to the logged-in user\r\n$uploadedAvatars = [];\r\n\r\n$stmt = $db->prepare(\"SELECT path FROM uploaded_avatars WHERE user_id = :user_id\");\r\n$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $uploadedAvatars[] = $row['path'];\r\n}\r\n?>\r\n\r\n<script src=\"scripts/libs/sortable.min.js\"></script>\r\n<script src=\"scripts/libs/qrcode.min.js\"></script>\r\n<style>\r\n    .logo-preview:after {\r\n        content: '<?= translate('upload_logo', $i18n) ?>';\r\n    }\r\n</style>\r\n<section class=\"contain settings\">\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('user_details', $i18n) ?></h2>\r\n        </header>\r\n        <form action=\"endpoints/user/saveuser.php\" method=\"post\" id=\"userForm\" enctype=\"multipart/form-data\">\r\n            <div class=\"user-form\">\r\n                <div class=\"fields\">\r\n                    <div>\r\n                        <div class=\"user-avatar\">\r\n                            <img src=\"<?= htmlspecialchars($userData['avatar'], ENT_QUOTES, 'UTF-8') ?>\" alt=\"avatar\" class=\"avatar\" id=\"avatarImg\"\r\n                                onClick=\"toggleAvatarSelect()\" />\r\n                            <span class=\"edit-avatar\" onClick=\"toggleAvatarSelect()\" title=\"Change Avatar\">\r\n                                <i class=\"fa-solid fa-pencil\"></i>\r\n                            </span>\r\n                        </div>\r\n\r\n                        <input type=\"hidden\" name=\"avatar\" value=\"<?= htmlspecialchars($userData['avatar'], ENT_QUOTES, 'UTF-8') ?>\" id=\"avatarUser\" />\r\n                        <div class=\"avatar-select\" id=\"avatarSelect\">\r\n                            <div class=\"avatar-list\">\r\n                                <?php foreach (scandir('images/avatars') as $image): ?>\r\n                                    <?php if (!str_starts_with($image, '.')): ?>\r\n                                        <img src=\"images/avatars/<?= $image ?>\" alt=\"<?= $image ?>\" class=\"avatar-option\"\r\n                                            data-src=\"images/avatars/<?= $image ?>\">\r\n                                    <?php endif ?>\r\n                                <?php endforeach ?>\r\n\r\n                                <?php foreach ($uploadedAvatars as $path): ?>\r\n                                    <?php \r\n                                        $filename = basename($path); \r\n                                    ?>\r\n                                    <div class=\"avatar-container\" data-src=\"<?= $filename ?>\">\r\n                                        <img src=\"<?= $path ?>\" alt=\"<?= $filename ?>\"\r\n                                            class=\"avatar-option\" data-src=\"<?= $path ?>\">\r\n                                        \r\n                                        <div class=\"remove-avatar\" onclick=\"deleteAvatar('<?= $filename ?>')\"\r\n                                            title=\"Delete avatar\">\r\n                                            <i class=\"fa-solid fa-xmark\"></i>\r\n                                        </div>\r\n                                    </div>\r\n                                <?php endforeach ?>\r\n\r\n                                <label for=\"profile_pic\" class=\"add-avatar\"\r\n                                    title=\"<?= translate('upload_avatar', $i18n) ?>\">\r\n                                    <i class=\"fa-solid fa-arrow-up-from-bracket\"></i>\r\n                                </label>\r\n                            </div>\r\n                            \r\n                            <input type=\"file\" id=\"profile_pic\" class=\"hidden-input\" name=\"profile_pic\"\r\n                                accept=\"image/jpeg, image/png, image/gif, image/webp\"\r\n                                onChange=\"successfulUpload(this, '<?= addslashes(translate('file_type_error', $i18n)) ?>')\" />\r\n                        </div>\r\n                    </div>\r\n                    <div class=\"grow\">\r\n                        <div class=\"form-group\">\r\n                            <label for=\"username\"><?= translate('username', $i18n) ?>:</label>\r\n                            <input type=\"text\" id=\"username\" name=\"username\" value=\"<?= $userData['username'] ?>\"\r\n                                disabled>\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"firstname\"><?= translate('firstname', $i18n) ?>:</label>\r\n                            <input type=\"text\" id=\"firstname\" name=\"firstname\" autocomplete=\"given-name\"\r\n                                value=\"<?= $userData['firstname'] ?>\">\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"lastname\"><?= translate('lastname', $i18n) ?>:</label>\r\n                            <input type=\"text\" id=\"lastname\" name=\"lastname\" autocomplete=\"family-name\"\r\n                                value=\"<?= $userData['lastname'] ?>\">\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"email\"><?= translate('email', $i18n) ?>:</label>\r\n                            <input type=\"email\" id=\"email\" name=\"email\" autocomplete=\"email\"\r\n                                value=\"<?= $userData['email'] ?>\" required>\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"password\"><?= translate('password', $i18n) ?>:</label>\r\n                            <input type=\"password\" id=\"password\" name=\"password\" autocomplete=\"new-password\"\r\n                                <?= $demoMode ? 'disabled title=\"Not available on Demo Mode\"' : '' ?>>\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"confirm_password\"><?= translate('confirm_password', $i18n) ?>:</label>\r\n                            <input type=\"password\" id=\"confirm_password\" name=\"confirm_password\" autocomplete=\"new-password\"\r\n                                <?= $demoMode ? 'disabled title=\"Not available on Demo Mode\"' : '' ?>>\r\n                        </div>\r\n                        <?php\r\n                        $currencies = array();\r\n                        $query = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n                        $query = $db->prepare($query);\r\n                        $query->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $query->execute();\r\n                        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n                            $currencyId = $row['id'];\r\n                            $currencies[$currencyId] = $row;\r\n                        }\r\n                        $userData['currency_symbol'] = \"€\";\r\n                        ?>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"currency\"><?= translate('main_currency', $i18n) ?>:</label>\r\n                            <select id=\"currency\" name=\"main_currency\" placeholder=\"Currency\">\r\n                                <?php\r\n                                foreach ($currencies as $currency) {\r\n                                    $selected = \"\";\r\n                                    if ($currency['id'] == $userData['main_currency']) {\r\n                                        $selected = \"selected\";\r\n                                        $userData['currency_symbol'] = $currency['symbol'];\r\n                                    }\r\n                                    ?>\r\n                                    <option value=\"<?= $currency['id'] ?>\" <?= $selected ?>><?= $currency['name'] ?></option>\r\n                                    <?php\r\n                                }\r\n                                ?>\r\n                            </select>\r\n                        </div>\r\n                        <div class=\"form-group\">\r\n                            <label for=\"language\"><?= translate('language', $i18n) ?>:</label>\r\n                            <select id=\"language\" name=\"language\" placeholder=\"Language\">\r\n                                <?php\r\n                                foreach ($languages as $code => $language) {\r\n                                    $selected = ($code === $lang) ? 'selected' : '';\r\n                                    ?>\r\n                                    <option value=\"<?= $code ?>\" <?= $selected ?>><?= $language['name'] ?></option>\r\n                                    <?php\r\n                                }\r\n                                ?>\r\n                            </select>\r\n                        </div>\r\n                    </div>\r\n                </div>\r\n                <div class=\"buttons\">\r\n                    <input type=\"submit\" value=\"<?= translate('save', $i18n) ?>\" id=\"userSubmit\"\r\n                        class=\"thin mobile-grow\" />\r\n                </div>\r\n            </div>\r\n        </form>\r\n    </section>\r\n\r\n    <?php\r\n    $sql = \"SELECT login_disabled FROM admin\";\r\n    $stmt = $db->prepare($sql);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n    $loginDisabled = $row['login_disabled'];\r\n\r\n    $showTotpSection = true;\r\n    if ($loginDisabled && !$userData['totp_enabled']) {\r\n        $showTotpSection = false;\r\n    }\r\n\r\n    if ($showTotpSection) {\r\n        ?>\r\n        <section class=\"account-section\">\r\n            <header>\r\n                <h2><?= translate(\"two_factor_authentication\", $i18n) ?></h2>\r\n            </header>\r\n            <div class=\"account-2fa\">\r\n                <div class=\"buttons\">\r\n                    <?php\r\n                    if (!$userData['totp_enabled']) {\r\n                        ?>\r\n                        <input type=\"button\" value=\"<?= translate('enable_two_factor_authentication', $i18n) ?>\" id=\"enableTotp\"\r\n                            onClick=\"enableTotp()\" class=\"button thin mobile-grow\"/>\r\n                        <div class=\"totp-popup\" id=\"totp-popup\">\r\n                            <header>\r\n                                <h3><?= translate('enable_two_factor_authentication', $i18n) ?></h3>\r\n                                <span class=\"fa-solid fa-xmark close-form\" onclick=\"closeTotpPopup()\"></span>\r\n                            </header>\r\n                            <div class=\"totp-popup-content\">\r\n                                <div class=\"totp-setup\" id=\"totp-setup\">\r\n                                    <div class=\"totp-qrcode-container\">\r\n                                        <div id=\"totp-qr-code\"></div>\r\n                                    </div>\r\n                                    <p class=\"totp-secret\" id=\"totp-secret-code\"></p>\r\n                                    <div class=\"form-group-inline\">\r\n                                        <input type=\"hidden\" name=\"totp-secret\" id=\"totp-secret\" value=\"\" />\r\n                                        <input type=\"text\" id=\"totp\" name=\"totp\" autocomplete=\"one-time-code\"\r\n                                            placeholder=\"<?= translate(\"totp_code\", $i18n) ?>\" />\r\n                                        <input type=\"button\" value=\"<?= translate('enable', $i18n) ?>\" id=\"enableTotpButton\"\r\n                                            onClick=\"submitTotp()\" />\r\n                                    </div>\r\n                                </div>\r\n                                <div class=\"totp-setup hide\" id=\"totp-backup-codes\">\r\n                                    <h4><?= translate('backup_codes', $i18n) ?></h4>\r\n                                    <ul class=\"totp-backup-codes\" id=\"backup-codes\"></ul>\r\n                                    <div class=\"form-group-inline wrap\">\r\n                                        <input type=\"button\" class=\"button secondary-button grow\"\r\n                                            value=\"<?= translate('copy_to_clipboard', $i18n) ?>\" id=\"copyBackupCodes\"\r\n                                            onClick=\"copyBackupCodes()\" />\r\n                                        <input type=\"button\" class=\"grow\"\r\n                                            value=\"<?= translate('download_backup_codes', $i18n) ?>\" id=\"downloadBackupCodes\"\r\n                                            onClick=\"downloadBackupCodes()\" />\r\n                                    </div>\r\n                                    <div class=\"settings-notes\">\r\n                                        <p>\r\n                                            <i class=\"fa-solid fa-circle-info\"></i>\r\n                                            <?= translate('totp_backup_codes_info', $i18n) ?>\r\n                                        </p>\r\n                                    </div>\r\n                                </div>\r\n                            </div>\r\n                        </div>\r\n                        <?php\r\n                    } else {\r\n                        ?>\r\n                        <input type=\"button\" class=\"button secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('disable_two_factor_authentication', $i18n) ?>\" id=\"disableTotp\"\r\n                            onClick=\"disableTotp()\" />\r\n                        <div class=\"totp-popup\" id=\"totp-disable-popup\">\r\n                            <header>\r\n                                <h3><?= translate('disable_two_factor_authentication', $i18n) ?></h3>\r\n                                <span class=\"fa-solid fa-xmark close-form\" onclick=\"closeTotpDisablePopup()\"></span>\r\n                            </header>\r\n                            <div class=\"totp-popup-content\">\r\n                                <div class=\"form-group-inline\">\r\n                                    <input type=\"text\" id=\"totp-disable\" name=\"totp-disable\" autocomplete=\"one-time-code\"\r\n                                        placeholder=\"totp\" />\r\n                                    <input type=\"button\" value=\"<?= translate('disable', $i18n) ?>\" id=\"disableTotpButton\"\r\n                                        onClick=\"submitDisableTotp()\" />\r\n                                </div>\r\n                            </div>\r\n                        </div>\r\n                        <?php\r\n                    }\r\n                    ?>\r\n                </div>\r\n                <div class=\"settings-notes\">\r\n                    <p>\r\n                        <i class=\"fa-solid fa-circle-info\"></i>\r\n                        <?php\r\n                        if (!$userData['totp_enabled']) {\r\n                            echo translate('two_factor_info', $i18n);\r\n                        } else {\r\n                            echo translate('two_factor_enabled_info', $i18n);\r\n                        }\r\n                        ?>\r\n                    </p>\r\n                </div>\r\n            </div>\r\n        </section>\r\n        <?php\r\n    }\r\n\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('api_key', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-api-key\">\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"text\" id=\"apikey\" name=\"apikey\" value=\"<?= $userData['api_key'] ?>\" placeholder=\"API Key\" readonly>\r\n                <input type=\"submit\" value=\"<?= translate('regenerate', $i18n) ?>\" id=\"regenerateApiKey\" onClick=\"regenerateApiKey()\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i> <?= translate('api_key_info', $i18n) ?>\r\n                </p>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('account', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-list\">\r\n            <div>\r\n                <h3><?= translate('export_subscriptions', $i18n) ?></h3>\r\n                <div class=\"form-group-inline wrap\">\r\n                    <input type=\"button\" value=\"<?= translate('export_as_json', $i18n) ?>\" onClick=\"exportAsJson()\"\r\n                        class=\"secondary-button thin mobile-grow\" id=\"export-json\" <?= $demoMode ? 'disabled title=\"Not available on Demo Mode\"' : '' ?>>\r\n                    <input type=\"button\" value=\"<?= translate('export_as_csv', $i18n) ?>\" onClick=\"exportAsCsv()\"\r\n                        class=\"secondary-button thin mobile-grow\" id=\"export-csv\" <?= $demoMode ? 'disabled title=\"Not available on Demo Mode\"' : '' ?>>\r\n                </div>\r\n            </div>\r\n        </div>\r\n        <div>\r\n            <?php\r\n            if ($userId != 1 && !$demoMode) {\r\n                ?>\r\n                <h3><?= translate('danger_zone', $i18n) ?></h3>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"button\" value=\"<?= translate('delete_account', $i18n) ?>\"\r\n                        onClick=\"deleteAccount(<?= $userId ?>)\" class=\"warning-button thin mobile-grow\" id=\"delete-account\">\r\n                </div>\r\n                <div class=\"settings-notes\">\r\n                    <p>\r\n                        <i class=\"fa-solid fa-circle-info\"></i>\r\n                        <?= translate('delete_account_info', $i18n) ?>\r\n                    </p>\r\n                </div>\r\n                <?php\r\n            }\r\n            ?>\r\n        </div>\r\n    </section>\r\n\r\n   \r\n\r\n</section>\r\n<script src=\"scripts/profile.js?<?= $version ?>\"></script>\r\n<script src=\"scripts/theme.js?<?= $version ?>\"></script>\r\n<script src=\"scripts/notifications.js?<?= $version ?>\"></script>\r\n\r\n<?php\r\nrequire_once 'includes/footer.php';\r\n?>\r\n"
  },
  {
    "path": "registration.php",
    "content": "<?php\r\nrequire_once 'includes/connect.php';\r\nrequire_once 'includes/checkuser.php';\r\n\r\nrequire_once 'includes/i18n/languages.php';\r\nrequire_once 'includes/i18n/getlang.php';\r\nrequire_once 'includes/i18n/' . $lang . '.php';\r\n\r\nrequire_once 'includes/version.php';\r\n\r\nfunction validate($value)\r\n{\r\n    $value = trim($value);\r\n    $value = stripslashes($value);\r\n    $value = htmlspecialchars($value);\r\n    $value = htmlentities($value);\r\n    return $value;\r\n}\r\n\r\n// If logo folder doesn't exist, create it\r\nif (!file_exists('images/uploads/logos')) {\r\n    mkdir('images/uploads/logos', 0777, true);\r\n    mkdir('images/uploads/logos/avatars', 0777, true);\r\n}\r\n\r\n// If there's already a user on the database, redirect to login page if registrations are closed or maxn users is reached\r\n$stmt = $db->prepare('SELECT COUNT(*) as userCount FROM user');\r\n$result = $stmt->execute();\r\n$userCountResult = $result->fetchArray(SQLITE3_ASSOC);\r\n$userCount = $userCountResult['userCount'];\r\n\r\nif ($userCount > 0) {\r\n    $stmt = $db->prepare('SELECT * FROM admin');\r\n    $result = $stmt->execute();\r\n    $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($settings['registrations_open'] == 0) {\r\n        header(\"Location: login.php\");\r\n        exit();\r\n    }\r\n\r\n    if ($settings['max_users'] != 0) {\r\n\r\n        if ($userCount >= $settings['max_users']) {\r\n            header(\"Location: login.php\");\r\n            exit();\r\n        }\r\n    }\r\n}\r\n\r\n\r\n$theme = \"light\";\r\n$updateThemeSettings = false;\r\nif (isset($_COOKIE['theme'])) {\r\n    $theme = $_COOKIE['theme'];\r\n} else {\r\n    $updateThemeSettings = true;\r\n}\r\n\r\n$colorTheme = \"blue\";\r\nif (isset($_COOKIE['colorTheme'])) {\r\n    $colorTheme = $_COOKIE['colorTheme'];\r\n}\r\n\r\n$currencies = [\r\n    ['id' => 1, 'name' => 'Euro', 'symbol' => '€', 'code' => 'EUR'],\r\n    ['id' => 2, 'name' => 'US Dollar', 'symbol' => '$', 'code' => 'USD'],\r\n    ['id' => 3, 'name' => 'Japanese Yen', 'symbol' => '¥', 'code' => 'JPY'],\r\n    ['id' => 4, 'name' => 'Bulgarian Lev', 'symbol' => 'лв', 'code' => 'BGN'],\r\n    ['id' => 5, 'name' => 'Czech Republic Koruna', 'symbol' => 'Kč', 'code' => 'CZK'],\r\n    ['id' => 6, 'name' => 'Danish Krone', 'symbol' => 'kr', 'code' => 'DKK'],\r\n    ['id' => 7, 'name' => 'British Pound Sterling', 'symbol' => '£', 'code' => 'GBP'],\r\n    ['id' => 8, 'name' => 'Hungarian Forint', 'symbol' => 'Ft', 'code' => 'HUF'],\r\n    ['id' => 9, 'name' => 'Polish Zloty', 'symbol' => 'zł', 'code' => 'PLN'],\r\n    ['id' => 10, 'name' => 'Romanian Leu', 'symbol' => 'lei', 'code' => 'RON'],\r\n    ['id' => 11, 'name' => 'Swedish Krona', 'symbol' => 'kr', 'code' => 'SEK'],\r\n    ['id' => 12, 'name' => 'Swiss Franc', 'symbol' => 'Fr', 'code' => 'CHF'],\r\n    ['id' => 13, 'name' => 'Icelandic Króna', 'symbol' => 'kr', 'code' => 'ISK'],\r\n    ['id' => 14, 'name' => 'Norwegian Krone', 'symbol' => 'kr', 'code' => 'NOK'],\r\n    ['id' => 15, 'name' => 'Russian Ruble', 'symbol' => '₽', 'code' => 'RUB'],\r\n    ['id' => 16, 'name' => 'Turkish Lira', 'symbol' => '₺', 'code' => 'TRY'],\r\n    ['id' => 17, 'name' => 'Australian Dollar', 'symbol' => '$', 'code' => 'AUD'],\r\n    ['id' => 18, 'name' => 'Brazilian Real', 'symbol' => 'R$', 'code' => 'BRL'],\r\n    ['id' => 19, 'name' => 'Canadian Dollar', 'symbol' => '$', 'code' => 'CAD'],\r\n    ['id' => 20, 'name' => 'Chinese Yuan', 'symbol' => '¥', 'code' => 'CNY'],\r\n    ['id' => 21, 'name' => 'Hong Kong Dollar', 'symbol' => 'HK$', 'code' => 'HKD'],\r\n    ['id' => 22, 'name' => 'Indonesian Rupiah', 'symbol' => 'Rp', 'code' => 'IDR'],\r\n    ['id' => 23, 'name' => 'Israeli New Sheqel', 'symbol' => '₪', 'code' => 'ILS'],\r\n    ['id' => 24, 'name' => 'Indian Rupee', 'symbol' => '₹', 'code' => 'INR'],\r\n    ['id' => 25, 'name' => 'South Korean Won', 'symbol' => '₩', 'code' => 'KRW'],\r\n    ['id' => 26, 'name' => 'Mexican Peso', 'symbol' => 'Mex$', 'code' => 'MXN'],\r\n    ['id' => 27, 'name' => 'Malaysian Ringgit', 'symbol' => 'RM', 'code' => 'MYR'],\r\n    ['id' => 28, 'name' => 'New Zealand Dollar', 'symbol' => 'NZ$', 'code' => 'NZD'],\r\n    ['id' => 29, 'name' => 'Philippine Peso', 'symbol' => '₱', 'code' => 'PHP'],\r\n    ['id' => 30, 'name' => 'Singapore Dollar', 'symbol' => 'S$', 'code' => 'SGD'],\r\n    ['id' => 31, 'name' => 'Thai Baht', 'symbol' => '฿', 'code' => 'THB'],\r\n    ['id' => 32, 'name' => 'South African Rand', 'symbol' => 'R', 'code' => 'ZAR'],\r\n    ['id' => 33, 'name' => 'Ukrainian Hryvnia', 'symbol' => '₴', 'code' => 'UAH'],\r\n    ['id' => 34, 'name' => 'New Taiwan Dollar', 'symbol' => 'NT$', 'code' => 'TWD'],\r\n];\r\n\r\n$categories = [\r\n    ['id' => 1, 'name' => 'No category'],\r\n    ['id' => 2, 'name' => 'Entertainment'],\r\n    ['id' => 3, 'name' => 'Music'],\r\n    ['id' => 4, 'name' => 'Utilities'],\r\n    ['id' => 5, 'name' => 'Food & Beverages'],\r\n    ['id' => 6, 'name' => 'Health & Wellbeing'],\r\n    ['id' => 7, 'name' => 'Productivity'],\r\n    ['id' => 8, 'name' => 'Banking'],\r\n    ['id' => 9, 'name' => 'Transport'],\r\n    ['id' => 10, 'name' => 'Education'],\r\n    ['id' => 11, 'name' => 'Insurance'],\r\n    ['id' => 12, 'name' => 'Gaming'],\r\n    ['id' => 13, 'name' => 'News & Magazines'],\r\n    ['id' => 14, 'name' => 'Software'],\r\n    ['id' => 15, 'name' => 'Technology'],\r\n    ['id' => 16, 'name' => 'Cloud Services'],\r\n    ['id' => 17, 'name' => 'Charity & Donations'],\r\n];\r\n\r\n$payment_methods = [\r\n    ['id' => 1, 'name' => 'PayPal', 'icon' => 'images/uploads/icons/paypal.png'],\r\n    ['id' => 2, 'name' => 'Credit Card', 'icon' => 'images/uploads/icons/creditcard.png'],\r\n    ['id' => 3, 'name' => 'Bank Transfer', 'icon' => 'images/uploads/icons/banktransfer.png'],\r\n    ['id' => 4, 'name' => 'Direct Debit', 'icon' => 'images/uploads/icons/directdebit.png'],\r\n    ['id' => 5, 'name' => 'Money', 'icon' => 'images/uploads/icons/money.png'],\r\n    ['id' => 6, 'name' => 'Google Pay', 'icon' => 'images/uploads/icons/googlepay.png'],\r\n    ['id' => 7, 'name' => 'Samsung Pay', 'icon' => 'images/uploads/icons/samsungpay.png'],\r\n    ['id' => 8, 'name' => 'Apple Pay', 'icon' => 'images/uploads/icons/applepay.png'],\r\n    ['id' => 9, 'name' => 'Crypto', 'icon' => 'images/uploads/icons/crypto.png'],\r\n    ['id' => 10, 'name' => 'Klarna', 'icon' => 'images/uploads/icons/klarna.png'],\r\n    ['id' => 11, 'name' => 'Amazon Pay', 'icon' => 'images/uploads/icons/amazonpay.png'],\r\n    ['id' => 12, 'name' => 'SEPA', 'icon' => 'images/uploads/icons/sepa.png'],\r\n    ['id' => 13, 'name' => 'Skrill', 'icon' => 'images/uploads/icons/skrill.png'],\r\n    ['id' => 14, 'name' => 'Sofort', 'icon' => 'images/uploads/icons/sofort.png'],\r\n    ['id' => 15, 'name' => 'Stripe', 'icon' => 'images/uploads/icons/stripe.png'],\r\n    ['id' => 16, 'name' => 'Affirm', 'icon' => 'images/uploads/icons/affirm.png'],\r\n    ['id' => 17, 'name' => 'AliPay', 'icon' => 'images/uploads/icons/alipay.png'],\r\n    ['id' => 18, 'name' => 'Elo', 'icon' => 'images/uploads/icons/elo.png'],\r\n    ['id' => 19, 'name' => 'Facebook Pay', 'icon' => 'images/uploads/icons/facebookpay.png'],\r\n    ['id' => 20, 'name' => 'GiroPay', 'icon' => 'images/uploads/icons/giropay.png'],\r\n    ['id' => 21, 'name' => 'iDeal', 'icon' => 'images/uploads/icons/ideal.png'],\r\n    ['id' => 22, 'name' => 'Union Pay', 'icon' => 'images/uploads/icons/unionpay.png'],\r\n    ['id' => 23, 'name' => 'Interac', 'icon' => 'images/uploads/icons/interac.png'],\r\n    ['id' => 24, 'name' => 'WeChat', 'icon' => 'images/uploads/icons/wechat.png'],\r\n    ['id' => 25, 'name' => 'Paysafe', 'icon' => 'images/uploads/icons/paysafe.png'],\r\n    ['id' => 26, 'name' => 'Poli', 'icon' => 'images/uploads/icons/poli.png'],\r\n    ['id' => 27, 'name' => 'Qiwi', 'icon' => 'images/uploads/icons/qiwi.png'],\r\n    ['id' => 28, 'name' => 'ShopPay', 'icon' => 'images/uploads/icons/shoppay.png'],\r\n    ['id' => 29, 'name' => 'Venmo', 'icon' => 'images/uploads/icons/venmo.png'],\r\n    ['id' => 30, 'name' => 'VeriFone', 'icon' => 'images/uploads/icons/verifone.png'],\r\n    ['id' => 31, 'name' => 'WebMoney', 'icon' => 'images/uploads/icons/webmoney.png'],\r\n];\r\n\r\n$passwordMismatch = false;\r\n$usernameExists = false;\r\n$emailExists = false;\r\n$registrationFailed = false;\r\n$hasErrors = false;\r\nif (isset($_POST['username'])) {\r\n    $username = validate($_POST['username']);\r\n    $firstname = validate($_POST['firstname']);\r\n    $lastname = validate($_POST['lastname']);\r\n    $email = validate($_POST['email']);\r\n    $password = $_POST['password'];\r\n    $confirm_password = $_POST['confirm_password'];\r\n    $main_currency = $_POST['main_currency'];\r\n    $main_currency_index = array_search($main_currency, array_column($currencies, 'code'));\r\n    $main_currency_id = $currencies[$main_currency_index]['id'];\r\n    $language = $_POST['language'];\r\n    $avatar = \"images/avatars/0.svg\";\r\n\r\n    if ($password != $confirm_password) {\r\n        $passwordMismatch = true;\r\n        $hasErrors = true;\r\n    }\r\n\r\n    $emailQuery = \"SELECT * FROM user WHERE email = :email\";\r\n    $stmtEmail = $db->prepare($emailQuery);\r\n    $stmtEmail->bindValue(':email', $email, SQLITE3_TEXT);\r\n    $resultEmail = $stmtEmail->execute();\r\n\r\n    if ($resultEmail->fetchArray()) {\r\n        $emailExists = true;\r\n        $hasErrors = true;\r\n    }\r\n\r\n    $usernameQuery = \"SELECT * FROM user WHERE username = :username\";\r\n    $stmtUsername = $db->prepare($usernameQuery);\r\n    $stmtUsername->bindValue(':username', $username, SQLITE3_TEXT);\r\n    $resultUsername = $stmtUsername->execute();\r\n\r\n    if ($resultUsername->fetchArray()) {\r\n        $usernameExists = true;\r\n        $hasErrors = true;\r\n    }\r\n\r\n    $requireValidation = false;\r\n\r\n    if ($hasErrors == false) {\r\n        $query = \"INSERT INTO user (username, firstname, lastname, email, password, main_currency, avatar, language, budget) VALUES (:username, :firstname, :lastname, :email, :password, :main_currency, :avatar, :language, :budget)\";\r\n        $stmt = $db->prepare($query);\r\n        $hashedPassword = password_hash($password, PASSWORD_DEFAULT);\r\n        $stmt->bindValue(':username', $username, SQLITE3_TEXT);\r\n        $stmt->bindValue(':firstname', $firstname, SQLITE3_TEXT);\r\n        $stmt->bindValue(':lastname', $lastname, SQLITE3_TEXT);\r\n        $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n        $stmt->bindValue(':password', $hashedPassword, SQLITE3_TEXT);\r\n        $stmt->bindValue(':main_currency', $main_currency_id, SQLITE3_TEXT);\r\n        $stmt->bindValue(':avatar', $avatar, SQLITE3_TEXT);\r\n        $stmt->bindValue(':language', $language, SQLITE3_TEXT);\r\n        $stmt->bindValue(':budget', 0, SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n\r\n        if ($result) {\r\n\r\n            // Get id of the newly created user\r\n            $userId = $db->lastInsertRowID();\r\n\r\n            // Add username as household member for that user\r\n            $query = \"INSERT INTO household (name, user_id) VALUES (:name, :user_id)\";\r\n            $stmt = $db->prepare($query);\r\n            $stmt->bindValue(':name', $username, SQLITE3_TEXT);\r\n            $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n            $stmt->execute();\r\n\r\n            if ($userId > 1) {\r\n\r\n                // Add categories for that user\r\n                $query = 'INSERT INTO categories (name, \"order\", user_id) VALUES (:name, :order, :user_id)';\r\n                $stmt = $db->prepare($query);\r\n                foreach ($categories as $index => $category) {\r\n                    $stmt->bindValue(':name', $category['name'], SQLITE3_TEXT);\r\n                    $stmt->bindValue(':order', $index + 1, SQLITE3_INTEGER);\r\n                    $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                    $stmt->execute();\r\n                }\r\n\r\n                // Add payment methods for that user\r\n                $query = 'INSERT INTO payment_methods (name, icon, \"order\", user_id) VALUES (:name, :icon, :order, :user_id)';\r\n                $stmt = $db->prepare($query);\r\n                foreach ($payment_methods as $index => $payment_method) {\r\n                    $stmt->bindValue(':name', $payment_method['name'], SQLITE3_TEXT);\r\n                    $stmt->bindValue(':icon', $payment_method['icon'], SQLITE3_TEXT);\r\n                    $stmt->bindValue(':order', $index + 1, SQLITE3_INTEGER);\r\n                    $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                    $stmt->execute();\r\n                }\r\n\r\n                // Add currencies for that user\r\n                $query = \"INSERT INTO currencies (name, symbol, code, rate, user_id) VALUES (:name, :symbol, :code, :rate, :user_id)\";\r\n                $stmt = $db->prepare($query);\r\n                foreach ($currencies as $currency) {\r\n                    $stmt->bindValue(':name', $currency['name'], SQLITE3_TEXT);\r\n                    $stmt->bindValue(':symbol', $currency['symbol'], SQLITE3_TEXT);\r\n                    $stmt->bindValue(':code', $currency['code'], SQLITE3_TEXT);\r\n                    $stmt->bindValue(':rate', 1, SQLITE3_FLOAT);\r\n                    $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                    $stmt->execute();\r\n                }\r\n\r\n                // Retrieve main currency id\r\n                $query = \"SELECT id FROM currencies WHERE code = :code AND user_id = :user_id\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindValue(':code', $main_currency, SQLITE3_TEXT);\r\n                $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                $result = $stmt->execute();\r\n                $currency = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                // Update user main currency\r\n                $query = \"UPDATE user SET main_currency = :main_currency WHERE id = :user_id\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindValue(':main_currency', $currency['id'], SQLITE3_INTEGER);\r\n                $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                $stmt->execute();\r\n\r\n                // Add settings for that user\r\n                $query = \"INSERT INTO settings (dark_theme, monthly_price, convert_currency, remove_background, color_theme, hide_disabled, user_id, disabled_to_bottom, show_original_price, mobile_nav) \r\n                          VALUES (2, 0, 0, 0, 'blue', 0, :user_id, 0, 0, 0)\";\r\n                $stmt = $db->prepare($query);\r\n                $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                $stmt->execute();\r\n\r\n                // If email verification is required add the user to the email_verification table\r\n                $query = \"SELECT * FROM admin\";\r\n                $stmt = $db->prepare($query);\r\n                $result = $stmt->execute();\r\n                $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n                if ($settings['require_email_verification'] == 1) {\r\n                    $query = \"INSERT INTO email_verification (user_id, email, token, email_sent) VALUES (:user_id, :email, :token, 0)\";\r\n                    $stmt = $db->prepare($query);\r\n                    $token = bin2hex(random_bytes(32));\r\n                    $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);\r\n                    $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n                    $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n                    $stmt->execute();\r\n\r\n                    $requireValidation = true;\r\n                }\r\n            }\r\n\r\n            $db->close();\r\n            header(\"Location: login.php?registered=true&requireValidation=$requireValidation\");\r\n            exit();\r\n        } else {\r\n            $registrationFailed = true;\r\n        }\r\n    }\r\n}\r\n?>\r\n<!DOCTYPE html>\r\n<html dir=\"<?= $languages[$lang]['dir'] ?>\">\r\n\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\r\n    <meta name=\"theme-color\" content=\"<?= $theme == \"light\" ? \"#FFFFFF\" : \"#222222\" ?>\" id=\"theme-color\" />\r\n    <meta name=\"apple-mobile-web-app-title\" content=\"Wallos\">\r\n    <title>Wallos - Subscription Tracker</title>\r\n    <link rel=\"icon\" type=\"image/png\" href=\"images/icon/favicon.ico\" sizes=\"16x16\">\r\n    <link rel=\"apple-touch-icon\" href=\"images/icon/apple-touch-icon.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"images/icon/apple-touch-icon-152.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"images/icon/apple-touch-icon-180.png\">\r\n    <link rel=\"manifest\" href=\"manifest.json\">\r\n    <link rel=\"stylesheet\" href=\"styles/theme.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/login.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/themes/red.css?<?= $version ?>\" id=\"red-theme\" <?= $colorTheme != \"red\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/green.css?<?= $version ?>\" id=\"green-theme\" <?= $colorTheme != \"green\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/yellow.css?<?= $version ?>\" id=\"yellow-theme\" <?= $colorTheme != \"yellow\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/purple.css?<?= $version ?>\" id=\"purple-theme\" <?= $colorTheme != \"purple\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/login-dark-theme.css?<?= $version ?>\" id=\"dark-theme\" <?= $theme == \"light\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/font-awesome.min.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/barlow.css\">\r\n    <script type=\"text/javascript\">\r\n        window.update_theme_settings = \"<?= $updateThemeSettings ?>\";\r\n        window.colorTheme = \"<?= $colorTheme ?>\";\r\n    </script>\r\n    <script type=\"text/javascript\" src=\"scripts/registration.js?<?= $version ?>\"></script>\r\n</head>\r\n\r\n<body class=\"<?= $languages[$lang]['dir'] ?>\">\r\n    <div class=\"content\">\r\n        <section class=\"container\">\r\n            <header>\r\n                <div class=\"logo-image\" title=\"Wallos - Subscription Tracker\">\r\n                    <?php include \"images/siteicons/svg/logo.php\"; ?>\r\n                </div>\r\n                <p>\r\n                    <?= translate('create_account', $i18n) ?>\r\n                </p>\r\n            </header>\r\n            <form action=\"registration.php\" method=\"post\">\r\n                <div class=\"form-group\">\r\n                    <label for=\"username\"><?= translate('username', $i18n) ?>:</label>\r\n                    <input type=\"text\" id=\"username\" name=\"username\" autocomplete=\"username\" required>\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <label for=\"firstname\"><?= translate('firstname', $i18n) ?>:</label>\r\n                    <input type=\"text\" id=\"firstname\" name=\"firstname\" autocomplete=\"given-name\">\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <label for=\"lastname\"><?= translate('lastname', $i18n) ?>:</label>\r\n                    <input type=\"text\" id=\"lastname\" name=\"lastname\" autocomplete=\"family-name\">\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <label for=\"email\"><?= translate('email', $i18n) ?>:</label>\r\n                    <input type=\"email\" id=\"email\" name=\"email\" autocomplete=\"email\" required>\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <label for=\"password\"><?= translate('password', $i18n) ?>:</label>\r\n                    <input type=\"password\" id=\"password\" name=\"password\" autocomplete=\"new-password\" required>\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <label for=\"confirm_password\"><?= translate('confirm_password', $i18n) ?>:</label>\r\n                    <input type=\"password\" id=\"confirm_password\" name=\"confirm_password\" autocomplete=\"new-password\" required>\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <label for=\"currency\"><?= translate('main_currency', $i18n) ?>:</label>\r\n                    <select id=\"currency\" name=\"main_currency\" placeholder=\"Currency\">\r\n                        <?php\r\n                        foreach ($currencies as $currency) {\r\n                            ?>\r\n                            <option value=\"<?= $currency['code'] ?>\"><?= $currency['name'] ?></option>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                    </select>\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <label for=\"language\"><?= translate('language', $i18n) ?>:</label>\r\n                    <select id=\"language\" name=\"language\" placeholder=\"Language\" onchange=\"changeLanguage(this.value)\">\r\n                        <?php\r\n                        foreach ($languages as $code => $language) {\r\n                            $selected = ($code === $lang) ? 'selected' : '';\r\n                            ?>\r\n                            <option value=\"<?= $code ?>\" <?= $selected ?>><?= $language['name'] ?></option>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                    </select>\r\n                </div>\r\n\r\n                <?php\r\n                if ($hasErrors) {\r\n                    ?>\r\n                    <ul class=\"error-box\">\r\n                        <?php\r\n                        if ($passwordMismatch) {\r\n                            ?>\r\n                            <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('passwords_dont_match', $i18n) ?>\r\n                            </li>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                        <?php\r\n                        if ($usernameExists) {\r\n                            ?>\r\n                            <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('username_exists', $i18n) ?></li>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                        <?php\r\n                        if ($emailExists) {\r\n                            ?>\r\n                            <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('email_exists', $i18n) ?></li>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                        <?php\r\n                        if ($registrationFailed) {\r\n                            ?>\r\n                            <li><i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('registration_failed', $i18n) ?>\r\n                            </li>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                    </ul>\r\n                    <?php\r\n                }\r\n                ?>\r\n\r\n\r\n                <div class=\"form-group\">\r\n                    <input type=\"submit\" value=\"<?= translate('register', $i18n) ?>\">\r\n                </div>\r\n            </form>\r\n            <?php\r\n            if ($userCount == 0) {\r\n                ?>\r\n                <div class=\"separator\">\r\n                    <input type=\"button\" class=\"secondary-button\" value=\"<?= translate('restore_database', $i18n) ?>\"\r\n                        id=\"restoreDB\" onClick=\"openRestoreDBFileSelect()\" />\r\n                    <input type=\"file\" name=\"restoreDBFile\" id=\"restoreDBFile\" style=\"display: none;\" onChange=\"restoreDB()\"\r\n                        accept=\".zip\">\r\n                </div>\r\n                <?php\r\n            } else {\r\n                ?>\r\n                <div class=\"separator\">\r\n                    <input id=\"goToLoginButton\" type=\"button\" class=\"secondary-button\" value=\"<?= translate('login', $i18n) ?>\">\r\n                </div>\r\n                <?php\r\n            }\r\n            ?>\r\n        </section>\r\n    </div>\r\n    <?php\r\n    require_once 'includes/footer.php';\r\n    ?>\r\n</body>\r\n\r\n</html>\r\n"
  },
  {
    "path": "robots.txt",
    "content": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "scripts/admin.js",
    "content": "function makeFetchCall(url, data, button) {\r\n  return fetch(url, {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data),\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n      button.disabled = false;\r\n    })\r\n    .catch((error) => {\r\n      showErrorMessage(error);\r\n      button.disabled = false;\r\n    });\r\n\r\n}\r\n\r\nfunction testSmtpSettingsButton() {\r\n  const button = document.getElementById(\"testSmtpSettingsButton\");\r\n  button.disabled = true;\r\n\r\n  const smtpAddress = document.getElementById(\"smtpaddress\").value;\r\n  const smtpPort = document.getElementById(\"smtpport\").value;\r\n  const encryption = document.querySelector('input[name=\"encryption\"]:checked').value;\r\n  const smtpUsername = document.getElementById(\"smtpusername\").value;\r\n  const smtpPassword = document.getElementById(\"smtppassword\").value;\r\n  const fromEmail = document.getElementById(\"fromemail\").value;\r\n\r\n  const data = {\r\n    smtpaddress: smtpAddress,\r\n    smtpport: smtpPort,\r\n    encryption: encryption,\r\n    smtpusername: smtpUsername,\r\n    smtppassword: smtpPassword,\r\n    fromemail: fromEmail\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/testemailnotifications.php', data, button);\r\n}\r\n\r\nfunction saveSmtpSettingsButton() {\r\n  const button = document.getElementById(\"saveSmtpSettingsButton\");\r\n  button.disabled = true;\r\n\r\n  const smtpAddress = document.getElementById(\"smtpaddress\").value;\r\n  const smtpPort = document.getElementById(\"smtpport\").value;\r\n  const encryption = document.querySelector('input[name=\"encryption\"]:checked').value;\r\n  const smtpUsername = document.getElementById(\"smtpusername\").value;\r\n  const smtpPassword = document.getElementById(\"smtppassword\").value;\r\n  const fromEmail = document.getElementById(\"fromemail\").value;\r\n\r\n  const data = {\r\n    smtpaddress: smtpAddress,\r\n    smtpport: smtpPort,\r\n    encryption: encryption,\r\n    smtpusername: smtpUsername,\r\n    smtppassword: smtpPassword,\r\n    fromemail: fromEmail\r\n  };\r\n\r\n  fetch('endpoints/admin/savesmtpsettings.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data),\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        const emailVerificationCheckbox = document.getElementById('requireEmail');\r\n        emailVerificationCheckbox.disabled = false;\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n      button.disabled = false;\r\n    })\r\n    .catch((error) => {\r\n      showErrorMessage(error);\r\n      button.disabled = false;\r\n    });\r\n\r\n}\r\n\r\nfunction backupDB() {\r\n  const button = document.getElementById(\"backupDB\");\r\n  button.disabled = true;\r\n\r\n  fetch(\"endpoints/db/backup.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        const link = document.createElement(\"a\");\r\n        const filename = data.file;\r\n        link.href = \".tmp/\" + filename;\r\n\r\n        const date = new Date();\r\n        const year = date.getFullYear();\r\n        const month = String(date.getMonth() + 1).padStart(2, \"0\");\r\n        const day = String(date.getDate()).padStart(2, \"0\");\r\n        const hours = String(date.getHours()).padStart(2, \"0\");\r\n        const minutes = String(date.getMinutes()).padStart(2, \"0\");\r\n        const timestamp = `${year}${month}${day}-${hours}${minutes}`;\r\n        link.download = `Wallos-Backup-${timestamp}.zip`;\r\n\r\n        document.body.appendChild(link);\r\n        link.click();\r\n        document.body.removeChild(link);\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"backup_failed\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    })\r\n    .finally(() => {\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction openRestoreDBFileSelect() {\r\n  document.getElementById('restoreDBFile').click();\r\n};\r\n\r\nfunction restoreDB() {\r\n  const input = document.getElementById('restoreDBFile');\r\n  const file = input.files[0];\r\n\r\n  if (!file) {\r\n    showErrorMessage(translate('no_file_selected'));\r\n    return;\r\n  }\r\n\r\n  const formData = new FormData();\r\n  formData.append('file', file);\r\n\r\n  const button = document.getElementById('restoreDB');\r\n  button.disabled = true;\r\n\r\n  fetch('endpoints/db/restore.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'X-CSRF-Token': window.csrfToken, // ✅ CSRF protection\r\n    },\r\n    body: formData,\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n\r\n        // After restoring, run migrations then log out (force re-login)\r\n        fetch('endpoints/db/migrate.php')\r\n          .then(() => {\r\n            window.location.href = 'logout.php';\r\n          })\r\n          .catch(() => {\r\n            window.location.href = 'logout.php';\r\n          });\r\n      } else {\r\n        showErrorMessage(data.message || translate('restore_failed'));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate('unknown_error'));\r\n    })\r\n    .finally(() => {\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\nfunction saveAccountRegistrationsButton() {\r\n  const button = document.getElementById('saveAccountRegistrations');\r\n  button.disabled = true;\r\n\r\n  const open_registrations = document.getElementById('registrations').checked ? 1 : 0;\r\n  const max_users = document.getElementById('maxUsers').value;\r\n  const require_email_validation = document.getElementById('requireEmail').checked ? 1 : 0;\r\n  const server_url = document.getElementById('serverUrl').value;\r\n  const disable_login = document.getElementById('disableLogin').checked ? 1 : 0;\r\n\r\n  const data = {\r\n    open_registrations: open_registrations,\r\n    max_users: max_users,\r\n    require_email_validation: require_email_validation,\r\n    server_url: server_url,\r\n    disable_login: disable_login\r\n  };\r\n\r\n  fetch('endpoints/admin/saveopenregistrations.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data)\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        button.disabled = false;\r\n      } else {\r\n        showErrorMessage(data.message);\r\n        button.disabled = false;\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(error);\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\nfunction saveSecuritySettingsButton() {\r\n  const button = document.getElementById('saveSecuritySettingsButton');\r\n  button.disabled = true;\r\n\r\n  const allowlist = document.getElementById('local_webhook_notifications_allowlist').value;\r\n\r\n  const data = {\r\n    local_webhook_notifications_allowlist: allowlist\r\n  };\r\n\r\n  fetch('endpoints/admin/savesecuritysettings.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data)\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        button.disabled = false;\r\n      } else {\r\n        showErrorMessage(data.message);\r\n        button.disabled = false;\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(error);\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\nfunction removeUser(userId) {\r\n  const data = {\r\n    userId: userId\r\n  };\r\n\r\n  fetch('endpoints/admin/deleteuser.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data)\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        const userContainer = document.querySelector(`.form-group-inline[data-userid=\"${userId}\"]`);\r\n        if (userContainer) {\r\n          userContainer.remove();\r\n        }\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => showErrorMessage('Error:', error));\r\n\r\n}\r\n\r\nfunction addUserButton() {\r\n  const button = document.getElementById('addUserButton');\r\n  button.disabled = true;\r\n\r\n  const username = document.getElementById('newUsername').value;\r\n  const email = document.getElementById('newEmail').value;\r\n  const password = document.getElementById('newPassword').value;\r\n\r\n  const data = {\r\n    username: username,\r\n    email: email,\r\n    password: password\r\n  };\r\n\r\n  fetch('endpoints/admin/adduser.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data)\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        button.disabled = false;\r\n        window.location.reload();\r\n      } else {\r\n        showErrorMessage(data.message);\r\n        button.disabled = false;\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(error);\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\nfunction deleteUnusedLogos() {\r\n  const button = document.getElementById('deleteUnusedLogos');\r\n  button.disabled = true;\r\n\r\n  fetch('endpoints/admin/deleteunusedlogos.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    }\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        const numberOfLogos = document.querySelector('.number-of-logos');\r\n        numberOfLogos.innerText = '0';\r\n      } else {\r\n        showErrorMessage(data.message);\r\n        button.disabled = false;\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(error);\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\nfunction toggleUpdateNotification() {\r\n  const notificationEnabledCheckbox = document.getElementById('updateNotification');\r\n  const notificationEnabled = notificationEnabledCheckbox.checked ? 1 : 0;\r\n\r\n  const data = {\r\n    notificationEnabled: notificationEnabled\r\n  };\r\n\r\n  fetch('endpoints/admin/updatenotification.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data)\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        if (notificationEnabled === 1) {\r\n          fetch('endpoints/cronjobs/checkforupdates.php');\r\n        }\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => showErrorMessage('Error:', error));\r\n\r\n}\r\n\r\nfunction executeCronJob(job) {\r\n  const url = `endpoints/cronjobs/${job}.php`;\r\n  const resultTextArea = document.getElementById('cronjobResult');\r\n\r\n  fetch(url)\r\n    .then(response => {\r\n      return response.text();\r\n    })\r\n    .then(data => {\r\n      const formattedData = data.replace(/<br\\s*\\/?>/gi, '\\n');\r\n      resultTextArea.value = formattedData;\r\n    })\r\n    .catch(error => {\r\n      console.error('Fetch error:', error);\r\n      showErrorMessage('Error:', error);\r\n    });\r\n}\r\n\r\nfunction toggleOidcEnabled() {\r\n  const toggle = document.getElementById(\"oidcEnabled\");\r\n  toggle.disabled = true;\r\n\r\n  const oidcEnabled = toggle.checked ? 1 : 0;\r\n\r\n  const data = {\r\n    oidcEnabled: oidcEnabled\r\n  };\r\n\r\n  fetch('endpoints/admin/enableoidc.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data)\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n      toggle.disabled = false;\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage('Error:', error);\r\n      toggle.disabled = false;\r\n    });\r\n\r\n}\r\n\r\nfunction saveOidcSettingsButton() {\r\n  const button = document.getElementById(\"saveOidcSettingsButton\");\r\n  button.disabled = true;\r\n\r\n  const oidcName = document.getElementById(\"oidcName\").value;\r\n  const oidcClientId = document.getElementById(\"oidcClientId\").value;\r\n  const oidcClientSecret = document.getElementById(\"oidcClientSecret\").value;\r\n  const oidcAuthUrl = document.getElementById(\"oidcAuthUrl\").value;\r\n  const oidcTokenUrl = document.getElementById(\"oidcTokenUrl\").value;\r\n  const oidcUserInfoUrl = document.getElementById(\"oidcUserInfoUrl\").value;\r\n  const oidcRedirectUrl = document.getElementById(\"oidcRedirectUrl\").value;\r\n  const oidcLogoutUrl = document.getElementById(\"oidcLogoutUrl\").value;\r\n  const oidcUserIdentifierField = document.getElementById(\"oidcUserIdentifierField\").value;\r\n  const oidcScopes = document.getElementById(\"oidcScopes\").value;\r\n  const oidcAuthStyle = document.getElementById(\"oidcAuthStyle\").value;\r\n  const oidcAutoCreateUser = document.getElementById(\"oidcAutoCreateUser\").checked ? 1 : 0;\r\n  const oidcPasswordLoginDisabled = document.getElementById(\"oidcPasswordLoginDisabled\").checked ? 1 : 0;\r\n\r\n  const data = {\r\n    oidcName: oidcName,\r\n    oidcClientId: oidcClientId,\r\n    oidcClientSecret: oidcClientSecret,\r\n    oidcAuthUrl: oidcAuthUrl,\r\n    oidcTokenUrl: oidcTokenUrl,\r\n    oidcUserInfoUrl: oidcUserInfoUrl,\r\n    oidcRedirectUrl: oidcRedirectUrl,\r\n    oidcLogoutUrl: oidcLogoutUrl,\r\n    oidcUserIdentifierField: oidcUserIdentifierField,\r\n    oidcScopes: oidcScopes,\r\n    oidcAuthStyle: oidcAuthStyle,\r\n    oidcAutoCreateUser: oidcAutoCreateUser,\r\n    oidcPasswordLoginDisabled: oidcPasswordLoginDisabled\r\n  };\r\n\r\n\r\n  fetch('endpoints/admin/saveoidcsettings.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify(data)\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n      button.disabled = false;\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage('Error:', error);\r\n      button.disabled = false;\r\n    });\r\n}"
  },
  {
    "path": "scripts/all.js",
    "content": "if ('serviceWorker' in navigator) {\r\n  window.addEventListener('load', function() {\r\n    navigator.serviceWorker.register('service-worker.js').then(function(registration) {\r\n      //console.log('ServiceWorker registration successful with scope: ', registration.scope);\r\n    }, function(err) {\r\n      console.log('ServiceWorker registration failed: ', err);\r\n    });\r\n  });\r\n}"
  },
  {
    "path": "scripts/calendar.js",
    "content": "function nextMonth(currentMonth, currentYear) {\r\n  let nextMonth = currentMonth + 1;\r\n  let nextYear = currentYear;\r\n  if (nextMonth > 12) {\r\n    nextMonth = 1;\r\n    nextYear += 1;\r\n  }\r\n  window.location.href = `calendar.php?month=${nextMonth}&year=${nextYear}`;\r\n}\r\n\r\nfunction prevMonth(currentMonth, currentYear) {\r\n  let prevMonth = currentMonth - 1;\r\n  let prevYear = currentYear;\r\n  if (prevMonth < 1) {\r\n    prevMonth = 12;\r\n    prevYear -= 1;\r\n  }\r\n  window.location.href = `calendar.php?month=${prevMonth}&year=${prevYear}`;\r\n}\r\n\r\nfunction currentMoth() {\r\n    window.location.href = `calendar.php`;\r\n}\r\n\r\nfunction closeSubscriptionModal() {\r\n    const modal = document.getElementById('subscriptionModal');\r\n    modal.classList.remove('is-open');\r\n}\r\n\r\nfunction openSubscriptionModal(subscriptionId) {\r\n    const modal = document.getElementById('subscriptionModal');\r\n    const modalContent = document.getElementById('subscriptionModalContent');\r\n\r\n    modalContent.innerHTML = '';\r\n\r\n    fetch('endpoints/subscription/getcalendar.php', {\r\n        method: 'POST',\r\n        body: JSON.stringify({id: subscriptionId}),\r\n        headers: {\r\n          'Content-Type': 'application/json'\r\n        }\r\n      })\r\n      .then(response => response.json())\r\n      .then(data => {\r\n        if (data.success && data.data) {\r\n          const subscription = data.data;\r\n          const html = `\r\n            <div class=\"modal-header\">\r\n                <h3>${subscription.name}</h3>\r\n                <span class=\"fa-solid fa-xmark close-modal\" onclick=\"closeSubscriptionModal()\"></span>\r\n            </div>\r\n            <div class=\"modal-body\">\r\n                ${subscription.logo ? `<div class=\"subscription-logo\">\r\n                <img src=\"images/uploads/logos/${subscription.logo}\" alt=\"${subscription.name}\">\r\n                </div>` : ''}\r\n                <div class=\"subscription-info\">\r\n                ${subscription.price ? `<p><strong>${translate('price')}:</strong> ${subscription.currency}${subscription.price}</p>` : ''}\r\n                ${subscription.category ? `<p><strong>${translate('category')}:</strong> ${subscription.category}</p>` : ''}\r\n                ${subscription.payer_user ? `<p><strong>${translate('paid_by')}:</strong> ${subscription.payer_user}</p>` : ''}\r\n                ${subscription.payment_method ? `<p><strong>${translate('payment_method')}:</strong> ${subscription.payment_method}</p>` : ''}\r\n                ${subscription.notes ? `<p><strong>${translate('notes')}:</strong> ${subscription.notes}</p>` : ''}\r\n                </div>\r\n            </div>\r\n            <div class=\"modal-footer\">\r\n                <button class=\"button tiny\" onclick=\"exportCalendar(${subscription.id})\">${translate('export')}</button>\r\n            </div>`;\r\n          modalContent.innerHTML = html;\r\n          modal.classList.add('is-open');\r\n        } else {\r\n          console.error(data.message);\r\n        }\r\n      })\r\n      .catch(error => console.error('Error:', error));\r\n}\r\n\r\nfunction decodeHtmlEntities(str) {\r\n  const txt = document.createElement('textarea');\r\n  txt.innerHTML = str;\r\n  return txt.value;\r\n}\r\n\r\nfunction exportCalendar(subscriptionId) {\r\n  fetch('endpoints/subscription/exportcalendar.php', {\r\n    method: 'POST',\r\n    body: JSON.stringify({id: subscriptionId}),\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    }\r\n  })\r\n  .then(response => response.json())\r\n  .then(data => {\r\n    if (data.success && data.ics) {\r\n      const blob = new Blob([data.ics], {type: 'text/calendar'});\r\n      const url = window.URL.createObjectURL(blob);\r\n      const a = document.createElement('a');\r\n      a.href = url;\r\n      // Use the subscription name for the file name, replacing any characters that are invalid in file names\r\n      a.download = `${decodeHtmlEntities(data.name).replace(/[\\/\\\\:*?\"<>|]/g, '_').toLowerCase()}.ics`;\r\n      document.body.appendChild(a);\r\n      a.click();\r\n      window.URL.revokeObjectURL(url);\r\n    } else {\r\n      showErrorMessage(data.message);\r\n    }\r\n  })\r\n  .catch(error => console.error('Error:', error));\r\n}\r\n\r\nfunction showExportPopup() {\r\n  const host = window.location.href;\r\n  const apiPath = \"api/subscriptions/get_ical_feed.php\";\r\n  const apiKey = document.getElementById('apiKey').value;\r\n  const queryParams = `?api_key=${apiKey}`;\r\n  const fullUrl = host.replace('calendar.php', apiPath) + queryParams;\r\n  document.getElementById('iCalendarUrl').value = fullUrl;\r\n  document.getElementById('subscriptions_calendar').classList.add('is-open');\r\n}\r\n\r\nfunction closePopup() {\r\n  document.getElementById('subscriptions_calendar').classList.remove('is-open');\r\n}\r\n\r\nfunction copyToClipboard() {\r\n  const urlField = document.getElementById('iCalendarUrl');\r\n  urlField.select();\r\n  urlField.setSelectionRange(0, 99999); // For mobile devices\r\n  navigator.clipboard.writeText(urlField.value)\r\n      .then(() => {\r\n          showSuccessMessage(translate('copied_to_clipboard'));\r\n      })\r\n      .catch(() => {\r\n          showErrorMessage(translate('unknown_error'));\r\n      });\r\n}"
  },
  {
    "path": "scripts/common.js",
    "content": "let isDropdownOpen = false;\r\n\r\nfunction toggleDropdown() {\r\n  const dropdown = document.querySelector('.dropdown');\r\n  dropdown.classList.toggle('is-open');\r\n  isDropdownOpen = !isDropdownOpen;\r\n}\r\n\r\nfunction showErrorMessage(message) {\r\n  const toast = document.querySelector(\".toast#errorToast\");\r\n  const closeIcon = document.querySelector(\".close-error\");\r\n  const errorMessage = document.querySelector(\".errorMessage\");\r\n  const progress = document.querySelector(\".progress.error\");\r\n  let timer1, timer2;\r\n  errorMessage.textContent = message;\r\n  toast.classList.add(\"active\");\r\n  progress.classList.add(\"active\");\r\n  timer1 = setTimeout(() => {\r\n    toast.classList.remove(\"active\");\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  }, 5000);\r\n\r\n  timer2 = setTimeout(() => {\r\n    progress.classList.remove(\"active\");\r\n  }, 5300);\r\n\r\n  closeIcon.addEventListener(\"click\", () => {\r\n    toast.classList.remove(\"active\");\r\n\r\n    setTimeout(() => {\r\n      progress.classList.remove(\"active\");\r\n    }, 300);\r\n\r\n    clearTimeout(timer1);\r\n    clearTimeout(timer2);\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  });\r\n}\r\n\r\nfunction showSuccessMessage(message) {\r\n  const toast = document.querySelector(\".toast#successToast\");\r\n  const closeIcon = document.querySelector(\".close-success\");\r\n  const successMessage = document.querySelector(\".successMessage\");\r\n  const progress = document.querySelector(\".progress.success\");\r\n  let timer1, timer2;\r\n  successMessage.textContent = message;\r\n  toast.classList.add(\"active\");\r\n  progress.classList.add(\"active\");\r\n  timer1 = setTimeout(() => {\r\n    toast.classList.remove(\"active\");\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  }, 5000);\r\n\r\n  timer2 = setTimeout(() => {\r\n    progress.classList.remove(\"active\");\r\n  }, 5300);\r\n\r\n  closeIcon.addEventListener(\"click\", () => {\r\n    toast.classList.remove(\"active\");\r\n\r\n    setTimeout(() => {\r\n      progress.classList.remove(\"active\");\r\n    }, 300);\r\n\r\n    clearTimeout(timer1);\r\n    clearTimeout(timer2);\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  });\r\n}\r\n\r\ndocument.addEventListener('DOMContentLoaded', function () {\r\n\r\n  const userLocale = navigator.language || navigator.languages[0];\r\n  document.cookie = `user_locale=${userLocale}; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict`;\r\n\r\n  if (window.update_theme_settings) {\r\n    const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\r\n    const themePreference = prefersDarkMode ? 'dark' : 'light';\r\n    const darkThemeCss = document.querySelector(\"#dark-theme\");\r\n    darkThemeCss.disabled = themePreference === 'light';\r\n\r\n    // Preserve existing classes on the body tag\r\n    const existingClasses = document.body.className.split(' ').filter(cls => cls !== 'dark' && cls !== 'light');\r\n    document.body.className = [...existingClasses, themePreference].join(' ');\r\n\r\n    document.cookie = `inUseTheme=${themePreference}; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict`;\r\n    const themeColorMetaTag = document.querySelector('meta[name=\"theme-color\"]');\r\n    themeColorMetaTag.setAttribute('content', themePreference === 'dark' ? '#222222' : '#FFFFFF');\r\n  }\r\n\r\n  document.addEventListener('mousedown', function (event) {\r\n    var dropdown = document.querySelector('.dropdown');\r\n    var dropdownContent = document.querySelector('.dropdown-content');\r\n\r\n    if (!dropdown.contains(event.target) && isDropdownOpen) {\r\n      dropdown.classList.remove('is-open');\r\n      isDropdownOpen = false;\r\n    }\r\n  });\r\n\r\n  document.querySelector('.dropdown-content').addEventListener('focus', function () {\r\n    isDropdownOpen = true;\r\n  });\r\n});\r\n\r\nfunction getCookie(name) {\r\n  const cookies = document.cookie.split(';');\r\n  for (let cookie of cookies) {\r\n    cookie = cookie.trim();\r\n    if (cookie.startsWith(`${name}=`)) {\r\n      return cookie.substring(name.length + 1);\r\n    }\r\n  }\r\n  return null;\r\n}"
  },
  {
    "path": "scripts/dashboard.js",
    "content": "document.addEventListener(\"DOMContentLoaded\", function () {\r\n  function updateAiRecommendationNumbers() {\r\n    document.querySelectorAll(\".ai-recommendation-item\").forEach(function (item, index) {\r\n      const numberSpan = item.querySelector(\".ai-recommendation-header h3 > span\");\r\n      if (numberSpan) {\r\n        numberSpan.textContent = `${index + 1}. `;\r\n      }\r\n    });\r\n  }\r\n\r\n  document.querySelectorAll(\".ai-recommendation-item\").forEach(function (item) {\r\n    item.addEventListener(\"click\", function () {\r\n      item.classList.toggle(\"expanded\");\r\n    });\r\n  });\r\n\r\n  document.querySelectorAll(\".delete-ai-recommendation\").forEach(function (el) {\r\n    el.addEventListener(\"click\", function (e) {\r\n      e.preventDefault();\r\n      e.stopPropagation();\r\n\r\n      const item = el.closest(\".ai-recommendation-item\");\r\n      const id = item.getAttribute(\"data-id\");\r\n\r\n      fetch(\"endpoints/ai/delete_recommendation.php\", {\r\n        method: \"POST\",\r\n        headers: {\r\n          \"Content-Type\": \"application/json\",\r\n          \"X-CSRF-Token\": window.csrfToken,\r\n        },\r\n        body: JSON.stringify({ id: id }),\r\n      })\r\n        .then(res => res.json())\r\n        .then(data => {\r\n          if (data.success) {\r\n            item.remove();\r\n            updateAiRecommendationNumbers();\r\n            showSuccessMessage(translate(\"success\"));\r\n          } else {\r\n            showErrorMessage(data.message || translate(\"failed_delete_ai_recommendation\"));\r\n          }\r\n        })\r\n        .catch(error => {\r\n          console.error(error);\r\n          showErrorMessage(translate(\"unknown_error\"));\r\n        });\r\n    });\r\n  });\r\n\r\n});\r\n\r\n"
  },
  {
    "path": "scripts/i18n/ca.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Error al recarregar la subscripció:\",\r\n  error_fetching_image_results: \"Error al obtenir resultats d'imatges:\",\r\n  subscription_deleted: \"Subscripció eliminada\",\r\n  error_deleting_subscription: \"Error al eliminar la subscripció\",\r\n  failed_to_load_subscription: \"Error al carregar la subscripció\",\r\n  edit_subscription: \"Editar Subscripció\",\r\n  add_subscription: \"Afegir Subscripció\",\r\n  confirm_delete_subscription: \"¿Segur que vols eliminar aquesta subscripció?\",\r\n  // Settings\r\n  network_response_error: \"Error en la resposta de la xarxa\",\r\n  failed_add_member: \"Error al afegir membre\",\r\n  member: \"Membre\",\r\n  email: \"Correu electrònic\",\r\n  firstname: \"Nom\",\r\n  lastname: \"Cognom\",\r\n  save_member: \"Desar membre\",\r\n  delete_member: \"Eliminar membre\",\r\n  failed_remove_member: \"Error al eliminar membre\",\r\n  failed_save_member: \"Error al desar membre\",\r\n  failed_add_category: \"Error al afegir categoria\",\r\n  category: \"Categoria\",\r\n  save_category: \"Desar categoria\",\r\n  delete_category: \"Eliminar categoria\",\r\n  failed_remove_category: \"Error al eliminar categoria\",\r\n  currency: \"Divisa\",\r\n  currency_code: \"Còdi de moneda\",\r\n  save_currency: \"Desar divisa\",\r\n  delete_currency: \"Eliminar divisa\",\r\n  failed_remove_currency: \"Error al eliminar divisa\",\r\n  failed_save_currency: \"Error al desar divisa\",\r\n  cant_disable_payment_in_use: \"No es pot desactivar un mètode de pagament en ús\",\r\n  failed_save_payment_method: \"Error al desar el mètode de pagament\",\r\n  unknown_error: \"Error desconegut, si us plau prova-ho de nou.\",\r\n  error_saving_notification_data: \"Error al desar les dades de notificació\",\r\n  error_sending_notification: \"Error al enviar la notificació\",\r\n  delete_account_confirmation: \"Segur que vols eliminar el teu compte?\",\r\n  this_will_delete_all_data: \"S'eliminaran totes les teves dades i no es podran recuperar. Continuar?\",\r\n  success: \"Èxit\",\r\n  copied_to_clipboard: \"Copiat al porta-retalls\",\r\n  // Calendar\r\n  price: \"Preu\",\r\n  category: \"Categoria\",\r\n  paid_by: \"Pagat per\",\r\n  payment_method: \"Mètode de pagment\",\r\n  notes: \"Notes\",\r\n  export: \"Exportar\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/cs.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Chyba při načítání předplatného:\",\n  error_fetching_image_results: \"Chyba při načítání výsledků obrázků:\",\n  subscription_deleted: \"Předplatné odstraněno\",\n  error_deleting_subscription: \"Chyba při odstraňování předplatného\",\n  failed_to_load_subscription: \"Nepodařilo se načíst předplatné\",\n  edit_subscription: \"Upravit předplatné\",\n  add_subscription: \"Přidat předplatné\",\n  confirm_delete_subscription: \"Opravdu chcete odstranit toto předplatné?\",\n  // Settings\n  network_response_error: \"Odezva sítě nebyla v pořádku\",\n  failed_add_member: \"Nepodařilo se přidat člena\",\n  member: \"Člen\",\n  email: \"E-mail\",\n  firstname: \"Křestní jméno\",\n  lastname: \"Příjmení\",\n  save_member: \"Uložit člena\",\n  delete_member: \"Odstranit člena\",\n  failed_remove_member: \"Nepodařilo se odebrat člena\",\n  failed_save_member: \"Nepodařilo se uložit člena\",\n  failed_add_category: \"Nepodařilo se přidat kategorii\",\n  category: \"Kategorie\",\n  save_category: \"Uložit kategorii\",\n  delete_category: \"Odstranit kategorii\",\n  failed_remove_category: \"Nepodařilo se odebrat kategorii\",\n  currency: \"Měna\",\n  currency_code: \"Kód měny\",\n  save_currency: \"Uložit měnu\",\n  delete_currency: \"Odstranit měnu\",\n  failed_remove_currency: \"Nepodařilo se odebrat měnu\",\n  failed_save_currency: \"Nepodařilo se uložit měnu\",\n  cant_disable_payment_in_use: \"Nelze zakázat používanou platbu\",\n  failed_save_payment_method: \"Nepodařilo se uložit platební metodu\",\n  unknown_error: \"Neznámá chyba, zkuste to prosím znovu.\",\n  error_saving_notification_data: \"Chyba při ukládání dat oznámení\",\n  error_sending_notification: \"Chyba při odesílání oznámení\",\n  delete_account_confirmation: \"Opravdu chcete odstranit svůj účet?\",\n  this_will_delete_all_data: \"Tím se odstraní všechna vaše data a nelze to vrátit zpět. Pokračovat?\",\n  success: \"Úspěch\",\n  copied_to_clipboard: \"Zkopírováno do schránky\",\n  // Calendar \n  price: \"Cena\",\n  category: \"Kategorie\",\n  paid_by: \"Platí\",\n  payment_method: \"Platební metoda\",\n  notes: \"Poznámky\",\n  export: \"Exportovat\",\n}\n"
  },
  {
    "path": "scripts/i18n/da.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Fejl ved genindlæsning af abonnement:\",\n  error_fetching_image_results: \"Fejl ved hentning af billedresultater:\",\n  subscription_deleted: \"Abonnement slettet\",\n  error_deleting_subscription: \"Fejl ved sletning af abonnement\",\n  failed_to_load_subscription: \"Kunne ikke indlæse abonnement\",\n  edit_subscription: \"Redigér abonnement\",\n  add_subscription: \"Tilføj abonnement\",\n  confirm_delete_subscription: \"Er du sikker på, at du vil slette dette abonnement?\",\n  // Settings\n  network_response_error: \"Netværkssvaret var ikke i orden\",\n  failed_add_member: \"Kunne ikke tilføje medlem\",\n  member: \"Medlem\",\n  email: \"E-mail\",\n  firstname: \"Fornavn\",\n  lastname: \"Efternavn\",\n  save_member: \"Gem medlem\",\n  delete_member: \"Slet medlem\",\n  failed_remove_member: \"Kunne ikke fjerne medlem\",\n  failed_save_member: \"Kunne ikke gemme medlem\",\n  failed_add_category: \"Kunne ikke tilføje kategori\",\n  category: \"Kategori\",\n  save_category: \"Gem kategori\",\n  delete_category: \"Slet kategori\",\n  failed_remove_category: \"Kunne ikke fjerne kategori\",\n  currency: \"Valuta\",\n  currency_code: \"Valutakode\",\n  save_currency: \"Gem valuta\",\n  delete_currency: \"Slet valuta\",\n  failed_remove_currency: \"Kunne ikke fjerne valuta\",\n  failed_save_currency: \"Kunne ikke gemme valuta\",\n  cant_disable_payment_in_use: \"Kan ikke deaktivere betalingsmetode i brug\",\n  failed_save_payment_method: \"Kunne ikke gemme betalingsmetode\",\n  unknown_error: \"Ukendt fejl, prøv venligst igen.\",\n  error_saving_notification_data: \"Fejl ved lagring af notifikationsdata\",\n  error_sending_notification: \"Fejl ved afsendelse af notifikation\",\n  delete_account_confirmation: \"Er du sikker på, at du vil slette din konto?\",\n  this_will_delete_all_data: \"Dette vil slette alle dine data og kan ikke fortrydes. Fortsæt?\",\n  success: \"Succes\",\n  copied_to_clipboard: \"Kopieret til udklipsholder\",\n  // Calendar \n  price: \"Pris\",\n  category: \"Kategori\",\n  paid_by: \"Betalt af\",\n  payment_method: \"Betalingsmetode\",\n  notes: \"Noter\",\n  export: \"Eksportér\",\n}\n"
  },
  {
    "path": "scripts/i18n/de.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Fehler beim Aktualisieren des Abonnements:\",\r\n  error_fetching_image_results: \"Fehler beim Laden der Bilder:\",\r\n  subscription_deleted: \"Abonnement gelöscht\",\r\n  error_deleting_subscription: \"Fehler beim Löschen des Abonnements\",\r\n  failed_to_load_subscription: \"Fehler beim Laden des Abonnements\",\r\n  edit_subscription: \"Abonnement bearbeiten\",\r\n  add_subscription: \"Abonnement hinzufügen\",\r\n  confirm_delete_subscription: \"Sind Sie sicher, dass Sie dieses Abonnement löschen möchten?\",\r\n  // Settings\r\n  network_response_error: \"Netzwerkfehler\",\r\n  failed_add_member: \"Hinzufügen von Mitglied fehlgeschlagen\",\r\n  member: \"Mitglied\",\r\n  email: \"E-Mail\",\r\n  firstname: \"Vorname\",\r\n  lastname: \"Nachname\",\r\n  save_member: \"Mitglied speichern\",\r\n  delete_member: \"Mitglied löschen\",\r\n  failed_remove_member: \"Mitglied konnte nicht gelöscht werden\",\r\n  failed_save_member: \"Mitglied konnte nicht gespeichert werden\",\r\n  failed_add_category: \"Kategorie konnte nicht hinzugefügt werden\",\r\n  category: \"Kategorie\",\r\n  save_category: \"Kategorie speichern\",\r\n  delete_category: \"Kategorie löschen\",\r\n  failed_remove_category: \"Kategorie konnte nicht gelöscht werden\",\r\n  currency: \"Währung\",\r\n  currency_code: \"Währungscode\",\r\n  save_currency: \"Währung speichern\",\r\n  delete_currency: \"Währung löschen\",\r\n  failed_remove_currency: \"Währung konnte nicht gelöscht werden\",\r\n  failed_save_currency: \"Währung konnte nicht gespeichert werden\",\r\n  cant_disable_payment_in_use:\r\n    \"Genutzte Währungen können nicht deaktiviert werden\",\r\n  failed_save_payment_method: \"Zahlungsmethode konnte nicht gespeichert werden\",\r\n  unknown_error: \"Unbekannter Fehler, bitte erneut versuchen.\",\r\n  error_saving_notification_data:\r\n    \"Fehler beim Speichern der Benachrichtigungsangaben\",\r\n  error_sending_notification: \"Fehler beim Senden der Benachrichtigung\",\r\n  delete_account_confirmation: \"Möchten Sie Ihr Konto wirklich löschen?\",\r\n  this_will_delete_all_data: \"Dadurch werden alle Daten gelöscht und können nicht wiederhergestellt werden. Fortfahren?\",\r\n  success: \"Erfolg\",\r\n  copied_to_clipboard: \"In die Zwischenablage kopiert\",\r\n  // Calendar\r\n  price: \"Preis\",\r\n  category: \"Kategorie\",\r\n  paid_by: \"Bezahlt von\",\r\n  payment_method: \"Zahlungsmethode\",\r\n  notes: \"Notizen\",\r\n  export: \"Exportieren\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/el.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Σφάλμα επαναφόρτωσης συνδρομής:\",\r\n  error_fetching_image_results: \"Σφάλμα λήψης αποτελεσμάτων εικόνας:\",\r\n  subscription_deleted: \"Η συνδρομή διαγράφηκε\",\r\n  error_deleting_subscription: \"Σφάλμα διαγραφής συνδρομής\",\r\n  failed_to_load_subscription: \"Απέτυχε η φόρτωση της συνδρομής\",\r\n  edit_subscription: \"Επεξεργασία συνδρομής\",\r\n  add_subscription: \"Προσθήκη συνδρομής\",\r\n  confirm_delete_subscription: \"Είστε σίγουρος ότι θέλετε να διαγράψετε αυτή τη συνδρομή;\",\r\n  // Settings\r\n  network_response_error: \"Η ανταπόκριση του δικτύου δεν ήταν εντάξει\",\r\n  failed_add_member: \"Αποτυχία προσθήκης μέλους\",\r\n  member: \"Μέλος\",\r\n  email: \"Email\",\r\n  firstname: \"Ονομα\",\r\n  lastname: \"Επώνυμο\",\r\n  save_member: \"Αποθήκευση μέλους\",\r\n  delete_member: \"Διαγραφή μέλους\",\r\n  failed_remove_member: \"Αποτυχία διαγραφής μέλους\",\r\n  failed_save_member: \"Αποτυχία αποθήκευσης μέλους\",\r\n  failed_add_category: \"Αποτυχία προσθήκης μέλους\",\r\n  category: \"Κατηγορία\",\r\n  save_category: \"Αποθήκευση κατηγορίας\",\r\n  delete_category: \"Διαγραφή κατηγορίας\",\r\n  failed_remove_category: \"Αποτυχία διαγραφής κατηγορίας\",\r\n  currency: \"Νόμισμα\",\r\n  currency_code: \"Κωδικός νομίσματος\",\r\n  save_currency: \"Αποθήκευση νομίσματος\",\r\n  delete_currency: \"Διαγραφή νομίσματος\",\r\n  failed_remove_currency: \"Αποτυχία διαγραφής νομίσματος\",\r\n  failed_save_currency: \"Αποτυχία αποθήκευσης νομίσματος\",\r\n  cant_disable_payment_in_use: \"Δεν ειναι εφικτή η απενεργοποίηση της πληρωμή που βρίσκεται σε χρήση\",\r\n  failed_save_payment_method: \"Failed to save payment method\",\r\n  unknown_error: \"Άγνωστο σφάλμα, προσπάθησε ξανά.\",\r\n  error_saving_notification_data: \"Σφάλμα αποθήκευσης δεδομένων ειδοποίησης\",\r\n  error_sending_notification: \"Σφάλμα αποστολής ειδοποίησης\",\r\n  delete_account_confirmation: \"Είστε σίγουρος ότι θέλετε να διαγράψετε το λογαριασμό σας;\",\r\n  this_will_delete_all_data: \"Αυτό θα διαγράψει όλα τα δεδομένα σας και δεν μπορεί να ανακτηθεί. Να συνεχίσω;\",\r\n  success: \"Επιτυχία\",\r\n  copied_to_clipboard: \"Αντιγράφηκε στο πρόχειρο\",\r\n  // Calendar\r\n  price: \"Τιμή\",\r\n  category: \"Κατηγορία\",\r\n  paid_by: \"Πληρώθηκε από\",\r\n  payment_method: \"Μέθοδος πληρωμής\",\r\n  notes: \"Σημειώσεις\",\r\n  export: \"Εξαγωγή\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/en.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Error reloading subscription:\",\r\n  error_fetching_image_results: \"Error fetching image results:\",\r\n  subscription_deleted: \"Subscription deleted\",\r\n  error_deleting_subscription: \"Error deleting subscription\",\r\n  failed_to_load_subscription: \"Failed to load subscription\",\r\n  edit_subscription: \"Edit subscription\",\r\n  add_subscription: \"Add subscription\",\r\n  confirm_delete_subscription: \"Are you sure you want to delete this subscription?\",\r\n  // Settings\r\n  network_response_error: \"Network response was not ok\",\r\n  failed_add_member: \"Failed to add member\",\r\n  member: \"Member\",\r\n  email: \"Email\",\r\n  firstname: \"First name\",\r\n  lastname: \"Last name\",\r\n  save_member: \"Save member\",\r\n  delete_member: \"Delete member\",\r\n  failed_remove_member: \"Failed to remove member\",\r\n  failed_save_member: \"Failed to save member\",\r\n  failed_add_category: \"Failed to add category\",\r\n  category: \"Category\",\r\n  save_category: \"Save category\",\r\n  delete_category: \"Delete category\",\r\n  failed_remove_category: \"Failed to remove category\",\r\n  currency: \"Currency\",\r\n  currency_code: \"Currency code\",\r\n  save_currency: \"Save currency\",\r\n  delete_currency: \"Delete currency\",\r\n  failed_remove_currency: \"Failed to remove currency\",\r\n  failed_save_currency: \"Failed to save currency\",\r\n  cant_disable_payment_in_use: \"Can't disable payment in use\",\r\n  failed_save_payment_method: \"Failed to save payment method\",\r\n  unknown_error: \"Unknown error, please try again.\",\r\n  error_saving_notification_data: \"Error saving notification data\",\r\n  error_sending_notification: \"Error sending notification\",\r\n  delete_account_confirmation: \"Are you sure you want to delete your account?\",\r\n  this_will_delete_all_data: \"This will delete all your data and can't be undone. Continue?\",\r\n  success: \"Success\",\r\n  copied_to_clipboard: \"Copied to clipboard\",\r\n  // Calendar \r\n  price: \"Price\",\r\n  category: \"Category\",\r\n  paid_by: \"Paid by\",\r\n  payment_method: \"Payment method\",\r\n  notes: \"Notes\",\r\n  export: \"Export\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/es.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Error al recargar la suscripción:\",\r\n  error_fetching_image_results: \"Error al obtener resultados de imágenes:\",\r\n  subscription_deleted: \"Suscripción eliminada\",\r\n  error_deleting_subscription: \"Error al eliminar la suscripción\",\r\n  failed_to_load_subscription: \"Error al cargar la suscripción\",\r\n  edit_subscription: \"Editar suscripción\",\r\n  add_subscription: \"Añadir suscripción\",\r\n  confirm_delete_subscription: \"¿Estás seguro de que quieres eliminar esta suscripción?\",\r\n  // Settings\r\n  network_response_error: \"Error en la respuesta de la red\",\r\n  failed_add_member: \"Error al añadir miembro\",\r\n  member: \"Miembro\",\r\n  email: \"Correo electrónico\",\r\n  firstname: \"Nombre de pila\",\r\n  lastname: \"Apellido\",\r\n  save_member: \"Guardar miembro\",\r\n  delete_member: \"Eliminar miembro\",\r\n  failed_remove_member: \"Error al eliminar miembro\",\r\n  failed_save_member: \"Error al guardar miembro\",\r\n  failed_add_category: \"Error al añadir categoría\",\r\n  category: \"Categoría\",\r\n  save_category: \"Guardar categoría\",\r\n  delete_category: \"Eliminar categoría\",\r\n  failed_remove_category: \"Error al eliminar categoría\",\r\n  currency: \"Moneda\",\r\n  currency_code: \"Código de moneda\",\r\n  save_currency: \"Guardar moneda\",\r\n  delete_currency: \"Eliminar moneda\",\r\n  failed_remove_currency: \"Error al eliminar moneda\",\r\n  failed_save_currency: \"Error al guardar moneda\",\r\n  cant_disable_payment_in_use: \"No se puede desactivar el método de pago en uso\",\r\n  failed_save_payment_method: \"Error al guardar el método de pago\",\r\n  unknown_error: \"Error desconocido, por favor inténtalo de nuevo.\",\r\n  error_saving_notification_data: \"Error al guardar los datos de notificación\",\r\n  error_sending_notification: \"Error al enviar la notificación\",\r\n  delete_account_confirmation: \"¿Estás seguro de que quieres eliminar tu cuenta?\",\r\n  this_will_delete_all_data: \"Esto eliminará todos tus datos y no se podrán recuperar. ¿Continuar?\",\r\n  success: \"Éxito\",\r\n  copied_to_clipboard: \"Copiado al portapapeles\",\r\n  // Calendar\r\n  price: \"Precio\",\r\n  category: \"Categoría\",\r\n  paid_by: \"Pagado por\",\r\n  payment_method: \"Método de pago\",\r\n  notes: \"Notas\",\r\n  export: \"Exportar\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/fr.js",
    "content": "let i18n = {\n    // Tableau de bord\n    error_reloading_subscription: \"Erreur lors du rechargement de l'abonnement :\",\n    error_fetching_image_results: \"Erreur lors de la récupération des résultats d'images :\",\n    subscription_deleted: \"Abonnement supprimé\",\n    error_deleting_subscription: \"Erreur lors de la suppression de l'abonnement\",\n    failed_to_load_subscription: \"Impossible de charger l'abonnement\",\n    edit_subscription: \"Modifier l'abonnement\",\n    add_subscription: \"Ajouter un abonnement\",\n    confirm_delete_subscription: \"Êtes-vous sûr de vouloir supprimer cet abonnement ?\",\n    // Paramètres\n    network_response_error: \"La réponse du réseau n'était pas correcte\",\n    failed_add_member: \"Échec de l'ajout du membre\",\n    member: \"Membre\",\n    email: \"Courriel\",\n    firstname: \"Prénom\",\n    lastname: \"Nom de famille\",\n    save_member: \"Enregistrer le membre\",\n    delete_member: \"Supprimer le membre\",\n    failed_remove_member: \"Échec de la suppression du membre\",\n    failed_save_member: \"Échec de l'enregistrement du membre\",\n    failed_add_category: \"Échec de l'ajout de la catégorie\",\n    category: \"Catégorie\",\n    save_category: \"Enregistrer la catégorie\",\n    delete_category: \"Supprimer la catégorie\",\n    failed_remove_category: \"Échec de la suppression de la catégorie\",\n    currency: \"Devise\",\n    currency_code: \"Code de devise\",\n    save_currency: \"Enregistrer la devise\",\n    delete_currency: \"Supprimer la devise\",\n    failed_remove_currency: \"Échec de la suppression de la devise\",\n    failed_save_currency: \"Échec de l'enregistrement de la devise\",\n    cant_disable_payment_in_use: \"Impossible de désactiver le paiement en cours d'utilisation\",\n    failed_save_payment_method: \"Échec de l'enregistrement de la méthode de paiement\",\n    unknown_error: \"Erreur inconnue, veuillez réessayer.\",\n    error_saving_notification_data: \"Erreur lors de l'enregistrement des données de notification\",\n    error_sending_notification: \"Erreur lors de l'envoi de la notification\",\n    delete_account_confirmation: \"Êtes-vous sûr de vouloir supprimer votre compte ?\",\n    this_will_delete_all_data: \"Cela supprimera toutes vos données et ne pourra pas être annulé. Continuer ?\",\n    success: \"Succès\",\n    copied_to_clipboard: \"Copié dans le presse-papiers\",\n    // Calendar\n    price: \"Prix\",\n    category: \"Catégorie\",\n    paid_by: \"Payé par\",\n    payment_method: \"Méthode de paiement\",\n    notes: \"Notes\",\n    export: \"Exporter\",\n};\n"
  },
  {
    "path": "scripts/i18n/getlang.js",
    "content": "function translate(key) {\r\n    if (i18n[key]) {\r\n        return i18n[key];\r\n    } else {\r\n        return \"[Translation Missing]\";\r\n    }\r\n}"
  },
  {
    "path": "scripts/i18n/id.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Terjadi kesalahan saat memuat ulang langganan:\",\n  error_fetching_image_results: \"Terjadi kesalahan saat mengambil hasil gambar:\",\n  subscription_deleted: \"Langganan dihapus\",\n  error_deleting_subscription: \"Terjadi kesalahan saat menghapus langganan\",\n  failed_to_load_subscription: \"Gagal memuat langganan\",\n  edit_subscription: \"Edit langganan\",\n  add_subscription: \"Tambah langganan\",\n  confirm_delete_subscription: \"Apakah Anda yakin ingin menghapus langganan ini?\",\n  // Settings\n  network_response_error: \"Respons jaringan tidak baik\",\n  failed_add_member: \"Gagal menambahkan anggota\",\n  member: \"Anggota\",\n  email: \"Email\",\n  firstname: \"Nama depan\",\n  lastname: \"Nama belakang\",\n  save_member: \"Simpan anggota\",\n  delete_member: \"Hapus anggota\",\n  failed_remove_member: \"Gagal menghapus anggota\",\n  failed_save_member: \"Gagal menyimpan anggota\",\n  failed_add_category: \"Gagal menambahkan kategori\",\n  category: \"Kategori\",\n  save_category: \"Simpan kategori\",\n  delete_category: \"Hapus kategori\",\n  failed_remove_category: \"Gagal menghapus kategori\",\n  currency: \"Mata Uang\",\n  currency_code: \"Kode mata uang\",\n  save_currency: \"Simpan mata uang\",\n  delete_currency: \"Hapus mata uang\",\n  failed_remove_currency: \"Gagal menghapus mata uang\",\n  failed_save_currency: \"Gagal menyimpan mata uang\",\n  cant_disable_payment_in_use: \"Tidak dapat menonaktifkan metode pembayaran yang sedang digunakan\",\n  failed_save_payment_method: \"Gagal menyimpan metode pembayaran\",\n  unknown_error: \"Kesalahan tidak diketahui, silakan coba lagi.\",\n  error_saving_notification_data: \"Terjadi kesalahan saat menyimpan data notifikasi\",\n  error_sending_notification: \"Terjadi kesalahan saat mengirim notifikasi\",\n  delete_account_confirmation: \"Apakah Anda yakin ingin menghapus akun Anda?\",\n  this_will_delete_all_data: \"Ini akan menghapus semua data Anda dan tidak dapat dibatalkan. Lanjutkan?\",\n  success: \"Sukses\",\n  copied_to_clipboard: \"Disalin ke papan klip\",\n  // Calendar\n  price: \"Harga\",\n  category: \"Kategori\", // Kunci ini sudah ada di bagian \"Settings\", namun karena konteksnya bisa berbeda (misalnya di tampilan Kalender), saya tetap menerjemahkannya di sini juga.\n  paid_by: \"Dibayar oleh\",\n  payment_method: \"Metode pembayaran\",\n  notes: \"Catatan\",\n  export: \"Ekspor\",\n}\n"
  },
  {
    "path": "scripts/i18n/it.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Errore nel ricaricare l'abbonamento:\",\r\n  error_fetching_image_results: \"Errore nel recupero delle immagini:\",\r\n  subscription_deleted: \"Abbonamento eliminato\",\r\n  error_deleting_subscription: \"Errore nell'eliminare l'abbonamento\",\r\n  failed_to_load_subscription: \"Caricamento dell'abbonamento non riuscito\",\r\n  edit_subscription: \"Modifica abbonamento\",\r\n  add_subscription: \"Aggiungi abbonamento\",\r\n  confirm_delete_subscription: \"Sei sicuro di voler eliminare questo abbonamento?\",\r\n  \r\n  // Settings\r\n  network_response_error: \"Si è verificato un errore nella risposta del server\",\r\n  failed_add_member: \"Impossibile aggiungere il membro\",\r\n  member: \"Membro\",\r\n  email: \"Email\",\r\n  firstname: \"Nome di battesimo\",\r\n  lastname: \"Cognome\",\r\n  save_member: \"Salva membro\",\r\n  delete_member: \"Elimina membro\",\r\n  failed_remove_member: \"Impossibile rimuovere il membro\",\r\n  failed_save_member: \"Impossibile salvare il membro\",\r\n  failed_add_category: \"Impossibile aggiungere la categoria\",\r\n  category: \"Categoria\",\r\n  save_category: \"Salva categoria\",\r\n  delete_category: \"Elimina categoria\",\r\n  failed_remove_category: \"Impossibile rimuovere la categoria\",\r\n  currency: \"Valuta\",\r\n  currency_code: \"Codice valuta\",\r\n  save_currency: \"Salva valuta\",\r\n  delete_currency: \"Elimina valuta\",\r\n  failed_remove_currency: \"Impossibile rimuovere la valuta\",\r\n  failed_save_currency: \"Impossibile salvare la valuta\",\r\n  cant_disable_payment_in_use: \"Impossibile disabilitare il pagamento in uso\",\r\n  failed_save_payment_method: \"Impossibile salvare il metodo di pagamento\",\r\n  unknown_error: \"Errore sconosciuto, si prega di riprovare.\",\r\n  error_saving_notification_data: \"Errore nel salvataggio delle impostazioni di notifica\",\r\n  error_sending_notification: \"Errore nell'invio della notifica\",\r\n  delete_account_confirmation: \"Sei sicuro di voler eliminare il tuo account?\",\r\n  this_will_delete_all_data: \"Questo eliminerà tutti i tuoi dati e non potrà essere annullato. Continuare?\",\r\n  success: \"Successo\",\r\n  copied_to_clipboard: \"Copiato negli appunti\",\r\n  // Calendar\r\n  price: \"Prezzo\",\r\n  category: \"Categoria\",\r\n  paid_by: \"Pagato da\",\r\n  payment_method: \"Metodo di pagamento\",\r\n  notes: \"Note\",\r\n  export: \"Esporta\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/jp.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"定期購入の再読み込みエラー:\",\r\n  error_fetching_image_results: \"画像取得結果エラー:\",\r\n  subscription_deleted: \"定期購入の削除\",\r\n  error_deleting_subscription: \"定期購入の削除エラー\",\r\n  failed_to_load_subscription: \"定期購入の読み込みに失敗しました\",\r\n  edit_subscription: \"定期購入の編集\",\r\n  add_subscription: \"定期購入の追加\",\r\n  confirm_delete_subscription: \"この定期購入を削除してもよろしいですか？\",\r\n  // Settings\r\n  network_response_error: \"ネットワークの応答異常\",\r\n  failed_add_member: \"世帯員の追加に失敗\",\r\n  member: \"世帯員\",\r\n  email: \"メール\",\r\n  firstname: \"ファーストネーム\",\r\n  lastname: \"苗字\",\r\n  save_member: \"世帯員の保存\",\r\n  delete_member: \"世帯員の削除\",\r\n  failed_remove_member: \"世帯員の削除に失敗\",\r\n  failed_save_member: \"世帯員の削除に失敗\",\r\n  failed_add_category: \"カテゴリの追加に失敗\",\r\n  category: \"カテゴリ\",\r\n  save_category: \"カテゴリの保存\",\r\n  delete_category: \"カテゴリの削除\",\r\n  failed_remove_category: \"カテゴリの削除に失敗\",\r\n  currency: \"通貨\",\r\n  currency_code: \"通貨コード\",\r\n  save_currency: \"通貨の保存\",\r\n  delete_currency: \"通貨の削除\",\r\n  failed_remove_currency: \"通貨の削除に失敗\",\r\n  failed_save_currency: \"通貨の保存に失敗\",\r\n  cant_disable_payment_in_use: \"使用中の支払いは無効にできません\",\r\n  failed_save_payment_method: \"支払い方法の保存に失敗\",\r\n  unknown_error: \"不明なエラー。もう一度試してください。\",\r\n  error_saving_notification_data: \"通知データの保存エラー\",\r\n  error_sending_notification: \"通知の送信エラー\",\r\n  delete_account_confirmation: \"アカウントを削除してもよろしいですか？\",\r\n  this_will_delete_all_data: \"これによりすべてのデータが削除され、元に戻すことはできません。続行しますか？\",\r\n  success: \"成功\",\r\n  copied_to_clipboard: \"クリップボードにコピーされました\",\r\n  // Calendar\r\n  price: \"価格\",\r\n  category: \"カテゴリ\",\r\n  paid_by: \"支払い者\",\r\n  payment_method: \"支払い方法\",\r\n  notes: \"メモ\",\r\n  export: \"エクスポート\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/ko.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"고독 새로고침 중 에러가 발생했습니다:\",\n  error_fetching_image_results: \"이미지 가져오기에 실패했습니다:\",\n  subscription_deleted: \"구독이 삭제되었습니다\",\n  error_deleting_subscription: \"구독 삭제중 에러가 발생했습니다\",\n  failed_to_load_subscription: \"구독 불러오기에 실패했습니다\",\n  edit_subscription: \"구독 수정\",\n  add_subscription: \"구독 추가\",\n  confirm_delete_subscription: \"이 구독을 정말 삭제하시겠습니까?\",\n  // Settings\n  network_response_error: \"네트워크 응답 오류가 발생했습니다\",\n  failed_add_member: \"구성원 추가에 실패했습니다\",\n  member: \"구성원\",\n  email: \"이메일\",\n  firstname: \"이름\",\n  lastname: \"성\",\n  save_member: \"구성원 저장\",\n  delete_member: \"구성원 삭제\",\n  failed_remove_member: \"구성원 삭제에 실패했습니다\",\n  failed_save_member: \"구성원 저장에 실패했습니다\",\n  failed_add_category: \"카테고리 추가에 실패했습니다\",\n  category: \"카테고리\",\n  save_category: \"카테고리 저장\",\n  delete_category: \"카테고리 삭제\",\n  failed_remove_category: \"카테고리 삭제에 실패했습니다\",\n  currency: \"통화\",\n  currency_code: \"통화 코드\",\n  save_currency: \"통화 저장\",\n  delete_currency: \"통화 삭제\",\n  failed_remove_currency: \"통화 삭제에 실패했습니다\",\n  failed_save_currency: \"통화 저장에 실패했습니다\",\n  cant_disable_payment_in_use: \"사용 중인 결제 수단을 비활성화 할 수 없습니다\",\n  failed_save_payment_method: \"결제 수단 저장에 실패했습니다\",\n  unknown_error: \"알 수 없는 에러입니다. 다시 시도해 주세요.\",\n  error_saving_notification_data: \"알림 데이터 저장 에러\",\n  error_sending_notification: \"알림 전송 에러\",\n  delete_account_confirmation: \"정말 계정을 삭제하시겠습니까?\",\n  this_will_delete_all_data: \"이로 인해 모든 데이터가 삭제되며 복구할 수 없습니다. 계속하시겠습니까?\",\n  success: \"성공\",\n  copied_to_clipboard: \"클립보드에 복사되었습니다\",\n  // Calendar\n  price: \"가격\",\n  category: \"카테고리\",\n  paid_by: \"지불자\",\n  payment_method: \"결제 수단\",\n  notes: \"메모\",\n  export: \"내보내기\",\n};\n"
  },
  {
    "path": "scripts/i18n/nl.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Fout bij het herladen van abonnement:\",\n  error_fetching_image_results: \"Fout bij het ophalen van afbeeldingsresultaten:\",\n  subscription_deleted: \"Abonnement verwijderd\",\n  error_deleting_subscription: \"Fout bij het verwijderen van abonnement\",\n  failed_to_load_subscription: \"Laden van abonnement mislukt\",\n  edit_subscription: \"Abonnement bewerken\",\n  add_subscription: \"Abonnement toevoegen\",\n  confirm_delete_subscription: \"Weet u zeker dat u dit abonnement wilt verwijderen?\",\n  // Settings\n  network_response_error: \"Netwerkreactie was niet in orde\",\n  failed_add_member: \"Lid toevoegen mislukt\",\n  member: \"Lid\",\n  email: \"E-mail\",\n  firstname: \"Voornaam\",\n  lastname: \"Achternaam\",\n  save_member: \"Lid opslaan\",\n  delete_member: \"Lid verwijderen\",\n  failed_remove_member: \"Lid verwijderen mislukt\",\n  failed_save_member: \"Lid opslaan mislukt\",\n  failed_add_category: \"Categorie toevoegen mislukt\",\n  category: \"Categorie\",\n  save_category: \"Categorie opslaan\",\n  delete_category: \"Categorie verwijderen\",\n  failed_remove_category: \"Categorie verwijderen mislukt\",\n  currency: \"Valuta\",\n  currency_code: \"Valutacode\",\n  save_currency: \"Valuta opslaan\",\n  delete_currency: \"Valuta verwijderen\",\n  failed_remove_currency: \"Valuta verwijderen mislukt\",\n  failed_save_currency: \"Valuta opslaan mislukt\",\n  cant_disable_payment_in_use: \"Kan in gebruik zijnde betaalmethode niet uitschakelen\",\n  failed_save_payment_method: \"Betaalmethode opslaan mislukt\",\n  unknown_error: \"Onbekende fout, probeer het opnieuw.\",\n  error_saving_notification_data: \"Fout bij opslaan van notificatiegegevens\",\n  error_sending_notification: \"Fout bij versturen van notificatie\",\n  delete_account_confirmation: \"Weet je zeker dat je je account wilt verwijderen?\",\n  this_will_delete_all_data: \"Dit zal al je gegevens verwijderen en kan niet ongedaan worden gemaakt. Doorgaan?\",\n  success: \"Succes\",\n  copied_to_clipboard: \"Gekopieerd naar klembord\",\n  // Calendar \n  price: \"Prijs\",\n  category: \"Categorie\",\n  paid_by: \"Betaald door\",\n  payment_method: \"Betaalmethode\",\n  notes: \"Notities\",\n  export: \"Exporteren\"\n}\n"
  },
  {
    "path": "scripts/i18n/pl.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Błąd przeładowania subskrypcji:\",\r\n  error_fetching_image_results: \"Błąd pobierania wyników obrazu:\",\r\n  subscription_deleted: \"Subskrypcja usunięta\",\r\n  error_deleting_subscription: \"Błąd usunięcia subskrypcji\",\r\n  failed_to_load_subscription: \"Nie udało się załadować subskrypcji\",\r\n  edit_subscription: \"Edytuj subskrypcję\",\r\n  add_subscription: \"Dodaj subskrypcję\",\r\n  confirm_delete_subscription: \"Czy na pewno chcesz usunąć tę subskrypcję?\",\r\n  // Settings\r\n  network_response_error: \"Odpowiedź sieciowa nie była prawidłowa\",\r\n  failed_add_member: \"Nie udało się dodać użytkownika\",\r\n  member: \"Użytkownik\",\r\n  email: \"E-mail\",\r\n  firstname: \"Imię\",\r\n  lastname: \"Nazwisko\",\r\n  save_member: \"Zapisz użytkownika\",\r\n  delete_member: \"Usuń użytkownika\",\r\n  failed_remove_member: \"Nie udało się usunąć użytkownika\",\r\n  failed_save_member: \"Nie udało się zapisać użytkownika\",\r\n  failed_add_category: \"Nie udało się dodać kategorii\",\r\n  category: \"Kategoria\",\r\n  save_category: \"Zapisz kategorię\",\r\n  delete_category: \"Usuń kategorię\",\r\n  failed_remove_category: \"Nie udało się usunąć kategorii\",\r\n  currency: \"Waluta\",\r\n  currency_code: \"Kod waluty\",\r\n  save_currency: \"Zapisz walutę\",\r\n  delete_currency: \"Usuń walutę\",\r\n  failed_remove_currency: \"Nie udało się usunąć waluty\",\r\n  failed_save_currency: \"Nie udało się zapisać waluty\",\r\n  cant_disable_payment_in_use: \"Nie można wyłączyć płatności w użyciu\",\r\n  failed_save_payment_method: \"Nie udało się zapisać metody płatności\",\r\n  unknown_error: \"Nieznany błąd, spróbuj ponownie.\",\r\n  error_saving_notification_data: \"Błąd zapisywania danych powiadomienia\",\r\n  error_sending_notification: \"Błąd wysyłania powiadomienia\",\r\n  delete_account_confirmation: \"Czy na pewno chcesz usunąć swoje konto?\",\r\n  this_will_delete_all_data: \"Spowoduje to usunięcie wszystkich danych i nie będzie można tego cofnąć. Kontynuować?\",\r\n  success: \"Sukces\",\r\n  copied_to_clipboard: \"Skopiowano do schowka\",\r\n  // Calendar\r\n  price: \"Cena\",\r\n  category: \"Kategoria\",\r\n  paid_by: \"Zapłacone przez\",\r\n  payment_method: \"Metoda płatności\",\r\n  notes: \"Notatki\",\r\n  export: \"Eksport\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/pt.js",
    "content": "let i18n = {\r\n    // Dashboard\r\n    error_reloading_subscription: 'Erro ao carregar a subscrição:',\r\n    error_fetching_image_results: 'Erro ao obter imagens:',\r\n    subscription_deleted: 'Subscrição eliminada',\r\n    error_deleting_subscription: 'Erro ao eliminar a subscrição',\r\n    failed_to_load_subscription: 'Falha ao carregar a subscrição',\r\n    edit_subscription: 'Editar subscrição',\r\n    add_subscription: 'Adicionar subscrição',\r\n    confirm_delete_subscription: 'Tem a certeza de que deseja eliminar esta subscrição?',\r\n    // Settings\r\n    network_response_error: 'Erro de resposta de rede',\r\n    failed_add_member: 'Falha ao adicionar membro',\r\n    member: 'Membro',\r\n    email: 'Email',\r\n    firstname: 'Nome próprio',\r\n    lastname: 'Último nome',\r\n    save_member: 'Guardar membro',\r\n    delete_member: 'Remover membro',\r\n    failed_remove_member: 'Erro ao remover membro',\r\n    failed_save_member: 'Erro ao guardar membro',\r\n    failed_add_category: 'Erro ao adicionar categoria',\r\n    category: 'Categoria',\r\n    save_category: 'Guardar categoria',\r\n    delete_category: 'Remover categoria',\r\n    failed_remove_category: 'Erro ao remover categoria',\r\n    currency: 'Moeda',\r\n    currency_code: 'Código de moeda',\r\n    save_currency: 'Guardar moeda',\r\n    delete_currency: 'Remover moeda',\r\n    failed_remove_currency: 'Erro ao remover moeda',\r\n    failed_save_currency: 'Erro ao guardar moeda',\r\n    cant_disable_payment_in_use: 'Não é possível desativar pagamento em uso',\r\n    failed_save_payment_method: 'Erro ao guardar método de pagamento',\r\n    unknown_error: 'Erro desconhecido, por favor, tente novamente.',\r\n    error_saving_notification_data: 'Erro ao guardar dados de notificação',\r\n    error_sending_notification: 'Erro ao enviar notificação',\r\n    delete_account_confirmation: \"Tem a certeza de que deseja eliminar a sua conta?\",\r\n    this_will_delete_all_data: \"Isto irá eliminar todos os seus dados e não poderão ser recuperados. Continuar?\",\r\n    success: \"Sucesso\",\r\n    copied_to_clipboard: \"Copiado para a área de transferência\",\r\n    // Calendar\r\n    price: \"Preço\",\r\n    category: \"Categoria\",\r\n    paid_by: \"Pago por\",\r\n    payment_method: \"Método de pagamento\",\r\n    notes: \"Notas\",\r\n    export: \"Exportar\",\r\n};\r\n"
  },
  {
    "path": "scripts/i18n/pt_br.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Erro ao recarregar assinaturas:\",\n  error_fetching_image_results: \"Erro ao carregar resultados de imagens:\",\n  subscription_deleted: \"Assinatura excluída\",\n  error_deleting_subscription: \"Erro ao excluir assinatura\",\n  failed_to_load_subscription: \"Erro ao carregar assinaturas\",\n  edit_subscription: \"Editar assinatura\",\n  add_subscription: \"Adicionar assinatura\",\n  confirm_delete_subscription: \"Você tem certeza que deseja excluir essa assinatura?\",\n  // Settings\n  network_response_error: \"Resposta da rede não foi OK\",\n  failed_add_member: \"Erro ao adicionar membro\",\n  member: \"Membro\",\n  email: \"Email\",\n  firstname: \"Primeiro nome\",\n  lastname: \"Sobrenome\",\n  save_member: \"Salvar membro\",\n  delete_member: \"Excluir membro\",\n  failed_remove_member: \"Erro ao excluir membro\",\n  failed_save_member: \"Erro ao salvar membro\",\n  failed_add_category: \"Erro ao adicionar categoria\",\n  category: \"Categoria\",\n  save_category: \"Salvar categoria\",\n  delete_category: \"Excluir categoria\",\n  failed_remove_category: \"Erro ao excluir categoria\",\n  currency: \"Moeda\",\n  currency_code: \"Código da moeda\",\n  save_currency: \"Salvar moeda\",\n  delete_currency: \"Excluir moeda\",\n  failed_remove_currency: \"Erro ao excluir moeda\",\n  failed_save_currency: \"Error ao salvar moeda\",\n  cant_disable_payment_in_use: \"Não é possível desativar uma moeda em uso\",\n  failed_save_payment_method: \"Erro ao salvar o método de pagamento\",\n  unknown_error: \"Erro desconhecido. Por favor, tente novamente\",\n  error_saving_notification_data: \"Erro ao salvar dados da notificação\",\n  error_sending_notification: \"Erro ao enviar notificação\",\n  delete_account_confirmation: \"Você tem certeza que deseja excluir sua conta?\",\n  this_will_delete_all_data: \"Isso excluirá todos os seus dados e não poderão ser recuperados. Continuar?\",\n  success: \"Sucesso\",\n  copied_to_clipboard: \"Copiado para a área de transferência\",\n  // Calendar\n  price: \"Preço\",\n  category: \"Categoria\",\n  paid_by: \"Pago por\",\n  payment_method: \"Método de pagamento\",\n  notes: \"Notas\",\n  export: \"Exportar\",\n}\n"
  },
  {
    "path": "scripts/i18n/ro.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Eroare la reîncărcarea abonamentului:\",\n  error_fetching_image_results: \"Eroare la încărcarea imaginilor:\",\n  subscription_deleted: \"Abonamentul a fost șters\",\n  error_deleting_subscription: \"Eroare la ștergerea abonamentului\",\n  failed_to_load_subscription: \"Nu s-a reușit încărcarea abonamentului\",\n  edit_subscription: \"Modifică abonamentul\",\n  add_subscription: \"Adaugă abonament\",\n  confirm_delete_subscription: \"Ești sigur că vrei să ștergi acest abonament?\",\n  // Settings\n  network_response_error: \"Răspuns de rețea invalid\",\n  failed_add_member: \"Nu s-a putut adăuga membrul\",\n  member: \"Membru\",\n  email: \"Email\",\n  firstname: \"Prenume\",\n  lastname: \"Nume\",\n  save_member: \"Salvează membru\",\n  delete_member: \"Șterge membru\",\n  failed_remove_member: \"Nu s-a putut elimina membrul\",\n  failed_save_member: \"Nu s-a putut salva membrul\",\n  failed_add_category: \"Nu s-a putut adăuga categoria\",\n  category: \"Categorie\",\n  save_category: \"Salvează categorie\",\n  delete_category: \"Șterge categorie\",\n  failed_remove_category: \"Nu s-a putut șterge categoria\",\n  currency: \"Valută\",\n  currency_code: \"Code de valută\",\n  save_currency: \"Salvează valuta\",\n  delete_currency: \"Șterge valuta\",\n  failed_remove_currency: \"Nu s-a putut șterge valuta\",\n  failed_save_currency: \"Nu s-a putut salva valuta\",\n  cant_disable_payment_in_use: \"Nu se poate dezactiva metoda de plată în uz\",\n  failed_save_payment_method: \"Nu s-a putut salva metoda de plată\",\n  unknown_error: \"Eroare necunoscută, te rugăm să încerci din nou.\",\n  error_saving_notification_data: \"Eroare la salvarea datelor de notificare\",\n  error_sending_notification: \"Eroare la trimiterea notificării\",\n  delete_account_confirmation: \"Ești sigur că vrei să-ți ștergi contul?\",\n  this_will_delete_all_data: \"Această acțiune va șterge toate datele tale și nu poate fi anulată. Vrei să continui?\",\n  success: \"Succes\",\n  copied_to_clipboard: \"Copiat în clipboard\",\n  // Calendar \n  price: \"Preț\",\n  category: \"Categorie\",\n  paid_by: \"Plătit de\",\n  payment_method: \",Metoda de plată\",\n  notes: \"Notițe\",\n  export: \"Export\",\n}\n"
  },
  {
    "path": "scripts/i18n/ru.js",
    "content": "let i18n = {\r\n  // Dashboard\r\n  error_reloading_subscription: \"Ошибка перезагрузки подписки:\",\r\n  error_fetching_image_results: \"Ошибка при получении результатов изображения:\",\r\n  subscription_deleted: \"Подписка удалена\",\r\n  error_deleting_subscription: \"Ошибка удаления подписки\",\r\n  failed_to_load_subscription: \"Не удалось загрузить подписку\",\r\n  edit_subscription: \"Изменить подписку\",\r\n  add_subscription: \"Добавить подписку\",\r\n  confirm_delete_subscription: \"Вы уверены, что хотите удалить эту подписку?\",\r\n  // Settings\r\n  network_response_error: \"Отсутствует сетевое соединение\",\r\n  failed_add_member: \"Не удалось добавить пользователя\",\r\n  member: \"Пользователь\",\r\n  email: \"Электронная почта\",\r\n  firstname: \"Имя\",\r\n  lastname: \"Фамилия\",\r\n  save_member: \"Сохранить пользователя\",\r\n  delete_member: \"Удалить пользователя\",\r\n  failed_remove_member: \"Не удалось удалить пользователя\",\r\n  failed_save_member: \"Не удалось сохранить пользователя\",\r\n  failed_add_category: \"Не удалось добавить категорию\",\r\n  category: \"Категория\",\r\n  save_category: \"Сохранить категорию\",\r\n  delete_category: \"Удалить категорию\",\r\n  failed_remove_category: \"Не удалось удалить категорию\",\r\n  currency: \"Валюта\",\r\n  currency_code: \"Код валюты\",\r\n  save_currency: \"Сохранить валюту\",\r\n  delete_currency: \"Удалить валюту\",\r\n  failed_remove_currency: \"Не удалось удалить валюту.\",\r\n  failed_save_currency: \"Не удалось сохранить валюту.\",\r\n  cant_disable_payment_in_use: \"Невозможно отключить используемый платеж\",\r\n  failed_save_payment_method: \"Не удалось сохранить способ оплаты.\",\r\n  unknown_error: \"Неизвестная ошибка. Повторите попытку.\",\r\n  error_saving_notification_data: \"Ошибка сохранения данных уведомления.\",\r\n  error_sending_notification: \"Ошибка отправки уведомления\",\r\n  delete_account_confirmation: \"Вы уверены, что хотите удалить свою учетную запись?\",\r\n  this_will_delete_all_data: \"Это удалит все ваши данные и не может быть отменено. Продолжить?\",\r\n  success: \"Успешно\",\r\n  copied_to_clipboard: \"Скопировано в буфер обмена\",\r\n  // Calendar\r\n  price: \"Цена\",\r\n  category: \"Категория\",\r\n  paid_by: \"Оплачено\",\r\n  payment_method: \"Способ оплаты\",\r\n  notes: \"Примечания\",\r\n  export: \"Экспорт\",\r\n}\r\n"
  },
  {
    "path": "scripts/i18n/sl.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Napaka pri ponovnem nalaganju naročnine:\",\n  error_fetching_image_results: \"Napaka pri pridobivanju rezultatov slik:\",\n  subscription_deleted: \"Naročnina je izbrisana\",\n  error_deleting_subscription: \"Napaka pri brisanju naročnine\",\n  failed_to_load_subscription: \"Nalaganje naročnine ni uspelo\",\n  edit_subscription: \"Uredi naročnino\",\n  add_subscription: \"Dodaj naročnino\",\n  confirm_delete_subscription: \"Ali ste prepričani, da želite izbrisati to naročnino?\",\n  // Settings\n  network_response_error: \"Odziv omrežja ni bil v redu\",\n  failed_add_member: \"Dodajanje člana ni uspelo\",\n  member: \"Član\",\n  email: \"E-pošta\",\n  firstname: \"Ime\",\n  lastname: \"Priimek\",\n  save_member: \"Shrani člana\",\n  delete_member: \"Izbriši člana\",\n  failed_remove_member: \"Odstranitev člana ni uspela\",\n  failed_save_member: \"Člana ni bilo mogoče shraniti\",\n  failed_add_category: \"Dodajanje kategorije ni uspelo\",\n  category: \"Kategorija\",\n  save_category: \"Shrani kategorijo\",\n  delete_category: \"Izbriši kategorijo\",\n  failed_remove_category: \"Odstranitev kategorije ni uspela\",\n  currency: \"Valuta\",\n  currency_code: \"Koda valute\",\n  save_currency: \"Shrani valuto\",\n  delete_currency: \"Izbriši valuto\",\n  failed_remove_currency: \"Odstranitev valute ni uspela\",\n  failed_save_currency: \"valute ni bilo mogoče shraniti\",\n  cant_disable_payment_in_use: \"Plačila v uporabi ni mogoče onemogočiti\",\n  failed_save_payment_method: \"Način plačila ni uspel shraniti\",\n  unknown_error: \"Neznana napaka, poskusite znova.\",\n  error_saving_notification_data: \"Napaka pri shranjevanju obvestilnih podatkov\",\n  error_sending_notification: \"Napaka pri pošiljanju obvestila\",\n  delete_account_confirmation: \"Ali ste prepričani, da želite izbrisati svoj račun?\",\n  this_will_delete_all_data: \"To bo izbrisalo vse vaše podatke in jih ni mogoče obnoviti. Nadaljujem?\",\n  success: \"Uspeh\",\n  copied_to_clipboard: \"Kopirano v odložišče\",\n  // Calendar\n  price: \"Cena\",\n  category: \"Kategorija\",\n  paid_by: \"Plačal/a\",\n  payment_method: \"Način plačila\",\n  notes: \"Opombe\",\n  export: \"Izvozi\",\n}\n"
  },
  {
    "path": "scripts/i18n/sr.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Грешка при освежавању претплате:\",\n  error_fetching_image_results: \"Грешка при преузимању резултата слика:\",\n  subscription_deleted: \"Претплата је избрисана\",\n  error_deleting_subscription: \"Грешка при брисању претплате\",\n  failed_to_load_subscription: \"Неуспешно учитавање претплате\",\n  edit_subscription: \"Уреди претплату\",\n  add_subscription: \"Додај претплату\",\n  confirm_delete_subscription: \"Да ли сте сигурни да желите да избришете ову претплату?\",\n  // Settings\n  network_response_error: \"Мрежни одговор није био у реду\",\n  failed_add_member: \"Неуспешно додавање члана\",\n  member: \"Члан\",\n  email: \"Е-пошта\",\n  firstname: \"Име\",\n  lastname: \"Презиме\",\n  save_member: \"Сачувај члана\",\n  delete_member: \"Избриши члана\",\n  failed_remove_member: \"Неуспешно уклањање члана\",\n  failed_save_member: \"Неуспешно чување члана\",\n  failed_add_category: \"Неуспешно додавање категорије\",\n  category: \"Категорија\",\n  save_category: \"Сачувај категорију\",\n  delete_category: \"Избриши категорију\",\n  failed_remove_category: \"Неуспешно уклањање категорије\",\n  currency: \"Валута\",\n  currency_code: \"Кôд валуте\",\n  save_currency: \"Сачувај валуту\",\n  delete_currency: \"Избриши валуту\",\n  failed_remove_currency: \"Неуспешно уклањање валуте\",\n  failed_save_currency: \"Неуспешно чување валуте\",\n  cant_disable_payment_in_use: \"Није могуће онемогућити плаћање у употреби\",\n  failed_save_payment_method: \"Неуспешно чување начина плаћања\",\n  unknown_error: \"Непозната грешка, молимо покушајте поново.\",\n  error_saving_notification_data: \"Грешка при чувању података о обавештењима\",\n  error_sending_notification: \"Грешка при слању обавештења\",\n  delete_account_confirmation: \"Да ли сте сигурни да желите да избришете свој налог?\",\n  this_will_delete_all_data: \"Ово ће избрисати све ваше податке и не може се поништити. Настави?\",\n  success: \"Успех\",\n  copied_to_clipboard: \"Копирано у привремену меморију\",\n  // Calendar\n  price: \"Цена\",\n  category: \"Категорија\",\n  paid_by: \"Плаћено од стране\",\n  payment_method: \"Метод плаћања\",\n  notes: \"Белешке\",\n  export: \"Извоз\",\n}\n"
  },
  {
    "path": "scripts/i18n/sr_lat.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Greška pri osvežavanju pretplate:\",\n  error_fetching_image_results: \"Greška pri preuzimanju rezultata slika:\",\n  subscription_deleted: \"Pretplata je izbrisana\",\n  error_deleting_subscription: \"Greška pri brisanju pretplate\",\n  failed_to_load_subscription: \"Neuspešno učitavanje pretplate\",\n  edit_subscription: \"Uredi pretplatu\",\n  add_subscription: \"Dodaj pretplatu\",\n  confirm_delete_subscription: \"Da li ste sigurni da želite da izbrišete ovu pretplatu?\",\n  // Settings\n  network_response_error: \"Mrežni odgovor nije bio u redu\",\n  failed_add_member: \"Neuspešno dodavanje člana\",\n  member: \"Član\",\n  email: \"E-pošta\",\n  firstname: \"Име\",\n  lastname: \"Презиме\",\n  save_member: \"Sačuvaj člana\",\n  delete_member: \"Izbriši člana\",\n  failed_remove_member: \"Neuspešno uklanjanje člana\",\n  failed_save_member: \"Neuspešno čuvanje člana\",\n  failed_add_category: \"Neuspešno dodavanje kategorije\",\n  category: \"Kategorija\",\n  save_category: \"Sačuvaj kategoriju\",\n  delete_category: \"Izbriši kategoriju\",\n  failed_remove_category: \"Neuspešno uklanjanje kategorije\",\n  currency: \"Valuta\",\n  currency_code: \"Kôd valute\",\n  save_currency: \"Sačuvaj valutu\",\n  delete_currency: \"Izbriši valutu\",\n  failed_remove_currency: \"Neuspešno uklanjanje valute\",\n  failed_save_currency: \"Neuspešno čuvanje valute\",\n  cant_disable_payment_in_use: \"Nije moguće onemogućiti plaćanje u upotrebi\",\n  failed_save_payment_method: \"Neuspešno čuvanje načina plaćanja\",\n  unknown_error: \"Nepoznata greška, molimo pokušajte ponovo.\",\n  error_saving_notification_data: \"Greška pri čuvanju podataka o obaveštenjima\",\n  error_sending_notification: \"Greška pri slanju obaveštenja\",\n  delete_account_confirmation: \"Da li ste sigurni da želite da izbrišete svoj nalog?\",\n  this_will_delete_all_data: \"Ovo će izbrisati sve vaše podatke i ne može se poništiti. Da li nastaviti?\",\n  success: \"Uspeh\",\n  copied_to_clipboard: \"Kopirano u privremenu memoriju\",\n  // Calendar\n  price: \"Cena\",\n  category: \"Kategorija\",\n  paid_by: \"Platio/la\",\n  payment_method: \"Način plaćanja\",\n  notes: \"Beleške\",\n  export: \"Izvezi\",\n}\n"
  },
  {
    "path": "scripts/i18n/tr.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Abonelik yeniden yüklenirken hata oluştu:\",\n  error_fetching_image_results: \"Görüntü sonuçları alınırken hata oluştu:\",\n  subscription_deleted: \"Abonelik silindi\",\n  error_deleting_subscription: \"Abonelik silinirken hata oluştu\",\n  failed_to_load_subscription: \"Abonelik yüklenemedi\",\n  edit_subscription: \"Aboneliği Düzenle\",\n  add_subscription: \"Abonelik Ekle\",\n  confirm_delete_subscription: \"Bu aboneliği silmek istediğinizden emin misiniz?\",\n  // Ayarlar\n  network_response_error: \"Ağ yanıtı kabul edilmedi\",\n  failed_add_member: \"Üye eklenemedi\",\n  member: \"Üye\",\n  email: \"E-posta\",\n  firstname: \"İlk adı\",\n  lastname: \"Soy isim\",\n  save_member: \"Üyeyi Kaydet\",\n  delete_member: \"Üyeyi Sil\",\n  failed_remove_member: \"Üye silinmedi\",\n  failed_save_member: \"Üye kaydedilemedi\",\n  failed_add_category: \"Kategori eklenemedi\",\n  category: \"Kategori\",\n  save_category: \"Kategoriyi Kaydet\",\n  delete_category: \"Kategoriyi Sil\",\n  failed_remove_category: \"Kategori silinmedi\",\n  currency: \"Para Birimi\",\n  currency_code: \"Para Birimi Kodu\",\n  save_currency: \"Para Birimini Kaydet\",\n  delete_currency: \"Para Birimini Sil\",\n  failed_remove_currency: \"Para birimi kaldırılamadı\",\n  failed_save_currency: \"Para birimi kaydedilemedi\",\n  cant_disable_payment_in_use: \"Kullanımdaki ödemeyi devre dışı bırakamazsınız\",\n  failed_save_payment_method: \"Ödeme yöntemi kaydedilemedi\",\n  unknown_error: \"Bilinmeyen hata, lütfen tekrar deneyin.\",\n  error_saving_notification_data: \"Bildirim verisi kaydedilirken hata oluştu\",\n  error_sending_notification: \"Bildirim gönderilirken hata oluştu\",\n  delete_account_confirmation: \"Hesabınızı silmek istediğinizden emin misiniz?\",\n  this_will_delete_all_data: \"Bu tüm verilerinizi silecek ve geri alınamaz. Devam etmek istiyor musunuz?\",\n  success: \"Başarılı\",\n  copied_to_clipboard: \"Panoya kopyalandı\",\n  // Calendar\n  price: \"Price\",\n  category: \"Category\",\n  paid_by: \"Paid by\",\n  payment_method: \"Payment method\",\n  notes: \"Notes\",\n  export: \"Export\",\n}\n\n\n\n"
  },
  {
    "path": "scripts/i18n/uk.js",
    "content": "let i18n = {\n    // Dashboard\n    error_reloading_subscription: \"Помилка перезавантаження підписки:\",\n    error_fetching_image_results: \"Помилка при отриманні результатів зображення:\",\n    subscription_deleted: \"Підписка видалена\",\n    error_deleting_subscription: \"Помилка видалення підписки\",\n    failed_to_load_subscription: \"Не вдалося завантажити підписку\",\n    edit_subscription: \"Редагувати підписку\",\n    add_subscription: \"Додати підписку\",\n    confirm_delete_subscription: \"Ви впевнені, що хочете видалити цю підписку?\",\n    // Settings\n    network_response_error: \"Відсутнє мережеве з'єднання\",\n    failed_add_member: \"Не вдалося додати користувача\",\n    member: \"Користувач\",\n    email: \"Електронна пошта\",\n    firstname: \"Ім'я\",\n    lastname: \"Прізвище\",\n    save_member: \"Зберегти користувача\",\n    delete_member: \"Видалити користувача\",\n    failed_remove_member: \"Не вдалося видалити користувача\",\n    failed_save_member: \"Не вдалося зберегти користувача\",\n    failed_add_category: \"Не вдалося додати категорію\",\n    category: \"Категорія\",\n    save_category: \"Зберегти категорію\",\n    delete_category: \"Видалити категорію\",\n    failed_remove_category: \"Не вдалося видалити категорію\",\n    currency: \"Валюта\",\n    currency_code: \"Код валюти\",\n    save_currency: \"Зберегти валюту\",\n    delete_currency: \"Видалити валюту\",\n    failed_remove_currency: \"Не вдалося видалити валюту.\",\n    failed_save_currency: \"Не вдалося зберегти валюту.\",\n    cant_disable_payment_in_use: \"Неможливо відключити використовуваний платіж\",\n    failed_save_payment_method: \"Не вдалося зберегти спосіб оплати.\",\n    unknown_error: \"Невідома помилка. Повторіть спробу.\",\n    error_saving_notification_data: \"Помилка збереження даних сповіщення.\",\n    error_sending_notification: \"Помилка відправки сповіщення\",\n    delete_account_confirmation: \"Ви впевнені, що хочете видалити свій обліковий запис?\",\n    this_will_delete_all_data: \"Це видалить всі ваші дані і не може бути скасовано. Продовжити?\",\n    success: \"Успішно\",\n    copied_to_clipboard: \"Скопійовано в буфер обміну\",\n    // Calendar\n    price: \"Ціна\",\n    category: \"Категорія\",\n    paid_by: \"Оплачено\",\n    payment_method: \"Спосіб оплати\",\n    notes: \"Примітки\",\n    export: \"Експорт\",\n}\n"
  },
  {
    "path": "scripts/i18n/vi.js",
    "content": "let i18n = {\n  // Dashboard\n  error_reloading_subscription: \"Lỗi tải lại đăng ký:\",\n  error_fetching_image_results: \"Lỗi khi tìm nạp kết quả hình ảnh:\",\n  subscription_deleted: \"Đã xóa đăng ký\",\n  error_deleting_subscription: \"Lỗi khi xóa đăng ký\",\n  failed_to_load_subscription: \"Không thể tải đăng ký\",\n  edit_subscription: \"Chỉnh sửa đăng ký\",\n  add_subscription: \"Thêm đăng ký\",\n  confirm_delete_subscription: \"Bạn có chắc chắn muốn xóa đăng ký này không?\",\n  // Settings\n  network_response_error: \"Phản hồi mạng không ổn\",\n  failed_add_member: \"Không thể thêm thành viên\",\n  member: \"Thành viên\",\n  email: \"Email\",\n  firstname: \"Tên đầu tiên\",\n  lastname: \"Họ\",\n  save_member: \"Lưu thành viên\",\n  delete_member: \"Xóa thành viên\",\n  failed_remove_member: \"Không thể xóa thành viên\",\n  failed_save_member: \"Không thể lưu thành viên\",\n  failed_add_category: \"Không thể thêm danh mục\",\n  category: \"Danh mục\",\n  save_category: \"Lưu danh mục\",\n  delete_category: \"Xóa danh mục\",\n  failed_remove_category: \"Không thể xóa danh mục\",\n  currency: \"Tiền tệ\",\n  currency_code: \"Mã tiền tệ\",\n  save_currency: \"Lưu tiền tệ\",\n  delete_currency: \"Xóa tiền tệ\",\n  failed_remove_currency: \"Không thể xóa tiền tệ\",\n  failed_save_currency: \"Không thể lưu tiền tệ\",\n  cant_disable_payment_in_use: \"Không thể vô hiệu hóa phương thức thanh toán đang được sử dụng\",\n  failed_save_payment_method: \"Không thể lưu phương thức thanh toán\",\n  unknown_error: \"Lỗi không xác định, vui lòng thử lại.\",\n  error_saving_notification_data: \"Lỗi khi lưu dữ liệu thông báo\",\n  error_sending_notification: \"Lỗi khi gửi thông báo\",\n  delete_account_confirmation: \"Bạn có chắc chắn muốn xóa tài khoản của mình không?\",\n  this_will_delete_all_data: \"Điều này sẽ xóa tất cả dữ liệu của bạn và không thể hoàn tác. Tiếp tục?\",\n  success: \"Thành công\",\n  copied_to_clipboard: \"Đã sao chép vào bảng tạm\",\n  // Calendar \n  price: \"Giá\",\n  category: \"Danh mục\",\n  paid_by: \"Người thanh toán\",\n  payment_method: \"Phương thức thanh toán\",\n  notes: \"Ghi chú\",\n  export: \"Xuất\",\n}\n"
  },
  {
    "path": "scripts/i18n/zh_cn.js",
    "content": "let i18n = {\n    // Dashboard\n    'error_reloading_subscription': '重新加载订阅时出错：',\n    'error_fetching_image_results': '获取图片结果时出错：',\n    'subscription_deleted': '订阅已删除',\n    'error_deleting_subscription': \"删除订阅时出错\",\n    'failed_to_load_subscription': \"加载订阅失败\",\n    'edit_subscription': \"编辑订阅\",\n    'add_subscription': \"添加订阅\",\n    'confirm_delete_subscription': \"您确定要删除此订阅吗？\",\n    // Settings\n    'network_response_error': \"网络响应不正常\",\n    'failed_add_member': '添加成员失败',\n    'member': '成员',\n    'email': '电子邮箱',\n    'firstname': '名',\n    'lastname': '姓',\n    'save_member': '保存成员',\n    'delete_member': '删除成员',\n    'failed_remove_member': '移除成员失败',\n    'failed_save_member': '保存成员失败',\n    'failed_add_category': '添加类别失败',\n    'category': '类别',\n    'save_category': '保存类别',\n    'delete_category': '删除类别',\n    'failed_remove_category': '移除类别失败',\n    'currency': '货币',\n    'currency_code': '货币代码',\n    'save_currency': '保存货币',\n    'delete_currency': '删除货币',\n    'failed_remove_currency': '移除货币失败',\n    'failed_save_currency': '保存货币失败',\n    'cant_disable_payment_in_use': '无法禁用正在使用的支付方式',\n    'failed_save_payment_method': '保存支付方式失败',\n    'unknown_error': '未知错误，请重试。',\n    'error_saving_notification_data': '保存通知数据时出错',\n    'error_sending_notification': '发送通知时出错',\n    'delete_account_confirmation': \"您确定要删除您的帐户吗？\",\n    'this_will_delete_all_data': \"这将删除所有您的数据，且无法撤销。是否继续？\",\n    'success': \"成功\",\n    'copied_to_clipboard': \"已复制到剪贴板\",\n    // Calendar\n    price: \"价格\",\n    category: \"类别\",\n    paid_by: \"支付者\",\n    payment_method: \"支付方式\",\n    notes: \"备注\",\n    export: \"导出\",\n};\n"
  },
  {
    "path": "scripts/i18n/zh_tw.js",
    "content": "let i18n = {\n    // Dashboard\n    error_reloading_subscription: '重新讀取訂閱時發生錯誤：',\n    error_fetching_image_results: '抓取圖片時發生錯誤：',\n    subscription_deleted: '訂閱已刪除',\n    error_deleting_subscription: \"刪除訂閱時發生錯誤\",\n    failed_to_load_subscription: \"讀取訂閱失敗\",\n    edit_subscription: \"編輯訂閱\",\n    add_subscription: \"新增訂閱\",\n    confirm_delete_subscription: \"您確定要刪除此訂閱嗎？\",\n    // Settings\n    network_response_error: \"網路無回應\",\n    failed_add_member: '新增成員失敗',\n    member: '成員',\n    email: '電子信箱',\n    firstname: '名',\n    lastname: '姓',\n    save_member: '保存成員',\n    delete_member: '刪除成員',\n    failed_remove_member: '移除成員失敗',\n    failed_save_member: '保存成員失敗',\n    failed_add_category: '新增類別失敗',\n    category: '類別',\n    save_category: '保存類別',\n    delete_category: '刪除類別',\n    failed_remove_category: '移除類別失敗',\n    currency: '貨幣',\n    currency_code: '貨幣代碼',\n    save_currency: '保存貨幣',\n    delete_currency: '刪除貨幣',\n    failed_remove_currency: '移除貨幣失敗',\n    failed_save_currency: '保存貨幣失敗',\n    cant_disable_payment_in_use: '無法停用正在使用中的支付方式',\n    failed_save_payment_method: '保存支付方式失敗',\n    unknown_error: '發生未知的錯誤，請再試一次。',\n    error_saving_notification_data: '保存通知資料時發生錯誤',\n    error_sending_notification: '發送通知時發生錯誤',\n    delete_account_confirmation: \"您確定要刪除您的帳戶嗎？\",\n    this_will_delete_all_data: \"這將刪除所有資料，且無法復原。繼續？\",\n    success: \"成功\",\n    copied_to_clipboard: \"已複製到剪貼簿\",\n    // Calendar\n    price: \"價格\",\n    category: \"類別\",\n    paid_by: \"支付者\",\n    payment_method: \"支付方式\",\n    notes: \"備註\",\n    export: \"匯出\",\n};\n"
  },
  {
    "path": "scripts/libs/chart.js",
    "content": "/**\n * Skipped minification because the original files appears to be already minified.\n * Original file: /npm/chart.js@4.4.0/dist/chart.umd.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n/*!\n * Chart.js v4.4.0\n * https://www.chartjs.org\n * (c) 2023 Chart.js Contributors\n * Released under the MIT License\n */\n!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define(e):(t=\"undefined\"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){\"use strict\";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return\"[object\"===e.slice(0,7)&&\"Array]\"===e.slice(-6)}function o(t){return null!==t&&\"[object Object]\"===Object.prototype.toString.call(t)}function a(t){return(\"number\"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>\"string\"==typeof t&&t.endsWith(\"%\")?parseFloat(t)/100:+t/e,c=(t,e)=>\"string\"==typeof t&&t.endsWith(\"%\")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&\"function\"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a<r;a++)e.call(i,t[a],a);else if(o(t))for(l=Object.keys(t),r=l.length,a=0;a<r;a++)e.call(i,t[l[a]],l[a])}function f(t,e){let i,s,n,o;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(n=t[i],o=e[i],n.datasetIndex!==o.datasetIndex||n.index!==o.index)return!1;return!0}function g(t){if(n(t))return t.map(g);if(o(t)){const e=Object.create(null),i=Object.keys(t),s=i.length;let n=0;for(;n<s;++n)e[i[n]]=g(t[i[n]]);return e}return t}function p(t){return-1===[\"__proto__\",\"prototype\",\"constructor\"].indexOf(t)}function m(t,e,i,s){if(!p(t))return;const n=e[t],a=i[t];o(n)&&o(a)?b(n,a,s):e[t]=g(a)}function b(t,e,i){const s=n(e)?e:[e],a=s.length;if(!o(t))return t;const r=(i=i||{}).merger||m;let l;for(let e=0;e<a;++e){if(l=s[e],!o(l))continue;const n=Object.keys(l);for(let e=0,s=n.length;e<s;++e)r(n[e],t,l,i)}return t}function x(t,e){return b(t,e,{merger:_})}function _(t,e,i){if(!p(t))return;const s=e[t],n=i[t];o(s)&&o(n)?x(s,n):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=g(n))}const y={\"\":t=>t,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split(\".\"),i=[];let s=\"\";for(const t of e)s+=t,s.endsWith(\"\\\\\")?s=s.slice(0,-1)+\".\":(i.push(s),s=\"\");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(\"\"===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>\"function\"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return\"mouseup\"===t.type||\"click\"===t.type||\"contextmenu\"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)<i}function B(t){const e=Math.round(t);t=V(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(z(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function W(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;s<n;s++)o=t[s][i],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function $(t){return t*(C/180)}function Y(t){return t*(180/C)}function U(t){if(!a(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function X(t,e){const i=e.x-t.x,s=e.y-t.y,n=Math.sqrt(i*i+s*s);let o=Math.atan2(s,i);return o<-.5*C&&(o+=O),{angle:o,distance:n}}function q(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function K(t,e){return(t-e+A)%O-C}function G(t){return(t%O+O)%O}function Z(t,e,i,s){const n=G(t),o=G(e),a=G(i),r=G(o-n),l=G(a-n),h=G(n-o),c=G(n-a);return n===o||n===a||s&&o===a||r>l&&h<c}function J(t,e,i){return Math.max(e,Math.min(i,t))}function Q(t){return J(t,-32768,32767)}function tt(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]<e);let s,n=t.length-1,o=0;for(;n-o>1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return n<i||n===i&&t[s+1][e]===i}:s=>t[s][e]<i),st=(t,e,i)=>et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;s<n&&t[s]<e;)s++;for(;n>s&&t[n-1]>i;)n--;return s>0||n<t.length?t.slice(s,n):t}const ot=[\"push\",\"pop\",\"shift\",\"splice\",\"unshift\"];function at(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,\"_chartjs\",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),ot.forEach((e=>{const i=\"_onData\"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{\"function\"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht=\"undefined\"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>\"start\"===t?\"left\":\"end\"===t?\"right\":\"center\",ft=(t,e,i)=>\"start\"===t?e:\"end\"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?\"left\":\"right\")?i:\"center\"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class bt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,\"progress\")),n.length||(i.running=!1,this._notify(s,i,t,\"complete\"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),\"complete\")}remove(t){return this._charts.delete(t)}}var xt=new bt;\n/*!\n * @kurkle/color v0.3.2\n * https://github.com/kurkle/color#readme\n * (c) 2023 Jukka Kurkela\n * Released under the MIT License\n */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[...\"0123456789ABCDEF\"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?\"#\"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):\"\")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\\(\\s*([-+.e\\d]+)(?:deg)?[\\s,]+([-+.e\\d]+)%[\\s,]+([-+.e\\d]+)%(?:[\\s,]+([-+.e\\d]+)(%)?)?\\s*\\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e<i?6:0):e===n?(i-t)/s+2:(t-e)/s+4}(e,i,s,h,n),r=60*r+.5),[0|r,l||0,a]}function zt(t,e,i,s){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,s)).map(Mt)}function Ft(t,e,i){return zt(Lt,t,e,i)}function Vt(t){return(t%360+360)%360}function Bt(t){const e=Tt.exec(t);let i,s=255;if(!e)return;e[5]!==i&&(s=e[6]?vt(+e[5]):Mt(+e[5]));const n=Vt(+e[2]),o=+e[3]/100,a=+e[4]/100;return i=\"hwb\"===e[1]?function(t,e,i){return zt(Rt,t,e,i)}(n,o,a):\"hsv\"===e[1]?function(t,e,i){return zt(Et,t,e,i)}(n,o,a):Ft(n,o,a),{r:i[0],g:i[1],b:i[2],a:s}}const Wt={x:\"dark\",Z:\"light\",Y:\"re\",X:\"blu\",W:\"gr\",V:\"medium\",U:\"slate\",A:\"ee\",T:\"ol\",S:\"or\",B:\"ra\",C:\"lateg\",D:\"ights\",R:\"in\",Q:\"turquois\",E:\"hi\",P:\"ro\",O:\"al\",N:\"le\",M:\"de\",L:\"yello\",F:\"en\",K:\"ch\",G:\"arks\",H:\"ea\",I:\"ightg\",J:\"wh\"},Nt={OiceXe:\"f0f8ff\",antiquewEte:\"faebd7\",aqua:\"ffff\",aquamarRe:\"7fffd4\",azuY:\"f0ffff\",beige:\"f5f5dc\",bisque:\"ffe4c4\",black:\"0\",blanKedOmond:\"ffebcd\",Xe:\"ff\",XeviTet:\"8a2be2\",bPwn:\"a52a2a\",burlywood:\"deb887\",caMtXe:\"5f9ea0\",KartYuse:\"7fff00\",KocTate:\"d2691e\",cSO:\"ff7f50\",cSnflowerXe:\"6495ed\",cSnsilk:\"fff8dc\",crimson:\"dc143c\",cyan:\"ffff\",xXe:\"8b\",xcyan:\"8b8b\",xgTMnPd:\"b8860b\",xWay:\"a9a9a9\",xgYF:\"6400\",xgYy:\"a9a9a9\",xkhaki:\"bdb76b\",xmagFta:\"8b008b\",xTivegYF:\"556b2f\",xSange:\"ff8c00\",xScEd:\"9932cc\",xYd:\"8b0000\",xsOmon:\"e9967a\",xsHgYF:\"8fbc8f\",xUXe:\"483d8b\",xUWay:\"2f4f4f\",xUgYy:\"2f4f4f\",xQe:\"ced1\",xviTet:\"9400d3\",dAppRk:\"ff1493\",dApskyXe:\"bfff\",dimWay:\"696969\",dimgYy:\"696969\",dodgerXe:\"1e90ff\",fiYbrick:\"b22222\",flSOwEte:\"fffaf0\",foYstWAn:\"228b22\",fuKsia:\"ff00ff\",gaRsbSo:\"dcdcdc\",ghostwEte:\"f8f8ff\",gTd:\"ffd700\",gTMnPd:\"daa520\",Way:\"808080\",gYF:\"8000\",gYFLw:\"adff2f\",gYy:\"808080\",honeyMw:\"f0fff0\",hotpRk:\"ff69b4\",RdianYd:\"cd5c5c\",Rdigo:\"4b0082\",ivSy:\"fffff0\",khaki:\"f0e68c\",lavFMr:\"e6e6fa\",lavFMrXsh:\"fff0f5\",lawngYF:\"7cfc00\",NmoncEffon:\"fffacd\",ZXe:\"add8e6\",ZcSO:\"f08080\",Zcyan:\"e0ffff\",ZgTMnPdLw:\"fafad2\",ZWay:\"d3d3d3\",ZgYF:\"90ee90\",ZgYy:\"d3d3d3\",ZpRk:\"ffb6c1\",ZsOmon:\"ffa07a\",ZsHgYF:\"20b2aa\",ZskyXe:\"87cefa\",ZUWay:\"778899\",ZUgYy:\"778899\",ZstAlXe:\"b0c4de\",ZLw:\"ffffe0\",lime:\"ff00\",limegYF:\"32cd32\",lRF:\"faf0e6\",magFta:\"ff00ff\",maPon:\"800000\",VaquamarRe:\"66cdaa\",VXe:\"cd\",VScEd:\"ba55d3\",VpurpN:\"9370db\",VsHgYF:\"3cb371\",VUXe:\"7b68ee\",VsprRggYF:\"fa9a\",VQe:\"48d1cc\",VviTetYd:\"c71585\",midnightXe:\"191970\",mRtcYam:\"f5fffa\",mistyPse:\"ffe4e1\",moccasR:\"ffe4b5\",navajowEte:\"ffdead\",navy:\"80\",Tdlace:\"fdf5e6\",Tive:\"808000\",TivedBb:\"6b8e23\",Sange:\"ffa500\",SangeYd:\"ff4500\",ScEd:\"da70d6\",pOegTMnPd:\"eee8aa\",pOegYF:\"98fb98\",pOeQe:\"afeeee\",pOeviTetYd:\"db7093\",papayawEp:\"ffefd5\",pHKpuff:\"ffdab9\",peru:\"cd853f\",pRk:\"ffc0cb\",plum:\"dda0dd\",powMrXe:\"b0e0e6\",purpN:\"800080\",YbeccapurpN:\"663399\",Yd:\"ff0000\",Psybrown:\"bc8f8f\",PyOXe:\"4169e1\",saddNbPwn:\"8b4513\",sOmon:\"fa8072\",sandybPwn:\"f4a460\",sHgYF:\"2e8b57\",sHshell:\"fff5ee\",siFna:\"a0522d\",silver:\"c0c0c0\",skyXe:\"87ceeb\",UXe:\"6a5acd\",UWay:\"708090\",UgYy:\"708090\",snow:\"fffafa\",sprRggYF:\"ff7f\",stAlXe:\"4682b4\",tan:\"d2b48c\",teO:\"8080\",tEstN:\"d8bfd8\",tomato:\"ff6347\",Qe:\"40e0d0\",viTet:\"ee82ee\",JHt:\"f5deb3\",wEte:\"ffffff\",wEtesmoke:\"f5f5f5\",Lw:\"ffff00\",LwgYF:\"9acd32\"};let Ht;function jt(t){Ht||(Ht=function(){const t={},e=Object.keys(Nt),i=Object.keys(Wt);let s,n,o,a,r;for(s=0;s<e.length;s++){for(a=r=e[s],n=0;n<i.length;n++)o=i[n],r=r.replace(o,Wt[o]);o=parseInt(Nt[a],16),t[r]=[o>>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\\(\\s*([-+.\\d]+)(%)?[\\s,]+([-+.e\\d]+)(%)?[\\s,]+([-+.e\\d]+)(%)?(?:[\\s,/]+([-+.e\\d]+)(%)?)?\\s*\\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return\"r\"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;\"object\"===e?i=Kt(t):\"string\"===e&&(o=(s=t).length,\"#\"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&\"object\"==typeof t){const e=t.toString();return\"[object CanvasPattern]\"===e||\"[object CanvasGradient]\"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=[\"x\",\"y\",\"borderWidth\",\"radius\",\"tension\"],ie=[\"color\",\"borderColor\",\"backgroundColor\"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:\"\"+t,numeric(t,e,i){if(0===t)return\"0\";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n=\"scientific\"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return\"0\";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):\"\"}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(\".\");for(let e=0,s=i.length;e<s;++e){const s=i[e];t=t[s]||(t[s]=Object.create(null))}return t}function ce(t,e,i){return\"string\"==typeof e?b(he(t,e),i):b(he(t,\"\"),e)}class de{constructor(t,e){this.animation=void 0,this.backgroundColor=\"rgba(0,0,0,0.1)\",this.borderColor=\"rgba(0,0,0,0.1)\",this.color=\"#666\",this.datasets={},this.devicePixelRatio=t=>t.chart.platform.getDevicePixelRatio(),this.elements={},this.events=[\"mousemove\",\"mouseout\",\"click\",\"touchstart\",\"touchmove\"],this.font={family:\"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif\",size:12,style:\"normal\",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis=\"x\",this.interaction={mode:\"nearest\",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r=\"_\"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith(\"on\"),_indexable:t=>\"events\"!==t,hover:{_fallback:\"interaction\"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set(\"animation\",{delay:void 0,duration:1e3,easing:\"easeOutQuart\",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe(\"animation\",{_fallback:!1,_indexable:!1,_scriptable:t=>\"onProgress\"!==t&&\"onComplete\"!==t&&\"fn\"!==t}),t.set(\"animations\",{colors:{type:\"color\",properties:ie},numbers:{type:\"number\",properties:ee}}),t.describe(\"animations\",{_fallback:\"animation\"}),t.set(\"transitions\",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:\"transparent\"},visible:{type:\"boolean\",duration:0}}},hide:{animations:{colors:{to:\"transparent\"},visible:{type:\"boolean\",easing:\"linear\",fn:t=>0|t}}}})},function(t){t.set(\"layout\",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set(\"scale\",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:\"ticks\",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:\"\",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:\"\",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:\"center\",crossAlign:\"near\",showLabelBackdrop:!1,backdropColor:\"rgba(255, 255, 255, 0.75)\",backdropPadding:2}}),t.route(\"scale.ticks\",\"color\",\"\",\"color\"),t.route(\"scale.grid\",\"color\",\"\",\"borderColor\"),t.route(\"scale.border\",\"color\",\"\",\"borderColor\"),t.route(\"scale.title\",\"color\",\"\",\"color\"),t.describe(\"scale\",{_fallback:!1,_scriptable:t=>!t.startsWith(\"before\")&&!t.startsWith(\"after\")&&\"callback\"!==t&&\"parser\"!==t,_indexable:t=>\"borderDash\"!==t&&\"tickBorderDash\"!==t&&\"dash\"!==t}),t.describe(\"scales\",{_fallback:\"scale\"}),t.describe(\"scale.ticks\",{_scriptable:t=>\"backdropPadding\"!==t&&\"callback\"!==t,_indexable:t=>\"backdropPadding\"!==t})}]);function fe(){return\"undefined\"!=typeof window&&\"undefined\"!=typeof document}function ge(t){let e=t.parentNode;return e&&\"[object ShadowRoot]\"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return\"string\"==typeof t?(s=parseInt(t,10),-1!==t.indexOf(\"%\")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=[\"top\",\"right\",\"bottom\",\"left\"];function _e(t,e,i){const s={};i=i?\"-\"+i:\"\";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+\"-\"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if(\"native\"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o=\"border-box\"===n.boxSizing,a=_e(n,\"padding\"),r=_e(n,\"border\",\"width\"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,\"margin\"),a=pe(n.maxWidth,t,\"clientWidth\")||T,r=pe(n.maxHeight,t,\"clientHeight\")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,\"border\",\"width\"),l=_e(a,\"padding\");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,\"clientWidth\"),n=pe(a.maxHeight,o,\"clientHeight\")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if(\"content-box\"===n.boxSizing){const t=_e(n,\"border\",\"width\"),e=_e(n,\"padding\");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};window.addEventListener(\"test\",null,e),window.removeEventListener(\"test\",null,e)}catch(t){}return t}();function Pe(t,e){const i=be(t,e),s=i&&i.match(/^(\\d+)(\\.\\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+\" \":\"\")+(t.weight?t.weight+\" \":\"\")+t.size+\"px \"+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;h<l;h++)if(u=i[h],null==u||n(u)){if(n(u))for(c=0,d=u.length;c<d;c++)f=u[c],null==f||n(f)||(r=Ce(t,o,a,r,f))}else r=Ce(t,o,a,r,u);t.restore();const g=a.length/2;if(g>i.length){for(h=0;h<g;h++)delete o[a[h]];a.splice(0,g)}return r}function Ae(t,e,i){const s=t.currentDevicePixelRatio,n=0!==i?Math.max(i/2,.5):0;return Math.round((e-n)*s)/s+n}function Te(t,e){(e=e||t.getContext(\"2d\")).save(),e.resetTransform(),e.clearRect(0,0,t.width,t.height),e.restore()}function Le(t,e,i,s){Ee(t,e,i,s,null)}function Ee(t,e,i,s,n){let o,a,r,l,h,c,d,u;const f=e.pointStyle,g=e.rotation,p=e.radius;let m=(g||0)*L;if(f&&\"object\"==typeof f&&(o=f.toString(),\"[object HTMLImageElement]\"===o||\"[object HTMLCanvasElement]\"===o))return t.save(),t.translate(i,s),t.rotate(m),t.drawImage(f,-f.width/2,-f.height/2,f.width,f.height),void t.restore();if(!(isNaN(p)||p<=0)){switch(t.beginPath(),f){default:n?t.ellipse(i,s,n/2,p,0,0,O):t.arc(i,s,p,0,O),t.closePath();break;case\"triangle\":c=n?n/2:p,t.moveTo(i+Math.sin(m)*c,s-Math.cos(m)*p),m+=I,t.lineTo(i+Math.sin(m)*c,s-Math.cos(m)*p),m+=I,t.lineTo(i+Math.sin(m)*c,s-Math.cos(m)*p),t.closePath();break;case\"rectRounded\":h=.516*p,l=p-h,a=Math.cos(m+R)*l,d=Math.cos(m+R)*(n?n/2-h:l),r=Math.sin(m+R)*l,u=Math.sin(m+R)*(n?n/2-h:l),t.arc(i-d,s-r,h,m-C,m-E),t.arc(i+u,s-a,h,m-E,m),t.arc(i+d,s+r,h,m,m+E),t.arc(i-u,s+a,h,m+E,m+C),t.closePath();break;case\"rect\":if(!g){l=Math.SQRT1_2*p,c=n?n/2:l,t.rect(i-c,s-l,2*c,2*l);break}m+=R;case\"rectRot\":d=Math.cos(m)*(n?n/2:p),a=Math.cos(m)*p,r=Math.sin(m)*p,u=Math.sin(m)*(n?n/2:p),t.moveTo(i-d,s-r),t.lineTo(i+u,s-a),t.lineTo(i+d,s+r),t.lineTo(i-u,s+a),t.closePath();break;case\"crossRot\":m+=R;case\"cross\":d=Math.cos(m)*(n?n/2:p),a=Math.cos(m)*p,r=Math.sin(m)*p,u=Math.sin(m)*(n?n/2:p),t.moveTo(i-d,s-r),t.lineTo(i+d,s+r),t.moveTo(i+u,s-a),t.lineTo(i-u,s+a);break;case\"star\":d=Math.cos(m)*(n?n/2:p),a=Math.cos(m)*p,r=Math.sin(m)*p,u=Math.sin(m)*(n?n/2:p),t.moveTo(i-d,s-r),t.lineTo(i+d,s+r),t.moveTo(i+u,s-a),t.lineTo(i-u,s+a),m+=R,d=Math.cos(m)*(n?n/2:p),a=Math.cos(m)*p,r=Math.sin(m)*p,u=Math.sin(m)*(n?n/2:p),t.moveTo(i-d,s-r),t.lineTo(i+d,s+r),t.moveTo(i+u,s-a),t.lineTo(i-u,s+a);break;case\"line\":a=n?n/2:Math.cos(m)*p,r=Math.sin(m)*p,t.moveTo(i-a,s-r),t.lineTo(i+a,s+r);break;case\"dash\":t.moveTo(i,s),t.lineTo(i+Math.cos(m)*(n?n/2:p),s+Math.sin(m)*p);break;case!1:t.closePath()}t.fill(),e.borderWidth>0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.x<e.right+i&&t.y>e.top-i&&t.y<e.bottom+i}function Ie(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()}function ze(t){t.restore()}function Fe(t,e,i,s,n){if(!e)return t.lineTo(i.x,i.y);if(\"middle\"===n){const s=(e.x+i.x)/2;t.lineTo(s,e.y),t.lineTo(s,i.y)}else\"after\"===n!=!!s?t.lineTo(e.x,i.y):t.lineTo(i.x,e.y);t.lineTo(i.x,i.y)}function Ve(t,e,i,s){if(!e)return t.lineTo(i.x,i.y);t.bezierCurveTo(s?e.cp1x:e.cp2x,s?e.cp1y:e.cp2y,s?i.cp2x:i.cp1x,s?i.cp2y:i.cp1y,i.x,i.y)}function Be(t,e,i,s,n){if(n.strikethrough||n.underline){const o=t.measureText(s),a=e-o.actualBoundingBoxLeft,r=e+o.actualBoundingBoxRight,l=i-o.actualBoundingBoxAscent,h=i+o.actualBoundingBoxDescent,c=n.strikethrough?(l+h)/2:h;t.strokeStyle=t.fillStyle,t.beginPath(),t.lineWidth=n.decorationWidth||2,t.moveTo(a,c),t.lineTo(r,c),t.stroke()}}function We(t,e){const i=t.fillStyle;t.fillStyle=e.color,t.fillRect(e.left,e.top,e.width,e.height),t.fillStyle=i}function Ne(t,e,i,o,a,r={}){const l=n(e)?e:[e],h=r.strokeWidth>0&&\"\"!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;c<l.length;++c)d=l[c],r.backdrop&&We(t,r.backdrop),h&&(r.strokeColor&&(t.strokeStyle=r.strokeColor),s(r.strokeWidth)||(t.lineWidth=r.strokeWidth),t.strokeText(d,i,o,r.maxWidth)),t.fillText(d,i,o,r.maxWidth),Be(t,i,o,d,r),o+=Number(a.lineHeight);t.restore()}function He(t,e){const{x:i,y:s,w:n,h:o,radius:a}=e;t.arc(i+a.topLeft,s+a.topLeft,a.topLeft,1.5*C,C,!0),t.lineTo(i,s+o-a.bottomLeft),t.arc(i+a.bottomLeft,s+o-a.bottomLeft,a.bottomLeft,C,E,!0),t.lineTo(i+n-a.bottomRight,s+o),t.arc(i+n-a.bottomRight,s+o-a.bottomRight,a.bottomRight,E,0,!0),t.lineTo(i+n,s+a.topRight),t.arc(i+n-a.topRight,s+a.topRight,a.topRight,0,-E,!0),t.lineTo(i+a.topLeft,s)}function je(t,e=[\"\"],i,s,n=(()=>t[0])){const o=i||t;void 0===s&&(s=ti(\"_fallback\",t));const a={[Symbol.toStringTag]:\"Object\",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error(\"Recursion detected: \"+Array.from(r).join(\"->\")+\"->\"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&\"adapters\"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e))return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:\"string\"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[\"\"],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith(\"_\"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o=\"r\"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;r<l;++r)h=r+i,c=e[h],a[r]={r:n.parse(M(c,o),h)};return a}const si=Number.EPSILON||1e-14,ni=(t,e)=>e<t.length&&!t[e].skip&&t[e],oi=t=>\"x\"===t?\"y\":\"x\";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e=\"x\"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a<s;++a)if(r=l,l=h,h=ni(t,a+1),l){if(h){const t=h[e]-l[e];n[a]=0!==t?(h[i]-l[i])/t:0}o[a]=r?h?F(n[a-1])!==F(n[a])?0:(n[a-1]+n[a])/2:n[a-1]:n[a]}!function(t,e,i){const s=t.length;let n,o,a,r,l,h=ni(t,0);for(let c=0;c<s-1;++c)l=h,h=ni(t,c+1),l&&h&&(V(e[c],0,si)?i[c]=i[c+1]=0:(n=i[c]/e[c],o=i[c+1]/e[c],r=Math.pow(n,2)+Math.pow(o,2),r<=9||(a=3/Math.sqrt(r),i[c]=n*a*e[c],i[c+1]=o*a*e[c])))}(t,n,o),function(t,e,i=\"x\"){const s=oi(i),n=t.length;let o,a,r,l=ni(t,0);for(let h=0;h<n;++h){if(a=r,r=l,l=ni(t,h+1),!r)continue;const n=r[i],c=r[s];a&&(o=(n-a[i])/3,r[`cp1${i}`]=n-o,r[`cp1${s}`]=c-o*e[h]),l&&(o=(l[i]-n)/3,r[`cp2${i}`]=n+o,r[`cp2${s}`]=c+o*e[h])}}(t,o,e)}function li(t,e,i){return Math.max(Math.min(t,i),e)}function hi(t,e,i,s,n){let o,a,r,l;if(e.spanGaps&&(t=t.filter((t=>!t.skip))),\"monotone\"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o<a;++o)r=t[o],l=ai(i,r,t[Math.min(o+1,a-(s?0:1))%a],e.tension),r.cp1x=l.previous.x,r.cp1y=l.previous.y,r.cp2x=l.next.x,r.cp2y=l.next.y,i=r}e.capBezierPoints&&function(t,e){let i,s,n,o,a,r=Re(t[0],e);for(i=0,s=t.length;i<s;++i)a=o,o=r,r=i<s-1&&Re(t[i+1],e),o&&(n=t[i],a&&(n.cp1x=li(n.cp1x,e.left,e.right),n.cp1y=li(n.cp1y,e.top,e.bottom)),r&&(n.cp2x=li(n.cp2x,e.left,e.right),n.cp2y=li(n.cp2y,e.top,e.bottom)))}(t,i)}const ci=t=>0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:\"middle\"===s?i<.5?t.y:e.y:\"after\"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const bi=/^(normal|(\\d+(?:\\.\\d+)?)(px|em|%)?)$/,xi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(\"\"+t).match(bi);if(!i||\"normal\"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case\"px\":return t;case\"%\":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:\"y\",right:\"x\",bottom:\"y\",left:\"x\"})}function wi(t){return vi(t,[\"topLeft\",\"topRight\",\"bottomLeft\",\"bottomRight\"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);\"string\"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(\"\"+s).match(xi)&&(console.warn('Invalid font style specified: \"'+s+'\"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:\"\"};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;o<a;++o)if(r=t[o],void 0!==r&&(void 0!==e&&\"function\"==typeof r&&(r=r(e),l=!1),void 0!==i&&n(r)&&(r=r[i%r.length],l=!1),void 0!==r))return s&&!l&&(s.cacheable=!1),r}function Di(t,e,i){const{min:s,max:n}=t,o=c(e,(n-s)/2),a=(t,e)=>i&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>\"center\"===t?t:\"right\"===t?\"left\":\"right\",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;\"ltr\"!==e&&\"rtl\"!==e||(i=t.canvas.style,s=[i.getPropertyValue(\"direction\"),i.getPropertyPriority(\"direction\")],i.setProperty(\"direction\",e,\"important\"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty(\"direction\",e[0],e[1]))}function Li(t){return\"angle\"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;h<c&&a(r(e[d%l][s]),n,o);++h)d--,u--;d%=l,u%=l}return u<d&&(u+=l),{start:d,end:u,loop:f,style:t.style}}(t,e,i),g=[];let p,m,b,x=!1,_=null;const y=()=>x||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;n<s.length;n++){const o=Ri(s[n],t.points,e);o.length&&i.push(...o)}return i}function zi(t,e){const i=t.points,s=t.options.spanGaps,n=i.length;if(!n)return[];const o=!!t._loop,{start:a,end:r}=function(t,e,i,s){let n=0,o=e-1;if(i&&!s)for(;n<e&&!t[n].skip;)n++;for(;n<e&&t[n].skip;)n++;for(n%=e,i&&(o+=n);o>n&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r<a?r+n:r,!!t._fullLoop&&0===a&&r===n-1),i,e)}function Fi(t,e,i,s){return s&&s.setContext&&i?function(t,e,i,s){const n=t._chart.getContext(),o=Vi(t.options),{_datasetIndex:a,options:{spanGaps:r}}=t,l=i.length,h=[];let c=o,d=e[0].start,u=d;function f(t,e,s,n){const o=r?-1:1;if(t!==e){for(t+=l;i[t%l].skip;)t-=o;for(;i[e%l].skip;)e+=o;t%l!=e%l&&(h.push({start:t%l,end:e%l,loop:s,style:n}),c=n,d=e%l)}}for(const t of e){d=r?d:t.start;let e,o=i[d%l];for(u=d+1;u<=t.end;u++){const r=i[u%l];e=Vi(s.setContext(Ci(n,{type:\"segment\",p0:o,p1:r,p0DataIndex:(u-1)%l,p1DataIndex:u%l,datasetIndex:a}))),Bi(e,c)&&f(d,u-1,t.loop,c),o=r,c=e}d<u-1&&f(d,u-1,t.loop,c)}return h}(t,e,i,s):e}function Vi(t){return{backgroundColor:t.backgroundColor,borderCapStyle:t.borderCapStyle,borderDash:t.borderDash,borderDashOffset:t.borderDashOffset,borderJoinStyle:t.borderJoinStyle,borderWidth:t.borderWidth,borderColor:t.borderColor}}function Bi(t,e){if(!e)return!1;const i=[],s=function(t,e){return Jt(e)?(i.includes(e)||i.push(e),i.indexOf(e)):e};return JSON.stringify(t,s)!==JSON.stringify(e,s)}var Wi=Object.freeze({__proto__:null,HALF_PI:E,INFINITY:T,PI:C,PITAU:A,QUARTER_PI:R,RAD_PER_DEG:L,TAU:O,TWO_THIRDS_PI:I,_addGrace:Di,_alignPixel:Ae,_alignStartEnd:ft,_angleBetween:Z,_angleDiff:K,_arrayUnique:lt,_attachContext:$e,_bezierCurveTo:Ve,_bezierInterpolation:mi,_boundSegment:Ri,_boundSegments:Ii,_capitalize:w,_computeSegments:zi,_createResolver:je,_decimalPlaces:U,_deprecated:function(t,e,i,s){void 0!==e&&console.warn(t+': \"'+i+'\" is deprecated. Please use \"'+s+'\" instead')},_descriptors:Ye,_elementsEqual:f,_factorize:W,_filterBetween:nt,_getParentNode:ge,_getStartAndCountOfVisiblePoints:pt,_int16Range:Q,_isBetween:tt,_isClickEvent:D,_isDomSupported:fe,_isPointInArea:Re,_limitValue:J,_longestText:Oe,_lookup:et,_lookupByKey:it,_measureText:Ce,_merger:m,_mergerIf:_,_normalizeAngle:G,_parseObjectDataRadialScale:ii,_pointInLine:gi,_readValueToProps:vi,_rlookupByKey:st,_scaleRangesChanged:mt,_setMinAndMaxByKey:j,_splitKey:v,_steppedInterpolation:pi,_steppedLineTo:Fe,_textX:gt,_toLeftRightCenter:ut,_updateBezierControlPoints:hi,addRoundedRectPath:He,almostEquals:V,almostWhole:H,callback:d,clearCanvas:Te,clipArea:Ie,clone:g,color:Qt,createContext:Ci,debounce:dt,defined:k,distanceBetweenPoints:q,drawPoint:Le,drawPointLegend:Ee,each:u,easingEffects:fi,finiteOrDefault:r,fontString:function(t,e,i){return e+\" \"+t+\"px \"+i},formatNumber:ne,getAngleFromPoint:X,getHoverColor:te,getMaximumSize:we,getRelativePosition:ve,getRtlAdapter:Oi,getStyle:be,isArray:n,isFinite:a,isFunction:S,isNullOrUndef:s,isNumber:N,isObject:o,isPatternOrGradient:Jt,listenArrayEvents:at,log10:z,merge:b,mergeIf:x,niceNum:B,noop:e,overrideTextDirection:Ai,readUsedSize:Pe,renderText:Ne,requestAnimFrame:ht,resolve:Pi,resolveObjectKey:M,restoreTextDirection:Ti,retinaScale:ke,setsEqual:P,sign:F,splineCurve:ai,splineCurveMonotone:ri,supportsEventListenerOptions:Se,throttled:ct,toDegrees:Y,toDimension:c,toFont:Si,toFontString:De,toLineHeight:_i,toPadding:ki,toPercentage:h,toRadians:$,toTRBL:Mi,toTRBLCorners:wi,uid:i,unclipArea:ze,unlistenArrayEvents:rt,valueOrDefault:l});function Ni(t,e,i,s){const{controller:n,data:o,_sorted:a}=t,r=n._cachedMeta.iScale;if(r&&e===r.axis&&\"r\"!==e&&a&&o.length){const t=r._reversePixels?st:it;if(!s)return t(o,e,i);if(n._sharedOptions){const s=o[0],n=\"function\"==typeof s.getRange&&s.getRange(e);if(n){const s=t(o,e,i-n),a=t(o,e,i+n);return{lo:s.lo,hi:a.hi}}}}return{lo:0,hi:o.length-1}}function Hi(t,e,i,s,n){const o=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=o.length;t<i;++t){const{index:i,data:r}=o[t],{lo:l,hi:h}=Ni(o[t],e,a,n);for(let t=l;t<=h;++t){const e=r[t];e.skip||s(e,i,t)}}}function ji(t,e,i,s,n){const o=[];if(!n&&!t.isPointInArea(e))return o;return Hi(t,i,e,(function(i,a,r){(n||Re(i,t.chartArea,0))&&i.inRange(e.x,e.y,s)&&o.push({element:i,datasetIndex:a,index:r})}),!0),o}function $i(t,e,i,s,n,o){let a=[];const r=function(t){const e=-1!==t.indexOf(\"x\"),i=-1!==t.indexOf(\"y\");return function(t,s){const n=e?Math.abs(t.x-s.x):0,o=i?Math.abs(t.y-s.y):0;return Math.sqrt(Math.pow(n,2)+Math.pow(o,2))}}(i);let l=Number.POSITIVE_INFINITY;return Hi(t,i,e,(function(i,h,c){const d=i.inRange(e.x,e.y,n);if(s&&!d)return;const u=i.getCenterPoint(n);if(!(!!o||t.isPointInArea(u))&&!d)return;const f=r(e,u);f<l?(a=[{element:i,datasetIndex:h,index:c}],l=f):f===l&&a.push({element:i,datasetIndex:h,index:c})})),a}function Yi(t,e,i,s,n,o){return o||t.isPointInArea(e)?\"r\"!==i||s?$i(t,e,i,s,n,o):function(t,e,i,s){let n=[];return Hi(t,i,e,(function(t,i,o){const{startAngle:a,endAngle:r}=t.getProps([\"startAngle\",\"endAngle\"],s),{angle:l}=X(t,{x:e.x,y:e.y});Z(l,a,r)&&n.push({element:t,datasetIndex:i,index:o})})),n}(t,e,i,n):[]}function Ui(t,e,i,s,n){const o=[],a=\"x\"===i?\"inXRange\":\"inYRange\";let r=!1;return Hi(t,i,e,((t,s,l)=>{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||\"x\",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||\"xy\",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;t<i.length;++t)r.push({element:i[t],datasetIndex:e,index:t})}return r},point:(t,e,i,s)=>ji(t,ve(e,t),i.axis||\"xy\",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||\"xy\",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),\"x\",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),\"y\",i.intersect,s)}};const qi=[\"left\",\"top\",\"right\",\"bottom\"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o<a;++o){r=t[o];const{fullSize:a}=r.box,l=i[r.stack],h=l&&r.stackWeight/l.weight;r.horizontal?(r.width=h?h*s:a&&e.availableWidth,r.height=n):(r.width=s,r.height=h?h*n:a&&e.availableHeight)}return i}function Qi(t,e,i,s){return Math.max(t[i],e[i])+Math.max(t[s],e[s])}function ts(t,e){t.top=Math.max(t.top,e.top),t.left=Math.max(t.left,e.left),t.bottom=Math.max(t.bottom,e.bottom),t.right=Math.max(t.right,e.right)}function es(t,e,i,s){const{pos:n,box:a}=i,r=t.maxPadding;if(!o(n)){i.size&&(t[n]-=i.size);const e=s[i.stack]||{size:0,count:1};e.size=Math.max(e.size,i.horizontal?a.height:a.width),i.size=e.size/e.count,t[n]+=i.size}a.getPadding&&ts(r,a.getPadding());const l=Math.max(0,e.outerWidth-Qi(r,t,\"left\",\"right\")),h=Math.max(0,e.outerHeight-Qi(r,t,\"top\",\"bottom\")),c=l!==t.w,d=h!==t.h;return t.w=l,t.h=h,i.horizontal?{same:c,other:d}:{same:d,other:c}}function is(t,e){const i=e.maxPadding;function s(t){const s={left:0,top:0,right:0,bottom:0};return t.forEach((t=>{s[t]=Math.max(e[t],i[t])})),s}return s(t?[\"left\",\"right\"]:[\"top\",\"bottom\"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;o<a;++o){r=t[o],l=r.box,l.update(r.width||e.w,r.height||e.h,is(r.horizontal,e));const{same:a,other:d}=es(e,i,r,s);h|=a&&n.length,c=c||d,l.fullSize||n.push(r)}return h&&ss(n,e,i,s)||c}function ns(t,e,i,s,n){t.top=i,t.left=e,t.right=e+s,t.bottom=i+n,t.width=s,t.height=n}function os(t,e,i,s){const n=i.padding;let{x:o,y:a}=e;for(const r of t){const t=r.box,l=s[r.stack]||{count:1,placed:0,weight:1},h=r.stackWeight/l.weight||1;if(r.horizontal){const s=e.w*h,o=l.size||t.height;k(l.start)&&(a=l.start),t.fullSize?ns(t,n.left,a,i.outerWidth-n.right-n.left,o):ns(t,e.left+l.placed,a,s,o),l.start=a,l.placed+=s,a=t.bottom}else{const s=e.h*h,a=l.size||t.width;k(l.start)&&(o=l.start),t.fullSize?ns(t,o,n.top,a,i.outerHeight-n.bottom-n.top):ns(t,o,e.top+l.placed,a,s),l.start=o,l.placed+=s,o=t.right}}e.x=o,e.y=a}var as={addBox(t,e){t.boxes||(t.boxes=[]),e.fullSize=e.fullSize||!1,e.position=e.position||\"top\",e.weight=e.weight||0,e._layers=e._layers||function(){return[{z:0,draw(t){e.draw(t)}}]},t.boxes.push(e)},removeBox(t,e){const i=t.boxes?t.boxes.indexOf(e):-1;-1!==i&&t.boxes.splice(i,1)},configure(t,e,i){e.fullSize=i.fullSize,e.position=i.position,e.weight=i.weight},update(t,e,i,s){if(!t)return;const n=ki(t.options.layout.padding),o=Math.max(e-n.width,0),a=Math.max(i-n.height,0),r=function(t){const e=function(t){const e=[];let i,s,n,o,a,r;for(i=0,s=(t||[]).length;i<s;++i)n=t[i],({position:o,options:{stack:a,stackWeight:r=1}}=n),e.push({index:i,box:n,pos:o,horizontal:n.isHorizontal(),weight:n.weight,stack:a&&o+a,stackWeight:r});return e}(t),i=Zi(e.filter((t=>t.box.fullSize)),!0),s=Zi(Ki(e,\"left\"),!0),n=Zi(Ki(e,\"right\")),o=Zi(Ki(e,\"top\"),!0),a=Zi(Ki(e,\"bottom\")),r=Gi(e,\"x\"),l=Gi(e,\"y\");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,\"chartArea\"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{\"function\"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i(\"top\"),t.x+=i(\"left\"),i(\"right\"),i(\"bottom\")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext(\"2d\")||null}updateConfig(t){t.options.animation=!1}}const hs=\"$chartjs\",cs={touchstart:\"mousedown\",touchmove:\"mousemove\",touchend:\"mouseup\",pointerenter:\"mouseenter\",pointerdown:\"mousedown\",pointermove:\"mousemove\",pointerup:\"mouseup\",pointerleave:\"mouseout\",pointerout:\"mouseout\"},ds=t=>null===t||\"\"===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const bs=new Map;let xs=0;function _s(){const t=window.devicePixelRatio;t!==xs&&(xs=t,bs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s<n.clientWidth&&i()}),window),a=new ResizeObserver((t=>{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){bs.size||window.addEventListener(\"resize\",_s),bs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),\"resize\"===e&&function(t){bs.delete(t),bs.size||window.removeEventListener(\"resize\",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext(\"2d\");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute(\"height\"),n=t.getAttribute(\"width\");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||\"block\",i.boxSizing=i.boxSizing||\"border-box\",ds(n)){const e=Pe(t,\"width\");void 0!==e&&(t.width=e)}if(ds(s))if(\"\"===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,\"height\");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;[\"height\",\"width\"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||\"undefined\"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps=\"transparent\",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e<i),!this._active)return this._target[s]=a,void this._notify(!0);e<0?this._target[s]=n:(r=e/i%2,r=o&&r>1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?\"res\":\"rej\",i=this._promises||[];for(let t=0;t<i.length;t++)i[t][e]()}}class Os{constructor(t,e){this._chart=t,this._properties=new Map,this.configure(e)}configure(t){if(!o(t))return;const e=Object.keys(ue.animation),i=this._properties;Object.getOwnPropertyNames(t).forEach((s=>{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e<s.length;e++){const n=t[s[e]];n&&n.active()&&i.push(n.wait())}return Promise.all(i)}(t.options.$animations,i).then((()=>{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if(\"$\"===l.charAt(0))continue;if(\"options\"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(xt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n<o;++n)i.push(s[n].index);return i}function Ls(t,e,i,s={}){const n=t.keys,o=\"single\"===s.mode;let r,l,h,c;if(null!==e){for(r=0,l=n.length;r<l;++r){if(h=+n[r],h===i){if(s.all)continue;break}c=t.values[h],a(c)&&(o||0===e||F(e)===F(c))&&(e+=c)}return e}}function Es(t,e){const i=t&&t.options.stacked;return i||void 0===i&&void 0!==e.stack}function Rs(t,e,i){const s=t[e]||(t[e]={});return s[i]||(s[i]={})}function Is(t,e,i,s){for(const n of e.getMatchingVisibleMetas(s).reverse()){const e=t[n.index];if(i&&e>0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;t<d;++t){const i=e[t],{[l]:o,[h]:d}=i;u=(i._stacks||(i._stacks={}))[h]=Rs(n,c,o),u[r]=d,u._top=Is(u,a,!0,s.type),u._bottom=Is(u,a,!1,s.type);(u._visualValues||(u._visualValues={}))[r]=d}}function Fs(t,e){const i=t.scales;return Object.keys(i).filter((t=>i[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>\"reset\"===t||\"none\"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled(\"filler\")&&console.warn(\"Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options\")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>\"x\"===t?e:\"r\"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,\"x\")),o=e.yAxisID=l(i.yAxisID,Fs(t,\"y\")),a=e.rAxisID=l(i.rAxisID,Fs(t,\"r\")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update(\"reset\")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e))this._data=function(t){const e=Object.keys(t),i=new Array(e.length);let s,n,o;for(s=0,n=e.length;s<n;++s)o=e[s],i[s]={x:o,y:t[o]};return i}(e);else if(i!==e){if(i){rt(i,this);const t=this._cachedMeta;Vs(t),t._parsed=[]}e&&Object.isExtensible(e)&&at(e,this),this._syncList=[],this._data=e}}addElements(){const t=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(t.dataset=new this.datasetElementType)}buildOrUpdateElements(t){const e=this._cachedMeta,i=this.getDataset();let s=!1;this._dataCheck();const n=e._stacked;e._stacked=Es(e.vScale,e),e.stack!==i.stack&&(s=!0,Vs(e),e.stack=i.stack),this._resyncElements(t),(s||n!==e._stacked)&&zs(this,e._parsed)}configure(){const t=this.chart.config,e=t.datasetScopeKeys(this._type),i=t.getOptionScopes(this.getDataset(),e,!0);this.options=t.createResolver(i,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(t,e){const{_cachedMeta:i,_data:s}=this,{iScale:a,_stacked:r}=i,l=a.axis;let h,c,d,u=0===t&&e===s.length||i._sorted,f=t>0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]<f[l];for(h=0;h<e;++h)i._parsed[h+t]=c=d[h],u&&(a()&&(u=!1),f=c);i._sorted=u}r&&zs(this,d)}parsePrimitiveData(t,e,i,s){const{iScale:n,vScale:o}=t,a=n.axis,r=o.axis,l=n.getLabels(),h=n===o,c=new Array(s);let d,u,f;for(d=0,u=s;d<u;++d)f=d+i,c[d]={[a]:h||n.parse(l[f],f),[r]:o.parse(e[f],f)};return c}parseArrayData(t,e,i,s){const{xScale:n,yScale:o}=t,a=new Array(s);let r,l,h,c;for(r=0,l=s;r<l;++r)h=r+i,c=e[h],a[r]={x:n.parse(c[0],h),y:o.parse(c[1],h)};return a}parseObjectData(t,e,i,s){const{xScale:n,yScale:o}=t,{xAxisKey:a=\"x\",yAxisKey:r=\"y\"}=this._parsing,l=new Array(s);let h,c,d,u;for(h=0,c=s;h<c;++h)d=h+i,u=e[d],l[h]={x:n.parse(M(u,a),d),y:o.parse(M(u,r),d)};return l}getParsed(t){return this._cachedMeta._parsed[t]}getDataElement(t){return this._cachedMeta.data[t]}applyStack(t,e,i){const s=this.chart,n=this._cachedMeta,o=e[t.axis];return Ls({keys:Ts(s,!0),values:e._stacks[t.axis]._visualValues},o,n.index,{mode:i})}updateRangeFromParsed(t,e,i,s){const n=i[e.axis];let o=null===n?NaN:n;const a=s&&i._stacks[e.axis];s&&a&&(s.values=a,o=Ls(s,n,this._cachedMeta.index)),t.min=Math.min(t.min,o),t.max=Math.max(t.max,o)}getMinMax(t,e){const i=this._cachedMeta,s=i._parsed,n=i._sorted&&t===i.iScale,o=s.length,r=this._getOtherScale(t),l=((t,e,i)=>t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d<e}for(u=0;u<o&&(g()||(this.updateRangeFromParsed(h,t,f,l),!n));++u);if(n)for(u=o-1;u>=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s<n;++s)o=e[s][t.axis],a(o)&&i.push(o);return i}getMaxOverflow(){return!1}getLabelAndValue(t){const e=this._cachedMeta,i=e.iScale,s=e.vScale,n=this.getParsed(t);return{label:i?\"\"+i.getLabelForValue(n[i.axis]):\"\",value:s?\"\"+s.getLabelForValue(n[s.axis]):\"\"}}_update(t){const e=this._cachedMeta;this.update(t||\"default\"),e._clip=function(t){let e,i,s,n;return o(t)?(e=t.top,i=t.right,s=t.bottom,n=t.left):e=i=s=n=t,{top:e,right:i,bottom:s,left:n,disabled:!1===t}}(l(this.options.clip,function(t,e,i){if(!1===i)return!1;const s=As(t,i),n=As(e,i);return{top:n.end,right:s.end,bottom:n.start,left:s.start}}(e.xScale,e.yScale,this.getMaxOverflow())))}update(t){}draw(){const t=this._ctx,e=this.chart,i=this._cachedMeta,s=i.data||[],n=e.chartArea,o=[],a=this._drawStart||0,r=this._drawCount||s.length-a,l=this.options.drawActiveElementsOnTop;let h;for(i.dataset&&i.dataset.draw(t,n,a,r),h=a;h<a+r;++h){const e=s[h];e.hidden||(e.active&&l?o.push(e):e.draw(t,n))}for(h=0;h<o.length;++h)o[h].draw(t,n)}getStyle(t,e){const i=e?\"active\":\"default\";return void 0===t&&this._cachedMeta.dataset?this.resolveDatasetElementOptions(i):this.resolveDataElementOptions(t||0,i)}getContext(t,e,i){const s=this.getDataset();let n;if(t>=0&&t<this._cachedMeta.data.length){const e=this._cachedMeta.data[t];n=e.$context||(e.$context=function(t,e,i){return Ci(t,{active:!1,dataIndex:e,parsed:void 0,raw:void 0,element:i,index:e,mode:\"default\",type:\"data\"})}(this.getContext(),t,e)),n.parsed=this.getParsed(t),n.raw=s.data[t],n.index=n.dataIndex=t}else n=this.$context||(this.$context=function(t,e){return Ci(t,{active:!1,dataset:void 0,datasetIndex:e,index:e,mode:\"default\",type:\"dataset\"})}(this.chart.getContext(),this.index)),n.dataset=s,n.index=n.datasetIndex=this.index;return n.active=!!e,n.mode=i,n}resolveDatasetElementOptions(t){return this._resolveElementOptions(this.datasetElementType.id,t)}resolveDataElementOptions(t,e){return this._resolveElementOptions(this.dataElementType.id,e,t)}_resolveElementOptions(t,e=\"default\",i){const s=\"active\"===e,n=this._cachedDataOpts,o=t+\"-\"+e,a=n[o],r=this.enableOptionSharing&&k(i);if(a)return Ws(a,r);const l=this.chart.config,h=l.datasetElementScopeKeys(this._type,t),c=s?[`${t}Hover`,\"hover\",t,\"\"]:[t,\"\"],d=l.getOptionScopes(this.getDataset(),h),u=Object.keys(ue.elements[t]),f=l.resolveNamedOptions(d,u,(()=>this.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,\"active\",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,\"active\",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,\"active\",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,\"active\",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n<s&&this._removeElements(n,s-n)}_insertElements(t,e,i=!0){const s=this._cachedMeta,n=s.data,o=t+e;let a;const r=t=>{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a<o;++a)n[a]=new this.dataElementType;this._parsing&&r(s._parsed),this.parse(t,e),i&&this.updateElements(n,t,e,\"reset\")}updateElements(t,e,i,s){}_removeElements(t,e){const i=this._cachedMeta;if(this._parsing){const s=i._parsed.splice(t,e);i._stacked&&Vs(i,s)}i.data.splice(t,e)}_sync(t){if(this._parsing)this._syncList.push(t);else{const[e,i,s]=t;this[e](i,s)}this.chart._dataChanges.push([this.index,...t])}_onDataPush(){const t=arguments.length;this._sync([\"_insertElements\",this.getDataset().data.length-t,t])}_onDataPop(){this._sync([\"_removeElements\",this._cachedMeta.data.length-1,1])}_onDataShift(){this._sync([\"_removeElements\",0,1])}_onDataSplice(t,e){e&&this._sync([\"_removeElements\",t,e]);const i=arguments.length-2;i&&this._sync([\"_insertElements\",t,i])}_onDataUnshift(){this._sync([\"_insertElements\",0,arguments.length])}}class Hs{static defaults={};static defaultRoutes=void 0;x;y;active=!1;options;$animations;tooltipPosition(t){const{x:e,y:i}=this.getProps([\"x\",\"y\"],t);return{x:e,y:i}}hasValue(){return N(this.x)&&N(this.y)}getProps(t,e){const i=this.$animations;if(!e||!i)return this;const s={};return t.forEach((t=>{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;i<s;i++)t[i].major&&e.push(i);return e}(e):[],r=a.length,l=a[0],h=a[r-1],c=[];if(r>o)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;n<t.length;n++)n===a&&(e.push(t[n]),o++,a=i[o*s])}(e,c,a,r/o),c;const d=function(t,e,i){const s=function(t){const e=t.length;let i,s;if(e<2)return!1;for(s=t[0],i=1;i<e;++i)if(t[i]-t[i-1]!==s)return!1;return s}(t),n=e.length/i;if(!s)return Math.max(n,1);const o=W(s);for(let t=0,e=o.length-1;t<e;t++){const e=o[t];if(e>n)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t<i;t++)$s(e,c,d,a[t],a[t+1]);return $s(e,c,d,h,s(n)?e.length:h+n),c}return $s(e,c,d),c}function $s(t,e,i,s,n){const o=l(s,0),a=Math.min(l(n,t.length),t.length);let r,h,c,d=0;for(i=Math.ceil(i),n&&(r=n-s,i=r/Math.floor(r/i)),c=o;c<0;)d++,c=Math.round(o+d*i);for(h=Math.max(o,0);h<a;h++)h===c&&(e.push(t[h]),d++,c=Math.round(o+d*i))}const Ys=(t,e,i)=>\"top\"===e||\"left\"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;o<n;o+=s)i.push(t[Math.floor(o)]);return i}function qs(t,e,i){const s=t.ticks.length,n=Math.min(e,s-1),o=t._startPixel,a=t._endPixel,r=1e-6;let l,h=t.getPixelForTick(n);if(!(i&&(l=1===s?Math.max(h-o,a-h):0===e?(t.getPixelForTick(1)-h)/2:(h-t.getPixelForTick(n-1))/2,h+=n<e?l:-l,h<o-r||h>a+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&\"right\"!==e||!i&&\"right\"===e)&&(s=(t=>\"left\"===t?\"right\":\"right\"===t?\"left\":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;r<l;++r)e=a[r].controller.getMinMax(this,t),n||(i=Math.min(i,e.min)),o||(s=Math.max(s,e.max));return i=o&&i>s?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a<this.ticks.length;this._convertTicksToLabels(r?Xs(this.ticks,a):this.ticks),this.configure(),this.beforeCalculateLabelRotation(),this.calculateLabelRotation(),this.afterCalculateLabelRotation(),o.display&&(o.autoSkip||\"auto\"===o.source)&&(this.ticks=js(this,this.ticks),this._labelSizes=null,this.afterAutoSkip()),r&&this._convertTicksToLabels(this.ticks),this.beforeFit(),this.fit(),this.afterFit(),this.afterUpdate()}configure(){let t,e,i=this.options.reverse;this.isHorizontal()?(t=this.left,e=this.right):(t=this.top,e=this.bottom,i=!i),this._startPixel=t,this._endPixel=e,this._reversePixels=i,this._length=e-t,this._alignToPixels=this.options.alignToPixels}afterUpdate(){d(this.options.afterUpdate,[this])}beforeSetDimensions(){d(this.options.beforeSetDimensions,[this])}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=0,this.right=this.width):(this.height=this.maxHeight,this.top=0,this.bottom=this.height),this.paddingLeft=0,this.paddingTop=0,this.paddingRight=0,this.paddingBottom=0}afterSetDimensions(){d(this.options.afterSetDimensions,[this])}_callHooks(t){this.chart.notifyPlugins(t,this.getContext()),d(this.options[t],[this])}beforeDataLimits(){this._callHooks(\"beforeDataLimits\")}determineDataLimits(){}afterDataLimits(){this._callHooks(\"afterDataLimits\")}beforeBuildTicks(){this._callHooks(\"beforeBuildTicks\")}buildTicks(){return[]}afterBuildTicks(){this._callHooks(\"afterBuildTicks\")}beforeTickToLabelConversion(){d(this.options.beforeTickToLabelConversion,[this])}generateTickLabels(t){const e=this.options.ticks;let i,s,n;for(i=0,s=t.length;i<s;i++)n=t[i],n.label=d(e.callback,[n.value,i,t],this)}afterTickToLabelConversion(){d(this.options.afterTickToLabelConversion,[this])}beforeCalculateLabelRotation(){d(this.options.beforeCalculateLabelRotation,[this])}calculateLabelRotation(){const t=this.options,e=t.ticks,i=Us(this.ticks.length,t.ticks.maxTicksLimit),s=e.minRotation||0,n=e.maxRotation;let o,a,r,l=s;if(!this._isVisible()||!e.display||s>=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l=\"top\"!==a&&\"x\"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):\"start\"===n?d=e.width:\"end\"===n?c=t.width:\"inner\"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;\"start\"===n?(i=0,s=t.height):\"end\"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return\"top\"===e||\"bottom\"===e||\"x\"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e<i;e++)s(t[e].label)&&(t.splice(e,1),i--,e--);this.afterTickToLabelConversion()}_getLabelSizes(){let t=this._labelSizes;if(!t){const e=this.options.ticks.sampleSize;let i=this.ticks;e<i.length&&(i=Xs(i,e)),this._labelSizes=t=this._computeLabelSizes(i,i.length,this.options.ticks.maxTicksLimit)}return t}_computeLabelSizes(t,e,i){const{ctx:o,_longestTextCache:a}=this,r=[],l=[],h=Math.floor(e/Us(e,i));let c,d,f,g,p,m,b,x,_,y,v,M=0,w=0;for(c=0;c<e;c+=h){if(g=t[c].label,p=this._resolveTickFontOptions(c),o.font=m=p.string,b=a[m]=a[m]||{data:{},gc:[]},x=p.lineHeight,_=y=0,s(g)||n(g)){if(n(g))for(d=0,f=g.length;d<f;++d)v=g[d],s(v)||n(v)||(_=Ce(o,b.data,b.gc,_,v),y+=x)}else _=Ce(o,b.data,b.gc,_,g),y=x;r.push(_),l.push(y),M=Math.max(_,M),w=Math.max(y,w)}!function(t,e){u(t,(t=>{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n<s;++n)delete t.data[i[n]];i.splice(0,s)}}))}(a,e);const k=r.indexOf(M),S=l.indexOf(w),P=t=>({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&t<e.length){const i=e[t];return i.$context||(i.$context=function(t,e,i){return Ci(t,{tick:i,index:e,type:\"tick\"})}(this.getContext(),t,i))}return this.$context||(this.$context=Ci(this.chart.getContext(),{scale:this,type:\"scale\"}))}_tickSize(){const t=this.options.ticks,e=$(this.labelRotation),i=Math.abs(Math.cos(e)),s=Math.abs(Math.sin(e)),n=this._getLabelSizes(),o=t.autoSkipPadding||0,a=n?n.widest.width+o:0,r=n?n.highest.height+o:0;return this.isHorizontal()?r*i>a*s?a/i:r/s:r*s<a*i?r/i:a/s}_isVisible(){const t=this.options.display;return\"auto\"!==t?!!t:this.getMatchingVisibleMetas().length>0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(t){return Ae(i,t,p)};let x,_,y,v,M,w,k,S,P,D,C,O;if(\"top\"===a)x=b(this.bottom),w=this.bottom-u,S=x-m,D=b(t.top)+m,O=t.bottom;else if(\"bottom\"===a)x=b(this.top),D=t.top,O=b(t.bottom)-m,w=x+m,S=this.top+u;else if(\"left\"===a)x=b(this.right),M=this.right-u,k=x-m,P=b(t.left)+m,C=t.right;else if(\"right\"===a)x=b(this.left),P=t.left,C=b(t.right)-m,M=x+m,k=this.left+u;else if(\"x\"===e){if(\"center\"===a)x=b((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=x+m,S=w+u}else if(\"y\"===e){if(\"center\"===a)x=b((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}M=x-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_<d;_+=T){const t=this.getContext(_),e=n.setContext(t),s=r.setContext(t),o=e.lineWidth,a=e.color,l=s.dash||[],d=s.dashOffset,u=e.tickWidth,g=e.tickColor,p=e.tickBorderDash||[],m=e.tickBorderDashOffset;y=qs(this,_,h),void 0!==y&&(v=Ae(i,y,o),c?M=k=P=C=v:w=S=D=O=v,f.push({tx1:M,ty1:w,tx2:k,ty2:S,x1:P,y1:D,x2:C,y2:O,width:o,color:a,borderDash:l,borderDashOffset:d,tickWidth:u,tickColor:g,tickBorderDash:p,tickBorderDashOffset:m}))}return this._ticksLength=d,this._borderValue=x,f}_computeLabelItems(t){const e=this.axis,i=this.options,{position:s,ticks:a}=i,r=this.isHorizontal(),l=this.ticks,{align:h,crossAlign:c,padding:d,mirror:u}=a,f=Ks(i.grid),g=f+d,p=u?-d:g,m=-$(this.labelRotation),b=[];let x,_,y,v,M,w,k,S,P,D,C,O,A=\"middle\";if(\"top\"===s)w=this.bottom-p,k=this._getXAxisLabelAlignment();else if(\"bottom\"===s)w=this.top+p,k=this._getXAxisLabelAlignment();else if(\"left\"===s){const t=this._getYAxisLabelAlignment(f);k=t.textAlign,M=t.x}else if(\"right\"===s){const t=this._getYAxisLabelAlignment(f);k=t.textAlign,M=t.x}else if(\"x\"===e){if(\"center\"===s)w=(t.top+t.bottom)/2+g;else if(o(s)){const t=Object.keys(s)[0],e=s[t];w=this.chart.scales[t].getPixelForValue(e)+g}k=this._getXAxisLabelAlignment()}else if(\"y\"===e){if(\"center\"===s)M=(t.left+t.right)/2-g;else if(o(s)){const t=Object.keys(s)[0],e=s[t];M=this.chart.scales[t].getPixelForValue(e)}k=this._getYAxisLabelAlignment(f).textAlign}\"y\"===e&&(\"start\"===h?A=\"top\":\"end\"===h&&(A=\"bottom\"));const T=this._getLabelSizes();for(x=0,_=l.length;x<_;++x){y=l[x],v=y.label;const t=a.setContext(this.getContext(x));S=this.getPixelForTick(x)+a.labelOffset,P=this._resolveTickFontOptions(x),D=P.lineHeight,C=n(v)?v.length:1;const e=C/2,i=t.color,o=t.textStrokeColor,h=t.textStrokeWidth;let d,f=k;if(r?(M=S,\"inner\"===k&&(f=x===_-1?this.options.reverse?\"left\":\"right\":0===x?this.options.reverse?\"right\":\"left\":\"center\"),O=\"top\"===s?\"near\"===c||0!==m?-C*D+D/2:\"center\"===c?-T.highest.height/2-e*D+D:-T.highest.height+D/2:\"near\"===c||0!==m?D/2:\"center\"===c?T.highest.height/2-e*D:T.highest.height-C*D,u&&(O*=-1),0===m||t.showLabelBackdrop||(M+=D/2*Math.sin(m))):(w=S,O=(1-C)*D/2),t.showLabelBackdrop){const e=ki(t.backdropPadding),i=T.heights[x],s=T.widths[x];let n=O-e.top,o=0-e.left;switch(A){case\"middle\":n-=i/2;break;case\"bottom\":n-=i}switch(k){case\"center\":o-=s/2;break;case\"right\":o-=s}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}b.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return b}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return\"top\"===t?\"left\":\"right\";let i=\"center\";return\"start\"===e.align?i=\"left\":\"end\"===e.align?i=\"right\":\"inner\"===e.align&&(i=\"inner\"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return\"left\"===e?s?(l=this.right+n,\"near\"===i?r=\"left\":\"center\"===i?(r=\"center\",l+=a/2):(r=\"right\",l+=a)):(l=this.right-o,\"near\"===i?r=\"right\":\"center\"===i?(r=\"center\",l-=a/2):(r=\"left\",l=this.left)):\"right\"===e?s?(l=this.left+n,\"near\"===i?r=\"right\":\"center\"===i?(r=\"center\",l-=a/2):(r=\"left\",l-=a)):(l=this.left+o,\"near\"===i?r=\"left\":\"center\"===i?(r=\"center\",l+=a/2):(r=\"right\",l=this.right)):r=\"right\",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return\"left\"===e||\"right\"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:\"top\"===e||\"bottom\"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n<o;++n){const t=s[n];e.drawOnChartArea&&a({x:t.x1,y:t.y1},{x:t.x2,y:t.y2},t),e.drawTicks&&a({x:t.tx1,y:t.ty1},{x:t.tx2,y:t.ty2},{color:t.tickColor,width:t.tickWidth,borderDash:t.tickBorderDash,borderDashOffset:t.tickBorderDashOffset})}}drawBorder(){const{chart:t,ctx:e,options:{border:i,grid:s}}=this,n=i.setContext(this.getContext()),o=i.display?n.width:0;if(!o)return;const a=s.setContext(this.getContext(0)).lineWidth,r=this._borderValue;let l,h,c,d;this.isHorizontal()?(l=Ae(t,this.left,o)-o/2,h=Ae(t,this.right,a)+a/2,c=d=r):(c=Ae(t,this.top,o)-o/2,d=Ae(t,this.bottom,a)+a/2,l=h=r),e.save(),e.lineWidth=n.width,e.strokeStyle=n.color,e.beginPath(),e.moveTo(l,c),e.lineTo(h,d),e.stroke(),e.restore()}drawLabels(t){if(!this.options.ticks.display)return;const e=this.ctx,i=this._computeLabelArea();i&&Ie(e,i);const s=this.getLabelItems(t);for(const t of s){const i=t.options,s=t.font;Ne(e,t.label,0,t.textOffset,s,i)}i&&ze(e)}drawTitle(){const{ctx:t,options:{position:e,title:i,reverse:s}}=this;if(!i.display)return;const a=Si(i.font),r=ki(i.padding),l=i.align;let h=a.lineHeight/2;\"bottom\"===e||\"center\"===e||o(e)?(h+=r.bottom,n(i.text)&&(h+=a.lineHeight*(i.text.length-1))):h+=r.top;const{titleX:c,titleY:d,maxWidth:u,rotation:f}=function(t,e,i,s){const{top:n,left:a,bottom:r,right:l,chart:h}=t,{chartArea:c,scales:d}=h;let u,f,g,p=0;const m=r-n,b=l-a;if(t.isHorizontal()){if(f=ft(s,a,l),o(i)){const t=Object.keys(i)[0],s=i[t];g=d[t].getPixelForValue(s)+m-e}else g=\"center\"===i?(c.bottom+c.top)/2+m-e:Ys(t,i,e);u=l-a}else{if(o(i)){const t=Object.keys(i)[0],s=i[t];f=d[t].getPixelForValue(s)-b+e}else f=\"center\"===i?(c.left+c.right)/2-b+e:Ys(t,i,e);g=ft(s,r,n),p=\"left\"===i?-E:E}return{titleX:f,titleY:g,maxWidth:u,rotation:p}}(this,h,e,l);Ne(t,i.text,0,0,a,{color:i.color,maxWidth:u,rotation:f,textAlign:Zs(l,e,s),textBaseline:\"middle\",translation:[c,d]})}draw(t){this._isVisible()&&(this.drawBackground(),this.drawGrid(t),this.drawBorder(),this.drawTitle(),this.drawLabels(t))}_layers(){const t=this.options,e=t.ticks&&t.ticks.z||0,i=l(t.grid&&t.grid.z,-1),s=l(t.border&&t.border.z,0);return this._isVisible()&&this.draw===Js.prototype.draw?[{z:i,draw:t=>{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+\"AxisID\",s=[];let n,o;for(n=0,o=e.length;n<o;++n){const o=e[n];o[i]!==this.id||t&&o.type!==t||s.push(o)}return s}_resolveTickFontOptions(t){return Si(this.options.ticks.setContext(this.getContext(t)).font)}_maxDigits(){const t=this._resolveTickFontOptions(0).lineHeight;return(this.isHorizontal()?this.width:this.height)/t}}class Qs{constructor(t,e,i){this.type=t,this.scope=e,this.override=i,this.items=Object.create(null)}isForType(t){return Object.prototype.isPrototypeOf.call(this.type.prototype,t.prototype)}register(t){const e=Object.getPrototypeOf(t);let i;(function(t){return\"id\"in t&&\"defaults\"in t})(e)&&(i=this.register(e));const s=this.items,n=t.id,o=this.scope+\".\"+n;if(!n)throw new Error(\"class does not have id: \"+t);return n in s||(s[n]=t,function(t,e,i){const s=b(Object.create(null),[i?ue.get(i):{},ue.get(e),t.defaults]);ue.set(e,s),t.defaultRoutes&&function(t,e){Object.keys(e).forEach((i=>{const s=i.split(\".\"),n=s.pop(),o=[t].concat(s).join(\".\"),a=e[i].split(\".\"),r=a.pop(),l=a.join(\".\");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,\"datasets\",!0),this.elements=new Qs(Hs,\"elements\"),this.plugins=new Qs(Object,\"plugins\"),this.scales=new Qs(Js,\"scales\"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each(\"register\",t)}remove(...t){this._each(\"unregister\",t)}addControllers(...t){this._each(\"register\",t,this.controllers)}addElements(...t){this._each(\"register\",t,this.elements)}addPlugins(...t){this._each(\"register\",t,this.plugins)}addScales(...t){this._each(\"register\",t,this.scales)}getController(t){return this._get(t,this.controllers,\"controller\")}getElement(t){return this._get(t,this.elements,\"element\")}getPlugin(t){return this._get(t,this.plugins,\"plugin\")}getScale(t){return this._get(t,this.scales,\"scale\")}removeControllers(...t){this._each(\"unregister\",t,this.controllers)}removeElements(...t){this._each(\"unregister\",t,this.elements)}removePlugins(...t){this._each(\"unregister\",t,this.plugins)}removeScales(...t){this._each(\"unregister\",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i[\"before\"+s],[],i),e[t](i),d(i[\"after\"+s],[],i)}_getRegistryForType(t){for(let e=0;e<this._typedRegistries.length;e++){const i=this._typedRegistries[e];if(i.isForType(t))return i}return this.plugins}_get(t,e,i){const s=e.get(t);if(void 0===s)throw new Error('\"'+t+'\" is not a registered '+i+\".\");return s}}var en=new tn;class sn{constructor(){this._init=[]}notify(t,e,i,s){\"beforeInit\"===e&&(this._init=this._createDescriptors(t,!0),this._notify(this._init,t,\"install\"));const n=s?this._descriptors(t).filter(s):this._descriptors(t),o=this._notify(n,t,e,i);return\"afterDestroy\"===e&&(this._notify(n,t,\"stop\"),this._notify(this._init,t,\"uninstall\")),o}_notify(t,e,i,s){s=s||{};for(const n of t){const t=n.plugin;if(!1===d(t[i],[e,s,n.options],t)&&s.cancelable)return!1}return!0}invalidate(){s(this._cache)||(this._oldCache=this._cache,this._cache=void 0)}_descriptors(t){if(this._cache)return this._cache;const e=this._cache=this._createDescriptors(t);return this._notifyStateChanges(t),e}_createDescriptors(t,e){const i=t&&t.config,s=l(i.options&&i.options.plugins,{}),n=function(t){const e={},i=[],s=Object.keys(en.plugins.items);for(let t=0;t<s.length;t++)i.push(en.getPlugin(s[t]));const n=t.plugins||[];for(let t=0;t<n.length;t++){const s=n[t];-1===i.indexOf(s)&&(i.push(s),e[s.id]=!0)}return{plugins:i,localIds:e}}(i);return!1!==s||e?function(t,{plugins:e,localIds:i},s,n){const o=[],a=t.getContext();for(const r of e){const e=r.id,l=nn(s[e],n);null!==l&&o.push({plugin:r,options:on(t.config,{plugin:r,local:i[e]},l,a)})}return o}(t,n,s,e):[]}_notifyStateChanges(t){const e=this._oldCache||[],i=this._cache,s=(t,e)=>t.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,\"stop\"),this._notify(s(i,e),t,\"start\")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[\"\"],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||\"x\"}function rn(t){if(\"x\"===t||\"y\"===t||\"r\"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||(\"top\"===(i=s.position)||\"bottom\"===i?\"x\":\"left\"===i||\"right\"===i?\"y\":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+\"AxisID\"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,\"x\",i[0])||hn(t,\"y\",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?\"_index_\":\"_value_\"}(l,n),c=i.scales||{};a[e]=x(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return\"_index_\"===t?i=e:\"_value_\"===t&&(i=\"x\"===e?\"y\":\"x\"),i}(t,o),n=i[e+\"AxisID\"]||e;a[n]=a[n]||Object.create(null),x(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];x(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class bn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,\"\"]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,\"\"]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,\"\"]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[\"\"]){const o={$shared:!0},{resolver:a,subPrefixes:r}=xn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[\"\"],s){const{resolver:n}=xn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function xn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes(\"hover\")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).reduce(((e,i)=>e||S(t[i])),!1);const yn=[\"top\",\"bottom\",\"left\",\"right\",\"chartArea\"];function vn(t,e){return\"top\"===t||\"bottom\"===t||-1===yn.indexOf(t)&&\"x\"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins(\"afterRender\"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&\"string\"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version=\"4.4.0\";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new bn(e),n=Sn(t),o=Dn(n);if(o)throw new Error(\"Canvas is already in use. Chart with ID '\"+o.id+\"' must be destroyed before the canvas with ID '\"+o.canvas.id+\"' can be reused.\");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(xt.listen(this,\"complete\",wn),xt.listen(this,\"progress\",kn),this._initialize(),this.attached&&this.update()):console.error(\"Failed to create chart: can't acquire context from the given item\")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins(\"beforeInit\"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins(\"afterInit\"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?\"resize\":\"attach\";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins(\"resize\",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n=\"r\"===s,o=\"x\"===s;return{options:i,dposition:n?\"chartArea\":o?\"bottom\":\"left\",dtype:n?\"radialLinear\":o?\"category\":\"linear\"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;t<i;++t)this._destroyDatasetMeta(t);t.splice(e,i-e)}this._sortedMetasets=t.slice(0).sort(Mn(\"order\",\"index\"))}_removeUnreferencedMetasets(){const{_metasets:t,data:{datasets:e}}=this;t.length>e.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i<s;i++){const s=e[i];let n=this.getDatasetMeta(i);const o=s.type||this.config.type;if(n.type&&n.type!==o&&(this._destroyDatasetMeta(i),n=this.getDatasetMeta(i)),n.type=o,n.indexAxis=s.indexAxis||an(o,this.options),n.order=s.order||0,n.index=i,n.label=\"\"+s.label,n.visible=this.isDatasetVisible(i),n.controller)n.controller.updateIndex(i),n.controller.linkScales();else{const e=en.getController(o),{datasetElementType:s,dataElementType:a}=ue.datasets[o];Object.assign(e,{dataElementType:en.getElement(a),datasetElementType:s&&en.getElement(s)}),n.controller=new e(this,i),t.push(n.controller)}}return this._updateMetasets(),t}_resetElements(){u(this.data.datasets,((t,e)=>{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins(\"reset\")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins(\"beforeUpdate\",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins(\"beforeElementsUpdate\");let o=0;for(let t=0,e=this.data.datasets.length;t<e;t++){const{controller:e}=this.getDatasetMeta(t),i=!s&&-1===n.indexOf(e);e.buildOrUpdateElements(i),o=Math.max(+e.getMaxOverflow(),o)}o=this._minPadding=i.layout.autoPadding?o:0,this._updateLayout(o),s||u(n,(t=>{t.reset()})),this._updateDatasets(t),this.notifyPlugins(\"afterUpdate\",{mode:t}),this._layers.sort(Mn(\"z\",\"_idx\"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,\"_removeElements\"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+\",\"+t.splice(1).join(\",\")))),s=i(0);for(let t=1;t<e;t++)if(!P(s,i(t)))return;return Array.from(s).map((t=>t.split(\",\"))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins(\"beforeLayout\",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&\"chartArea\"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins(\"afterLayout\")}_updateDatasets(t){if(!1!==this.notifyPlugins(\"beforeDatasetsUpdate\",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t<e;++t)this.getDatasetMeta(t).controller.configure();for(let e=0,i=this.data.datasets.length;e<i;++e)this._updateDataset(e,S(t)?t({datasetIndex:e}):t);this.notifyPlugins(\"afterDatasetsUpdate\",{mode:t})}}_updateDataset(t,e){const i=this.getDatasetMeta(t),s={meta:i,index:t,mode:e,cancelable:!0};!1!==this.notifyPlugins(\"beforeDatasetUpdate\",s)&&(i.controller._update(e),s.cancelable=!1,this.notifyPlugins(\"afterDatasetUpdate\",s))}render(){!1!==this.notifyPlugins(\"beforeRender\",{cancelable:!0})&&(xt.has(this)?this.attached&&!xt.running(this)&&xt.start(this):(this.draw(),wn({chart:this})))}draw(){let t;if(this._resizeBeforeDraw){const{width:t,height:e}=this._resizeBeforeDraw;this._resize(t,e),this._resizeBeforeDraw=null}if(this.clear(),this.width<=0||this.height<=0)return;if(!1===this.notifyPlugins(\"beforeDraw\",{cancelable:!0}))return;const e=this._layers;for(t=0;t<e.length&&e[t].z<=0;++t)e[t].draw(this.chartArea);for(this._drawDatasets();t<e.length;++t)e[t].draw(this.chartArea);this.notifyPlugins(\"afterDraw\")}_getSortedDatasetMetas(t){const e=this._sortedMetasets,i=[];let s,n;for(s=0,n=e.length;s<n;++s){const n=e[s];t&&!n.visible||i.push(n)}return i}getSortedVisibleDatasetMetas(){return this._getSortedDatasetMetas(!0)}_drawDatasets(){if(!1===this.notifyPlugins(\"beforeDatasetsDraw\",{cancelable:!0}))return;const t=this.getSortedVisibleDatasetMetas();for(let e=t.length-1;e>=0;--e)this._drawDataset(t[e]);this.notifyPlugins(\"afterDatasetsDraw\")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,\"left\"),right:On(i,e,\"right\"),top:On(s,e,\"top\"),bottom:On(s,e,\"bottom\")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins(\"beforeDatasetDraw\",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins(\"afterDatasetDraw\",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return\"function\"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:\"chart\"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return\"boolean\"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?\"show\":\"hide\",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t<e;++t)this._destroyDatasetMeta(t)}destroy(){this.notifyPlugins(\"beforeDestroy\");const{canvas:t,ctx:e}=this;this._stop(),this.config.clearCache(),t&&(this.unbindEvents(),Te(t,e),this.platform.releaseContext(e),this.canvas=null,this.ctx=null),delete Pn[this.id],this.notifyPlugins(\"afterDestroy\")}toBase64Image(...t){return this.canvas.toDataURL(...t)}bindEvents(){this.bindUserEvents(),this.options.responsive?this.bindResponsiveEvents():this.attached=!0}bindUserEvents(){const t=this._listeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s(\"attach\",a),this.attached=!0,this.resize(),i(\"resize\",n),i(\"detach\",o)};o=()=>{this.attached=!1,s(\"resize\",n),this._stop(),this._resize(0,0),i(\"attach\",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?\"set\":\"remove\";let n,o,a,r;for(\"dataset\"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller[\"_\"+s+\"DatasetHoverStyle\"]()),a=0,r=t.length;a<r;++a){o=t[a];const e=o&&this.getDatasetMeta(o.datasetIndex).controller;e&&e[s+\"HoverStyle\"](o.element,o.datasetIndex,o.index)}}getActiveElements(){return this._active||[]}setActiveElements(t){const e=this._active||[],i=t.map((({datasetIndex:t,index:e})=>{const i=this.getDatasetMeta(t);if(!i)throw new Error(\"No dataset found at index \"+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins(\"beforeEvent\",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins(\"afterEvent\",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&\"mouseout\"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if(\"mouseout\"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error(\"This method is not implemented: Check that a complete date adapter is provided.\")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;e<n;e++)s=s.concat(i[e].controller.getAllParsedValues(t));t._cache.$bar=lt(s.sort(((t,e)=>t-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;s<n;++s)o=e.getPixelForValue(i[s]),l();for(a=void 0,s=0,n=e.ticks.length;s<n;++s)o=e.getPixelForTick(s),l();return r}function zn(t,e,i,s){return n(t)?function(t,e,i,s){const n=i.parse(t[0],s),o=i.parse(t[1],s),a=Math.min(n,o),r=Math.max(n,o);let l=a,h=r;Math.abs(a)>Math.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;h<c;++h)u=e[h],d={},d[n.axis]=r||n.parse(a[h],h),l.push(zn(u,d,o,h));return l}function Vn(t){return t&&void 0!==t.barStart&&void 0!==t.barEnd}function Bn(t,e,i,s){let n=e.borderSkipped;const o={};if(!n)return void(t.borderSkipped=o);if(!0===n)return void(t.borderSkipped={top:!0,right:!0,bottom:!0,left:!0});const{start:a,end:r,reverse:l,top:h,bottom:c}=function(t){let e,i,s,n,o;return t.horizontal?(e=t.base>t.x,i=\"left\",s=\"right\"):(e=t.base<t.y,i=\"bottom\",s=\"top\"),e?(n=\"end\",o=\"start\"):(n=\"start\",o=\"end\"),{start:i,end:s,reverse:e,top:n,bottom:o}}(t);\"middle\"===n&&i&&(t.enableBorderRadius=!0,(i._top||0)===s?n=h:(i._bottom||0)===s?n=c:(o[Wn(c,a,r,l)]=!0,n=h)),o[Wn(n,a,r,l)]=!0,t.borderSkipped=o}function Wn(t,e,i,s){var n,o,a;return s?(a=i,t=Nn(t=(n=t)===(o=e)?a:n===a?o:n,i,e)):t=Nn(t,e,i),t}function Nn(t,e,i){return\"start\"===t?e:\"end\"===t?i:t}function Hn(t,{inflateAmount:e},i){t.inflateAmount=\"auto\"===e?1===i?.33:0:e}class jn extends Ns{static id=\"doughnut\";static defaults={datasetElementType:!1,dataElementType:\"arc\",animation:{animateRotate:!0,animateScale:!1},animations:{numbers:{type:\"number\",properties:[\"circumference\",\"endAngle\",\"innerRadius\",\"outerRadius\",\"startAngle\",\"x\",\"y\",\"offset\",\"borderWidth\",\"spacing\"]}},cutout:\"50%\",rotation:0,circumference:360,radius:\"100%\",spacing:0,indexAxis:\"r\"};static descriptors={_scriptable:t=>\"spacing\"!==t,_indexable:t=>\"spacing\"!==t&&!t.startsWith(\"borderDash\")&&!t.startsWith(\"hoverBorderDash\")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t=\"value\"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;n<a;++n)s._parsed[n]=r(n)}}_getRotation(){return $(this.options.rotation-90)}_getCircumference(){return $(this.options.circumference)}_getRotationExtents(){let t=O,e=-O;for(let i=0;i<this.chart.data.datasets.length;++i)if(this.chart.isDatasetVisible(i)&&this.chart.getDatasetMeta(i).type===this._type){const s=this.chart.getDatasetMeta(i).controller,n=s._getRotation(),o=s._getCircumference();t=Math.min(t,n),e=Math.max(e,n+o)}return{rotation:t,circumference:e-t}}update(t){const e=this.chart,{chartArea:i}=e,s=this._cachedMeta,n=s.data,o=this.getMaxBorderWidth()+this.getMaxOffset(n)+this.options.spacing,a=Math.max((Math.min(i.width,i.height)-o)/2,0),r=Math.min(h(this.options.cutout,a),1),l=this._getRingWeight(this.index),{circumference:d,rotation:u}=this._getRotationExtents(),{ratioX:f,ratioY:g,offsetX:p,offsetY:m}=function(t,e,i){let s=1,n=1,o=0,a=0;if(e<O){const r=t,l=r+e,h=Math.cos(r),c=Math.sin(r),d=Math.cos(l),u=Math.sin(l),f=(t,e,s)=>Z(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),b=g(C,h,d),x=g(C+E,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n=\"reset\"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p<e;++p)m+=this._circumference(p,n);for(p=e;p<e+i;++p){const e=this._circumference(p,n),i=t[p],o={x:l+this.offsetX,y:h+this.offsetY,startAngle:m,endAngle:m+e,circumference:e,outerRadius:u,innerRadius:d};g&&(o.options=f||this.resolveDataElementOptions(p,i.active?\"active\":s)),m+=e,this.updateElement(i,p,o,s)}}calculateTotal(){const t=this._cachedMeta,e=t.data;let i,s=0;for(i=0;i<e.length;i++){const n=t._parsed[i];null===n||isNaN(n)||!this.chart.getDataVisibility(i)||e[i].hidden||(s+=Math.abs(n))}return s}calculateCircumference(t){const e=this._cachedMeta.total;return e>0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||\"\",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s<n;++s)if(i.isDatasetVisible(s)){o=i.getDatasetMeta(s),t=o.data,a=o.controller;break}if(!t)return 0;for(s=0,n=t.length;s<n;++s)r=a.resolveDataElementOptions(s),\"inner\"!==r.borderAlign&&(e=Math.max(e,r.borderWidth||0,r.hoverBorderWidth||0));return e}getMaxOffset(t){let e=0;for(let i=0,s=t.length;i<s;++i){const t=this.resolveDataElementOptions(i);e=Math.max(e,t.offset||0,t.hoverOffset||0)}return e}_getRingWeightOffset(t){let e=0;for(let i=0;i<t;++i)this.chart.isDatasetVisible(i)&&(e+=this._getRingWeight(i));return e}_getRingWeight(t){return Math.max(l(this.chart.data.datasets[t].weight,1),0)}_getVisibleDatasetWeightTotal(){return this._getRingWeightOffset(this.chart.data.datasets.length)||1}}class $n extends Ns{static id=\"polarArea\";static defaults={dataElementType:\"arc\",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:\"number\",properties:[\"x\",\"y\",\"startAngle\",\"endAngle\",\"innerRadius\",\"outerRadius\"]}},indexAxis:\"r\",startAngle:0};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:\"radialLinear\",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||\"\",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(s<e.min&&(e.min=s),s>e.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n=\"reset\"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d<e;++d)u+=this._computeAngle(d,s,f);for(d=e;d<e+i;d++){const e=t[d];let i=u,g=u+this._computeAngle(d,s,f),p=o.getDataVisibility(d)?r.getDistanceFromCenterForValue(this.getParsed(d).r):0;u=g,n&&(a.animateScale&&(p=0),a.animateRotate&&(i=g=c));const m={x:l,y:h,innerRadius:0,outerRadius:p,startAngle:i,endAngle:g,options:this.resolveDataElementOptions(d,e.active?\"active\":s)};this.updateElement(e,d,m,s)}}countVisibleElements(){const t=this._cachedMeta;let e=0;return t.data.forEach(((t,i)=>{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id=\"bar\";static defaults={datasetElementType:!1,dataElementType:\"bar\",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:\"number\",properties:[\"x\",\"y\",\"base\",\"width\",\"height\"]}}};static overrides={scales:{_index_:{type:\"category\",offset:!0,grid:{offset:!0}},_value_:{type:\"linear\",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a=\"x\",yAxisKey:r=\"y\"}=this._parsing,l=\"x\"===n.axis?a:r,h=\"x\"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;d<u;++d)g=e[d],f={},f[n.axis]=n.parse(M(g,l),d),c.push(zn(M(g,h),f,o,d));return c}updateRangeFromParsed(t,e,i,s){super.updateRangeFromParsed(t,e,i,s);const n=i._custom;n&&e===this._cachedMeta.vScale&&(t.min=Math.min(t.min,n.min),t.max=Math.max(t.max,n.max))}getMaxOverflow(){return 0}getLabelAndValue(t){const e=this._cachedMeta,{iScale:i,vScale:s}=e,n=this.getParsed(t),o=n._custom,a=Vn(o)?\"[\"+o.start+\", \"+o.end+\"]\":\"\"+s.getLabelForValue(n[s.axis]);return{label:\"\"+i.getLabelForValue(n[i.axis]),value:a}}initialize(){this.enableOptionSharing=!0,super.initialize();this._cachedMeta.stack=this.getDataset().stack}update(t){const e=this._cachedMeta;this.updateElements(e.data,0,e.data.length,t)}updateElements(t,e,i,n){const o=\"reset\"===n,{index:a,_cachedMeta:{vScale:r}}=this,l=r.getBasePixel(),h=r.isHorizontal(),c=this._getRuler(),{sharedOptions:d,includeOptions:u}=this._getSharedOptions(e,n);for(let f=e;f<e+i;f++){const e=this.getParsed(f),i=o||s(e[r.axis])?{base:l,head:l}:this._calculateBarValuePixels(f),g=this._calculateBarIndexPixels(f,c),p=(e._stacks||{})[r.axis],m={horizontal:h,base:i.base,enableBorderRadius:!p||Vn(e._custom)||a===p._top||a===p._bottom,x:h?i.head:g.center,y:h?g.center:i.head,height:h?g.size:Math.abs(i.size),width:h?Math.abs(i.size):g.size};u&&(m.options=d||this.resolveDataElementOptions(f,t[f].active?\"active\":n));const b=m.options||t[f].options;Bn(m,b,p,a),Hn(m,b,c.ratio),this.updateElement(t[f],f,m,n)}}_getStacks(t,e){const{iScale:i}=this._cachedMeta,n=i.getMatchingVisibleMetas(this._type).filter((t=>t.controller.options.grouped)),o=i.options.stacked,a=[],r=t=>{const i=t.controller.getParsed(e),n=i&&i[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n<o;++n)s.push(i.getPixelForValue(this.getParsed(n)[i.axis],n));const a=t.barThickness;return{min:a||In(e),pixels:s,start:i._startPixel,end:i._endPixel,stackCount:this._getStackCount(),scale:i,grouped:t.grouped,ratio:a?1:t.categoryPercentage*t.barPercentage}}_calculateBarValuePixels(t){const{_cachedMeta:{vScale:e,_stacked:i,index:n},options:{base:o,minBarLength:a}}=this,r=o||0,l=this.getParsed(t),h=l._custom,c=Vn(h);let d,u,f=l[e.axis],g=0,p=i?this.applyStack(e,l,i):f;p!==f&&(g=p-f,p=f),c&&(f=h.barStart,p=h.barEnd-h.barStart,0!==f&&F(f)!==F(h.barEnd)&&(g=0),g+=f);const m=s(o)||c?g:o;let b=e.getPixelForValue(m);if(d=this.chart.getDataVisibility(t)?e.getPixelForValue(g+p):b,u=d-b,Math.abs(u)<a){u=function(t,e,i){return 0!==t?F(t):(e.isHorizontal()?1:-1)*(e.min>=i?1:-1)}(u,e,r)*a,f===r&&(b-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);b=Math.max(Math.min(b,h),o),d=b+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(b))}if(b===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;b+=t,u-=t}return{size:u,base:b,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l=\"flex\"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t<n.length-1?n[t+1]:null;const l=i.categoryPercentage;null===a&&(a=o-(null===r?e.end-e.start:r-o)),null===r&&(r=o+o-a);const h=o-(o-Math.min(a,r))/2*l;return{chunk:Math.abs(r-a)/2*l/s,ratio:i.barPercentage,start:h}}(t,e,n,i):function(t,e,i,n){const o=i.barThickness;let a,r;return s(o)?(a=e.min*i.categoryPercentage,r=i.barPercentage):(a=o*n,r=1),{chunk:a/n,ratio:r,start:e.pixels[t]-a/2}}(t,e,n,i),c=this._getStackIndex(this.index,this._cachedMeta.stack,o?t:void 0);r=l.start+l.chunk*c+l.chunk/2,h=Math.min(a,l.chunk*l.ratio)}else r=i.getPixelForValue(this.getParsed(t)[i.axis],t),h=Math.min(a,e.min*e.ratio);return{base:r-h/2,head:r+h/2,center:r,size:h}}draw(){const t=this._cachedMeta,e=t.vScale,i=t.data,s=i.length;let n=0;for(;n<s;++n)null!==this.getParsed(n)[e.axis]&&i[n].draw(this._ctx)}},BubbleController:class extends Ns{static id=\"bubble\";static defaults={datasetElementType:!1,dataElementType:\"point\",animations:{numbers:{type:\"number\",properties:[\"x\",\"y\",\"borderWidth\",\"radius\"]}}};static overrides={scales:{x:{type:\"linear\"},y:{type:\"linear\"}}};initialize(){this.enableOptionSharing=!0,super.initialize()}parsePrimitiveData(t,e,i,s){const n=super.parsePrimitiveData(t,e,i,s);for(let t=0;t<n.length;t++)n[t]._custom=this.resolveDataElementOptions(t+i).radius;return n}parseArrayData(t,e,i,s){const n=super.parseArrayData(t,e,i,s);for(let t=0;t<n.length;t++){const s=e[i+t];n[t]._custom=l(s[2],this.resolveDataElementOptions(t+i).radius)}return n}parseObjectData(t,e,i,s){const n=super.parseObjectData(t,e,i,s);for(let t=0;t<n.length;t++){const s=e[i+t];n[t]._custom=l(s&&s.r&&+s.r,this.resolveDataElementOptions(t+i).radius)}return n}getMaxOverflow(){const t=this._cachedMeta.data;let e=0;for(let i=t.length-1;i>=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||\"\",value:\"(\"+a+\", \"+r+(l?\", \"+l:\"\")+\")\"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n=\"reset\"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d<e+i;d++){const e=t[d],i=!n&&this.getParsed(d),u={},f=u[h]=n?o.getPixelForDecimal(.5):o.getPixelForValue(i[h]),g=u[c]=n?a.getBasePixel():a.getPixelForValue(i[c]);u.skip=isNaN(f)||isNaN(g),l&&(u.options=r||this.resolveDataElementOptions(d,e.active?\"active\":s),n&&(u.options.radius=0)),this.updateElement(e,d,u,s)}}resolveDataElementOptions(t,e){const i=this.getParsed(t);let s=super.resolveDataElementOptions(t,e);s.$shared&&(s=Object.assign({},s,{$shared:!1}));const n=s.radius;return\"active\"!==e&&(s.radius=0),s.radius+=l(i&&i._custom,n),s}},DoughnutController:jn,LineController:class extends Ns{static id=\"line\";static defaults={datasetElementType:\"line\",dataElementType:\"point\",showLine:!0,spanGaps:!1};static overrides={scales:{_index_:{type:\"category\"},_value_:{type:\"linear\"}}};initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(t){const e=this._cachedMeta,{dataset:i,data:s=[],_dataset:n}=e,o=this.chart._animationsDisabled;let{start:a,count:r}=pt(e,s,o);this._drawStart=a,this._drawCount=r,mt(e)&&(a=0,r=s.length),i._chart=this.chart,i._datasetIndex=this.index,i._decimated=!!n._decimated,i.points=s;const l=this.resolveDatasetElementOptions(t);this.options.showLine||(l.borderWidth=0),l.segment=this.options.segment,this.updateElement(i,void 0,{animated:!o,options:l},t),this.updateElements(s,a,r,t)}updateElements(t,e,i,n){const o=\"reset\"===n,{iScale:a,vScale:r,_stacked:l,_dataset:h}=this._cachedMeta,{sharedOptions:c,includeOptions:d}=this._getSharedOptions(e,n),u=a.axis,f=r.axis,{spanGaps:g,segment:p}=this.options,m=N(g)?g:Number.POSITIVE_INFINITY,b=this.chart._animationsDisabled||o||\"none\"===n,x=e+i,_=t.length;let y=e>0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=b?g:{};if(i<e||i>=x){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?\"active\":n)),b||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id=\"pie\";static defaults={cutout:0,rotation:0,circumference:360,radius:\"100%\"}},PolarAreaController:$n,RadarController:class extends Ns{static id=\"radar\";static defaults={datasetElementType:\"line\",dataElementType:\"point\",indexAxis:\"r\",showLine:!0,elements:{line:{fill:\"start\"}}};static overrides={aspectRatio:1,scales:{r:{type:\"radialLinear\"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:\"\"+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,\"resize\"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o=\"reset\"===s;for(let a=e;a<e+i;a++){const e=t[a],i=this.resolveDataElementOptions(a,e.active?\"active\":s),r=n.getPointPositionForValue(a,this.getParsed(a).r),l=o?n.xCenter:r.x,h=o?n.yCenter:r.y,c={x:l,y:h,angle:r.angle,skip:isNaN(l)||isNaN(h),options:i};this.updateElement(e,a,c,s)}}},ScatterController:class extends Ns{static id=\"scatter\";static defaults={datasetElementType:!1,dataElementType:\"point\",showLine:!1,fill:!1};static overrides={interaction:{mode:\"point\"},scales:{x:{type:\"linear\"},y:{type:\"linear\"}}};getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y);return{label:i[t]||\"\",value:\"(\"+a+\", \"+r+\")\"}}update(t){const e=this._cachedMeta,{data:i=[]}=e,s=this.chart._animationsDisabled;let{start:n,count:o}=pt(e,i,s);if(this._drawStart=n,this._drawCount=o,mt(e)&&(n=0,o=i.length),this.options.showLine){this.datasetElementType||this.addElements();const{dataset:n,_dataset:o}=e;n._chart=this.chart,n._datasetIndex=this.index,n._decimated=!!o._decimated,n.points=i;const a=this.resolveDatasetElementOptions(t);a.segment=this.options.segment,this.updateElement(n,void 0,{animated:!s,options:a},t)}else this.datasetElementType&&(delete e.dataset,this.datasetElementType=!1);this.updateElements(i,n,o,t)}addElements(){const{showLine:t}=this.options;!this.datasetElementType&&t&&(this.datasetElementType=this.chart.registry.getElement(\"line\")),super.addElements()}updateElements(t,e,i,n){const o=\"reset\"===n,{iScale:a,vScale:r,_stacked:l,_dataset:h}=this._cachedMeta,c=this.resolveDataElementOptions(e,n),d=this.getSharedOptions(c),u=this.includeOptions(n,d),f=a.axis,g=r.axis,{spanGaps:p,segment:m}=this.options,b=N(p)?p:Number.POSITIVE_INFINITY,x=this.chart._animationsDisabled||o||\"none\"===n;let _=e>0&&this.getParsed(e-1);for(let c=e;c<e+i;++c){const e=t[c],i=this.getParsed(c),p=x?e:{},y=s(i[g]),v=p[f]=a.getPixelForValue(i[f],c),M=p[g]=o||y?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,i,l):i[g],c);p.skip=isNaN(v)||isNaN(M)||y,p.stop=c>0&&Math.abs(i[f]-_[f])>b,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?\"active\":n)),x||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,[\"outerStart\",\"outerEnd\",\"innerStart\",\"innerEnd\"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,b-m),M=d-x,w=d-_,k=m+x/M,S=b-_/w,P=u+y,D=u+v,O=m+y/P,A=b-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,b+E)}const i=Xn(D,b,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,b+E,A+Math.PI)}const s=(b-v/u+(m+y/u))/2;if(t.arc(a,r,u,b-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),x>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,x,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f=\"inner\"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||\"round\"):(t.lineWidth=h,t.lineJoin=c||\"bevel\");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;e<o;++e)t.stroke();isNaN(r)||(g=a+(r%O||O))}f&&function(t,e,i){const{startAngle:s,pixelMargin:n,x:o,y:a,outerRadius:r,innerRadius:l}=e;let h=n/r;t.beginPath(),t.arc(o,a,r,s-h,i+h),l>n?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=n<a&&o<a||n>r&&o>r;return{count:s,start:l,loop:e.loop,ilen:h<l&&!c?s+h-l:h-l}}function Qn(t,e,i,s){const{points:n,options:o}=e,{count:a,start:r,loop:l,ilen:h}=Jn(n,i,s),c=function(t){return t.stepped?Fe:t.tension||\"monotone\"===t.cubicInterpolationMode?Ve:Zn}(o);let d,u,f,{move:g=!0,reverse:p}=s||{};for(d=0;d<=h;++d)u=n[(r+(p?h-d:d))%a],u.skip||(g?(t.moveTo(u.x,u.y),g=!1):c(t,f,u,p,o.stepped),f=u);return l&&(u=n[(r+(p?h:0))%a],c(t,f,u,p,o.stepped)),!!l}function to(t,e,i,s){const n=e.points,{count:o,start:a,ilen:r}=Jn(n,i,s),{move:l=!0,reverse:h}=s||{};let c,d,u,f,g,p,m=0,b=0;const x=t=>(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(i<f?f=i:i>g&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||\"monotone\"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io=\"function\"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id=\"line\";static defaults={borderCapStyle:\"butt\",borderDash:[],borderDashOffset:0,borderJoinStyle:\"miter\",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:\"default\",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:\"backgroundColor\",borderColor:\"borderColor\"};static descriptors={_scriptable:!0,_indexable:t=>\"borderDash\"!==t&&\"fill\"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||\"monotone\"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||\"monotone\"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l<h;++l){const{start:h,end:c}=o[l],d=n[h],u=n[c];if(d===u){a.push(d);continue}const f=r(d,u,Math.abs((s-d[e])/(u[e]-d[e])),i.stepped);f[e]=t[e],a.push(f)}return 1===a.length?a[0]:a}pathSegment(t,e,i){return eo(this)(t,this,e,i)}path(t,e,i){const s=this.segments,n=eo(this);let o=this._loop;e=e||0,i=i||this.points.length-e;for(const a of s)o&=n(t,this,a,{start:e,end:e+i-1});return!!o}draw(t,e,i,s){const n=this.options||{};(this.points||[]).length&&n.borderWidth&&(t.save(),so(t,this,i,s),t.restore()),this.animated&&(this._pointsUpdated=!1,this._path=void 0)}}function oo(t,e,i,s){const n=t.options,{[i]:o}=t.getProps([i],s);return Math.abs(e-o)<n.radius+n.hitRadius}function ao(t,e){const{x:i,y:s,base:n,width:o,height:a}=t.getProps([\"x\",\"y\",\"base\",\"width\",\"height\"],e);let r,l,h,c,d;return t.horizontal?(d=a/2,r=Math.min(i,n),l=Math.max(i,n),h=s-d,c=s+d):(d=o/2,r=i-d,l=i+d,h=Math.min(s,n),c=Math.max(s,n)),{left:r,top:h,right:l,bottom:c}}function ro(t,e,i,s){return t?0:J(e,i,s)}function lo(t){const e=ao(t),i=e.right-e.left,s=e.bottom-e.top,n=function(t,e,i){const s=t.options.borderWidth,n=t.borderSkipped,o=Mi(s);return{t:ro(n.top,o.top,0,i),r:ro(n.right,o.right,0,e),b:ro(n.bottom,o.bottom,0,i),l:ro(n.left,o.left,0,e)}}(t,i/2,s/2),a=function(t,e,i){const{enableBorderRadius:s}=t.getProps([\"enableBorderRadius\"]),n=t.options.borderRadius,a=wi(n),r=Math.min(e,i),l=t.borderSkipped,h=s||o(n);return{topLeft:ro(!h||l.top||l.left,a.topLeft,0,r),topRight:ro(!h||l.top||l.right,a.topRight,0,r),bottomLeft:ro(!h||l.bottom||l.left,a.bottomLeft,0,r),bottomRight:ro(!h||l.bottom||l.right,a.bottomRight,0,r)}}(t,i/2,s/2);return{outer:{x:e.left,y:e.top,w:i,h:s,radius:a},inner:{x:e.left+n.l,y:e.top+n.t,w:i-n.l-n.r,h:s-n.t-n.b,radius:{topLeft:Math.max(0,a.topLeft-Math.max(n.t,n.l)),topRight:Math.max(0,a.topRight-Math.max(n.t,n.r)),bottomLeft:Math.max(0,a.bottomLeft-Math.max(n.b,n.l)),bottomRight:Math.max(0,a.bottomRight-Math.max(n.b,n.r))}}}}function ho(t,e,i,s){const n=null===e,o=null===i,a=t&&!(n&&o)&&ao(t,s);return a&&(n||tt(e,a.left,a.right))&&(o||tt(i,a.top,a.bottom))}function co(t,e){t.rect(e.x,e.y,e.w,e.h)}function uo(t,e,i={}){const s=t.x!==i.x?-e:0,n=t.y!==i.y?-e:0,o=(t.x+t.w!==i.x+i.w?e:0)-s,a=(t.y+t.h!==i.y+i.h?e:0)-n;return{x:t.x+s,y:t.y+n,w:t.w+o,h:t.h+a,radius:t.radius}}var fo=Object.freeze({__proto__:null,ArcElement:class extends Hs{static id=\"arc\";static defaults={borderAlign:\"center\",borderColor:\"#fff\",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0};static defaultRoutes={backgroundColor:\"backgroundColor\"};static descriptors={_scriptable:!0,_indexable:t=>\"borderDash\"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps([\"x\",\"y\"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps([\"startAngle\",\"endAngle\",\"innerRadius\",\"outerRadius\",\"circumference\"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a)>=O||Z(n,a,r),g=tt(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps([\"x\",\"y\",\"startAngle\",\"endAngle\",\"innerRadius\",\"outerRadius\"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin=\"inner\"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e<o;++e)t.fill();isNaN(r)||(l=a+(r%O||O))}qn(t,e,i,s,l,n),t.fill()}(t,this,r,n,o),Kn(t,this,r,n,o),t.restore()}},BarElement:class extends Hs{static id=\"bar\";static defaults={borderSkipped:\"start\",borderWidth:0,borderRadius:0,inflateAmount:\"auto\",pointStyle:void 0};static defaultRoutes={backgroundColor:\"backgroundColor\",borderColor:\"borderColor\"};constructor(t){super(),this.options=void 0,this.horizontal=void 0,this.base=void 0,this.width=void 0,this.height=void 0,this.inflateAmount=void 0,t&&Object.assign(this,t)}draw(t){const{inflateAmount:e,options:{borderColor:i,backgroundColor:s}}=this,{inner:n,outer:o}=lo(this),a=(r=o.radius).topLeft||r.topRight||r.bottomLeft||r.bottomRight?He:co;var r;t.save(),o.w===n.w&&o.h===n.h||(t.beginPath(),a(t,uo(o,e,n)),t.clip(),a(t,uo(n,-e,o)),t.fillStyle=i,t.fill(\"evenodd\")),t.beginPath(),a(t,uo(n,e)),t.fillStyle=s,t.fill(),t.restore()}inRange(t,e,i){return ho(this,t,e,i)}inXRange(t,e){return ho(this,t,null,e)}inYRange(t,e){return ho(this,null,t,e)}getCenterPoint(t){const{x:e,y:i,base:s,horizontal:n}=this.getProps([\"x\",\"y\",\"base\",\"horizontal\"],t);return{x:n?(e+s)/2:e,y:n?i:(i+s)/2}}getRange(t){return\"x\"===t?this.width/2:this.height/2}},LineElement:no,PointElement:class extends Hs{static id=\"point\";parsed;skip;stop;static defaults={borderWidth:1,hitRadius:1,hoverBorderWidth:1,hoverRadius:4,pointStyle:\"circle\",radius:3,rotation:0};static defaultRoutes={backgroundColor:\"backgroundColor\",borderColor:\"borderColor\"};constructor(t){super(),this.options=void 0,this.parsed=void 0,this.skip=void 0,this.stop=void 0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.options,{x:n,y:o}=this.getProps([\"x\",\"y\"],i);return Math.pow(t-n,2)+Math.pow(e-o,2)<Math.pow(s.hitRadius+s.radius,2)}inXRange(t,e){return oo(this,t,\"x\",e)}inYRange(t,e){return oo(this,t,\"y\",e)}getCenterPoint(t){const{x:e,y:i}=this.getProps([\"x\",\"y\"],t);return{x:e,y:i}}size(t){let e=(t=t||this.options||{}).radius||0;e=Math.max(e,e&&t.hoverRadius||0);return 2*(e+(e&&t.borderWidth||0))}draw(t,e){const i=this.options;this.skip||i.radius<.1||!Re(this,e,this.size(i)/2)||(t.strokeStyle=i.borderColor,t.lineWidth=i.borderWidth,t.fillStyle=i.backgroundColor,Le(t,i,this.x,this.y))}getRange(){const t=this.options||{};return t.radius+t.hitRadius}}});function go(t,e,i,s){const n=t.indexOf(e);if(-1===n)return((t,e,i,s)=>(\"string\"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&t<e.length?e[t]:t}function mo(t,e,{horizontal:i,minRotation:s}){const n=$(s),o=(i?Math.sin(n):Math.cos(n))||.001,a=.75*e*(\"\"+t).length;return Math.min(e/o,a)}class bo extends Js{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return s(t)||(\"number\"==typeof t||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const{beginAtZero:t}=this.options,{minDefined:e,maxDefined:i}=this.getUserBounds();let{min:s,max:n}=this;const o=t=>s=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!s(a),x=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),\"ticks\"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),b&&x&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=b?a:M,w=x?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(b&&(u&&M!==a?(i.push({value:a}),M<a&&D++,V(Math.round((M+D*S)*v)/v,a,mo(a,y,t))&&D++):M<a&&D++);D<k;++D){const t=Math.round((M+D*S)*v)/v;if(x&&t>r)break;i.push({value:t})}return x&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):x&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return\"ticks\"===t.bounds&&j(n,this,\"value\"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class xo extends bo{static id=\"linear\";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f<i;)s.push({value:f,major:vo(f),significand:u}),u>=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id=\"logarithmic\";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return\"ticks\"===t.bounds&&j(e,this,\"value\"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?\"0\":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:t<s||t>n?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;u<a;u++){const a=r.setContext(t.getPointLabelContext(u));o[u]=a.padding;const f=t.getPointPosition(u,t.drawingArea+o[u],l),g=Si(a.font),p=(h=t.ctx,c=g,d=n(d=t._pointLabels[u])?d:[d],{w:Oe(h,c.string,d),h:d.length*c.lineHeight});s[u]=p;const m=G(t.getIndexAngle(u)+l),b=Math.round(Y(m));Co(i,e,m,Po(b,f.x,p.w,0,180),Po(b,f.y,p.h,90,270))}var h,c,d;t.setCenterPoint(e.l-i.l,i.r-e.r,e.t-i.t,i.b-e.b),t._pointLabelItems=function(t,e,i){const s=[],n=t._pointLabels.length,o=t.options,{centerPointLabels:a,display:r}=o.pointLabels,l={extra:So(o)/2,additionalAngle:a?C/n:0};let h;for(let o=0;o<n;o++){l.padding=i[o],l.size=e[o];const n=Oo(t,o,l);s.push(n),\"auto\"===r&&(n.visible=Ao(n,h),n.visible&&(h=n))}return s}(t,s,o)}function Co(t,e,i,s,n){const o=Math.abs(Math.sin(i)),a=Math.abs(Math.cos(i));let r=0,l=0;s.start<e.l?(r=(e.l-s.start)/o,t.l=Math.min(t.l,e.l-r)):s.end>e.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.start<e.t?(l=(e.t-n.start)/a,t.t=Math.min(t.t,e.t-l)):n.end>e.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return\"center\";if(t<180)return\"left\";return\"right\"}(h),u=function(t,e,i){\"right\"===i?t-=e:\"center\"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;o<s;o++)i=t.getPointPosition(o,e),n.lineTo(i.x,i.y)}}class Eo extends bo{static id=\"radialLinear\";static defaults={display:!0,animate:!0,position:\"chartArea\",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:ae.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback:t=>t,padding:5,centerPointLabels:!1}};static defaultRoutes={\"angleLines.color\":\"borderColor\",\"pointLabels.color\":\"color\",\"ticks.color\":\"color\"};static descriptors={angleLines:{_fallback:\"grid\"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:\"\"})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t<e.length){const i=e[t];return function(t,e,i){return Ci(t,{label:i,index:e,type:\"pointLabel\"})}(this.getContext(),t,i)}}getPointPosition(t,e,i=0){const s=this.getIndexAngle(t)-E+i;return{x:Math.cos(s)*e+this.xCenter,y:Math.sin(s)*e+this.yCenter,angle:s}}getPointPositionForValue(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))}getBasePosition(t){return this.getPointPositionForValue(t||0,this.getBaseValue())}getPointLabelPosition(t){const{left:e,top:i,right:s,bottom:n}=this._pointLabelItems[t];return{left:e,top:i,right:s,bottom:n}}drawBackground(){const{backgroundColor:t,grid:{circular:e}}=this.options;if(t){const i=this.ctx;i.save(),i.beginPath(),Lo(this,this.getDistanceFromCenterForValue(this._endValue),e,this._pointLabels.length),i.closePath(),i.fillStyle=t,i.fill(),i.restore()}}drawGrid(){const t=this.ctx,e=this.options,{angleLines:i,grid:s,border:n}=e,o=this._pointLabels.length;let a,r,l;if(e.pointLabels.display&&function(t,e){const{ctx:i,options:{pointLabels:s}}=t;for(let n=e-1;n>=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:\"middle\"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign=\"center\",t.textBaseline=\"middle\",this.ticks.forEach(((s,a)=>{if(0===a&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return\"function\"==typeof n&&(l=n(l)),a(l)||(l=\"string\"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l=\"week\"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,\"isoWeek\",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o<n-1;++o){const t=Ro[Io[o]],n=t.steps?t.steps:Number.MAX_SAFE_INTEGER;if(t.common&&Math.ceil((i-e)/(n*t.size))<=s)return Io[o]}return Io[n-1]}function Bo(t,e,i){if(i){if(i.length){const{lo:s,hi:n}=et(i,e);t[i[s]>=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a<o;++a)r=e[a],n[r]=a,s.push({value:r,major:!1});return 0!==o&&i?function(t,e,i,s){const n=t._adapter,o=+n.startOf(e[0].value,s),a=e[e.length-1].value;let r,l;for(r=o;r<=a;r=+n.add(r,1,s))l=i[r],l>=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id=\"time\";static defaults={bounds:\"data\",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:\"millisecond\",displayFormats:{}},ticks:{source:\"auto\",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit=\"day\",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),x(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||\"day\";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),\"ticks\"===t.bounds&&\"labels\"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s=\"labels\"===i.source?this.getLabelTimestamps():this._generate();\"ticks\"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&\"year\"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e<i;++e)if(Ro[Io[e]].common)return Io[e]}(this._unit):void 0,this.initOffsets(s),t.reverse&&o.reverse(),Wo(this,o,this._majorUnit)}afterAutoSkip(){this.options.offsetAfterAutoskip&&this.initOffsets(this.ticks.map((t=>+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r=\"week\"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,\"isoWeek\",r)),f=+t.startOf(f,h?\"day\":o),t.diff(i,e,o)>1e5*a)throw new Error(e+\" and \"+i+\" are too far apart with stepSize of \"+a+\" \"+o);const g=\"data\"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d<i;d=+t.add(d,a,o),u++)Bo(c,d,g);return d!==i&&\"ticks\"!==s.bounds&&1!==u||Bo(c,d,g),Object.keys(c).sort(zo).map((t=>+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e<i;++e)s=t[e],s.label=this._tickFormatFunction(s.value,e,t)}getDecimalForValue(t){return null===t?NaN:(t-this.min)/(this.max-this.min)}getPixelForValue(t){const e=this._offsets,i=this.getDecimalForValue(t);return this.getPixelForDecimal((e.start+i)*e.factor)}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return this.min+i*(this.max-this.min)}_getLabelSize(t){const e=this.options.ticks,i=this.ctx.measureText(t).width,s=$(this.isHorizontal()?e.maxRotation:e.minRotation),n=Math.cos(s),o=Math.sin(s),a=this._resolveTickFontOptions(0).size;return{w:i*n+a*o,h:i*o+a*n}}_getLabelCapacity(t){const e=this.options.time,i=e.displayFormats,s=i[e.unit]||i.millisecond,n=this._tickFormatFunction(t,0,Wo(this,[t],this._majorUnit),s),o=this._getLabelSize(n),a=Math.floor(this.isHorizontal()?this.width/o.w:this.height/o.h)-1;return a>0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t<e;++t)i=i.concat(s[t].controller.getAllParsedValues(this));return this._cache.data=this.normalize(i)}getLabelTimestamps(){const t=this._cache.labels||[];let e,i;if(t.length)return t;const s=this.getLabels();for(e=0,i=s.length;e<i;++e)t.push(Fo(this,s[e]));return this._cache.labels=this._normalized?t:this.normalize(t)}normalize(t){return lt(t.sort(zo))}}function Ho(t,e,i){let s,n,o,a,r=0,l=t.length-1;i?(e>=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,\"pos\",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,\"time\",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id=\"category\";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);\"ticks\"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return\"number\"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:xo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id=\"timeseries\";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o<a;++o)l=t[o],l>=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;o<a;++o)h=s[o+1],r=s[o-1],l=s[o],Math.round((h+r)/2)!==l&&n.push({time:l,pos:o/(a-1)});return n}_generate(){const t=this.min,e=this.max;let i=super.getDataTimestamps();return i.includes(t)&&i.length||i.splice(0,0,t),i.includes(e)&&1!==i.length||i.push(e),i.sort(((t,e)=>t-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=[\"rgb(54, 162, 235)\",\"rgb(255, 99, 132)\",\"rgb(255, 159, 64)\",\"rgb(255, 205, 86)\",\"rgb(75, 192, 192)\",\"rgb(153, 102, 255)\",\"rgb(201, 203, 207)\"],Yo=$o.map((t=>t.replace(\"rgb(\",\"rgba(\").replace(\")\",\", 0.5)\")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:\"colors\",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n;if(!i.forceOverride&&(Ko(s)||(a=n)&&(a.borderColor||a.backgroundColor)||o&&Ko(o)))return;var a;const r=qo(t);s.forEach(r)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,\"data\",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:\"decimation\",defaults:{algorithm:\"min-max\",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if(\"y\"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if(\"linear\"!==c.type&&\"time\"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,\"data\",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case\"lttb\":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;c<o-2;c++){let s,n=0,o=0;const h=Math.floor((c+1)*r)+1+e,m=Math.min(Math.floor((c+2)*r)+1,i)+e,b=m-h;for(s=h;s<m;s++)n+=t[s].x,o+=t[s].y;n/=b,o/=b;const x=Math.floor(c*r)+1+e,_=Math.min(Math.floor((c+1)*r)+1,i)+e,{x:y,y:v}=t[p];for(u=f=-1,s=x;s<_;s++)f=.5*Math.abs((y-n)*(t[s].y-v)-(y-t[s].x)*(o-v)),f>u&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case\"min-max\":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+i-1,_=t[e].x,y=t[x].x-_;for(o=e;o<e+i;++o){a=t[o],r=(a.x-_)/y*n,l=a.y;const e=0|r;if(e===h)l<f?(f=l,c=o):l>g&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&b.push({...t[e],x:p}),s!==u&&s!==i&&b.push({...t[s],x:p})}o>0&&i!==u&&b.push(t[i]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return\"angle\"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return\"origin\";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){\"-\"!==t&&\"+\"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):[\"origin\",\"start\",\"end\",\"stack\",\"shape\"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n<i.length;n++){const o=i[n],{first:a,last:r,point:l}=la(o,e,\"x\");if(!(!l||a&&r))if(a)s.unshift(l);else if(t.push(l),!r)break}t.push(...s)}function la(t,e,i){const s=t.interpolate(e,i);if(!s)return{};const n=s[i],o=t.segments,a=t.points;let r=!1,l=!1;for(let t=0;t<o.length;t++){const e=o[t],s=a[e.start][i],h=a[e.end][i];if(tt(n,s,h)){r=n===s,l=n===h;break}}return{first:r,last:l,point:s}}class ha{constructor(t){this.x=t.x,this.y=t.y,this.radius=t.radius}pathSegment(t,e,i){const{x:s,y:n,radius:o}=this;return e=e||{start:0,end:O},t.arc(s,n,o,e.end,e.start,!0),!i.bounds}interpolate(t){const{x:e,y:i,radius:s}=this,n=t.angle;return{x:e+Math.cos(n)*s,y:i+Math.sin(n)*s,angle:n}}}function ca(t){const{chart:e,fill:i,line:s}=t;if(a(i))return function(t,e){const i=t.getDatasetMeta(e),s=i&&t.isDatasetVisible(e);return s?i.dataset:null}(e,i);if(\"stack\"===i)return function(t){const{scale:e,index:i,line:s}=t,n=[],o=s.segments,a=s.points,r=function(t,e){const i=[],s=t.getMatchingVisibleMetas(\"line\");for(let t=0;t<s.length;t++){const n=s[t];if(n.index===e)break;n.hidden||i.unshift(n.dataset)}return i}(e,i);r.push(sa({x:null,y:e.bottom},s));for(let t=0;t<o.length;t++){const e=o[t];for(let t=e.start;t<=e.end;t++)ra(n,a[t],r)}return new no({points:n,options:{}})}(t);if(\"shape\"===i)return!0;const n=function(t){const e=t.scale||{};if(e.getPointPositionForValue)return function(t){const{scale:e,fill:i}=t,s=e.options,n=e.getLabels().length,a=s.reverse?e.max:e.min,r=function(t,e,i){let s;return s=\"start\"===t?i:\"end\"===t?e.options.reverse?e.min:e.max:o(t)?t.value:e.getBaseValue(),s}(i,e,a),l=[];if(s.grid.circular){const t=e.getPointPositionForValue(0,a);return new ha({x:t.x,y:t.y,radius:e.getDistanceFromCenterForValue(r)})}for(let t=0;t<n;++t)l.push(e.getPointPositionForValue(t,r));return l}(t);return function(t){const{scale:e={},fill:i}=t,s=function(t,e){let i=null;return\"start\"===t?i=e.bottom:\"end\"===t?i=e.top:o(t)?i=e.getPixelForValue(t.value):e.getBasePixel&&(i=e.getBasePixel()),i}(i,e);if(a(s)){const t=e.isHorizontal();return{x:t?s:null,y:t?null:s}}return null}(t)}(t);return n instanceof ha?n:sa(n,s)}function da(t,e,i){const s=ca(e),{line:n,scale:o,axis:a}=e,r=n.options,l=r.fill,h=r.backgroundColor,{above:c=h,below:d=h}=l||{};s&&n.points.length&&(Ie(t,i),function(t,e){const{line:i,target:s,above:n,below:o,area:a,scale:r}=e,l=i._loop?\"angle\":e.axis;t.save(),\"x\"===l&&o!==n&&(ua(t,s,a.top),fa(t,{line:i,target:s,color:n,scale:r,property:l}),t.restore(),t.save(),ua(t,s,a.bottom));fa(t,{line:i,target:s,color:o,scale:r,property:l}),t.restore()}(t,{line:n,target:s,above:c,below:d,area:i,scale:o,axis:a}),ze(t))}function ua(t,e,i){const{segments:s,points:n}=e;let o=!0,a=!1;t.beginPath();for(const r of s){const{start:s,end:l}=r,h=n[s],c=n[ea(s,l,n)];o?(t.moveTo(h.x,h.y),o=!1):(t.lineTo(h.x,i),t.lineTo(h.x,h.y)),a=!!e.pathSegment(t,r,{move:a}),a?t.closePath():t.lineTo(c.x,i)}t.lineTo(e.first().x,i),t.closePath(),t.clip()}function fa(t,e){const{line:i,target:s,property:n,color:o,scale:a}=e,r=function(t,e,i){const s=t.segments,n=t.points,o=e.points,a=[];for(const t of s){let{start:s,end:r}=t;r=ea(s,r,n);const l=ta(i,n[s],n[r],t.loop);if(!e.segments){a.push({source:t,target:l,start:n[s],end:n[r]});continue}const h=Ii(e,l);for(const e of h){const s=ta(i,o[e.start],o[e.end],e.loop),r=Ri(t,n,s);for(const t of r)a.push({source:t,target:e,start:{[i]:ia(l,s,\"start\",Math.max)},end:{[i]:ia(l,s,\"end\",Math.min)}})}}return a}(i,s,n);for(const{source:e,target:l,start:h,end:c}of r){const{style:{backgroundColor:r=o}={}}=e,d=!0!==s;t.save(),t.fillStyle=r,ga(t,a,d&&ta(n,h,c)),t.beginPath();const u=!!i.pathSegment(t,e);let f;if(d){u?t.closePath():pa(t,s,c,n);const e=!!s.pathSegment(t,l,{move:u,reverse:!0});f=u&&e,f||pa(t,s,h,n)}t.closePath(),t.fill(f?\"evenodd\":\"nonzero\"),t.restore()}}function ga(t,e,i){const{top:s,bottom:n}=e.chart.chartArea,{property:o,start:a,end:r}=i||{};\"x\"===o&&(t.beginPath(),t.rect(a,s,r-a,n-s),t.clip())}function pa(t,e,i,s){const n=e.interpolate(i,s);n&&t.lineTo(n.x,n.y)}var ma={id:\"filler\",afterDatasetsUpdate(t,e,i){const s=(t.data.datasets||[]).length,n=[];let o,a,r,l;for(a=0;a<s;++a)o=t.getDatasetMeta(a),r=o.dataset,l=null,r&&r.options&&r instanceof no&&(l={visible:t.isDatasetVisible(a),index:a,fill:aa(r,a,s),chart:t,axis:o.controller.options.indexAxis,scale:o.vScale,line:r}),o.$filler=l,n.push(l);for(a=0;a<s;++a)l=n[a],l&&!1!==l.fill&&(l.fill=oa(n,a,i.propagate))},beforeDraw(t,e,i){const s=\"beforeDraw\"===i.drawTime,n=t.getSortedVisibleDatasetMetas(),o=t.chartArea;for(let e=n.length-1;e>=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if(\"beforeDatasetsDraw\"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&\"beforeDatasetDraw\"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:\"beforeDatasetDraw\"}};const ba=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class xa extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=ba(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign=\"left\",n.textBaseline=\"middle\";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&\"string\"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;\"string\"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return\"top\"===this.options.position||\"bottom\"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign(\"left\"),s.textBaseline=\"middle\",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=ba(o,d),b=this.isHorizontal(),x=this._computeTitleHeight();f=b?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+x,line:0}:{x:this.left+c,y:ft(n,this.top+x+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),b?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+x+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,\"butt\"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,\"miter\"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,b?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),b)f.x+=k+c;else if(\"string\"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline=\"middle\",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;i<n.length;++i)if(s=n[i],tt(t,s.left,s.left+s.width)&&tt(e,s.top,s.top+s.height))return this.legendItems[i];return null}handleEvent(t){const e=this.options;if(!function(t,e){if((\"mousemove\"===t||\"mouseout\"===t)&&(e.onHover||e.onLeave))return!0;if(e.onClick&&(\"click\"===t||\"mouseup\"===t))return!0;return!1}(t.type,e))return;const i=this._getLegendItemAt(t.x,t.y);if(\"mousemove\"===t.type||\"mouseout\"===t.type){const o=this._hoveredItem,a=(n=i,null!==(s=o)&&null!==n&&s.datasetIndex===n.datasetIndex&&s.index===n.index);o&&!a&&d(e.onLeave,[t,o,this],this),this._hoveredItem=i,i&&!a&&d(e.onHover,[t,i,this],this)}else i&&d(e.onClick,[t,i,this],this);var s,n}}function _a(t,e){return e*(t.text?t.text.length:0)}var ya={id:\"legend\",_element:xa,start(t,e,i){const s=t.legend=new xa({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s)},stop(t){as.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,e,i){const s=t.legend;as.configure(t,s,i),s.options=i},afterUpdate(t){const e=t.legend;e.buildLabels(),e.adjustHitBoxes()},afterEvent(t,e){e.replay||t.legend.handleEvent(e.event)},defaults:{display:!0,position:\"top\",align:\"center\",fullSize:!0,reverse:!1,weight:1e3,onClick(t,e,i){const s=e.datasetIndex,n=i.chart;n.isDatasetVisible(s)?(n.hide(s),e.hidden=!0):(n.show(s),e.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:\"center\",text:\"\"}},descriptors:{_scriptable:t=>!t.startsWith(\"on\"),labels:{_scriptable:t=>![\"generateLabels\",\"filter\",\"sort\"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return\"top\"===t||\"bottom\"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):(\"left\"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:\"middle\",translation:[n,o]})}}var Ma={id:\"title\",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:\"center\",display:!1,font:{weight:\"bold\"},fullSize:!0,padding:10,position:\"top\",text:\"\",weight:2e3},defaultRoutes:{color:\"color\"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:\"subtitle\",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:\"center\",display:!1,font:{weight:\"normal\"},fullSize:!0,padding:0,position:\"top\",text:\"\",weight:1500},defaultRoutes:{color:\"color\"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=0,n=0,o=0;for(e=0,i=t.length;e<i;++e){const i=t[e].element;if(i&&i.hasValue()){const t=i.tooltipPosition();s+=t.x,n+=t.y,++o}}return{x:s/o,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i<s;++i){const s=t[i].element;if(s&&s.hasValue()){const t=q(e,s.getCenterPoint());t<r&&(r=t,n=s)}}if(n){const t=n.tooltipPosition();o=t.x,a=t.y}return{x:o,y:a}}};function Pa(t,e){return e&&(n(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function Da(t){return(\"string\"==typeof t||t instanceof String)&&t.indexOf(\"\\n\")>-1?t.split(\"\\n\"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h=\"center\";return\"center\"===s?h=n<=(r+l)/2?\"left\":\"right\":n<=o/2?h=\"left\":n>=a-o/2&&(h=\"right\"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return\"left\"===t&&n+o+a>e.width||\"right\"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h=\"center\"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return i<s/2?\"top\":i>t.height-s/2?\"bottom\":\"center\"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return\"right\"===e?i-=s:\"center\"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return\"top\"===e?s+=i:s-=\"bottom\"===e?n+i:n/2,s}(e,l,h);return\"center\"===l?\"left\"===r?g+=h:\"right\"===r&&(g-=h):\"left\"===r?g-=Math.max(c,u)+n:\"right\"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return\"center\"===e?t.x+t.width/2:\"right\"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&\"dataset\"===this.options.mode)return e.dataset.label||\"\";if(e.label)return e.label;if(s>0&&e.dataIndex<s)return i[e.dataIndex]}return\"\"},afterTitle:e,beforeBody:e,beforeLabel:e,label(t){if(this&&this.options&&\"dataset\"===this.options.mode)return t.label+\": \"+t.formattedValue||t.formattedValue;let e=t.dataset.label||\"\";e&&(e+=\": \");const i=t.formattedValue;return s(i)||(e+=i),e},labelColor(t){const e=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{borderColor:e.borderColor,backgroundColor:e.backgroundColor,borderWidth:e.borderWidth,borderDash:e.borderDash,borderDashOffset:e.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(t){const e=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{pointStyle:e.pointStyle,rotation:e.rotation}},afterLabel:e,afterBody:e,beforeFooter:e,footer:e,afterFooter:e};function Fa(t,e,i,s){const n=t[e].call(i,s);return void 0===n?za[e].call(i,s):n}class Va extends Hs{static positioners=Sa;constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this._cachedAnimations;if(t)return t;const e=this.chart,i=this.options.setContext(this.getContext()),s=i.enabled&&e.options.animation&&i.animations,n=new Os(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(n)),n}getContext(){return this.$context||(this.$context=(t=this.chart.getContext(),e=this,i=this._tooltipItems,Ci(t,{tooltip:e,tooltipItems:i,type:\"tooltip\"})));var t,e,i}getTitle(t,e){const{callbacks:i}=e,s=Fa(i,\"beforeTitle\",this,t),n=Fa(i,\"title\",this,t),o=Fa(i,\"afterTitle\",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}getBeforeBody(t,e){return Ra(Fa(e.callbacks,\"beforeBody\",this,t))}getBody(t,e){const{callbacks:i}=e,s=[];return u(t,(t=>{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,\"beforeLabel\",this,t))),Pa(e.lines,Fa(n,\"label\",this,t)),Pa(e.after,Da(Fa(n,\"afterLabel\",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,\"afterBody\",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,\"beforeFooter\",this,t),n=Fa(i,\"footer\",this,t),o=Fa(i,\"afterFooter\",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;a<r;++a)l.push(Ca(this.chart,e[a]));return t.filter&&(l=l.filter(((e,s,n)=>t.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,\"labelColor\",this,e)),n.push(Fa(i,\"labelPointStyle\",this,e)),o.push(Fa(i,\"labelTextColor\",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return\"center\"===n?(_=u+g/2,\"left\"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m=\"left\"===s?d+Math.max(r,h)+o:\"right\"===s?d+f-Math.max(l,c)-o:this.caretX,\"top\"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline=\"middle\",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r<n;++r)e.fillText(s[r],l.x(t.x),t.y+o.lineHeight/2),t.y+=o.lineHeight+a,r+1===n&&(t.y+=i.titleMarginBottom-a)}}_drawColorBox(t,e,i,s,n){const a=this.labelColors[i],r=this.labelPointStyles[i],{boxHeight:l,boxWidth:h}=n,c=Si(n.bodyFont),d=Ea(this,\"left\",n),u=s.x(d),f=l<c.lineHeight?(c.lineHeight-l)/2:0,g=e.y+f;if(n.usePointStyle){const e={radius:Math.min(h,l)/2,pointStyle:r.pointStyle,rotation:r.rotation,borderWidth:1},i=s.leftForLtr(u,h)+h/2,o=g+l/2;t.strokeStyle=n.multiKeyBackground,t.fillStyle=n.multiKeyBackground,Le(t,e,i,o),t.strokeStyle=a.borderColor,t.fillStyle=a.backgroundColor,Le(t,e,i,o)}else{t.lineWidth=o(a.borderWidth)?Math.max(...Object.values(a.borderWidth)):a.borderWidth||1,t.strokeStyle=a.borderColor,t.setLineDash(a.borderDash||[]),t.lineDashOffset=a.borderDashOffset||0;const e=s.leftForLtr(u,h),i=s.leftForLtr(s.xPlus(u,1),h-2),r=wi(a.borderRadius);Object.values(r).some((t=>0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let b,x,_,y,v,M,w;for(e.textAlign=o,e.textBaseline=\"middle\",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&\"right\"!==m?\"center\"===o?l/2+h:l+2+h:0,y=0,M=s.length;y<M;++y){for(b=s[y],x=this.labelTextColors[y],e.fillStyle=x,u(b.before,p),_=b.lines,a&&_.length&&(this._drawColorBox(e,t,y,g,i),d=Math.max(c.lineHeight,r)),v=0,w=_.length;v<w;++v)p(_[v]),d=c.lineHeight;u(b.after,p)}f=0,d=c.lineHeight,u(this.afterBody,p),t.y-=n}drawFooter(t,e,i){const s=this.footer,n=s.length;let o,a;if(n){const r=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.footerAlign,i),t.y+=i.footerMarginTop,e.textAlign=r.textAlign(i.footerAlign),e.textBaseline=\"middle\",o=Si(i.footerFont),e.fillStyle=i.footerColor,e.font=o.string,a=0;a<n;++a)e.fillText(s[a],r.x(t.x),t.y+o.lineHeight/2),t.y+=o.lineHeight+i.footerSpacing}}drawBackground(t,e,i,s){const{xAlign:n,yAlign:o}=this,{x:a,y:r}=t,{width:l,height:h}=i,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(s.cornerRadius);e.fillStyle=s.backgroundColor,e.strokeStyle=s.borderColor,e.lineWidth=s.borderWidth,e.beginPath(),e.moveTo(a+c,r),\"top\"===o&&this.drawCaret(t,e,i,s),e.lineTo(a+l-d,r),e.quadraticCurveTo(a+l,r,a+l,r+d),\"center\"===o&&\"right\"===n&&this.drawCaret(t,e,i,s),e.lineTo(a+l,r+h-f),e.quadraticCurveTo(a+l,r+h,a+l-f,r+h),\"bottom\"===o&&this.drawCaret(t,e,i,s),e.lineTo(a+u,r+h),e.quadraticCurveTo(a,r+h,a,r+h-u),\"center\"===o&&\"left\"===n&&this.drawCaret(t,e,i,s),e.lineTo(a,r+c),e.quadraticCurveTo(a,r,a+c,r),e.closePath(),e.fill(),s.borderWidth>0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error(\"Cannot find a dataset at index \"+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if(\"mouseout\"===t.type)return[];if(!s)return e;const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:\"tooltip\",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins(\"beforeTooltipDraw\",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins(\"afterTooltipDraw\",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:\"average\",backgroundColor:\"rgba(0,0,0,0.8)\",titleColor:\"#fff\",titleFont:{weight:\"bold\"},titleSpacing:2,titleMarginBottom:6,titleAlign:\"left\",bodyColor:\"#fff\",bodySpacing:2,bodyFont:{},bodyAlign:\"left\",footerColor:\"#fff\",footerSpacing:2,footerMarginTop:6,footerFont:{weight:\"bold\"},footerAlign:\"left\",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:\"#fff\",displayColors:!0,boxPadding:0,borderColor:\"rgba(0,0,0,0)\",borderWidth:0,animation:{duration:400,easing:\"easeOutQuart\"},animations:{numbers:{type:\"number\",properties:[\"x\",\"y\",\"width\",\"height\",\"caretX\",\"caretY\"]},opacity:{easing:\"linear\",duration:200}},callbacks:za},defaultRoutes:{bodyFont:\"font\",footerFont:\"font\",titleFont:\"font\"},descriptors:{_scriptable:t=>\"filter\"!==t&&\"itemSort\"!==t&&\"external\"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:\"animation\"}},additionalOptionScopes:[\"interaction\"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=xt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,\"undefined\"!=typeof window&&(window.Chart=An),An}));\n//# sourceMappingURL=chart.umd.js.map\n"
  },
  {
    "path": "scripts/login.js",
    "content": "document.addEventListener('DOMContentLoaded', function () {\r\n\r\n  const userLocale = navigator.language || navigator.languages[0];\r\n  document.cookie = `user_locale=${userLocale}; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict`;\r\n\r\n  if (window.update_theme_settings) {\r\n    const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\r\n    const themePreference = prefersDarkMode ? 'dark' : 'light';\r\n    const darkThemeCss = document.querySelector(\"#dark-theme\");\r\n    darkThemeCss.disabled = themePreference === 'light';\r\n    document.body.className = themePreference;\r\n    const themeColorMetaTag = document.querySelector('meta[name=\"theme-color\"]');\r\n    themeColorMetaTag.setAttribute('content', themePreference === 'dark' ? '#222222' : '#FFFFFF');\r\n  }\r\n\r\n});"
  },
  {
    "path": "scripts/notifications.js",
    "content": "function openNotificationsSettings(type) {\r\n    // Get all .account-notification-section-settings elements\r\n    var sections = document.querySelectorAll('.account-notification-section-settings');\r\n    var targetSection = document.querySelector(`.account-notification-section-settings[data-type=\"${type}\"]`);\r\n    \r\n    // Remove the is-open class from all elements\r\n    sections.forEach(function(section) {\r\n      if (section !== targetSection) {\r\n        section.classList.remove('is-open');\r\n      }\r\n    });\r\n  \r\n    // Add the is-open class to the element with data-type=type\r\n  \r\n    if (targetSection && !targetSection.classList.contains('is-open')) {\r\n      targetSection.classList.add('is-open');\r\n    } else {\r\n      targetSection.classList.remove('is-open');\r\n    }\r\n}\r\n\r\nfunction makeFetchCall(url, data, button) {\r\n    return fetch(url, {\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application/json',\r\n            \"X-CSRF-Token\": window.csrfToken,\r\n        },\r\n        body: JSON.stringify(data),\r\n    })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n        if (data.success) {\r\n            showSuccessMessage(data.message);\r\n        } else {\r\n            showErrorMessage(data.message);\r\n        }\r\n        button.disabled = false;\r\n    })\r\n    .catch((error) => {\r\n        showErrorMessage(error);\r\n        button.disabled = false;\r\n    });\r\n\r\n}\r\n\r\nfunction saveNotifications() {\r\n    const button = document.getElementById(\"saveNotifications\");\r\n    button.disabled = true;\r\n    const days = document.querySelector('#days').value;\r\n\r\n    const url = 'endpoints/notifications/savenotificationsettings.php';\r\n    const data = { days: days };\r\n\r\n    makeFetchCall(url, data, button);\r\n}\r\n\r\nfunction saveNotificationsEmailButton() {\r\n    const button = document.getElementById(\"saveNotificationsEmail\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"emailenabled\").checked ? 1 : 0;\r\n    const smtpAddress = document.getElementById(\"smtpaddress\").value;\r\n    const smtpPort = document.getElementById(\"smtpport\").value;\r\n    const encryption = document.querySelector('input[name=\"encryption\"]:checked').value;\r\n    const smtpUsername = document.getElementById(\"smtpusername\").value;\r\n    const smtpPassword = document.getElementById(\"smtppassword\").value;\r\n    const fromEmail = document.getElementById(\"fromemail\").value;\r\n    const otherEmails = document.getElementById(\"otheremails\").value;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      smtpaddress: smtpAddress,\r\n      smtpport: smtpPort,\r\n      encryption: encryption,\r\n      smtpusername: smtpUsername,\r\n      smtppassword: smtpPassword,\r\n      fromemail: fromEmail,\r\n      otheremails: otherEmails\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/saveemailnotifications.php', data, button);\r\n}\r\n  \r\nfunction testNotificationEmailButton()  {\r\n    const button = document.getElementById(\"testNotificationsEmail\");\r\n    button.disabled = true;\r\n  \r\n    const smtpAddress = document.getElementById(\"smtpaddress\").value;\r\n    const smtpPort = document.getElementById(\"smtpport\").value;\r\n    const encryption = document.querySelector('input[name=\"encryption\"]:checked').value;\r\n    const smtpUsername = document.getElementById(\"smtpusername\").value;\r\n    const smtpPassword = document.getElementById(\"smtppassword\").value;\r\n    const fromEmail = document.getElementById(\"fromemail\").value;\r\n  \r\n    const data = {\r\n      smtpaddress: smtpAddress,\r\n      smtpport: smtpPort,\r\n      encryption: encryption,\r\n      smtpusername: smtpUsername,\r\n      smtppassword: smtpPassword,\r\n      fromemail: fromEmail\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/testemailnotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsWebhookButton() {\r\n    const button = document.getElementById(\"saveNotificationsWebhook\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"webhookenabled\").checked ? 1 : 0;\r\n    const webhook_url = document.getElementById(\"webhookurl\").value;\r\n    const headers = document.getElementById(\"webhookcustomheaders\").value;\r\n    const payload = document.getElementById(\"webhookpayload\").value;\r\n    const cancelation_payload = document.getElementById(\"webhookcancelationpayload\").value;\r\n    const ignore_ssl = document.getElementById(\"webhookignoressl\").checked ? 1 : 0;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      webhook_url: webhook_url,\r\n      headers: headers,\r\n      payload: payload,\r\n      cancelation_payload: cancelation_payload,\r\n      ignore_ssl: ignore_ssl\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/savewebhooknotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsWebhookButton() {\r\n    const button = document.getElementById(\"testNotificationsWebhook\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"webhookenabled\").checked ? 1 : 0;\r\n    const requestmethod = document.getElementById(\"webhookrequestmethod\").value;\r\n    const url = document.getElementById(\"webhookurl\").value;\r\n    const customheaders = document.getElementById(\"webhookcustomheaders\").value;\r\n    const payload = document.getElementById(\"webhookpayload\").value;\r\n    const cancelation_payload = document.getElementById(\"webhookcancelationpayload\").value;\r\n    const ignore_ssl = document.getElementById(\"webhookignoressl\").checked ? 1 : 0;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      requestmethod: requestmethod,\r\n      url: url,\r\n      customheaders: customheaders,\r\n      payload: payload,\r\n      cancelation_payload: cancelation_payload,\r\n      ignore_ssl: ignore_ssl\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/testwebhooknotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsTelegramButton() {\r\n    const button = document.getElementById(\"saveNotificationsTelegram\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"telegramenabled\").checked ? 1 : 0;\r\n    const chat_id = document.getElementById(\"telegramchatid\").value;\r\n    const bot_token = document.getElementById(\"telegrambottoken\").value;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      chat_id: chat_id,\r\n      bot_token: bot_token\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/savetelegramnotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsTelegramButton() {\r\n    const button = document.getElementById(\"testNotificationsTelegram\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"telegramenabled\").checked ? 1 : 0;\r\n    const bottoken = document.getElementById(\"telegrambottoken\").value;\r\n    const chatid = document.getElementById(\"telegramchatid\").value;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      bottoken: bottoken,\r\n      chatid: chatid\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/testtelegramnotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsPushPlusButton() {\r\n    const button = document.getElementById(\"testNotificationsPushPlus\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"pushplusenabled\").checked ? 1 : 0;\r\n    const token = document.getElementById(\"pushplustoken\").value;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      token: token\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/testpushplusnotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsPushPlusButton() {\r\n    const button = document.getElementById(\"saveNotificationsPushPlus\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"pushplusenabled\").checked ? 1 : 0;\r\n    const token = document.getElementById(\"pushplustoken\").value;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      token: token\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/savepushplusnotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsMattermostButton() {\r\n    const button = document.getElementById(\"testNotificationsMattermost\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"mattermostenabled\").checked ? 1 : 0;\r\n    const webhook_url = document.getElementById(\"mattermostwebhookurl\").value;\r\n    const bot_username = document.getElementById(\"mattermostbotusername\").value;\r\n    const bot_icon_emoji = document.getElementById(\"mattermostboticonemoji\").value;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      webhook_url: webhook_url,\r\n      bot_username: bot_username,\r\n      bot_icon_emoji: bot_icon_emoji\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/testmattermostnotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsMattermostButton() {\r\n    const button = document.getElementById(\"saveNotificationsMattermost\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"mattermostenabled\").checked ? 1 : 0;\r\n    const webhook_url = document.getElementById(\"mattermostwebhookurl\").value;\r\n    const bot_username = document.getElementById(\"mattermostbotusername\").value;\r\n    const bot_icon_emoji = document.getElementById(\"mattermostboticonemoji\").value;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      webhook_url: webhook_url,\r\n      bot_username: bot_username,\r\n      bot_icon_emoji: bot_icon_emoji\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/savemattermostnotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsGotifyButton() {\r\n    const button = document.getElementById(\"saveNotificationsGotify\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"gotifyenabled\").checked ? 1 : 0;\r\n    const gotify_url = document.getElementById(\"gotifyurl\").value;\r\n    const token = document.getElementById(\"gotifytoken\").value;\r\n    const ignore_ssl = document.getElementById(\"gotifyignoressl\").checked ? 1 : 0;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      gotify_url: gotify_url,\r\n      token: token,\r\n      ignore_ssl: ignore_ssl\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/savegotifynotifications.php', data, button);\r\n}\r\n\r\n\r\nfunction testNotificationsGotifyButton() {\r\n    const button = document.getElementById(\"testNotificationsGotify\");\r\n    button.disabled = true;\r\n  \r\n    const enabled = document.getElementById(\"gotifyenabled\").checked ? 1 : 0;\r\n    const gotify_url = document.getElementById(\"gotifyurl\").value;\r\n    const token = document.getElementById(\"gotifytoken\").value;\r\n    const ignore_ssl = document.getElementById(\"gotifyignoressl\").checked ? 1 : 0;\r\n  \r\n    const data = {\r\n      enabled: enabled,\r\n      gotify_url: gotify_url,\r\n      token: token,\r\n      ignore_ssl: ignore_ssl\r\n    };\r\n\r\n    makeFetchCall('endpoints/notifications/testgotifynotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsPushoverButton() {\r\n  const button = document.getElementById(\"saveNotificationsPushover\");\r\n  button.disabled = true;\r\n\r\n  const enabled = document.getElementById(\"pushoverenabled\").checked ? 1 : 0;\r\n  const user_key = document.getElementById(\"pushoveruserkey\").value;\r\n  const token = document.getElementById(\"pushovertoken\").value;\r\n\r\n  const data = {\r\n    enabled: enabled,\r\n    user_key: user_key,\r\n    token: token\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/savepushovernotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsPushoverButton() {\r\n  const button = document.getElementById(\"testNotificationsPushover\");\r\n  button.disabled = true;\r\n\r\n  const enabled = document.getElementById(\"pushoverenabled\").checked ? 1 : 0;\r\n  const user_key = document.getElementById(\"pushoveruserkey\").value;\r\n  const token = document.getElementById(\"pushovertoken\").value;\r\n\r\n  const data = {\r\n    enabled: enabled,\r\n    user_key: user_key,\r\n    token: token\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/testpushovernotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsDiscordButton() {\r\n  const button = document.getElementById(\"saveNotificationsDiscord\");\r\n  button.disabled = true;\r\n\r\n  const enabled = document.getElementById(\"discordenabled\").checked ? 1 : 0;\r\n  const url = document.getElementById(\"discordurl\").value;\r\n  const bot_username = document.getElementById(\"discordbotusername\").value;\r\n  const bot_avatar = document.getElementById(\"discordbotavatar\").value;\r\n\r\n  const data = {\r\n    enabled: enabled,\r\n    url: url,\r\n    bot_username: bot_username,\r\n    bot_avatar: bot_avatar\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/savediscordnotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsDiscordButton() {\r\n  const button = document.getElementById(\"testNotificationsDiscord\");\r\n  button.disabled = true;\r\n\r\n  const enabled = document.getElementById(\"discordenabled\").checked ? 1 : 0;\r\n  const url = document.getElementById(\"discordurl\").value;\r\n  const bot_username = document.getElementById(\"discordbotusername\").value;\r\n  const bot_avatar = document.getElementById(\"discordbotavatar\").value;\r\n\r\n  const data = {\r\n    enabled: enabled,\r\n    url: url,\r\n    bot_username: bot_username,\r\n    bot_avatar: bot_avatar\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/testdiscordnotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsNtfyButton() {\r\n  const button = document.getElementById(\"testNotificationsNtfy\");\r\n  button.disabled = true;\r\n\r\n  const host = document.getElementById(\"ntfyhost\").value;\r\n  const topic = document.getElementById(\"ntfytopic\").value;\r\n  const headers = document.getElementById(\"ntfyheaders\").value;\r\n  const ignore_ssl = document.getElementById(\"ntfyignoressl\").checked ? 1 : 0;\r\n  \r\n  const data = {\r\n    host: host,\r\n    topic: topic,\r\n    headers: headers,\r\n    ignore_ssl: ignore_ssl\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/testntfynotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsNtfyButton() {\r\n  const button = document.getElementById(\"saveNotificationsNtfy\");\r\n  button.disabled = true;\r\n\r\n  const enabled = document.getElementById(\"ntfyenabled\").checked ? 1 : 0;\r\n  const host = document.getElementById(\"ntfyhost\").value;\r\n  const topic = document.getElementById(\"ntfytopic\").value;\r\n  const headers = document.getElementById(\"ntfyheaders\").value;\r\n  const ignore_ssl = document.getElementById(\"ntfyignoressl\").checked ? 1 : 0;\r\n\r\n  const data = {\r\n    enabled: enabled,\r\n    host: host,\r\n    topic: topic,\r\n    headers: headers,\r\n    ignore_ssl: ignore_ssl\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/saventfynotifications.php', data, button);\r\n}\r\n\r\nfunction testNotificationsServerchanButton() {\r\n  const button = document.getElementById(\"testNotificationsServerchan\");\r\n  button.disabled = true;\r\n\r\n  const enabled = document.getElementById(\"serverchanenabled\").checked ? 1 : 0;\r\n  const sendkey = document.getElementById(\"serverchansendkey\").value;\r\n\r\n  const data = {\r\n    enabled: enabled,\r\n    sendkey: sendkey\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/testserverchannotifications.php', data, button);\r\n}\r\n\r\nfunction saveNotificationsServerchanButton() {\r\n  const button = document.getElementById(\"saveNotificationsServerchan\");\r\n  button.disabled = true;\r\n\r\n  const enabled = document.getElementById(\"serverchanenabled\").checked ? 1 : 0;\r\n  const sendkey = document.getElementById(\"serverchansendkey\").value;\r\n\r\n  const data = {\r\n    enabled: enabled,\r\n    sendkey: sendkey\r\n  };\r\n\r\n  makeFetchCall('endpoints/notifications/saveserverchannotifications.php', data, button);\r\n}"
  },
  {
    "path": "scripts/profile.js",
    "content": "document.addEventListener('DOMContentLoaded', function () {\r\n\r\n    document.getElementById(\"userForm\").addEventListener(\"submit\", function (event) {\r\n        event.preventDefault();\r\n        const submitButton = document.getElementById(\"userSubmit\");\r\n        submitButton.disabled = true;\r\n\r\n        const formData = new FormData(event.target);\r\n        formData.append(\"action\", \"save\");\r\n\r\n        fetch(\"endpoints/user/save_user.php\", {\r\n            method: \"POST\",\r\n            headers: {\r\n                \"X-CSRF-Token\": window.csrfToken,\r\n            },\r\n            body: formData,\r\n        })\r\n            .then(response => response.json())\r\n            .then(data => {\r\n                if (data.success) {\r\n                    document.getElementById(\"avatar\").src = document.getElementById(\"avatarImg\").src;\r\n                    const newUsername = document.getElementById(\"username\").value;\r\n                    document.getElementById(\"user\").textContent = newUsername;\r\n                    document.getElementById(\"profile_pic\").value = \"\";\r\n                    showSuccessMessage(data.message);\r\n\r\n                    if (data.reload) {\r\n                        location.reload();\r\n                    }\r\n                } else {\r\n                    showErrorMessage(data.message || translate(\"failed_save_user\"));\r\n                }\r\n            })\r\n            .catch(error => {\r\n                console.error(error);\r\n                showErrorMessage(translate(\"unknown_error\"));\r\n            })\r\n            .finally(() => {\r\n                submitButton.disabled = false;\r\n            });\r\n    });\r\n\r\n});\r\n\r\nfunction toggleAvatarSelect() {\r\n    var avatarSelect = document.getElementById(\"avatarSelect\");\r\n    if (avatarSelect.classList.contains(\"is-open\")) {\r\n        avatarSelect.classList.remove(\"is-open\");\r\n    } else {\r\n        avatarSelect.classList.add(\"is-open\");\r\n    }\r\n}\r\n\r\nfunction closeAvatarSelect() {\r\n    var avatarSelect = document.getElementById(\"avatarSelect\");\r\n    avatarSelect.classList.remove(\"is-open\");\r\n}\r\n\r\ndocument.querySelectorAll('.avatar-option').forEach((avatar) => {\r\n    avatar.addEventListener(\"click\", () => {\r\n        changeAvatar(avatar.src);\r\n        document.getElementById('avatarUser').value = avatar.getAttribute('data-src');\r\n        closeAvatarSelect();\r\n    })\r\n});\r\n\r\nfunction changeAvatar(src) {\r\n    document.getElementById(\"avatarImg\").src = src;\r\n}\r\n\r\nfunction successfulUpload(field, msg) {\r\n    var reader = new FileReader();\r\n\r\n    if (field.files.length === 0) {\r\n        return;\r\n    }\r\n\r\n    if (!['image/jpeg', 'image/png', 'image/gif', 'image/jtif', 'image/webp'].includes(field.files[0]['type'])) {\r\n        showErrorMessage(msg);\r\n        return;\r\n    }\r\n\r\n    reader.onload = function () {\r\n        changeAvatar(reader.result);\r\n    };\r\n\r\n    reader.readAsDataURL(field.files[0]);\r\n    closeAvatarSelect();\r\n}\r\n\r\nfunction deleteAvatar(path) {\r\n    fetch('endpoints/user/delete_avatar.php', {\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application/json',\r\n            \"X-CSRF-Token\": window.csrfToken,\r\n        },\r\n        body: JSON.stringify({ avatar: path }),\r\n    })\r\n        .then(response => response.json())\r\n        .then(data => {\r\n            if (data.success) {\r\n                var avatarContainer = document.querySelector(`.avatar-container[data-src=\"${path}\"]`);\r\n                if (avatarContainer) {\r\n                    avatarContainer.remove();\r\n                }\r\n                showSuccessMessage();\r\n            } else {\r\n                showErrorMessage(data.message || \"\");\r\n            }\r\n        })\r\n        .catch((error) => {\r\n            console.error('Error:', error);\r\n        });\r\n}\r\n\r\nfunction enableTotp() {\r\n  const totpSecret = document.querySelector(\"#totp-secret\");\r\n  const totpSecretCode = document.querySelector(\"#totp-secret-code\");\r\n  const qrCode = document.getElementById(\"totp-qr-code\");\r\n  totpSecret.value = \"\";\r\n  totpSecretCode.textContent = \"\";\r\n  qrCode.innerHTML = \"\";\r\n\r\n  fetch(\"endpoints/user/enable_totp.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/json\",\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ action: \"generate\" }),\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        totpSecret.value = data.secret;\r\n        totpSecretCode.textContent = data.secret;\r\n        new QRCode(qrCode, data.qrCodeUrl);\r\n        openTotpPopup();\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    });\r\n}\r\n\r\nfunction openTotpPopup() {\r\n    const enableTotpButton = document.getElementById('enableTotp');\r\n    enableTotpButton.disabled = true;\r\n\r\n    const totpPopup = document.getElementById('totp-popup');\r\n    totpPopup.classList.add('is-open');\r\n}\r\n\r\nfunction closeTotpPopup() {\r\n    const enableTotpButton = document.getElementById('enableTotp');\r\n    enableTotpButton.disabled = false;\r\n    const totpPopup = document.getElementById('totp-popup');\r\n    totpPopup.classList.remove('is-open');\r\n\r\n    const totpBackupCodes = document.getElementById('totp-backup-codes');\r\n    if (!totpBackupCodes.classList.contains('hide')) {\r\n        location.reload();\r\n    }\r\n}\r\n\r\nfunction submitTotp() {\r\n    const totpCode = document.getElementById('totp').value;\r\n    const totpSecret = document.getElementById('totp-secret').value;\r\n\r\n    fetch('endpoints/user/enable_totp.php', {\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application/json',\r\n            \"X-CSRF-Token\": window.csrfToken,\r\n        },\r\n        body: JSON.stringify({ totpCode: totpCode, totpSecret: totpSecret, action: 'verify' }),\r\n    })\r\n        .then(response => response.json())\r\n        .then(data => {\r\n            if (data.success) {\r\n                showSuccessMessage(data.message);\r\n                const backupCodes = data.backupCodes;\r\n                const backupCodesList = document.getElementById('backup-codes');\r\n                backupCodesList.innerHTML = '';\r\n                backupCodes.forEach(code => {\r\n                    const li = document.createElement('li');\r\n                    li.textContent = code;\r\n                    backupCodesList.appendChild(li);\r\n                });\r\n\r\n                const totpSetup = document.getElementById('totp-setup');\r\n                const totpBackupCodes = document.getElementById('totp-backup-codes');\r\n\r\n                totpSetup.classList.add('hide');\r\n                totpBackupCodes.classList.remove('hide');\r\n            } else {\r\n                showErrorMessage(data.message);\r\n            }\r\n        })\r\n        .catch(error => {\r\n            showErrorMessage(error);\r\n            console.log(error);\r\n        });\r\n}\r\n\r\nfunction copyBackupCodes() {\r\n    const backupCodes = document.querySelectorAll('#backup-codes li');\r\n    const codes = Array.from(backupCodes).map(code => code.textContent).join('\\n');\r\n\r\n    navigator.clipboard.writeText(codes)\r\n        .then(() => {\r\n            showSuccessMessage(translate('copied_to_clipboard'));\r\n        })\r\n        .catch(() => {\r\n            showErrorMessage(translate('unknown_error'));\r\n        });\r\n}\r\n\r\nfunction downloadBackupCodes() {\r\n    const backupCodes = document.querySelectorAll('#backup-codes li');\r\n    const codes = Array.from(backupCodes).map(code => code.textContent).join('\\n');\r\n    const element = document.createElement('a');\r\n\r\n    element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(codes));\r\n    element.setAttribute('download', 'wallos-backup-codes.txt');\r\n    element.style.display = 'none';\r\n    document.body.appendChild(element);\r\n\r\n    element.click();\r\n\r\n    document.body.removeChild(element);\r\n}\r\n\r\nfunction closeTotpDisablePopup() {\r\n    const totpPopup = document.getElementById('totp-disable-popup');\r\n    totpPopup.classList.remove('is-open');\r\n}\r\n\r\nfunction disableTotp() {\r\n    const totpPopup = document.getElementById('totp-disable-popup');\r\n    totpPopup.classList.add('is-open');\r\n}\r\n\r\nfunction submitDisableTotp() {\r\n    const totpCode = document.getElementById('totp-disable').value;\r\n\r\n    fetch('endpoints/user/disable_totp.php', {\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application/json',\r\n            \"X-CSRF-Token\": window.csrfToken,\r\n        },\r\n        body: JSON.stringify({ totpCode: totpCode }),\r\n    })\r\n        .then(response => response.json())\r\n        .then(data => {\r\n            if (data.success) {\r\n                showSuccessMessage(data.message);\r\n                if (data.reload) {\r\n                    location.reload();\r\n                }\r\n            } else {\r\n                showErrorMessage(data.message);\r\n            }\r\n        })\r\n        .catch(error => {\r\n            showErrorMessage(error);\r\n        });\r\n}\r\n\r\nfunction regenerateApiKey() {\r\n  const regenerateButton = document.getElementById(\"regenerateApiKey\");\r\n  regenerateButton.disabled = true;\r\n\r\n  fetch(\"endpoints/user/regenerateapikey.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      regenerateButton.disabled = false;\r\n      if (data.success) {\r\n        const newApiKey = data.apiKey;\r\n        document.getElementById(\"apikey\").value = newApiKey;\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"failed_regenerate_api_key\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      regenerateButton.disabled = false;\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    });\r\n}\r\n\r\n\r\nfunction exportAsJson() {\r\n    fetch(\"endpoints/subscriptions/export.php\")\r\n        .then(response => response.json())\r\n        .then(data => {\r\n            if (data.success) {\r\n                const subscriptions = JSON.stringify(data.subscriptions);\r\n                const element = document.createElement('a');\r\n                element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(subscriptions));\r\n                element.setAttribute('download', 'subscriptions.json');\r\n                element.style.display = 'none';\r\n                document.body.appendChild(element);\r\n                element.click();\r\n                document.body.removeChild(element);\r\n            } else {\r\n                showErrorMessage(data.message);\r\n            }\r\n        })\r\n        .catch(error => {\r\n            console.log(error);\r\n            showErrorMessage(translate('unknown_error'));\r\n        });\r\n}\r\n\r\nfunction exportAsCsv() {\r\n    fetch(\"endpoints/subscriptions/export.php\")\r\n        .then(response => response.json())\r\n        .then(data => {\r\n            if (data.success) {\r\n                const subscriptions = data.subscriptions;\r\n                const header = Object.keys(subscriptions[0]).join(',');\r\n                const csv = subscriptions.map(subscription => Object.values(subscription).join(',')).join('\\n');\r\n                const csvWithHeader = header + '\\n' + csv;\r\n                const element = document.createElement('a');\r\n                element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(csvWithHeader));\r\n                element.setAttribute('download', 'subscriptions.csv');\r\n                element.style.display = 'none';\r\n                document.body.appendChild(element);\r\n                element.click();\r\n                document.body.removeChild(element);\r\n            } else {\r\n                showErrorMessage(data.message);\r\n            }\r\n        })\r\n        .catch(error => {\r\n            showErrorMessage(translate('unknown_error'));\r\n        });\r\n}\r\n\r\nfunction deleteAccount(userId) {\r\n    if (!confirm(translate('delete_account_confirmation'))) {\r\n        return;\r\n    }\r\n\r\n    if (!confirm(translate('this_will_delete_all_data'))) {\r\n        return;\r\n    }\r\n\r\n    fetch('endpoints/settings/deleteaccount.php', {\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application/json',\r\n            \"X-CSRF-Token\": window.csrfToken,\r\n        },\r\n        body: JSON.stringify({ userId: userId }),\r\n    })\r\n        .then(response => response.json())\r\n        .then(data => {\r\n            if (data.success) {\r\n                window.location.href = 'logout.php';\r\n            } else {\r\n                showErrorMessage(data.message);\r\n            }\r\n        })\r\n        .catch((error) => {\r\n            showErrorMessage(translate('unknown_error'));\r\n        });\r\n}\r\n\r\n"
  },
  {
    "path": "scripts/registration.js",
    "content": "function setCookie(name, value, days) {\r\n  var expires = \"\";\r\n  if (days) {\r\n    var date = new Date();\r\n    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));\r\n    expires = \"; expires=\" + date.toUTCString();\r\n  }\r\n  document.cookie = name + \"=\" + value + expires + \"; SameSite=Strict\";\r\n}\r\n\r\nfunction storeFormFieldValue(fieldId) {\r\n  var fieldElement = document.getElementById(fieldId);\r\n  if (fieldElement) {\r\n    localStorage.setItem(fieldId, fieldElement.value);\r\n  }\r\n}\r\n\r\nfunction storeFormFields() {\r\n  storeFormFieldValue('username');\r\n  storeFormFieldValue('firstname');\r\n  storeFormFieldValue('lastname');\r\n  storeFormFieldValue('email');\r\n  storeFormFieldValue('password');\r\n  storeFormFieldValue('confirm_password');\r\n  storeFormFieldValue('currency');\r\n}\r\n\r\nfunction restoreFormFieldValue(fieldId) {\r\n  var fieldElement = document.getElementById(fieldId);\r\n  if (localStorage.getItem(fieldId)) {\r\n    fieldElement.value = localStorage.getItem(fieldId) || '';\r\n  }\r\n}\r\n\r\nfunction restoreFormFields() {\r\n  restoreFormFieldValue('username');\r\n  restoreFormFieldValue('firstname');\r\n  restoreFormFieldValue('lastname');\r\n  restoreFormFieldValue('email');\r\n  restoreFormFieldValue('password');\r\n  restoreFormFieldValue('confirm_password');\r\n  restoreFormFieldValue('currency');\r\n}\r\n\r\nfunction removeFromStorage() {\r\n  localStorage.removeItem('username');\r\n  localStorage.removeItem('firstname');\r\n  localStorage.removeItem('lastname');\r\n  localStorage.removeItem('email');\r\n  localStorage.removeItem('password');\r\n  localStorage.removeItem('confirm_password');\r\n  localStorage.removeItem('currency');\r\n}\r\n\r\nfunction changeLanguage(selectedLanguage) {\r\n  storeFormFields();\r\n  setCookie(\"language\", selectedLanguage, 365);\r\n  location.reload();\r\n}\r\n\r\nfunction runDatabaseMigration() {\r\n  let url = \"endpoints/db/migrate.php\";\r\n  fetch(url)\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate('network_response_error'));\r\n      }\r\n    });\r\n}\r\n\r\nfunction showErrorMessage(message) {\r\n  const toast = document.querySelector(\".toast#errorToast\");\r\n  (closeIcon = document.querySelector(\".close-error\")),\r\n    (errorMessage = document.querySelector(\".errorMessage\")),\r\n    (progress = document.querySelector(\".progress.error\"));\r\n  let timer1, timer2;\r\n  errorMessage.textContent = message;\r\n  toast.classList.add(\"active\");\r\n  progress.classList.add(\"active\");\r\n  timer1 = setTimeout(() => {\r\n    toast.classList.remove(\"active\");\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  }, 5000);\r\n\r\n  timer2 = setTimeout(() => {\r\n    progress.classList.remove(\"active\");\r\n  }, 5300);\r\n\r\n  closeIcon.addEventListener(\"click\", () => {\r\n    toast.classList.remove(\"active\");\r\n\r\n    setTimeout(() => {\r\n      progress.classList.remove(\"active\");\r\n    }, 300);\r\n\r\n    clearTimeout(timer1);\r\n    clearTimeout(timer2);\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  });\r\n}\r\n\r\nfunction showSuccessMessage(message) {\r\n  const toast = document.querySelector(\".toast#successToast\");\r\n  (closeIcon = document.querySelector(\".close-success\")),\r\n    (successMessage = document.querySelector(\".successMessage\")),\r\n    (progress = document.querySelector(\".progress.success\"));\r\n  let timer1, timer2;\r\n  successMessage.textContent = message;\r\n  toast.classList.add(\"active\");\r\n  progress.classList.add(\"active\");\r\n  timer1 = setTimeout(() => {\r\n    toast.classList.remove(\"active\");\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  }, 5000);\r\n\r\n  timer2 = setTimeout(() => {\r\n    progress.classList.remove(\"active\");\r\n  }, 5300);\r\n\r\n  closeIcon.addEventListener(\"click\", () => {\r\n    toast.classList.remove(\"active\");\r\n\r\n    setTimeout(() => {\r\n      progress.classList.remove(\"active\");\r\n    }, 300);\r\n\r\n    clearTimeout(timer1);\r\n    clearTimeout(timer2);\r\n    closeIcon.removeEventListener(\"click\", () => { });\r\n  });\r\n}\r\n\r\n\r\nfunction openRestoreDBFileSelect() {\r\n  document.getElementById('restoreDBFile').click();\r\n};\r\n\r\nfunction restoreDB() {\r\n  const input = document.getElementById('restoreDBFile');\r\n  const file = input.files[0];\r\n\r\n  if (!file) {\r\n    console.error('No file selected');\r\n    return;\r\n  }\r\n\r\n  const formData = new FormData();\r\n  formData.append('file', file);\r\n\r\n  fetch('endpoints/db/import.php', {\r\n    method: 'POST',\r\n    body: formData\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        fetch('endpoints/db/migrate.php')\r\n          .then(response => response.text())\r\n          .then(() => {\r\n            window.location.href = 'logout.php';\r\n          })\r\n          .catch(error => {\r\n            window.location.href = 'logout.php';\r\n          });\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => showErrorMessage('Error:', error));\r\n}\r\n\r\nfunction checkThemeNeedsUpdate() {\r\n  if (window.update_theme_settings) {\r\n    const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\r\n    const themePreference = prefersDarkMode ? 'dark' : 'light';\r\n    const darkThemeCss = document.querySelector(\"#dark-theme\");\r\n    darkThemeCss.disabled = themePreference === 'light';\r\n    document.body.className = themePreference;\r\n    const themeColorMetaTag = document.querySelector('meta[name=\"theme-color\"]');\r\n    themeColorMetaTag.setAttribute('content', themePreference === 'dark' ? '#222222' : '#FFFFFF');\r\n  }\r\n}\r\n\r\nfunction enableGoToLoginButton() {\r\n  const goToLoginButton = document.getElementById('goToLoginButton');\r\n  if (goToLoginButton) {\r\n    goToLoginButton.addEventListener('click', function () {\r\n      window.location.href = 'login.php';\r\n    });\r\n  }\r\n}\r\n\r\nwindow.onload = function () {\r\n  restoreFormFields();\r\n  removeFromStorage();\r\n  runDatabaseMigration();\r\n  checkThemeNeedsUpdate();\r\n  enableGoToLoginButton();\r\n};\r\n"
  },
  {
    "path": "scripts/settings.js",
    "content": "const deleteSvgContent = `\r\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"Recycle-Bin-2--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <g id=\"recycle-bin-2--remove-delete-empty-bin-trash-garbage\">\r\n    <path id=\"Union\" class=\"accent-color\" d=\"M43.318 15.934a1.5 1.5 0 0 0 -1.618 -1.591c-3.016 0.246 -8.46 0.52 -17.721 0.52 -9.215 0 -14.65 -0.271 -17.675 -0.516a1.5 1.5 0 0 0 -1.618 1.59c0.888 13.84 1.74 21.07 2.253 24.547 0.332 2.252 1.85 4.217 4.226 4.788 2.445 0.588 6.55 1.227 12.837 1.227 6.286 0 10.392 -0.64 12.837 -1.227 2.375 -0.57 3.894 -2.536 4.226 -4.788 0.513 -3.477 1.365 -10.708 2.253 -24.55Z\" stroke-width=\"1\"/>\r\n    <path id=\"Union_2\" class=\"main-color\" d=\"M23.37 1a8 8 0 0 0 -7.034 4.188c-3.411 0.072 -6 0.182 -7.814 0.282 -2.312 0.127 -4.692 1.242 -5.7 3.605 -0.244 0.57 -0.475 1.212 -0.663 1.919 -0.68 2.548 1.302 4.622 3.657 4.822 3.057 0.258 8.614 0.548 18.161 0.548 9.549 0 15.106 -0.29 18.162 -0.549 2.374 -0.2 4.291 -2.261 3.751 -4.785a16.68 16.68 0 0 0 -0.294 -1.167c-0.824 -2.831 -3.517 -4.277 -6.188 -4.411a260.66 260.66 0 0 0 -7.744 -0.264A8 8 0 0 0 24.631 1H23.37Z\" stroke-width=\"1\"/>\r\n    <path id=\"Vector_831_Stroke\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M17.8 23.01a2 2 0 0 1 2.19 1.791l1 10a2 2 0 0 1 -3.98 0.398l-1 -10a2 2 0 0 1 1.79 -2.189Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n    <path id=\"Vector_832_Stroke\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M30.2 23.01a2 2 0 0 0 -2.19 1.791l-1 10a2 2 0 0 0 3.98 0.398l1 -10a2 2 0 0 0 -1.79 -2.189Z\" clip-rule=\"evenodd\" stroke-width=\"1\"/>\r\n  </g>\r\n</svg>\r\n`;\r\n\r\nconst editSvgContent = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 48 48\" id=\"File-Check-Alternate--Streamline-Plump.svg\" height=\"48\" width=\"48\">\r\n  <g id=\"file-check-alternate--file-common-check\">\r\n    <path id=\"Subtract\" class=\"accent-color\" d=\"M13.582 2.137C16.326 1.823 20.685 1.5 27 1.5a165 165 0 0 1 5.13 0.077 1.5 1.5 0 0 1 0.4 0.068c1.098 0.343 4.029 1.564 8.123 5.578 3.862 3.787 5.195 6.563 5.63 7.781a1.5 1.5 0 0 1 0.087 0.45c0.08 2.153 0.13 4.655 0.13 7.546 0 7.57 -0.343 12.478 -0.669 15.432 -0.32 2.9 -2.518 5.1 -5.413 5.431 -2.744 0.314 -7.103 0.637 -13.418 0.637 -1.044 0 -2.035 -0.009 -2.974 -0.025A14.458 14.458 0 0 0 28.5 34c0 -8.008 -6.492 -14.5 -14.5 -14.5a14.44 14.44 0 0 0 -6.492 1.531c0.053 -6.464 0.364 -10.773 0.66 -13.463 0.32 -2.9 2.519 -5.1 5.414 -5.431Z\" stroke-width=\"1\"></path>\r\n    <path id=\"Intersect\" class=\"main-color\" d=\"M46.348 15.25c-2.42 -0.001 -6.57 -0.04 -8.948 -0.268 -2.598 -0.249 -4.641 -2.321 -4.896 -4.975 -0.214 -2.233 -0.253 -5.99 -0.254 -8.421 0.095 0.01 0.188 0.03 0.28 0.059 1.098 0.343 4.029 1.564 8.123 5.578 3.862 3.787 5.195 6.563 5.63 7.781 0.029 0.08 0.05 0.163 0.065 0.246Z\" stroke-width=\"1\"></path>\r\n    <path id=\"Subtract_2\" class=\"main-color\" fill-rule=\"evenodd\" d=\"M14 46c6.627 0 12 -5.373 12 -12s-5.373 -12 -12 -12S2 27.373 2 34s5.373 12 12 12Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n    <path id=\"Subtract_3\" class=\"accent-color\" fill-rule=\"evenodd\" d=\"M20.611 31.185a2 2 0 1 0 -3.222 -2.37l-4.413 6.002L10.5 32.01a2 2 0 0 0 -3 2.647l4.118 4.666a2 2 0 0 0 3.111 -0.138l5.882 -8Z\" clip-rule=\"evenodd\" stroke-width=\"1\"></path>\r\n  </g>\r\n</svg>`;\r\n\r\nfunction saveBudget() {\r\n  const button = document.getElementById(\"saveBudget\");\r\n  button.disabled = true;\r\n\r\n  const budget = document.getElementById(\"budget\").value;\r\n\r\n  fetch('endpoints/user/budget.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({budget: budget}),\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate('unknown_error'));\r\n    })\r\n    .finally(() => {\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction addMemberButton(memberId) {\r\n  const addButton = document.getElementById(\"addMember\");\r\n  addButton.disabled = true;\r\n\r\n  fetch(\"endpoints/household/household.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({action: \"add\"}),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        showErrorMessage(translate(\"failed_add_member\"));\r\n        throw new Error(translate(\"network_response_error\"));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(responseData => {\r\n      if (responseData.success) {\r\n        const newMemberId = responseData.householdId;\r\n        const container = document.getElementById(\"householdMembers\");\r\n\r\n        const div = document.createElement(\"div\");\r\n        div.className = \"form-group-inline\";\r\n        div.dataset.memberid = newMemberId;\r\n\r\n        const input = document.createElement(\"input\");\r\n        input.type = \"text\";\r\n        input.placeholder = translate(\"member\");\r\n        input.name = \"member\";\r\n        input.value = translate(\"member\");\r\n\r\n        const emailInput = document.createElement(\"input\");\r\n        emailInput.type = \"text\";\r\n        emailInput.placeholder = translate(\"email\");\r\n        emailInput.name = \"email\";\r\n        emailInput.value = \"\";\r\n\r\n        const editLink = document.createElement(\"button\");\r\n        editLink.className = \"image-button medium\";\r\n        editLink.name = \"save\";\r\n        editLink.onclick = () => editMember(newMemberId);\r\n        editLink.innerHTML = editSvgContent;\r\n        editLink.title = translate(\"save_member\");\r\n\r\n        const deleteLink = document.createElement(\"button\");\r\n        deleteLink.className = \"image-button medium\";\r\n        deleteLink.name = \"delete\";\r\n        deleteLink.onclick = () => removeMember(newMemberId);\r\n        deleteLink.innerHTML = deleteSvgContent;\r\n        deleteLink.title = translate(\"delete_member\");\r\n\r\n        div.appendChild(input);\r\n        div.appendChild(emailInput);\r\n        div.appendChild(editLink);\r\n        div.appendChild(deleteLink);\r\n\r\n        container.appendChild(div);\r\n      } else {\r\n        showErrorMessage(responseData.message || translate(\"failed_add_member\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"failed_add_member\"));\r\n    })\r\n    .finally(() => {\r\n      addButton.disabled = false;\r\n    });\r\n}\r\n\r\nfunction removeMember(memberId) {\r\n  fetch(\"endpoints/household/household.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      action: \"delete\",\r\n      memberId: memberId,\r\n    }),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate(\"network_response_error\"));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(responseData => {\r\n      if (responseData.success) {\r\n        const divToRemove = document.querySelector(`[data-memberid=\"${memberId}\"]`);\r\n        if (divToRemove) divToRemove.remove();\r\n        showSuccessMessage(responseData.message);\r\n      } else {\r\n        showErrorMessage(responseData.message || translate(\"failed_remove_member\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"failed_remove_member\"));\r\n    });\r\n}\r\n\r\n\r\nfunction editMember(memberId) {\r\n  const saveButton = document.querySelector(`div[data-memberid=\"${memberId}\"] button[name=\"save\"]`);\r\n  const memberNameElement = document.querySelector(`div[data-memberid=\"${memberId}\"] input[name=\"member\"]`);\r\n  const memberEmailElement = document.querySelector(`div[data-memberid=\"${memberId}\"] input[name=\"email\"]`);\r\n\r\n  if (!memberNameElement) return;\r\n\r\n  saveButton.classList.add(\"disabled\");\r\n  saveButton.disabled = true;\r\n\r\n  const memberName = memberNameElement.value;\r\n  const memberEmail = memberEmailElement ? memberEmailElement.value : \"\";\r\n\r\n  fetch(\"endpoints/household/household.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      action: \"edit\",\r\n      memberId: memberId,\r\n      name: memberName,\r\n      email: memberEmail,\r\n    }),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        showErrorMessage(translate(\"failed_save_member\"));\r\n        throw new Error(translate(\"network_response_error\"));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(responseData => {\r\n      if (responseData.success) {\r\n        showSuccessMessage(responseData.message);\r\n      } else {\r\n        showErrorMessage(responseData.message || translate(\"failed_save_member\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"failed_save_member\"));\r\n    })\r\n    .finally(() => {\r\n      saveButton.classList.remove(\"disabled\");\r\n      saveButton.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction addCategoryButton(categoryId) {\r\n  const addButton = document.getElementById(\"addCategory\");\r\n  addButton.disabled = true;\r\n\r\n  fetch('endpoints/categories/category.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/x-www-form-urlencoded',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({action: 'add'}),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        showErrorMessage(translate('failed_add_category'));\r\n        throw new Error(translate('network_response_error'));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(responseData => {\r\n      if (responseData.success) {\r\n        const newCategoryId = responseData.categoryId;\r\n        const container = document.getElementById(\"categories\");\r\n\r\n        const row = document.createElement(\"div\");\r\n        row.className = \"form-group-inline\";\r\n        row.dataset.categoryid = newCategoryId;\r\n\r\n        const dragIcon = document.createElement(\"div\");\r\n        dragIcon.className = \"drag-icon\";\r\n\r\n        const input = document.createElement(\"input\");\r\n        input.type = \"text\";\r\n        input.placeholder = translate('category');\r\n        input.name = \"category\";\r\n        input.value = translate('category');\r\n\r\n        const editLink = document.createElement(\"button\");\r\n        editLink.className = \"image-button medium\";\r\n        editLink.name = \"save\";\r\n        editLink.onclick = function () {\r\n          editCategory(newCategoryId);\r\n        };\r\n        editLink.innerHTML = editSvgContent;\r\n        editLink.title = translate('save_member');\r\n\r\n        const deleteLink = document.createElement(\"button\");\r\n        deleteLink.className = \"image-button medium\";\r\n        deleteLink.name = \"delete\";\r\n        deleteLink.onclick = function () {\r\n          removeCategory(newCategoryId);\r\n        };\r\n        deleteLink.innerHTML = deleteSvgContent;\r\n        deleteLink.title = translate('delete_member');\r\n\r\n        row.appendChild(dragIcon);\r\n        row.appendChild(input);\r\n        row.appendChild(editLink);\r\n        row.appendChild(deleteLink);\r\n        container.appendChild(row);\r\n      } else {\r\n        showErrorMessage(responseData.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate('failed_add_category'));\r\n    })\r\n    .finally(() => {\r\n      addButton.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction removeCategory(categoryId) {\r\n  fetch('endpoints/categories/category.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/x-www-form-urlencoded',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      action: 'delete',\r\n      categoryId: categoryId,\r\n    }),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate('network_response_error'));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(responseData => {\r\n      if (responseData.success) {\r\n        const divToRemove = document.querySelector(`[data-categoryid=\"${categoryId}\"]`);\r\n        if (divToRemove) divToRemove.remove();\r\n        showSuccessMessage(responseData.message);\r\n      } else {\r\n        showErrorMessage(responseData.message || translate('failed_remove_category'));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate('failed_remove_category'));\r\n    });\r\n}\r\n\r\n\r\nfunction editCategory(categoryId) {\r\n  const saveButton = document.querySelector(`div[data-categoryid=\"${categoryId}\"] button[name=\"save\"]`);\r\n  const inputElement = document.querySelector(`div[data-categoryid=\"${categoryId}\"] input[name=\"category\"]`);\r\n\r\n  if (!inputElement) return;\r\n\r\n  saveButton.classList.add(\"disabled\");\r\n  saveButton.disabled = true;\r\n\r\n  const categoryName = inputElement.value;\r\n\r\n  fetch('endpoints/categories/category.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/x-www-form-urlencoded',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      action: 'edit',\r\n      categoryId: categoryId,\r\n      name: categoryName,\r\n    }),\r\n  })\r\n    .then(response => {\r\n      saveButton.classList.remove(\"disabled\");\r\n      saveButton.disabled = false;\r\n\r\n      if (!response.ok) {\r\n        showErrorMessage(translate('failed_save_category'));\r\n        throw new Error(translate('network_response_error'));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(responseData => {\r\n      if (responseData.success) {\r\n        showSuccessMessage(responseData.message);\r\n      } else {\r\n        showErrorMessage(responseData.message || translate('failed_save_category'));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate('failed_save_category'));\r\n      saveButton.classList.remove(\"disabled\");\r\n      saveButton.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction addCurrencyButton(currencyId) {\r\n  const addButton = document.getElementById(\"addCurrency\");\r\n  addButton.disabled = true;\r\n\r\n  fetch('endpoints/currency/currency.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/x-www-form-urlencoded',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({action: 'add'}),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate('network_response_error'));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(responseData => {\r\n      if (responseData.success) {\r\n        const newCurrencyId = responseData.currencyId;\r\n        const container = document.getElementById(\"currencies\");\r\n\r\n        const div = document.createElement(\"div\");\r\n        div.className = \"form-group-inline\";\r\n        div.dataset.currencyid = newCurrencyId;\r\n\r\n        const inputSymbol = document.createElement(\"input\");\r\n        inputSymbol.type = \"text\";\r\n        inputSymbol.placeholder = \"$\";\r\n        inputSymbol.name = \"symbol\";\r\n        inputSymbol.value = \"$\";\r\n        inputSymbol.classList.add(\"short\");\r\n\r\n        const inputName = document.createElement(\"input\");\r\n        inputName.type = \"text\";\r\n        inputName.placeholder = translate('currency');\r\n        inputName.name = \"currency\";\r\n        inputName.value = translate('currency');\r\n\r\n        const inputCode = document.createElement(\"input\");\r\n        inputCode.type = \"text\";\r\n        inputCode.placeholder = translate('currency_code');\r\n        inputCode.name = \"code\";\r\n        inputCode.value = \"CODE\";\r\n\r\n        const editLink = document.createElement(\"button\");\r\n        editLink.className = \"image-button medium\";\r\n        editLink.name = \"save\";\r\n        editLink.onclick = function () {\r\n          editCurrency(newCurrencyId);\r\n        };\r\n        editLink.innerHTML = editSvgContent;\r\n        editLink.title = translate('save_member');\r\n\r\n        const deleteLink = document.createElement(\"button\");\r\n        deleteLink.className = \"image-button medium\";\r\n        deleteLink.name = \"delete\";\r\n        deleteLink.onclick = function () {\r\n          removeCurrency(newCurrencyId);\r\n        };\r\n        deleteLink.innerHTML = deleteSvgContent;\r\n        deleteLink.title = translate('delete_member');\r\n\r\n        div.appendChild(inputSymbol);\r\n        div.appendChild(inputName);\r\n        div.appendChild(inputCode);\r\n        div.appendChild(editLink);\r\n        div.appendChild(deleteLink);\r\n\r\n        container.appendChild(div);\r\n      } else {\r\n        showErrorMessage(responseData.message || translate('failed_add_currency'));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate('failed_add_currency'));\r\n    })\r\n    .finally(() => {\r\n      addButton.disabled = false;\r\n    });\r\n}\r\n\r\nfunction removeCurrency(currencyId) {\r\n  fetch('endpoints/currency/currency.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/x-www-form-urlencoded',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      action: 'delete',\r\n      currencyId: currencyId,\r\n    }),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate('network_response_error'));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        const divToRemove = document.querySelector(`[data-currencyid=\"${currencyId}\"]`);\r\n        if (divToRemove) divToRemove.remove();\r\n      } else {\r\n        showErrorMessage(data.message || translate('failed_remove_currency'));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(error.message || translate('failed_remove_currency'));\r\n    });\r\n}\r\n\r\nfunction editCurrency(currencyId) {\r\n  const saveButton = document.querySelector(`div[data-currencyid=\"${currencyId}\"] button[name=\"save\"]`);\r\n  const inputSymbolElement = document.querySelector(`div[data-currencyid=\"${currencyId}\"] input[name=\"symbol\"]`);\r\n  const inputNameElement = document.querySelector(`div[data-currencyid=\"${currencyId}\"] input[name=\"currency\"]`);\r\n  const inputCodeElement = document.querySelector(`div[data-currencyid=\"${currencyId}\"] input[name=\"code\"]`);\r\n\r\n  if (!inputNameElement) return;\r\n\r\n  saveButton.classList.add(\"disabled\");\r\n  saveButton.disabled = true;\r\n\r\n  const currencyName = inputNameElement.value;\r\n  const currencySymbol = inputSymbolElement.value;\r\n  const currencyCode = inputCodeElement.value;\r\n\r\n  fetch('endpoints/currency/currency.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/x-www-form-urlencoded',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      action: 'edit',\r\n      currencyId: currencyId,\r\n      name: currencyName,\r\n      symbol: currencySymbol,\r\n      code: currencyCode,\r\n    }),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate('network_response_error'));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(data => {\r\n      saveButton.classList.remove(\"disabled\");\r\n      saveButton.disabled = false;\r\n\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message || translate('failed_save_currency'));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(error.message || translate('failed_save_currency'));\r\n      saveButton.classList.remove(\"disabled\");\r\n      saveButton.disabled = false;\r\n    });\r\n}\r\n\r\nfunction togglePayment(paymentId) {\r\n  const element = document.querySelector(`div[data-paymentid=\"${paymentId}\"]`);\r\n\r\n  if (element.dataset.inUse === \"yes\") {\r\n    return showErrorMessage(translate(\"cant_disable_payment_in_use\"));\r\n  }\r\n\r\n  const newEnabledState = element.dataset.enabled === \"1\" ? \"0\" : \"1\";\r\n  const paymentMethodName = element.querySelector(\".payment-name\").innerText;\r\n\r\n  fetch(\"endpoints/payments/toggle.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      paymentId: paymentId,\r\n      enabled: newEnabledState,\r\n    }),\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate(\"network_response_error\"));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(data => {\r\n      if (data.success) {\r\n        element.dataset.enabled = newEnabledState;\r\n        showSuccessMessage(`${paymentMethodName} ${data.message}`);\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"failed_save_payment_method\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(error.message || translate(\"failed_save_payment_method\"));\r\n    });\r\n}\r\n\r\ndocument.body.addEventListener('click', function (e) {\r\n  let targetElement = e.target;\r\n  do {\r\n    if (targetElement.classList && targetElement.classList.contains('payments-payment')) {\r\n      let targetChild = e.target;\r\n      do {\r\n        if (targetChild.classList && (targetChild.classList.contains('payment-name') || targetChild.classList.contains('drag-icon'))) {\r\n          return;\r\n        }\r\n        targetChild = targetChild.parentNode;\r\n      } while (targetChild && targetChild !== targetElement);\r\n\r\n      const paymentId = targetElement.dataset.paymentid;\r\n      togglePayment(paymentId);\r\n      return;\r\n    }\r\n    targetElement = targetElement.parentNode;\r\n  } while (targetElement);\r\n});\r\n\r\ndocument.body.addEventListener('blur', function (e) {\r\n  let targetElement = e.target;\r\n  if (targetElement.classList && targetElement.classList.contains('payment-name')) {\r\n    const paymentId = targetElement.closest('.payments-payment').dataset.paymentid;\r\n    const newName = targetElement.textContent;\r\n    renamePayment(paymentId, newName);\r\n  }\r\n}, true);\r\n\r\nfunction renamePayment(paymentId, newName) {\r\n  const name = newName.trim();\r\n  if (!name) return;\r\n\r\n  const formData = new FormData();\r\n  formData.append(\"paymentId\", paymentId);\r\n  formData.append(\"name\", name);\r\n\r\n  fetch(\"endpoints/payments/rename.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: formData,\r\n  })\r\n    .then(response => {\r\n      if (!response.ok) {\r\n        throw new Error(translate(\"network_response_error\"));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(`${newName} ${data.message}`);\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"failed_save_payment_method\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    });\r\n}\r\n\r\n\r\ndocument.body.addEventListener('keypress', function (e) {\r\n  let targetElement = e.target;\r\n  if (targetElement.classList && targetElement.classList.contains('payment-name')) {\r\n    if (e.key === 'Enter') {\r\n      e.preventDefault();\r\n      targetElement.blur();\r\n    }\r\n  }\r\n});\r\n\r\nfunction handleFileSelect(event) {\r\n  const fileInput = event.target;\r\n  const iconPreview = document.querySelector('.icon-preview');\r\n  const iconImg = iconPreview.querySelector('img');\r\n  const iconUrl = document.querySelector(\"#icon-url\");\r\n  iconUrl.value = \"\";\r\n\r\n  if (fileInput.files && fileInput.files[0]) {\r\n    const reader = new FileReader();\r\n\r\n    reader.onload = function (e) {\r\n      iconImg.src = e.target.result;\r\n      iconImg.style.display = 'block';\r\n    };\r\n\r\n    reader.readAsDataURL(fileInput.files[0]);\r\n  }\r\n}\r\n\r\nfunction setSearchButtonStatus() {\r\n\r\n  const nameInput = document.querySelector(\"#paymentname\");\r\n  const hasSearchTerm = nameInput.value.trim().length > 0;\r\n  const iconSearchButton = document.querySelector(\"#icon-search-button\");\r\n  if (hasSearchTerm) {\r\n    iconSearchButton.classList.remove(\"disabled\");\r\n  } else {\r\n    iconSearchButton.classList.add(\"disabled\");\r\n  }\r\n\r\n}\r\n\r\nfunction searchPaymentIcon() {\r\n  const nameInput = document.querySelector(\"#paymentname\");\r\n  const searchTerm = nameInput.value.trim();\r\n  if (searchTerm !== \"\") {\r\n    const iconSearchPopup = document.querySelector(\"#icon-search-results\");\r\n    iconSearchPopup.classList.add(\"is-open\");\r\n    const imageSearchUrl = `endpoints/payments/search.php?search=${searchTerm}`;\r\n    fetch(imageSearchUrl)\r\n      .then(response => response.json())\r\n      .then(data => {\r\n        if (data.imageUrls) {\r\n          displayImageResults(data.imageUrls);\r\n        } else if (data.error) {\r\n          console.error(data.error);\r\n        }\r\n      })\r\n      .catch(error => {\r\n        console.error(translate('error_fetching_image_results'), error);\r\n      });\r\n  } else {\r\n    nameInput.focus();\r\n  }\r\n}\r\n\r\nfunction displayImageResults(imageSources) {\r\n  const iconResults = document.querySelector(\"#icon-search-images\");\r\n  iconResults.innerHTML = \"\";\r\n\r\n  imageSources.forEach(src => {\r\n    const img = document.createElement(\"img\");\r\n    img.src = src;\r\n    img.onclick = function () {\r\n      selectWebIcon(src);\r\n    };\r\n    img.onerror = function () {\r\n      this.parentNode.removeChild(this);\r\n    };\r\n    iconResults.appendChild(img);\r\n  });\r\n}\r\n\r\nfunction selectWebIcon(url) {\r\n  closeIconSearch();\r\n  const iconPreview = document.querySelector(\"#form-icon\");\r\n  const iconUrl = document.querySelector(\"#icon-url\");\r\n  iconPreview.src = url;\r\n  iconPreview.style.display = 'block';\r\n  iconUrl.value = url;\r\n}\r\n\r\nfunction closeIconSearch() {\r\n  const iconSearchPopup = document.querySelector(\"#icon-search-results\");\r\n  iconSearchPopup.classList.remove(\"is-open\");\r\n  const iconResults = document.querySelector(\"#icon-search-images\");\r\n  iconResults.innerHTML = \"\";\r\n}\r\n\r\nfunction resetFormIcon() {\r\n  const iconPreview = document.querySelector(\"#form-icon\");\r\n  iconPreview.src = \"\";\r\n  iconPreview.style.display = 'none';\r\n}\r\n\r\nfunction reloadPaymentMethods() {\r\n  const paymentsContainer = document.querySelector(\"#payments-list\");\r\n  const paymentMethodsEndpoint = \"endpoints/payments/get.php\";\r\n\r\n  fetch(paymentMethodsEndpoint)\r\n    .then(response => response.text())\r\n    .then(data => {\r\n      paymentsContainer.innerHTML = data;\r\n    });\r\n}\r\n\r\nfunction addPaymentMethod() {\r\n  closeIconSearch();\r\n  const addPaymentMethodEndpoint = \"endpoints/payments/add.php\";\r\n  const paymentMethodForm = document.querySelector(\"#payments-form\");\r\n  const submitButton = document.querySelector(\"#add-payment-button\");\r\n\r\n  submitButton.disabled = true;\r\n  const formData = new FormData(paymentMethodForm);\r\n  formData.append(\"action\", \"add\");\r\n\r\n  fetch(addPaymentMethodEndpoint, {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: formData,\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        paymentMethodForm.reset();\r\n        resetFormIcon();\r\n        reloadPaymentMethods();\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"failed_add_payment_method\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    })\r\n    .finally(() => {\r\n      submitButton.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction deletePaymentMethod(paymentId) {\r\n  fetch(`endpoints/payments/delete.php?id=${paymentId}`, {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n       \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ id: paymentId }),\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        var paymentToRemove = document.querySelector('.payments-payment[data-paymentid=\"' + paymentId + '\"]');\r\n        if (paymentToRemove) {\r\n          paymentToRemove.remove();\r\n        }\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch((error) => {\r\n      console.error('Error:', error);\r\n    });\r\n}\r\n\r\nfunction savePaymentMethodsSorting() {\r\n  const paymentMethods = document.getElementById(\"payments-list\");\r\n  const paymentMethodIds = Array.from(paymentMethods.children).map(\r\n    paymentMethod => paymentMethod.dataset.paymentid\r\n  );\r\n\r\n  const formData = new FormData();\r\n  paymentMethodIds.forEach(id => formData.append(\"paymentMethodIds[]\", id));\r\n  formData.append(\"action\", \"sort\");\r\n\r\n  fetch(\"endpoints/payments/sort.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: formData,\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"failed_sort_payment_methods\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    });\r\n}\r\n\r\n\r\nvar el = document.getElementById('payments-list');\r\nvar sortable = Sortable.create(el, {\r\n  handle: '.drag-icon',\r\n  ghostClass: 'sortable-ghost',\r\n  delay: 500,\r\n  delayOnTouchOnly: true,\r\n  touchStartThreshold: 5,\r\n  onEnd: function (evt) {\r\n    savePaymentMethodsSorting();\r\n  },\r\n});\r\n\r\n\r\ndocument.addEventListener('DOMContentLoaded', function () {\r\n\r\n  var removePaymentButtons = document.querySelectorAll(\".delete-payment-method\");\r\n  removePaymentButtons.forEach(function (button) {\r\n    button.addEventListener('click', function (event) {\r\n      event.preventDefault();\r\n      event.stopPropagation();\r\n      let paymentId = event.target.getAttribute('data-paymentid');\r\n      deletePaymentMethod(paymentId);\r\n    });\r\n  });\r\n\r\n  if (document.getElementById(\"ai_type\")) {\r\n    toggleAiInputs();\r\n  }\r\n\r\n});\r\n\r\nfunction addFixerKeyButton() {\r\n  const addButton = document.getElementById(\"addFixerKey\");\r\n  addButton.disabled = true;\r\n\r\n  const apiKeyInput = document.querySelector(\"#fixerKey\");\r\n  const apiKey = apiKeyInput.value.trim();\r\n  const provider = document.querySelector(\"#fixerProvider\").value;\r\n  const convertCurrencyCheckbox = document.querySelector(\"#convertcurrency\");\r\n\r\n  fetch(\"endpoints/currency/fixer_api_key.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      api_key: apiKey,\r\n      provider: provider,\r\n    }),\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        addButton.disabled = false;\r\n        convertCurrencyCheckbox.disabled = false;\r\n\r\n        fetch(\"endpoints/currency/update_exchange.php\", {\r\n          method: \"POST\",\r\n          headers: {\r\n            \"Content-Type\": \"application/x-www-form-urlencoded\",\r\n            'X-CSRF-Token': window.csrfToken,\r\n          },\r\n          body: new URLSearchParams({force: \"true\"}),\r\n        }).catch(console.error);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n        addButton.disabled = false;\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n      addButton.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction storeSettingsOnDB(endpoint, value) {\r\n  fetch('endpoints/settings/' + endpoint + '.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ \"value\": value })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    });\r\n}\r\n\r\nfunction setShowMonthlyPrice() {\r\n  const showMonthlyPriceCheckbox = document.querySelector(\"#monthlyprice\");\r\n  const value = showMonthlyPriceCheckbox.checked;\r\n\r\n  storeSettingsOnDB('monthly_price', value);\r\n}\r\n\r\nfunction setConvertCurrency() {\r\n  const convertCurrencyCheckbox = document.querySelector(\"#convertcurrency\");\r\n  const value = convertCurrencyCheckbox.checked;\r\n\r\n  storeSettingsOnDB('convert_currency', value);\r\n}\r\n\r\nfunction setRemoveBackground() {\r\n  const removeBackgroundCheckbox = document.querySelector(\"#removebackground\");\r\n  const value = removeBackgroundCheckbox.checked;\r\n\r\n  storeSettingsOnDB('remove_background', value);\r\n}\r\n\r\nfunction setHideDisabled() {\r\n  const hideDisabledCheckbox = document.querySelector(\"#hidedisabled\");\r\n  const value = hideDisabledCheckbox.checked;\r\n\r\n  storeSettingsOnDB('hide_disabled', value);\r\n}\r\n\r\nfunction setDisabledToBottom() {\r\n  const disabledToBottomCheckbox = document.querySelector(\"#disabledtobottom\");\r\n  const value = disabledToBottomCheckbox.checked;\r\n\r\n  storeSettingsOnDB('disabled_to_bottom', value);\r\n}\r\n\r\nfunction setShowOriginalPrice() {\r\n  const showOriginalPriceCheckbox = document.querySelector(\"#showoriginalprice\");\r\n  const value = showOriginalPriceCheckbox.checked;\r\n\r\n  storeSettingsOnDB('show_original_price', value);\r\n}\r\n\r\nfunction setMobileNavigation() {\r\n  const mobileNavigationCheckbox = document.querySelector(\"#mobilenavigation\");\r\n  const value = mobileNavigationCheckbox.checked;\r\n\r\n  storeSettingsOnDB('mobile_navigation', value);\r\n}\r\n\r\nfunction setShowSubscriptionProgress() {\r\n  const showSubscriptionProgressCheckbox = document.querySelector(\"#showsubscriptionprogress\");\r\n  const value = showSubscriptionProgressCheckbox.checked;\r\n\r\n  storeSettingsOnDB('subscription_progress', value);\r\n}\r\n\r\nfunction saveCategorySorting() {\r\n  const categories = document.getElementById(\"categories\");\r\n  const categoryIds = Array.from(categories.children).map(c => c.dataset.categoryid);\r\n\r\n  const formData = new FormData();\r\n  categoryIds.forEach(categoryId => formData.append(\"categoryIds[]\", categoryId));\r\n  formData.append(\"action\", \"sort\");\r\n\r\n  fetch(\"endpoints/categories/category.php\", {\r\n    method: \"POST\",\r\n    headers: {\"X-CSRF-Token\": window.csrfToken},\r\n    body: formData,\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    });\r\n}\r\n\r\n\r\nvar el = document.getElementById('categories');\r\nvar sortable = Sortable.create(el, {\r\n  handle: '.drag-icon',\r\n  ghostClass: 'sortable-ghost',\r\n  delay: 500,\r\n  delayOnTouchOnly: true,\r\n  touchStartThreshold: 5,\r\n  onEnd: function (evt) {\r\n    saveCategorySorting();\r\n  },\r\n});\r\n\r\nfunction fetch_ai_models() {\r\n  const endpoint = 'endpoints/ai/fetch_models.php';\r\n  const type = document.querySelector(\"#ai_type\").value;\r\n  const api_key = document.querySelector(\"#ai_api_key\").value.trim();\r\n  const ollama_host = document.querySelector(\"#ai_ollama_host\").value.trim();\r\n  const modelSelect = document.querySelector(\"#ai_model\");\r\n\r\n  fetch(endpoint, {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ type, api_key, ollama_host })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        modelSelect.innerHTML = '';\r\n        data.models.forEach(model => {\r\n          const option = document.createElement('option');\r\n          option.value = model.id;\r\n          option.textContent = model.name;\r\n          modelSelect.appendChild(option);\r\n        });\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(translate('unknown_error'));\r\n    });\r\n}\r\n\r\nfunction toggleAiInputs() {\r\n  const aiTypeSelect = document.getElementById(\"ai_type\");\r\n  const apiKeyInput = document.getElementById(\"ai_api_key\");\r\n  const apiKeyToggleButton = document.getElementById(\"toggleAiApiKey\");\r\n  const apiKeyToggleIcon = apiKeyToggleButton ? apiKeyToggleButton.querySelector(\"i\") : null;\r\n  const ollamaHostInput = document.getElementById(\"ai_ollama_host\");\r\n  const type = aiTypeSelect.value;\r\n  if (type === \"ollama\") {\r\n    apiKeyInput.classList.add(\"hidden\");\r\n    if (apiKeyToggleButton) {\r\n      apiKeyToggleButton.classList.add(\"hidden\");\r\n    }\r\n    apiKeyInput.type = \"password\";\r\n    if (apiKeyToggleIcon) {\r\n      apiKeyToggleIcon.classList.remove(\"fa-eye-slash\");\r\n      apiKeyToggleIcon.classList.add(\"fa-eye\");\r\n    }\r\n    ollamaHostInput.classList.remove(\"hidden\");\r\n  } else {\r\n    apiKeyInput.classList.remove(\"hidden\");\r\n    if (apiKeyToggleButton) {\r\n      apiKeyToggleButton.classList.remove(\"hidden\");\r\n    }\r\n    apiKeyInput.type = \"password\";\r\n    if (apiKeyToggleIcon) {\r\n      apiKeyToggleIcon.classList.remove(\"fa-eye-slash\");\r\n      apiKeyToggleIcon.classList.add(\"fa-eye\");\r\n    }\r\n    ollamaHostInput.classList.add(\"hidden\");\r\n  }\r\n}\r\n\r\nfunction toggleAiApiKeyVisibility() {\r\n  const apiKeyInput = document.getElementById(\"ai_api_key\");\r\n  const apiKeyToggleButton = document.getElementById(\"toggleAiApiKey\");\r\n  if (!apiKeyInput || !apiKeyToggleButton) {\r\n    return;\r\n  }\r\n\r\n  const icon = apiKeyToggleButton.querySelector(\"i\");\r\n  const isPassword = apiKeyInput.type === \"password\";\r\n  apiKeyInput.type = isPassword ? \"text\" : \"password\";\r\n\r\n  if (icon) {\r\n    icon.classList.toggle(\"fa-eye\", !isPassword);\r\n    icon.classList.toggle(\"fa-eye-slash\", isPassword);\r\n  }\r\n}\r\n\r\nfunction saveAiSettingsButton() {\r\n  const aiEnabled = document.querySelector(\"#ai_enabled\").checked;\r\n  const aiType = document.querySelector(\"#ai_type\").value;\r\n  const aiApiKey = document.querySelector(\"#ai_api_key\").value.trim();\r\n  const aiOllamaHost = document.querySelector(\"#ai_ollama_host\").value.trim();\r\n  const aiModel = document.querySelector(\"#ai_model\").value;\r\n\r\n  fetch('endpoints/ai/save_settings.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ ai_enabled: aiEnabled, ai_type: aiType, api_key: aiApiKey, ollama_host: aiOllamaHost, model: aiModel })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        const runAiActionButton = document.querySelector(\"#runAiRecommendations\");\r\n        if (data.enabled) {\r\n          runAiActionButton.classList.remove(\"hidden\");\r\n        } else {\r\n          runAiActionButton.classList.add(\"hidden\");\r\n        }\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(translate('unknown_error'));\r\n    });\r\n}\r\n\r\nfunction runAiRecommendations() {\r\n  const endpoint = 'endpoints/ai/generate_recommendations.php';\r\n  const button = document.querySelector(\"#runAiRecommendations\");\r\n  const spinner = document.querySelector(\"#aiSpinner\");\r\n\r\n  button.classList.add(\"hidden\");\r\n  spinner.classList.remove(\"hidden\");\r\n\r\n  fetch(endpoint, {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    }\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(translate('unknown_error'));\r\n    })\r\n    .finally(() => {\r\n      button.classList.remove(\"hidden\");\r\n      spinner.classList.add(\"hidden\");\r\n    });\r\n\r\n}"
  },
  {
    "path": "scripts/stats.js",
    "content": "function loadGraph(container, dataPoints, currency, run) {\r\n    if (run) {\r\n        var ctx = document.getElementById(container).getContext('2d');\r\n\r\n        var chart = new Chart(ctx, {\r\n            type: 'pie',\r\n            data: {\r\n                datasets: [{\r\n                    data: dataPoints.map(point => point.y),\r\n                }],\r\n                labels: dataPoints.map(point => {\r\n                    if (currency) {\r\n                        return `${point.label} (${new Intl.NumberFormat(navigator.language, { style: 'currency', currency }).format(point.y)})`;\r\n                    } else {\r\n                        return `${point.label} (${new Intl.NumberFormat(navigator.language).format(point.y)})`;\r\n                    }\r\n                }),\r\n            },\r\n            options: {\r\n                animation: {\r\n                    animateRotate: true,\r\n                    animateScale: true,\r\n                },\r\n                plugins: {\r\n                    legend: {\r\n                        display: true,\r\n                        position: 'top',\r\n                    },\r\n                    tooltip: {\r\n                        callbacks: {\r\n                            label: function(context) {\r\n                                let label = \" \";\r\n                                if (currency) {\r\n                                    label += new Intl.NumberFormat(navigator.language, { style: 'currency', currency }).format(context.raw);\r\n                                } else {\r\n                                    label += new Intl.NumberFormat(navigator.language).format(context.raw);\r\n                                }\r\n                                return label;\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n            },\r\n        });\r\n    }\r\n}\r\n\r\nfunction loadLineGraph(container, dataPoints, currency, run) {\r\n    if (run) {\r\n        var ctx = document.getElementById(container).getContext('2d');\r\n\r\n        var chart = new Chart(ctx, {\r\n            type: 'line',\r\n            data: {\r\n                datasets: [{\r\n                    label: '',\r\n                    data: dataPoints.map(point => point.y),\r\n                }],\r\n                labels: dataPoints.map(point => {\r\n                    return `${point.label}`;\r\n                }),\r\n            },\r\n            options: {\r\n                animation: {\r\n                    animateRotate: true,\r\n                    animateScale: true,\r\n                },\r\n                scales: {\r\n                    y: {\r\n                        beginAtZero: false,\r\n                        ticks: {\r\n                            callback: function(value, index, values) {\r\n                                if (currency) {\r\n                                    return new Intl.NumberFormat(navigator.language, { style: 'currency', currency }).format(value);\r\n                                } else {\r\n                                    return new Intl.NumberFormat(navigator.language).format(value);\r\n                                }\r\n                            }\r\n                        }\r\n                    }\r\n                },\r\n                plugins: {\r\n                    legend: {\r\n                        display: false\r\n                    }\r\n                }\r\n            }\r\n        });\r\n    }\r\n}\r\n\r\n\r\nfunction closeSubMenus() {\r\n    var subMenus = document.querySelectorAll('.filtermenu-submenu-content');\r\n    subMenus.forEach(subMenu => {\r\n        subMenu.classList.remove('is-open');\r\n    });\r\n\r\n}\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", function() {\r\n    var filtermenu = document.querySelector('#filtermenu-button');\r\n    filtermenu.addEventListener('click', function() {\r\n        this.parentElement.querySelector('.filtermenu-content').classList.toggle('is-open');\r\n        closeSubMenus();\r\n    });\r\n\r\n    document.addEventListener('click', function(e) {\r\n        var filtermenuContent = document.querySelector('.filtermenu-content');\r\n        if (filtermenuContent.classList.contains('is-open')) {\r\n            var subMenus = document.querySelectorAll('.filtermenu-submenu');\r\n            var clickedInsideSubmenu = Array.from(subMenus).some(subMenu => subMenu.contains(e.target) || subMenu === e.target);\r\n\r\n            if (!filtermenu.contains(e.target) && !clickedInsideSubmenu) {\r\n                closeSubMenus();\r\n                filtermenuContent.classList.remove('is-open');\r\n            }\r\n        }\r\n    });\r\n});\r\n\r\nfunction toggleSubMenu(subMenu) {\r\n    var subMenu = document.getElementById(\"filter-\" + subMenu);\r\n    if (subMenu.classList.contains(\"is-open\")) {\r\n        closeSubMenus();\r\n    } else {\r\n        closeSubMenus();\r\n        subMenu.classList.add(\"is-open\");\r\n    }\r\n}\r\n\r\ndocument.querySelectorAll('.filter-item').forEach(function(item) {\r\n  item.addEventListener('click', function(e) {\r\n    if (this.hasAttribute('data-categoryid')) {\r\n        const categoryId = this.getAttribute('data-categoryid');\r\n        const urlParams = new URLSearchParams(window.location.search);\r\n        let newUrl = 'stats.php?';\r\n\r\n        if (urlParams.get('category') === categoryId) {\r\n            urlParams.delete('category');\r\n        } else {\r\n            urlParams.set('category', categoryId);\r\n        }\r\n\r\n        newUrl += urlParams.toString();\r\n        window.location.href = newUrl;\r\n    } else if (this.hasAttribute('data-memberid')) {\r\n        const memberId = this.getAttribute('data-memberid');\r\n        const urlParams = new URLSearchParams(window.location.search);\r\n        let newUrl = 'stats.php?';\r\n\r\n        if (urlParams.get('member') === memberId) {\r\n            urlParams.delete('member');\r\n        } else {\r\n            urlParams.set('member', memberId);\r\n        }\r\n\r\n        newUrl += urlParams.toString();\r\n        window.location.href = newUrl;\r\n    } else if (this.hasAttribute('data-paymentid')) {\r\n        const paymentId = this.getAttribute('data-paymentid');\r\n        const urlParams = new URLSearchParams(window.location.search);\r\n        let newUrl = 'stats.php?';\r\n\r\n        if (urlParams.get('payment') === paymentId) {\r\n            urlParams.delete('payment');\r\n        } else {\r\n            urlParams.set('payment', paymentId);\r\n        }\r\n\r\n        newUrl += urlParams.toString();\r\n        window.location.href = newUrl;\r\n    }\r\n  });\r\n});\r\n\r\nfunction clearFilters() {\r\n    window.location.href = 'stats.php';\r\n}"
  },
  {
    "path": "scripts/subscriptions.js",
    "content": "let isSortOptionsOpen = false;\r\nlet scrollTopBeforeOpening = 0;\r\nconst shouldScroll = window.innerWidth <= 768;\r\n\r\nfunction toggleOpenSubscription(subId) {\r\n  const subscriptionElement = document.querySelector('.subscription[data-id=\"' + subId + '\"]');\r\n  subscriptionElement.classList.toggle('is-open');\r\n}\r\n\r\nfunction toggleSortOptions() {\r\n  const sortOptions = document.querySelector(\"#sort-options\");\r\n  sortOptions.classList.toggle(\"is-open\");\r\n  isSortOptionsOpen = !isSortOptionsOpen;\r\n}\r\n\r\nfunction toggleNotificationDays() {\r\n  const notifyCheckbox = document.querySelector(\"#notifications\");\r\n  const notifyDaysBefore = document.querySelector(\"#notify_days_before\");\r\n  notifyDaysBefore.disabled = !notifyCheckbox.checked;\r\n}\r\n\r\nfunction resetForm() {\r\n  const id = document.querySelector(\"#id\");\r\n  id.value = \"\";\r\n  const formTitle = document.querySelector(\"#form-title\");\r\n  formTitle.textContent = translate('add_subscription');\r\n  const logo = document.querySelector(\"#form-logo\");\r\n  logo.src = \"\";\r\n  logo.style = 'display: none';\r\n  const logoUrl = document.querySelector(\"#logo-url\");\r\n  logoUrl.value = \"\";\r\n  const logoSearchButton = document.querySelector(\"#logo-search-button\");\r\n  logoSearchButton.classList.add(\"disabled\");\r\n  const submitButton = document.querySelector(\"#save-button\");\r\n  submitButton.disabled = false;\r\n  const autoRenew = document.querySelector(\"#auto_renew\");\r\n  autoRenew.checked = true;\r\n  const startDate = document.querySelector(\"#start_date\");\r\n  startDate.value = new Date().toISOString().split('T')[0];\r\n  const notifyDaysBefore = document.querySelector(\"#notify_days_before\");\r\n  notifyDaysBefore.disabled = true;\r\n  const replacementSubscriptionIdSelect = document.querySelector(\"#replacement_subscription_id\");\r\n  replacementSubscriptionIdSelect.value = \"0\";\r\n  const replacementSubscription = document.querySelector(`#replacement_subscritpion`);\r\n  replacementSubscription.classList.add(\"hide\");\r\n  const form = document.querySelector(\"#subs-form\");\r\n  form.reset();\r\n  closeLogoSearch();\r\n  const deleteButton = document.querySelector(\"#deletesub\");\r\n  deleteButton.style = 'display: none';\r\n  deleteButton.removeAttribute(\"onClick\");\r\n}\r\n\r\nfunction fillEditFormFields(subscription) {\r\n  const formTitle = document.querySelector(\"#form-title\");\r\n  formTitle.textContent = translate('edit_subscription');\r\n  const logo = document.querySelector(\"#form-logo\");\r\n  const logoFile = subscription.logo !== null ? \"images/uploads/logos/\" + subscription.logo : \"\";\r\n  if (logoFile) {\r\n    logo.src = logoFile;\r\n    logo.style = 'display: block';\r\n  }\r\n  const logoSearchButton = document.querySelector(\"#logo-search-button\");\r\n  logoSearchButton.classList.remove(\"disabled\");\r\n  const id = document.querySelector(\"#id\");\r\n  id.value = subscription.id;\r\n  const name = document.querySelector(\"#name\");\r\n  name.value = subscription.name;\r\n  const price = document.querySelector(\"#price\");\r\n  price.value = subscription.price;\r\n\r\n  const currencySelect = document.querySelector(\"#currency\");\r\n  currencySelect.value = subscription.currency_id.toString();\r\n  const frequencySelect = document.querySelector(\"#frequency\");\r\n  frequencySelect.value = subscription.frequency;\r\n  const cycleSelect = document.querySelector(\"#cycle\");\r\n  cycleSelect.value = subscription.cycle;\r\n  const paymentSelect = document.querySelector(\"#payment_method\");\r\n  paymentSelect.value = subscription.payment_method_id;\r\n  const categorySelect = document.querySelector(\"#category\");\r\n  categorySelect.value = subscription.category_id;\r\n  const payerSelect = document.querySelector(\"#payer_user\");\r\n  payerSelect.value = subscription.payer_user_id;\r\n\r\n  const startDate = document.querySelector(\"#start_date\");\r\n  startDate.value = subscription.start_date;\r\n  const nextPament = document.querySelector(\"#next_payment\");\r\n  nextPament.value = subscription.next_payment;\r\n  const cancellationDate = document.querySelector(\"#cancellation_date\");\r\n  cancellationDate.value = subscription.cancellation_date;\r\n\r\n  const notes = document.querySelector(\"#notes\");\r\n  notes.value = subscription.notes;\r\n  const inactive = document.querySelector(\"#inactive\");\r\n  inactive.checked = subscription.inactive;\r\n  const url = document.querySelector(\"#url\");\r\n  url.value = subscription.url;\r\n\r\n  const autoRenew = document.querySelector(\"#auto_renew\");\r\n  if (autoRenew) {\r\n    autoRenew.checked = subscription.auto_renew;\r\n  }\r\n\r\n  const notifications = document.querySelector(\"#notifications\");\r\n  if (notifications) {\r\n    notifications.checked = subscription.notify;\r\n  }\r\n\r\n  const notifyDaysBefore = document.querySelector(\"#notify_days_before\");\r\n  notifyDaysBefore.value = subscription.notify_days_before ?? 0;\r\n  if (subscription.notify === 1) {\r\n    notifyDaysBefore.disabled = false;\r\n  }\r\n\r\n  const replacementSubscriptionIdSelect = document.querySelector(\"#replacement_subscription_id\");\r\n  replacementSubscriptionIdSelect.value = subscription.replacement_subscription_id ?? 0;\r\n\r\n  const replacementSubscription = document.querySelector(`#replacement_subscritpion`);\r\n  if (subscription.inactive) {\r\n    replacementSubscription.classList.remove(\"hide\");\r\n  } else {\r\n    replacementSubscription.classList.add(\"hide\");\r\n  }\r\n\r\n  const deleteButton = document.querySelector(\"#deletesub\");\r\n  deleteButton.style = 'display: block';\r\n  deleteButton.setAttribute(\"onClick\", `deleteSubscription(event, ${subscription.id})`);\r\n\r\n  const modal = document.getElementById('subscription-form');\r\n  modal.classList.add(\"is-open\");\r\n}\r\n\r\nfunction openEditSubscription(event, id) {\r\n  event.stopPropagation();\r\n  scrollTopBeforeOpening = window.scrollY;\r\n  const body = document.querySelector('body');\r\n  body.classList.add('no-scroll');\r\n  const url = `endpoints/subscription/get.php?id=${id}`;\r\n  fetch(url)\r\n    .then((response) => {\r\n      if (response.ok) {\r\n        return response.json();\r\n      } else {\r\n        showErrorMessage(translate('failed_to_load_subscription'));\r\n      }\r\n    })\r\n    .then((data) => {\r\n      if (data.error || data === \"Error\") {\r\n        showErrorMessage(translate('failed_to_load_subscription'));\r\n      } else {\r\n        const subscription = data;\r\n        fillEditFormFields(subscription);\r\n      }\r\n    })\r\n    .catch((error) => {\r\n      console.log(error);\r\n      showErrorMessage(translate('failed_to_load_subscription'));\r\n    });\r\n}\r\n\r\nfunction addSubscription() {\r\n  resetForm();\r\n  const modal = document.getElementById('subscription-form');\r\n  \r\n  const startDate = document.querySelector(\"#start_date\");\r\n  startDate.value = new Date().toISOString().split('T')[0];\r\n\r\n  modal.classList.add(\"is-open\");\r\n  const body = document.querySelector('body');\r\n  body.classList.add('no-scroll');\r\n}\r\n\r\nfunction closeAddSubscription() {\r\n  const modal = document.getElementById('subscription-form');\r\n  modal.classList.remove(\"is-open\");\r\n  const body = document.querySelector('body');\r\n  body.classList.remove('no-scroll');\r\n  if (shouldScroll) {\r\n    window.scrollTo(0, scrollTopBeforeOpening);\r\n  }\r\n  resetForm();\r\n}\r\n\r\nfunction handleFileSelect(event) {\r\n  const fileInput = event.target;\r\n  const logoPreview = document.querySelector('.logo-preview');\r\n  const logoImg = logoPreview.querySelector('img');\r\n  const logoUrl = document.querySelector(\"#logo-url\");\r\n  logoUrl.value = \"\";\r\n\r\n  if (fileInput.files && fileInput.files[0]) {\r\n    const reader = new FileReader();\r\n\r\n    reader.onload = function (e) {\r\n      logoImg.src = e.target.result;\r\n      logoImg.style.display = 'block';\r\n    };\r\n\r\n    reader.readAsDataURL(fileInput.files[0]);\r\n  }\r\n}\r\n\r\nfunction deleteSubscription(event, id) {\r\n  event.stopPropagation();\r\n  event.preventDefault();\r\n\r\n  if (!confirm(translate('confirm_delete_subscription'))) {\r\n    return;\r\n  }\r\n\r\n  fetch(\"endpoints/subscription/delete.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/json\",\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ id: id }),\r\n  })\r\n    .then((response) => response.json())\r\n    .then((data) => {\r\n      if (data.success) {\r\n        showSuccessMessage(translate('subscription_deleted'));\r\n        fetchSubscriptions(null, null, \"delete\");\r\n        closeAddSubscription();\r\n      } else {\r\n        showErrorMessage(data.message || translate('error_deleting_subscription'));\r\n      }\r\n    })\r\n    .catch((error) => {\r\n      console.error(\"Error:\", error);\r\n      showErrorMessage(translate('error_deleting_subscription'));\r\n    });\r\n}\r\n\r\n\r\nfunction cloneSubscription(event, id) {\r\n  event.stopPropagation();\r\n  event.preventDefault();\r\n\r\n  fetch(\"endpoints/subscription/clone.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/json\",\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ id: id }),\r\n  })\r\n    .then((response) => {\r\n      if (!response.ok) {\r\n        throw new Error(translate(\"network_response_error\"));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then((data) => {\r\n      if (data.success) {\r\n        const newId = data.id;\r\n        fetchSubscriptions(newId, event, \"clone\");\r\n        showSuccessMessage(decodeURI(data.message));\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"error\"));\r\n      }\r\n    })\r\n    .catch((error) => {\r\n      showErrorMessage(error.message || translate(\"error\"));\r\n    });\r\n}\r\n\r\n\r\nfunction renewSubscription(event, id) {\r\n  event.stopPropagation();\r\n  event.preventDefault();\r\n\r\n  fetch(\"endpoints/subscription/renew.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"Content-Type\": \"application/json\",\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ id: id }),\r\n  })\r\n    .then((response) => {\r\n      if (!response.ok) {\r\n        throw new Error(translate(\"network_response_error\"));\r\n      }\r\n      return response.json();\r\n    })\r\n    .then((data) => {\r\n      if (data.success) {\r\n        const newId = data.id;\r\n        fetchSubscriptions(newId, event, \"renew\");\r\n        showSuccessMessage(decodeURI(data.message));\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"error\"));\r\n      }\r\n    })\r\n    .catch((error) => {\r\n      showErrorMessage(error.message || translate(\"error\"));\r\n    });\r\n}\r\n\r\n\r\nfunction setSearchButtonStatus() {\r\n\r\n  const nameInput = document.querySelector(\"#name\");\r\n  const hasSearchTerm = nameInput.value.trim().length > 0;\r\n  const logoSearchButton = document.querySelector(\"#logo-search-button\");\r\n  if (hasSearchTerm) {\r\n    logoSearchButton.classList.remove(\"disabled\");\r\n  } else {\r\n    logoSearchButton.classList.add(\"disabled\");\r\n  }\r\n\r\n}\r\n\r\nfunction searchLogo() {\r\n  const nameInput = document.querySelector(\"#name\");\r\n  const searchTerm = nameInput.value.trim();\r\n  if (searchTerm !== \"\") {\r\n    const logoSearchPopup = document.querySelector(\"#logo-search-results\");\r\n    logoSearchPopup.classList.add(\"is-open\");\r\n    const imageSearchUrl = `endpoints/logos/search.php?search=${searchTerm}`;\r\n    fetch(imageSearchUrl)\r\n      .then(response => response.json())\r\n      .then(data => {\r\n        if (data.imageUrls) {\r\n          displayImageResults(data.imageUrls);\r\n        } else if (data.error) {\r\n          console.error(data.error);\r\n        }\r\n      })\r\n      .catch(error => {\r\n        console.error(translate('error_fetching_image_results'), error);\r\n      });\r\n  } else {\r\n    nameInput.focus();\r\n  }\r\n}\r\n\r\nfunction displayImageResults(imageSources) {\r\n  const logoResults = document.querySelector(\"#logo-search-images\");\r\n  logoResults.innerHTML = \"\";\r\n\r\n  imageSources.forEach(src => {\r\n    const img = document.createElement(\"img\");\r\n    img.src = src;\r\n    img.onclick = function () {\r\n      selectWebLogo(src);\r\n    };\r\n    img.onerror = function () {\r\n      this.parentNode.removeChild(this);\r\n    };\r\n    logoResults.appendChild(img);\r\n  });\r\n}\r\n\r\nfunction selectWebLogo(url) {\r\n  closeLogoSearch();\r\n  const logoPreview = document.querySelector(\"#form-logo\");\r\n  const logoUrl = document.querySelector(\"#logo-url\");\r\n  logoPreview.src = url;\r\n  logoPreview.style.display = 'block';\r\n  logoUrl.value = url;\r\n}\r\n\r\nfunction closeLogoSearch() {\r\n  const logoSearchPopup = document.querySelector(\"#logo-search-results\");\r\n  logoSearchPopup.classList.remove(\"is-open\");\r\n  const logoResults = document.querySelector(\"#logo-search-images\");\r\n  logoResults.innerHTML = \"\";\r\n}\r\n\r\nfunction fetchSubscriptions(id, event, initiator) {\r\n  const subscriptionsContainer = document.querySelector(\"#subscriptions\");\r\n  let getSubscriptions = \"endpoints/subscriptions/get.php\";\r\n\r\n  if (activeFilters['categories'].length > 0) {\r\n    getSubscriptions += `?categories=${activeFilters['categories']}`;\r\n  }\r\n  if (activeFilters['members'].length > 0) {\r\n    getSubscriptions += getSubscriptions.includes(\"?\") ? `&members=${activeFilters['members']}` : `?members=${activeFilters['members']}`;\r\n  }\r\n  if (activeFilters['payments'].length > 0) {\r\n    getSubscriptions += getSubscriptions.includes(\"?\") ? `&payments=${activeFilters['payments']}` : `?payments=${activeFilters['payments']}`;\r\n  }\r\n  if (activeFilters['state'] !== \"\") {\r\n    getSubscriptions += getSubscriptions.includes(\"?\") ? `&state=${activeFilters['state']}` : `?state=${activeFilters['state']}`;\r\n  }\r\n  if (activeFilters['renewalType'] !== \"\") {\r\n    getSubscriptions += getSubscriptions.includes(\"?\") ? `&renewalType=${activeFilters['renewalType']}` : `?renewalType=${activeFilters['renewalType']}`;\r\n  }\r\n\r\n  fetch(getSubscriptions)\r\n    .then(response => response.text())\r\n    .then(data => {\r\n      if (data) {\r\n        subscriptionsContainer.innerHTML = data;\r\n        const mainActions = document.querySelector(\"#main-actions\");\r\n        if (data.includes(\"no-matching-subscriptions\")) {\r\n          // mainActions.classList.add(\"hidden\");\r\n        } else {\r\n          mainActions.classList.remove(\"hidden\");\r\n        }\r\n      }\r\n\r\n      if (initiator == \"clone\" && id && event) {\r\n        openEditSubscription(event, id);\r\n      }\r\n\r\n      setSwipeElements();\r\n      if (initiator === \"add\") {\r\n        if (document.getElementsByClassName('subscription').length === 1) {\r\n          setTimeout(() => {\r\n            swipeHintAnimation();\r\n          }, 1000);\r\n        }\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(translate('error_reloading_subscription'), error);\r\n    });\r\n}\r\n\r\nfunction setSortOption(sortOption) {\r\n  const sortOptionsContainer = document.querySelector(\"#sort-options\");\r\n  const sortOptionsList = sortOptionsContainer.querySelectorAll(\"li\");\r\n  sortOptionsList.forEach((option) => {\r\n    if (option.getAttribute(\"id\") === \"sort-\" + sortOption) {\r\n      option.classList.add(\"selected\");\r\n    } else {\r\n      option.classList.remove(\"selected\");\r\n    }\r\n  });\r\n  const daysToExpire = 30;\r\n  const expirationDate = new Date();\r\n  expirationDate.setDate(expirationDate.getDate() + daysToExpire);\r\n  const cookieValue = encodeURIComponent(sortOption) + '; expires=' + expirationDate.toUTCString();\r\n  document.cookie = 'sortOrder=' + cookieValue + '; SameSite=Strict';\r\n  fetchSubscriptions(null, null, \"sort\");\r\n  toggleSortOptions();\r\n}\r\n\r\nfunction convertSvgToPng(file, callback) {\r\n  const reader = new FileReader();\r\n\r\n  reader.onload = function (e) {\r\n    const img = new Image();\r\n    img.src = e.target.result;\r\n    img.onload = function () {\r\n      const canvas = document.createElement('canvas');\r\n      canvas.width = img.width;\r\n      canvas.height = img.height;\r\n      const ctx = canvas.getContext('2d');\r\n      ctx.drawImage(img, 0, 0);\r\n      const pngDataUrl = canvas.toDataURL('image/png');\r\n      const pngFile = dataURLtoFile(pngDataUrl, file.name.replace(\".svg\", \".png\"));\r\n      callback(pngFile);\r\n    };\r\n  };\r\n\r\n  reader.readAsDataURL(file);\r\n}\r\n\r\nfunction dataURLtoFile(dataurl, filename) {\r\n  let arr = dataurl.split(','),\r\n    mime = arr[0].match(/:(.*?);/)[1],\r\n    bstr = atob(arr[1]),\r\n    n = bstr.length,\r\n    u8arr = new Uint8Array(n);\r\n\r\n  while (n--) {\r\n    u8arr[n] = bstr.charCodeAt(n);\r\n  }\r\n\r\n  return new File([u8arr], filename, { type: mime });\r\n}\r\n\r\nfunction submitFormData(formData, submitButton, endpoint) {\r\n  fetch(endpoint, {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: formData,\r\n  })\r\n    .then((response) => response.json())\r\n    .then((data) => {\r\n      if (data.status === \"Success\") {\r\n        showSuccessMessage(data.message);\r\n        fetchSubscriptions(null, null, \"add\");\r\n        closeAddSubscription();\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"unknown_error\"));\r\n      }\r\n    })\r\n    .catch((error) => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    })\r\n    .finally(() => {\r\n      submitButton.disabled = false;\r\n    });\r\n}\r\n\r\ndocument.addEventListener('DOMContentLoaded', function () {\r\n  const subscriptionForm = document.querySelector(\"#subs-form\");\r\n  const submitButton = document.querySelector(\"#save-button\");\r\n  const endpoint = \"endpoints/subscription/add.php\";\r\n\r\n  subscriptionForm.addEventListener(\"submit\", function (e) {\r\n    e.preventDefault();\r\n\r\n    submitButton.disabled = true;\r\n    const formData = new FormData(subscriptionForm);\r\n\r\n    const fileInput = document.querySelector(\"#logo\");\r\n    const file = fileInput.files[0];\r\n\r\n    if (file && file.type === \"image/svg+xml\") {\r\n      convertSvgToPng(file, function (pngFile) {\r\n        formData.set(\"logo\", pngFile);\r\n        submitFormData(formData, submitButton, endpoint);\r\n      });\r\n    } else {\r\n      submitFormData(formData, submitButton, endpoint);\r\n    }\r\n  });\r\n\r\n  document.addEventListener('mousedown', function (event) {\r\n    const sortOptions = document.querySelector('#sort-options');\r\n    const sortButton = document.querySelector(\"#sort-button\");\r\n\r\n    if (!sortOptions.contains(event.target) && !sortButton.contains(event.target) && isSortOptionsOpen) {\r\n      sortOptions.classList.remove('is-open');\r\n      isSortOptionsOpen = false;\r\n    }\r\n  });\r\n\r\n  document.querySelector('#sort-options').addEventListener('focus', function () {\r\n    isSortOptionsOpen = true;\r\n  });\r\n});\r\n\r\nfunction searchSubscriptions() {\r\n  const searchInput = document.querySelector(\"#search\");\r\n  const searchContainer = searchInput.parentElement;\r\n  const searchTerm = searchInput.value.trim().toLowerCase();\r\n\r\n  if (searchTerm.length > 0) {\r\n    searchContainer.classList.add(\"has-text\");\r\n  } else {\r\n    searchContainer.classList.remove(\"has-text\");\r\n  }\r\n\r\n  const subscriptions = document.querySelectorAll(\".subscription\");\r\n  subscriptions.forEach(subscription => {\r\n    const name = subscription.getAttribute('data-name').toLowerCase();\r\n    if (!name.includes(searchTerm)) {\r\n      subscription.parentElement.classList.add(\"hide\");\r\n    } else {\r\n      subscription.parentElement.classList.remove(\"hide\");\r\n    }\r\n  });\r\n}\r\n\r\nfunction clearSearch() {\r\n  const searchInput = document.querySelector(\"#search\");\r\n\r\n  searchInput.value = \"\";\r\n  searchSubscriptions();\r\n}\r\n\r\nfunction closeSubMenus() {\r\n  var subMenus = document.querySelectorAll('.filtermenu-submenu-content');\r\n  subMenus.forEach(subMenu => {\r\n    subMenu.classList.remove('is-open');\r\n  });\r\n\r\n}\r\n\r\nfunction setSwipeElements() {\r\n  if (window.mobileNavigation) {\r\n    const swipeElements = document.querySelectorAll('.subscription');\r\n\r\n    swipeElements.forEach((element) => {\r\n      let startX = 0;\r\n      let startY = 0;\r\n      let currentX = 0;\r\n      let currentY = 0;\r\n      let translateX = 0;\r\n      const maxTranslateX = element.classList.contains('manual') ? -240 : -180;\r\n\r\n      element.addEventListener('touchstart', (e) => {\r\n        startX = e.touches[0].clientX;\r\n        startY = e.touches[0].clientY;\r\n        element.style.transition = ''; // Remove transition for smooth dragging\r\n      });\r\n\r\n      element.addEventListener('touchmove', (e) => {\r\n        currentX = e.touches[0].clientX;\r\n        currentY = e.touches[0].clientY;\r\n\r\n        const diffX = currentX - startX;\r\n        const diffY = currentY - startY;\r\n\r\n        // Check if the swipe is more horizontal than vertical\r\n        if (Math.abs(diffX) > Math.abs(diffY)) {\r\n          e.preventDefault(); // Prevent vertical scrolling\r\n\r\n          // Only update translateX if swiping within allowed range\r\n          if (!(translateX === maxTranslateX && diffX < 0)) {\r\n            translateX = Math.min(0, Math.max(maxTranslateX, diffX)); // Clamp translateX between -180 and 0\r\n            element.style.transform = `translateX(${translateX}px)`;\r\n          }\r\n        }\r\n      });\r\n\r\n      element.addEventListener('touchend', () => {\r\n        // Check the final swipe position to determine snap behavior\r\n        if (translateX < maxTranslateX / 2) {\r\n          // If more than halfway to the left, snap fully open\r\n          translateX = maxTranslateX;\r\n        } else {\r\n          // If swiped less than halfway left or swiped right, snap back to closed\r\n          translateX = 0;\r\n        }\r\n        element.style.transition = 'transform 0.2s ease'; // Smooth snap effect\r\n        element.style.transform = `translateX(${translateX}px)`;\r\n        element.style.zIndex = '1';\r\n      });\r\n    });\r\n\r\n  }\r\n}\r\n\r\nconst activeFilters = [];\r\nactiveFilters['categories'] = [];\r\nactiveFilters['members'] = [];\r\nactiveFilters['payments'] = [];\r\nactiveFilters['state'] = \"\";\r\nactiveFilters['renewalType'] = \"\";\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", function () {\r\n  var filtermenu = document.querySelector('#filtermenu-button');\r\n  filtermenu.addEventListener('click', function () {\r\n    this.parentElement.querySelector('.filtermenu-content').classList.toggle('is-open');\r\n    closeSubMenus();\r\n  });\r\n\r\n  document.addEventListener('click', function (e) {\r\n    var filtermenuContent = document.querySelector('.filtermenu-content');\r\n    if (filtermenuContent.classList.contains('is-open')) {\r\n      var subMenus = document.querySelectorAll('.filtermenu-submenu');\r\n      var clickedInsideSubmenu = Array.from(subMenus).some(subMenu => subMenu.contains(e.target) || subMenu === e.target);\r\n\r\n      if (!filtermenu.contains(e.target) && !clickedInsideSubmenu) {\r\n        closeSubMenus();\r\n        filtermenuContent.classList.remove('is-open');\r\n      }\r\n    }\r\n  });\r\n\r\n  setSwipeElements();\r\n\r\n});\r\n\r\nfunction toggleSubMenu(subMenu) {\r\n  var subMenu = document.getElementById(\"filter-\" + subMenu);\r\n  if (subMenu.classList.contains(\"is-open\")) {\r\n    closeSubMenus();\r\n  } else {\r\n    closeSubMenus();\r\n    subMenu.classList.add(\"is-open\");\r\n  }\r\n}\r\n\r\nfunction toggleReplacementSub() {\r\n  const checkbox = document.getElementById('inactive');\r\n  const replacementSubscription = document.querySelector(`#replacement_subscritpion`);\r\n\r\n  if (checkbox.checked) {\r\n    replacementSubscription.classList.remove(\"hide\");\r\n  } else {\r\n    replacementSubscription.classList.add(\"hide\");\r\n  }\r\n}\r\n\r\ndocument.querySelectorAll('.filter-item').forEach(function (item) {\r\n  item.addEventListener('click', function (e) {\r\n    const searchInput = document.querySelector(\"#search\");\r\n    searchInput.value = \"\";\r\n\r\n    if (this.hasAttribute('data-categoryid')) {\r\n      const categoryId = this.getAttribute('data-categoryid');\r\n      if (activeFilters['categories'].includes(categoryId)) {\r\n        const categoryIndex = activeFilters['categories'].indexOf(categoryId);\r\n        activeFilters['categories'].splice(categoryIndex, 1);\r\n        this.classList.remove('selected');\r\n      } else {\r\n        activeFilters['categories'].push(categoryId);\r\n        this.classList.add('selected');\r\n      }\r\n    } else if (this.hasAttribute('data-memberid')) {\r\n      const memberId = this.getAttribute('data-memberid');\r\n      if (activeFilters['members'].includes(memberId)) {\r\n        const memberIndex = activeFilters['members'].indexOf(memberId);\r\n        activeFilters['members'].splice(memberIndex, 1);\r\n        this.classList.remove('selected');\r\n      } else {\r\n        activeFilters['members'].push(memberId);\r\n        this.classList.add('selected');\r\n      }\r\n    } else if (this.hasAttribute('data-paymentid')) {\r\n      const paymentId = this.getAttribute('data-paymentid');\r\n      if (activeFilters['payments'].includes(paymentId)) {\r\n        const paymentIndex = activeFilters['payments'].indexOf(paymentId);\r\n        activeFilters['payments'].splice(paymentIndex, 1);\r\n        this.classList.remove('selected');\r\n      } else {\r\n        activeFilters['payments'].push(paymentId);\r\n        this.classList.add('selected');\r\n      }\r\n    } else if (this.hasAttribute('data-state')) {\r\n      const state = this.getAttribute('data-state');\r\n      if (activeFilters['state'] === state) {\r\n        activeFilters['state'] = \"\";\r\n        this.classList.remove('selected');\r\n      } else {\r\n        activeFilters['state'] = state;\r\n        Array.from(this.parentNode.children).forEach(sibling => {\r\n          sibling.classList.remove('selected');\r\n        });\r\n        this.classList.add('selected');\r\n      }\r\n    } else if (this.hasAttribute('data-renewaltype')) {\r\n      const renewalType = this.getAttribute('data-renewaltype');\r\n      if (activeFilters['renewalType'] === renewalType) {\r\n        activeFilters['renewalType'] = \"\";\r\n        this.classList.remove('selected');\r\n      } else {\r\n        activeFilters['renewalType'] = renewalType;\r\n        Array.from(this.parentNode.children).forEach(sibling => {\r\n          sibling.classList.remove('selected');\r\n        });\r\n        this.classList.add('selected');\r\n      }\r\n    }\r\n\r\n    if (activeFilters['categories'].length > 0 || activeFilters['members'].length > 0 ||\r\n       activeFilters['payments'].length > 0 || activeFilters['state'] !== \"\" || \r\n       activeFilters['renewalType'] !== \"\") {\r\n      document.querySelector('#clear-filters').classList.remove('hide');\r\n    } else {\r\n      document.querySelector('#clear-filters').classList.add('hide');\r\n    }\r\n\r\n    fetchSubscriptions(null, null, \"filter\");\r\n  });\r\n});\r\n\r\nfunction clearFilters() {\r\n  const searchInput = document.querySelector(\"#search\");\r\n  searchInput.value = \"\";\r\n  activeFilters['categories'] = [];\r\n  activeFilters['members'] = [];\r\n  activeFilters['payments'] = [];\r\n  activeFilters['state'] = \"\";\r\n  activeFilters['renewalType'] = \"\";\r\n  \r\n  document.querySelectorAll('.filter-item').forEach(function (item) {\r\n    item.classList.remove('selected');\r\n  });\r\n  document.querySelector('#clear-filters').classList.add('hide');\r\n  fetchSubscriptions(null, null, \"clearfilters\");\r\n}\r\n\r\nlet currentActions = null;\r\n\r\ndocument.addEventListener('click', function (event) {\r\n  // Check if click was outside currentActions\r\n  if (currentActions && !currentActions.contains(event.target)) {\r\n    // Click was outside currentActions, close currentActions\r\n    currentActions.classList.remove('is-open');\r\n    currentActions = null;\r\n  }\r\n});\r\n\r\nfunction expandActions(event, subscriptionId) {\r\n  event.stopPropagation();\r\n  event.preventDefault();\r\n  const subscriptionDiv = document.querySelector(`.subscription[data-id=\"${subscriptionId}\"]`);\r\n  const actions = subscriptionDiv.querySelector('.actions');\r\n\r\n  // Close all other open actions\r\n  const allActions = document.querySelectorAll('.actions.is-open');\r\n  allActions.forEach((openAction) => {\r\n    if (openAction !== actions) {\r\n      openAction.classList.remove('is-open');\r\n    }\r\n  });\r\n\r\n  // Toggle the clicked actions\r\n  actions.classList.toggle('is-open');\r\n\r\n  // Update currentActions\r\n  if (actions.classList.contains('is-open')) {\r\n    currentActions = actions;\r\n  } else {\r\n    currentActions = null;\r\n  }\r\n}\r\n\r\nfunction swipeHintAnimation() {\r\n  if (window.mobileNavigation && window.matchMedia('(max-width: 768px)').matches) {\r\n    const maxAnimations = 3;\r\n    const cookieName = 'swipeHintCount';\r\n\r\n    let count = parseInt(getCookie(cookieName)) || 0;\r\n    if (count < maxAnimations) {\r\n      const firstElement = document.querySelector('.subscription');\r\n      if (firstElement) {\r\n        firstElement.style.transition = 'transform 0.3s ease';\r\n        firstElement.style.transform = 'translateX(-80px)';\r\n\r\n        setTimeout(() => {\r\n          firstElement.style.transform = 'translateX(0px)';\r\n          firstElement.style.zIndex = '1';\r\n        }, 600);\r\n      }\r\n\r\n      count++;\r\n      document.cookie = `${cookieName}=${count}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/; SameSite=Strict`;\r\n    }\r\n  }\r\n}\r\n\r\nfunction autoFillNextPaymentDate(e) {\r\n  e.preventDefault();\r\n  const frequencySelect = document.querySelector(\"#frequency\");\r\n  const cycleSelect = document.querySelector(\"#cycle\"); \r\n  const startDate = document.querySelector(\"#start_date\");\r\n  const nextPayment = document.querySelector(\"#next_payment\"); \r\n\r\n  // Do nothing if frequency, cycle, or start date is not set\r\n  if (!frequencySelect.value || !cycleSelect.value || !startDate.value || isNaN(Date.parse(startDate.value))) {\r\n    console.log(frequencySelect.value, cycleSelect.value, startDate.value);\r\n    return;\r\n  }\r\n  \r\n  const today = new Date();  \r\n  const cycle = cycleSelect.value;\r\n  const frequency = Number(frequencySelect.value);\r\n\r\n  const nextDate = new Date(startDate.value);\r\n  let safetyCounter = 0;\r\n  const maxIterations = 1000;\r\n\r\n  while (nextDate <= today && safetyCounter < maxIterations) {\r\n    switch (cycle) {\r\n    case '1': // Days\r\n      nextDate.setDate(nextDate.getDate() + frequency);\r\n      break;\r\n    case '2': // Weeks\r\n      nextDate.setDate(nextDate.getDate() + 7 * frequency);\r\n      break;\r\n    case '3': // Months  \r\n      nextDate.setMonth(nextDate.getMonth() + frequency);\r\n      break;\r\n    case '4': // Years\r\n      nextDate.setFullYear(nextDate.getFullYear() + frequency);\r\n      break;\r\n    default:\r\n    }\r\n    safetyCounter++;\r\n  }\r\n\r\nif (safetyCounter === maxIterations) {\r\n  return;\r\n}\r\n\r\nnextPayment.value = toISOStringWithTimezone(nextDate).substring(0, 10);\r\n}\r\n\r\nfunction toISOStringWithTimezone(date) {\r\n  const pad = n => String(Math.floor(Math.abs(n))).padStart(2, '0');\r\n  const tzOffset = -date.getTimezoneOffset();\r\n  const sign = tzOffset >= 0 ? '+' : '-';\r\n  const hoursOffset = pad(tzOffset / 60);\r\n  const minutesOffset = pad(tzOffset % 60);\r\n\r\n  return date.getFullYear() +\r\n    '-' + pad(date.getMonth() + 1) +\r\n    '-' + pad(date.getDate()) +\r\n    'T' + pad(date.getHours()) +\r\n    ':' + pad(date.getMinutes()) +\r\n    ':' + pad(date.getSeconds()) +\r\n    sign + hoursOffset +\r\n    ':' + minutesOffset;\r\n}\r\n\r\nwindow.addEventListener('load', () => {\r\n  if (document.querySelector('.subscription')) {\r\n    swipeHintAnimation();\r\n  }\r\n});\r\n"
  },
  {
    "path": "scripts/theme.js",
    "content": "function switchTheme() {\r\n  const darkThemeCss = document.querySelector(\"#dark-theme\");\r\n  darkThemeCss.disabled = !darkThemeCss.disabled;\r\n\r\n  const themeChoice = darkThemeCss.disabled ? 'light' : 'dark';\r\n  document.cookie = 'theme=' + themeValue + '; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict';\r\n\r\n  document.body.className = themeChoice;\r\n\r\n  const button = document.getElementById(\"switchTheme\");\r\n  button.disabled = true;\r\n\r\n  fetch('endpoints/settings/theme.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ theme: themeChoice === 'dark' })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n      button.disabled = false;\r\n    }).catch(error => {\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\nfunction setDarkTheme(theme) {\r\n  const darkThemeButton = document.querySelector(\"#theme-dark\");\r\n  const lightThemeButton = document.querySelector(\"#theme-light\");\r\n  const automaticThemeButton = document.querySelector(\"#theme-automatic\");\r\n  const darkThemeCss = document.querySelector(\"#dark-theme\");\r\n  const themes = { 0: 'light', 1: 'dark', 2: 'automatic' };\r\n  const themeValue = themes[theme];\r\n  const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\r\n\r\n  darkThemeButton.disabled = true;\r\n  lightThemeButton.disabled = true;\r\n  automaticThemeButton.disabled = true;\r\n\r\n  fetch('endpoints/settings/theme.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ theme: theme })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        darkThemeButton.disabled = false;\r\n        lightThemeButton.disabled = false;\r\n        automaticThemeButton.disabled = false;\r\n        darkThemeButton.classList.remove('selected');\r\n        lightThemeButton.classList.remove('selected');\r\n        automaticThemeButton.classList.remove('selected');\r\n\r\n        document.cookie = `theme=${themeValue}; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict`;\r\n\r\n        if (theme == 0) {\r\n          darkThemeCss.disabled = true;\r\n          document.body.className = 'light';\r\n          lightThemeButton.classList.add('selected');\r\n        }\r\n\r\n        if (theme == 1) {\r\n          darkThemeCss.disabled = false;\r\n          document.body.className = 'dark';\r\n          darkThemeButton.classList.add('selected');\r\n        }\r\n\r\n        if (theme == 2) {\r\n          darkThemeCss.disabled = !prefersDarkMode;\r\n          document.body.className = prefersDarkMode ? 'dark' : 'light';\r\n          automaticThemeButton.classList.add('selected');\r\n          document.cookie = `inUseTheme=${prefersDarkMode ? 'dark' : 'light'}; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict`;\r\n        }\r\n\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n        darkThemeButton.disabled = false;\r\n        lightThemeButton.disabled = false;\r\n        automaticThemeButton.disabled = false;\r\n      }\r\n    }).catch(error => {\r\n      darkThemeButton.disabled = false;\r\n      lightThemeButton.disabled = false;\r\n      automaticThemeButton.disabled = false;\r\n    });\r\n}\r\n\r\nfunction setTheme(themeColor) {\r\n  var currentTheme = 'blue';\r\n  var themeIds = ['red-theme', 'green-theme', 'yellow-theme', 'purple-theme'];\r\n\r\n  themeIds.forEach(function (id) {\r\n    var themeStylesheet = document.getElementById(id);\r\n    if (themeStylesheet && !themeStylesheet.disabled) {\r\n      currentTheme = id.replace('-theme', '');\r\n      themeStylesheet.disabled = true;\r\n    }\r\n  });\r\n\r\n  if (themeColor !== \"blue\") {\r\n    var enableTheme = document.getElementById(themeColor + '-theme');\r\n    enableTheme.disabled = false;\r\n  }\r\n\r\n  var images = document.querySelectorAll('img');\r\n  images.forEach(function (img) {\r\n    if (img.src.includes('siteicons/' + currentTheme)) {\r\n      img.src = img.src.replace(currentTheme, themeColor);\r\n    }\r\n  });\r\n\r\n  var labels = document.querySelectorAll('.theme-preview');\r\n  labels.forEach(function (label) {\r\n    label.classList.remove('is-selected');\r\n  });\r\n\r\n  var targetLabel = document.querySelector(`.theme-preview.${themeColor}`);\r\n  if (targetLabel) {\r\n    targetLabel.classList.add('is-selected');\r\n  }\r\n\r\n  document.cookie = `colorTheme=${themeColor}; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict`;\r\n\r\n  fetch('endpoints/settings/colortheme.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ color: themeColor })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(translate('unknown_error'));\r\n    });\r\n\r\n}\r\n\r\nfunction resetCustomColors() {\r\n  const button = document.getElementById(\"reset-colors\");\r\n  button.disabled = true;\r\n\r\n  fetch(\"endpoints/settings/resettheme.php\", {\r\n    method: \"POST\",\r\n    headers: {\r\n      \"X-CSRF-Token\": window.csrfToken,\r\n    },\r\n    body: new URLSearchParams({\r\n      action: \"reset\",\r\n    }),\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n\r\n        const customThemeColors = document.getElementById(\"custom_theme_colors\");\r\n        if (customThemeColors) {\r\n          customThemeColors.remove();\r\n        }\r\n\r\n        document.documentElement.style.removeProperty(\"--main-color\");\r\n        document.documentElement.style.removeProperty(\"--accent-color\");\r\n        document.documentElement.style.removeProperty(\"--hover-color\");\r\n\r\n        document.getElementById(\"mainColor\").value = \"#FFFFFF\";\r\n        document.getElementById(\"accentColor\").value = \"#FFFFFF\";\r\n        document.getElementById(\"hoverColor\").value = \"#FFFFFF\";\r\n      } else {\r\n        showErrorMessage(data.message || translate(\"failed_reset_colors\"));\r\n      }\r\n    })\r\n    .catch(error => {\r\n      console.error(error);\r\n      showErrorMessage(translate(\"unknown_error\"));\r\n    })\r\n    .finally(() => {\r\n      button.disabled = false;\r\n    });\r\n}\r\n\r\n\r\nfunction saveCustomColors() {\r\n  const button = document.getElementById(\"save-colors\");\r\n  button.disabled = true;\r\n\r\n  const mainColor = document.getElementById(\"mainColor\").value;\r\n  const accentColor = document.getElementById(\"accentColor\").value;\r\n  const hoverColor = document.getElementById(\"hoverColor\").value;\r\n\r\n  fetch('endpoints/settings/customtheme.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ mainColor: mainColor, accentColor: accentColor, hoverColor: hoverColor })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n        document.documentElement.style.setProperty('--main-color', mainColor);\r\n        document.documentElement.style.setProperty('--accent-color', accentColor);\r\n        document.documentElement.style.setProperty('--hover-color', hoverColor);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n      button.disabled = false;\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(translate('unknown_error'));\r\n      button.disabled = false;\r\n    });\r\n\r\n}\r\n\r\nfunction saveCustomCss() {\r\n  const button = document.getElementById(\"save-css\");\r\n  button.disabled = true;\r\n\r\n  const customCss = document.getElementById(\"customCss\").value;\r\n\r\n  fetch('endpoints/settings/customcss.php', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n      'X-CSRF-Token': window.csrfToken,\r\n    },\r\n    body: JSON.stringify({ customCss: customCss })\r\n  })\r\n    .then(response => response.json())\r\n    .then(data => {\r\n      if (data.success) {\r\n        showSuccessMessage(data.message);\r\n      } else {\r\n        showErrorMessage(data.message);\r\n      }\r\n      button.disabled = false;\r\n    })\r\n    .catch(error => {\r\n      showErrorMessage(translate('unknown_error'));\r\n      button.disabled = false;\r\n    });\r\n}"
  },
  {
    "path": "service-worker.js",
    "content": "const STATIC_CACHE = 'static-cache-v1';\r\nconst PAGES_CACHE = 'pages-cache-v1';\r\nconst LOGOS_CACHE = 'logos-cache-v1';\r\n\r\nconst staticAssets = [\r\n    'manifest.json',\r\n    'styles/styles.css',\r\n    'styles/dark-theme.css',\r\n    'styles/login.css',\r\n    'styles/font-awesome.min.css',\r\n    'styles/brands.css',\r\n    'styles/barlow.css',\r\n    'styles/themes/red.css',\r\n    'styles/themes/green.css',\r\n    'styles/themes/yellow.css',\r\n    'styles/themes/purple.css',\r\n    'webfonts/fa-solid-900.woff2',\r\n    'webfonts/fa-solid-900.ttf',\r\n    'webfonts/fa-brands-400.woff2',\r\n    'webfonts/fa-brands-400.ttf',\r\n    'webfonts/fa-regular-400.woff2',\r\n    'webfonts/fa-regular-400.ttf',\r\n    'scripts/common.js',\r\n    'scripts/dashboard.js',\r\n    'scripts/subscriptions.js',\r\n    'scripts/stats.js',\r\n    'scripts/settings.js',\r\n    'scripts/theme.js',\r\n    'scripts/notifications.js',\r\n    'scripts/registration.js',\r\n    'scripts/login.js',\r\n    'scripts/admin.js',\r\n    'scripts/calendar.js',\r\n    'scripts/i18n/cs.js',\r\n    'scripts/i18n/da.js',\r\n    'scripts/i18n/de.js',\r\n    'scripts/i18n/el.js',\r\n    'scripts/i18n/en.js',\r\n    'scripts/i18n/es.js',\r\n    'scripts/i18n/fr.js',\r\n    'scripts/i18n/id.js',\r\n    'scripts/i18n/it.js',\r\n    'scripts/i18n/jp.js',\r\n    'scripts/i18n/ko.js',\r\n    'scripts/i18n/nl.js',\r\n    'scripts/i18n/pl.js',\r\n    'scripts/i18n/pt.js',\r\n    'scripts/i18n/pt_br.js',\r\n    'scripts/i18n/ro.js',\r\n    'scripts/i18n/ru.js',\r\n    'scripts/i18n/sl.js',\r\n    'scripts/i18n/sr_lat.js',\r\n    'scripts/i18n/sr.js',\r\n    'scripts/i18n/tr.js',\r\n    'scripts/i18n/uk.js',\r\n    'scripts/i18n/vi.js',\r\n    'scripts/i18n/zh_cn.js',\r\n    'scripts/i18n/zh_tw.js',\r\n    'scripts/i18n/getlang.js',\r\n    'scripts/libs/chart.js',\r\n    'scripts/libs/sortable.min.js',\r\n    'scripts/libs/qrcode.min.js',\r\n    'images/icon/favicon.ico',\r\n    'images/icon/android-chrome-192x192.png',\r\n    'images/icon/apple-touch-icon-180',\r\n    'images/icon/apple-touch-icon-152',\r\n    'images/icon/apple-touch-icon',\r\n    'images/screenshots/desktop.png',\r\n    'images/siteicons/wallos.png',\r\n    'images/siteicons/walloswhite.png',\r\n    'images/siteimages/empty.png',\r\n    'images/siteimages/mobilenav.png',\r\n    'images/siteimages/mobilenavdark.png',\r\n    'images/avatars/1.svg',\r\n    'images/avatars/2.svg',\r\n    'images/avatars/3.svg',\r\n    'images/avatars/4.svg',\r\n    'images/avatars/5.svg',\r\n    'images/avatars/6.svg',\r\n    'images/avatars/7.svg',\r\n    'images/avatars/8.svg',\r\n    'images/avatars/9.svg',\r\n    'images/siteicons/svg/logo.php',\r\n    'images/siteicons/svg/category.php',\r\n    'images/siteicons/svg/check.php',\r\n    'images/siteicons/svg/delete.php',\r\n    'images/siteicons/svg/edit.php',\r\n    'images/siteicons/svg/notes.php',\r\n    'images/siteicons/scg/payment.php',\r\n    'images/siteicons/svg/save.php',\r\n    'images/siteicons/svg/subscription.php',\r\n    'images/siteicons/svg/web.php',\r\n    'images/siteicons/svg/websearch.php',\r\n    'images/siteicons/svg/clone.php',\r\n    'images/siteicons/svg/mobile-menu/calendar.php',\r\n    'images/siteicons/svg/mobile-menu/home.php',\r\n    'images/siteicons/svg/mobile-menu/profile.php',\r\n    'images/siteicons/svg/mobile-menu/settings.php',\r\n    'images/siteicons/svg/mobile-menu/statistics.php',\r\n    'images/siteicons/svg/mobile-menu/subscriptions.php',\r\n    'images/siteicons/pwa/stats.png',\r\n    'images/siteicons/pwa/settings.png',\r\n    'images/siteicons/pwa/about.png',\r\n    'images/siteicons/pwa/calendar.png',\r\n    'images/siteicons/pwa/subscriptions.png',\r\n    'images/siteicons/pwa/dashboard.png',\r\n    'images/uploads/icons/paypal.png',\r\n    'images/uploads/icons/creditcard.png',\r\n    'images/uploads/icons/banktransfer.png',\r\n    'images/uploads/icons/directdebit.png',\r\n    'images/uploads/icons/money.png',\r\n    'images/uploads/icons/googlepay.png',\r\n    'images/uploads/icons/samsungpay.png',\r\n    'images/uploads/icons/applepay.png',\r\n    'images/uploads/icons/crypto.png',\r\n    'images/uploads/icons/klarna.png',\r\n    'images/uploads/icons/amazonpay.png',\r\n    'images/uploads/icons/sepa.png',\r\n    'images/uploads/icons/skrill.png',\r\n    'images/uploads/icons/sofort.png',\r\n    'images/uploads/icons/stripe.png',\r\n    'images/uploads/icons/affirm.png',\r\n    'images/uploads/icons/alipay.png',\r\n    'images/uploads/icons/elo.png',\r\n    'images/uploads/icons/facebookpay.png',\r\n    'images/uploads/icons/giropay.png',\r\n    'images/uploads/icons/ideal.png',\r\n    'images/uploads/icons/unionpay.png',\r\n    'images/uploads/icons/interac.png',\r\n    'images/uploads/icons/wechat.png',\r\n    'images/uploads/icons/paysafe.png',\r\n    'images/uploads/icons/poli.png',\r\n    'images/uploads/icons/qiwi.png',\r\n    'images/uploads/icons/shoppay.png',\r\n    'images/uploads/icons/venmo.png',\r\n    'images/uploads/icons/verifone.png',\r\n    'images/uploads/icons/webmoney.png',\r\n];\r\n\r\nconst pagesToPrefetch = [\r\n    'index.php',\r\n    'subscriptions.php',\r\n    'profile.php',\r\n    'calendar.php',\r\n    'settings.php',\r\n    'stats.php',\r\n    'about.php',\r\n    'login.php',\r\n    'admin.php',\r\n];\r\n\r\n// Install: cache static assets only\r\nself.addEventListener('install', function (event) {\r\n    event.waitUntil(\r\n        caches.open(STATIC_CACHE).then(function (cache) {\r\n            return Promise.allSettled(\r\n                staticAssets.map(url =>\r\n                    fetch(url).then(response => {\r\n                        if (response.ok) cache.put(url, response);\r\n                    }).catch(() => {}) // silently skip missing files\r\n                )\r\n            );\r\n        })\r\n    );\r\n    self.skipWaiting();\r\n});\r\n\r\n// Activate: clean up old caches\r\nself.addEventListener('activate', function (event) {\r\n    const validCaches = [STATIC_CACHE, PAGES_CACHE, LOGOS_CACHE];\r\n    event.waitUntil(\r\n        caches.keys().then(keys =>\r\n            Promise.all(\r\n                keys.filter(key => !validCaches.includes(key))\r\n                    .map(key => caches.delete(key))\r\n            )\r\n        )\r\n    );\r\n    self.clients.claim();\r\n});\r\n\r\n// Message: prefetch pages after login\r\nself.addEventListener('message', function (event) {\r\n    if (event.data && event.data.type === 'PREFETCH_PAGES') {\r\n        caches.open(PAGES_CACHE).then(cache => {\r\n            pagesToPrefetch.forEach(url => {\r\n                fetch(url).then(response => {\r\n                    // Only cache if user is actually logged in (no redirect)\r\n                    if (response.ok && !response.redirected) {\r\n                        cache.put(url, response);\r\n                    }\r\n                }).catch(() => {});\r\n            });\r\n        });\r\n    }\r\n});\r\n\r\n// Fetch: single handler for all requests\r\nself.addEventListener('fetch', function (event) {\r\n    const request = event.request;\r\n    const url = new URL(request.url);\r\n\r\n    // Never intercept non-GET requests (POST, etc.)\r\n    if (request.method !== 'GET') return;\r\n\r\n    // Logo images: cache-first, populate on first load\r\n    if (url.pathname.includes('images/uploads/logos')) {\r\n        event.respondWith(\r\n            caches.match(request).then(response => {\r\n                return response || fetch(request).then(networkResponse => {\r\n                    return caches.open(LOGOS_CACHE).then(cache => {\r\n                        cache.put(request, networkResponse.clone());\r\n                        return networkResponse;\r\n                    });\r\n                });\r\n            })\r\n        );\r\n        return;\r\n    }\r\n\r\n    // Static assets: cache-first (they only change on deploy)\r\n    if (staticAssets.some(asset => url.pathname.endsWith(asset))) {\r\n        event.respondWith(\r\n            caches.match(request).then(response => response || fetch(request))\r\n        );\r\n        return;\r\n    }\r\n\r\n    // PHP pages and everything else: network-first, cache as fallback\r\n    // Also update the pages cache on every successful load\r\n    event.respondWith(\r\n        fetch(request).then(response => {\r\n            if (response.ok && !response.redirected) {\r\n                const responseClone = response.clone(); // clone before any async operation\r\n                caches.open(PAGES_CACHE).then(cache => {\r\n                    cache.put(request, responseClone);\r\n                });\r\n            }\r\n            return response;\r\n        }).catch(() => {\r\n            return caches.match(request, { ignoreSearch: true });\r\n        })\r\n    );\r\n});\r\n"
  },
  {
    "path": "settings.php",
    "content": "<?php\r\nrequire_once 'includes/header.php';\r\n\r\n$currencies = array();\r\n$query = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n$query = $db->prepare($query);\r\n$query->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $query->execute();\r\nwhile ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $currencyId = $row['id'];\r\n    $currencies[$currencyId] = $row;\r\n}\r\n$userData['currency_symbol'] = $currencies[$main_currency]['symbol'];\r\n\r\n?>\r\n\r\n<script src=\"scripts/libs/sortable.min.js\"></script>\r\n<script src=\"scripts/libs/qrcode.min.js\"></script>\r\n<style>\r\n    .logo-preview:after {\r\n        content: '<?= translate('upload_logo', $i18n) ?>';\r\n    }\r\n</style>\r\n<section class=\"contain settings\">\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('monthly_budget', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-budget\">\r\n            <div class=\"form-group-inline\">\r\n                <label for=\"budget\"><?= $userData['currency_symbol'] ?></label>\r\n                <input type=\"number\" id=\"budget\" name=\"budget\" autocomplete=\"off\" value=\"<?= $userData['budget'] ?>\"\r\n                    placeholder=\"Budget\">\r\n                <input type=\"submit\" value=\"<?= translate('save', $i18n) ?>\" id=\"saveBudget\" onClick=\"saveBudget()\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i> <?= translate('budget_info', $i18n) ?>\r\n                </p>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    $sql = \"SELECT * FROM household WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $household = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $household[] = $row;\r\n        }\r\n    }\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('household', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-members\">\r\n            <div id=\"householdMembers\">\r\n                <?php\r\n                foreach ($household as $index => $member) {\r\n                    ?>\r\n                    <div class=\"form-group-inline\" data-memberid=\"<?= $member['id'] ?>\">\r\n                        <input type=\"text\" name=\"member\" autocomplete=\"off\" value=\"<?= $member['name'] ?>\"\r\n                            placeholder=\"Member\">\r\n                        <?php\r\n                        if ($index !== 0) {\r\n                            ?>\r\n                            <input type=\"text\" name=\"email\" autocomplete=\"off\" value=\"<?= $member['email'] ?? \"\" ?>\"\r\n                                placeholder=\"<?= translate(\"email\", $i18n) ?>\">\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                        <button class=\"image-button medium\" onClick=\"editMember(<?= $member['id'] ?>)\" name=\"save\"\r\n                            title=\"<?= translate('save_member', $i18n) ?>\">\r\n                            <?php include \"images/siteicons/svg/save.php\"; ?>\r\n                        </button>\r\n                        <?php\r\n                        if ($index !== 0) {\r\n                            ?>\r\n                            <button class=\"image-button medium\" onClick=\"removeMember(<?= $member['id'] ?>)\"\r\n                                title=\"<?= translate('delete_member', $i18n) ?>\">\r\n                                <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                            </button>\r\n                            <?php\r\n                        } else {\r\n                            ?>\r\n                            <button class=\"image-button medium disabled\" title=\"<?= translate('cant_delete_member', $i18n) ?>\">\r\n                                <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                            </button>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                    </div>\r\n                    <?php\r\n                }\r\n                ?>\r\n            </div>\r\n            <div class=\"buttons\">\r\n                <input type=\"submit\" value=\"<?= translate('add', $i18n) ?>\" id=\"addMember\" onClick=\"addMemberButton()\"\r\n                    class=\"thin mobile-grow\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i> <?= translate('household_info', $i18n) ?>\r\n                </p>\r\n                <p>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    // Notification settings\r\n    $sql = \"SELECT * FROM notification_settings WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notifications = $row;\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notifications['days'] = 1;\r\n    }\r\n\r\n    // Email notifications\r\n    $sql = \"SELECT * FROM email_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsEmail['enabled'] = $row['enabled'];\r\n        $notificationsEmail['smtp_address'] = $row['smtp_address'];\r\n        $notificationsEmail['smtp_port'] = $row['smtp_port'];\r\n        $notificationsEmail['encryption'] = $row['encryption'];\r\n        $notificationsEmail['smtp_username'] = $row['smtp_username'];\r\n        $notificationsEmail['smtp_password'] = $row['smtp_password'];\r\n        $notificationsEmail['from_email'] = $row['from_email'];\r\n        $notificationsEmail['other_emails'] = $row['other_emails'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsEmail['enabled'] = 0;\r\n        $notificationsEmail['smtp_address'] = \"\";\r\n        $notificationsEmail['smtp_port'] = 587;\r\n        $notificationsEmail['encryption'] = \"tls\";\r\n        $notificationsEmail['smtp_username'] = \"\";\r\n        $notificationsEmail['smtp_password'] = \"\";\r\n        $notificationsEmail['from_email'] = \"\";\r\n        $notificationsEmail['other_emails'] = \"\";\r\n    }\r\n\r\n    // Discord notifications\r\n    $sql = \"SELECT * FROM discord_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsDiscord['enabled'] = $row['enabled'];\r\n        $notificationsDiscord['webhook_url'] = $row['webhook_url'];\r\n        $notificationsDiscord['bot_username'] = $row['bot_username'];\r\n        $notificationsDiscord['bot_avatar'] = $row['bot_avatar_url'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsDiscord['enabled'] = 0;\r\n        $notificationsDiscord['webhook_url'] = \"\";\r\n        $notificationsDiscord['bot_username'] = \"\";\r\n        $notificationsDiscord['bot_avatar'] = \"\";\r\n    }\r\n\r\n    // Pushover notifications\r\n    $sql = \"SELECT * FROM pushover_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsPushover['enabled'] = $row['enabled'];\r\n        $notificationsPushover['token'] = $row['token'];\r\n        $notificationsPushover['user_key'] = $row['user_key'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsPushover['enabled'] = 0;\r\n        $notificationsPushover['token'] = \"\";\r\n        $notificationsPushover['user_key'] = \"\";\r\n    }\r\n\r\n    // Telegram notifications\r\n    $sql = \"SELECT * FROM telegram_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsTelegram['enabled'] = $row['enabled'];\r\n        $notificationsTelegram['bot_token'] = $row['bot_token'];\r\n        $notificationsTelegram['chat_id'] = $row['chat_id'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsTelegram['enabled'] = 0;\r\n        $notificationsTelegram['bot_token'] = \"\";\r\n        $notificationsTelegram['chat_id'] = \"\";\r\n    }\r\n\r\n\r\n    // PushPlus notifications\r\n    $sql = \"SELECT * FROM pushplus_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsPushPlus['enabled'] = $row['enabled'];\r\n        $notificationsPushPlus['token'] = $row['token'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsPushPlus['enabled'] = 0;\r\n        $notificationsPushPlus['token'] = \"\";\r\n    }\r\n\r\n    // Mattermost notifications\r\n    $sql = \"SELECT * FROM mattermost_notifications WHERE user_id = :userID LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userID', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsMattermost['enabled'] = $row['enabled'];\r\n        $notificationsMattermost['webhook_url'] = $row['webhook_url'];\r\n        $notificationsMattermost['bot_username'] = $row['bot_username'];\r\n        $notificationsMattermost['bot_icon_emoji'] = $row['bot_icon_emoji'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsMattermost['enabled'] = 0;\r\n        $notificationsMattermost['webhook_url'] = \"\";\r\n        $notificationsMattermost['bot_username'] = \"\";\r\n        $notificationsMattermost['bot_icon_emoji'] = \"\";\r\n    }\r\n\r\n    // Serverchan notifications\r\n    $sql = \"SELECT * FROM serverchan_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsServerchan['enabled'] = $row['enabled'];\r\n        $notificationsServerchan['sendkey'] = $row['sendkey'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsServerchan['enabled'] = 0;\r\n        $notificationsServerchan['sendkey'] = \"\";\r\n    }\r\n\r\n    // Ntfy notifications\r\n    $sql = \"SELECT * FROM ntfy_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsNtfy['enabled'] = $row['enabled'];\r\n        $notificationsNtfy['host'] = $row['host'];\r\n        $notificationsNtfy['topic'] = $row['topic'];\r\n        $notificationsNtfy['headers'] = $row['headers'];\r\n        $notificationsNtfy['ignore_ssl'] = $row['ignore_ssl'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsNtfy['enabled'] = 0;\r\n        $notificationsNtfy['host'] = \"\";\r\n        $notificationsNtfy['topic'] = \"\";\r\n        $notificationsNtfy['headers'] = \"\";\r\n        $notificationsNtfy['ignore_ssl'] = 0;\r\n    }\r\n\r\n    // Webhook notifications\r\n    $sql = \"SELECT * FROM webhook_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsWebhook['enabled'] = $row['enabled'];\r\n        $notificationsWebhook['url'] = $row['url'];\r\n        $notificationsWebhook['request_method'] = $row['request_method'];\r\n        $notificationsWebhook['headers'] = $row['headers'];\r\n        $notificationsWebhook['payload'] = $row['payload'];\r\n        $notificationsWebhook['cancelation_payload'] = $row['cancelation_payload'];\r\n        $notificationsWebhook['ignore_ssl'] = $row['ignore_ssl'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsWebhook['enabled'] = 0;\r\n        $notificationsWebhook['url'] = \"\";\r\n        $notificationsWebhook['request_method'] = \"POST\";\r\n        $notificationsWebhook['headers'] = \"\";\r\n        $notificationsWebhook['payload'] = '\r\n{\r\n    \"name\": \"{{subscription_name}}\",\r\n    \"price\": \"{{subscription_price}}\",\r\n    \"currency\": \"{{subscription_currency}}\",\r\n    \"category\": \"{{subscription_category}}\",\r\n    \"date\": \"{{subscription_date}}\",\r\n    \"payer\": \"{{subscription_payer}}\",\r\n    \"days\": \"{{subscription_days_until_payment}}\",\r\n    \"notes\": \"{{subscription_notes}}\",\r\n    \"url\": \"{{subscription_url}}\"\r\n}';\r\n        $notificationsWebhook['cancelation_payload'] = \"\";\r\n        $notificationsWebhook['ignore_ssl'] = 0;\r\n    }\r\n\r\n    // Gotify notifications\r\n    $sql = \"SELECT * FROM gotify_notifications WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $rowCount = 0;\r\n    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $notificationsGotify['enabled'] = $row['enabled'];\r\n        $notificationsGotify['url'] = $row['url'];\r\n        $notificationsGotify['token'] = $row['token'];\r\n        $notificationsGotify['ignore_ssl'] = $row['ignore_ssl'];\r\n        $rowCount++;\r\n    }\r\n\r\n    if ($rowCount == 0) {\r\n        $notificationsGotify['enabled'] = 0;\r\n        $notificationsGotify['url'] = \"\";\r\n        $notificationsGotify['token'] = \"\";\r\n        $notificationsGotify['ignore_ssl'] = 0;\r\n    }\r\n\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('notifications', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-notifications\">\r\n            <section>\r\n                <label for=\"days\"><?= translate('notify_me', $i18n) ?>:</label>\r\n                <div class=\"form-group-inline\">\r\n                    <select name=\"days\" id=\"days\">\r\n                        <option value=\"0\" <?= $notifications['days'] == 0 ? \"selected\" : \"\" ?>>\r\n                            <?= translate('on_due_date', $i18n) ?>\r\n                        </option>\r\n                        <option value=\"1\" <?= $notifications['days'] == 1 ? \"selected\" : \"\" ?>>\r\n                            1 <?= translate('day_before', $i18n) ?>\r\n                        </option>\r\n                        <?php\r\n                        for ($i = 2; $i <= 7; $i++) {\r\n                            $selected = $i == $notifications['days'] ? \"selected\" : \"\";\r\n                            ?>\r\n                            <option value=\"<?= $i ?>\" <?= $selected ?>>\r\n                                <?= $i ?>     <?= translate('day_before', $i18n) ?>\r\n                            </option>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n                    </select>\r\n                    <input type=\"submit\" class=\"thin\" value=\"<?= translate('save', $i18n) ?>\" id=\"saveNotifications\"\r\n                        onClick=\"saveNotifications()\" />\r\n                </div>\r\n            </section>\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('email')\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-envelope\"></i>\r\n                        <?= translate('email', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"email\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"emailenabled\" name=\"emailenabled\" <?= $notificationsEmail['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"emailenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"smtpaddress\" id=\"smtpaddress\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('smtp_address', $i18n) ?>\"\r\n                            value=\"<?= $notificationsEmail['smtp_address'] ?>\" />\r\n                        <input type=\"text\" name=\"smtpport\" id=\"smtpport\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('port', $i18n) ?>\" class=\"one-third\"\r\n                            value=\"<?= $notificationsEmail['smtp_port'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <div>\r\n                            <input type=\"radio\" name=\"encryption\" id=\"encryptionnone\" value=\"none\"\r\n                                <?= $notificationsEmail['encryption'] == \"none\" ? \"checked\" : \"\" ?> />\r\n                            <label for=\"encryptionnone\"><?= translate('none', $i18n) ?></label>\r\n                        </div>\r\n                        <div>\r\n                            <input type=\"radio\" name=\"encryption\" id=\"encryptiontls\" value=\"tls\"\r\n                                <?= $notificationsEmail['encryption'] == \"tls\" ? \"checked\" : \"\" ?> />\r\n                            <label for=\"encryptiontls\"><?= translate('tls', $i18n) ?></label>\r\n                        </div>\r\n                        <div>\r\n                            <input type=\"radio\" name=\"encryption\" id=\"encryptionssl\" value=\"ssl\"\r\n                                <?= $notificationsEmail['encryption'] == \"ssl\" ? \"checked\" : \"\" ?> />\r\n                            <label for=\"encryptionssl\"><?= translate('ssl', $i18n) ?></label>\r\n                        </div>\r\n\r\n\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"smtpusername\" id=\"smtpusername\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('smtp_username', $i18n) ?>\"\r\n                            value=\"<?= $notificationsEmail['smtp_username'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"password\" name=\"smtppassword\" id=\"smtppassword\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('smtp_password', $i18n) ?>\"\r\n                            value=\"<?= $notificationsEmail['smtp_password'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"fromemail\" id=\"fromemail\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('from_email', $i18n) ?>\"\r\n                            value=\"<?= $notificationsEmail['from_email'] ?>\" />\r\n                    </div>\r\n                    <label for=\"otheremails\"><?= translate('send_to_other_emails', $i18n) ?></label>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"otheremails\" id=\"otheremails\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('other_emails_placeholder', $i18n) ?>\"\r\n                            value=\"<?= $notificationsEmail['other_emails'] ?>\" />\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsEmail\"\r\n                            onClick=\"testNotificationEmailButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsEmail\" onClick=\"saveNotificationsEmailButton()\" />\r\n                    </div>\r\n                    <div class=\"settings-notes\">\r\n                        <p>\r\n                            <i class=\"fa-solid fa-circle-info\"></i> <?= translate('smtp_info', $i18n) ?>\r\n                        </p>\r\n                        <p>\r\n                    </div>\r\n                </div>\r\n            </section>\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('discord');\">\r\n                    <h3>\r\n                        <i class=\"fa-brands fa-discord\"></i>\r\n                        <?= translate('discord', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"discord\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"discordenabled\" name=\"discordenabled\"\r\n                            <?= $notificationsDiscord['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"discordenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"discordurl\" id=\"discordurl\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('webhook_url', $i18n) ?>\"\r\n                            value=\"<?= $notificationsDiscord['webhook_url'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"discordbotusername\" id=\"discordbotusername\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('discord_bot_username', $i18n) ?>\"\r\n                            value=\"<?= $notificationsDiscord['bot_username'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"discordbotavatar\" id=\"discordbotavatar\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('discord_bot_avatar_url', $i18n) ?>\"\r\n                            value=\"<?= $notificationsDiscord['bot_avatar'] ?>\" />\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsDiscord\"\r\n                            onClick=\"testNotificationsDiscordButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsDiscord\" onClick=\"saveNotificationsDiscordButton()\" />\r\n                    </div>\r\n                </div>\r\n            </section>\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('gotify');\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-envelopes-bulk\"></i>\r\n                        <?= translate('gotify', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"gotify\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"gotifyenabled\" name=\"gotifyenabled\"\r\n                            <?= $notificationsGotify['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"gotifyenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"gotifyurl\" id=\"gotifyurl\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('url', $i18n) ?>\" value=\"<?= $notificationsGotify['url'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"gotifytoken\" id=\"gotifytoken\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('token', $i18n) ?>\"\r\n                            value=\"<?= $notificationsGotify['token'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"gotifyignoressl\" name=\"gotifyignoressl\"\r\n                            <?= $notificationsGotify['ignore_ssl'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"gotifyignoressl\"><?= translate('ignore_ssl_errors', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsGotify\"\r\n                            onClick=\"testNotificationsGotifyButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsGotify\" onClick=\"saveNotificationsGotifyButton()\" />\r\n                    </div>\r\n                </div>\r\n            </section>\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('pushover');\">\r\n                    <h3>\r\n                        <i class=\"fa-brands fa-pinterest-p\"></i>\r\n                        <?= translate('pushover', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"pushover\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"pushoverenabled\" name=\"pushoverenabled\"\r\n                            <?= $notificationsPushover['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"pushoverenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"pushoveruserkey\" id=\"pushoveruserkey\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('pushover_user_key', $i18n) ?>\"\r\n                            value=\"<?= $notificationsPushover['user_key'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"pushovertoken\" id=\"pushovertoken\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('token', $i18n) ?>\"\r\n                            value=\"<?= $notificationsPushover['token'] ?>\" />\r\n                    </div>\r\n\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsPushover\"\r\n                            onClick=\"testNotificationsPushoverButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsPushover\" onClick=\"saveNotificationsPushoverButton()\" />\r\n                    </div>\r\n                </div>\r\n            </section>\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('telegram');\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-paper-plane\"></i>\r\n                        <?= translate('telegram', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"telegram\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"telegramenabled\" name=\"telegramenabled\"\r\n                            <?= $notificationsTelegram['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"telegramenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"telegrambottoken\" id=\"telegrambottoken\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('telegram_bot_token', $i18n) ?>\"\r\n                            value=\"<?= $notificationsTelegram['bot_token'] ? $notificationsTelegram['bot_token'] : \"\" ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"telegramchatid\" id=\"telegramchatid\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('telegram_chat_id', $i18n) ?>\"\r\n                            value=\"<?= $notificationsTelegram['chat_id'] ?>\" />\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsTelegram\"\r\n                            onClick=\"testNotificationsTelegramButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsTelegram\" onClick=\"saveNotificationsTelegramButton()\" />\r\n                    </div>\r\n                </div>\r\n            </section>\r\n\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('pushplus');\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-bell\"></i>\r\n                        <?= translate('pushplus', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"pushplus\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"pushplusenabled\" name=\"pushplusenabled\"\r\n                            <?= $notificationsPushPlus['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"pushplusenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"pushplustoken\" id=\"pushplustoken\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('pushplus_token', $i18n) ?>\"\r\n                            value=\"<?= $notificationsPushPlus['token'] ? $notificationsPushPlus['token'] : '' ?>\" />\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsPushPlus\"\r\n                            onClick=\"testNotificationsPushPlusButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsPushPlus\" onClick=\"saveNotificationsPushPlusButton()\" />\r\n                    </div>\r\n                </div>\r\n            </section>\r\n\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('mattermost');\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-gauge-simple-high\"></i>\r\n                        <?= translate('mattermost', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"mattermost\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"mattermostenabled\" name=\"mattermostenabled\"\r\n                            <?= $notificationsMattermost['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"mattermostenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"mattermostwebhookurl\" id=\"mattermostwebhookurl\"\r\n                            placeholder=\"<?= translate('mattermost_webhook_url', $i18n) ?>\"\r\n                            value=\"<?= $notificationsMattermost['webhook_url'] ? $notificationsMattermost['webhook_url'] : '' ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"mattermostbotusername\" id=\"mattermostbotusername\"\r\n                            placeholder=\"<?= translate('mattermost_bot_username', $i18n) ?>\"\r\n                            value=\"<?= $notificationsMattermost['bot_username'] ? $notificationsMattermost['bot_username'] : '' ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"mattermostboticonemoji\" id=\"mattermostboticonemoji\"\r\n                            placeholder=\"<?= translate('mattermost_bot_icon_emoji', $i18n) ?>\"\r\n                            value=\"<?= $notificationsMattermost['bot_icon_emoji'] ? $notificationsMattermost['bot_icon_emoji'] : '' ?>\" />\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsMattermost\"\r\n                            onClick=\"testNotificationsMattermostButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsMattermost\" onClick=\"saveNotificationsMattermostButton()\" />\r\n                    </div>\r\n                </div>\r\n            </section>\r\n\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('ntfy');\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-terminal\"></i> Ntfy\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"ntfy\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"ntfyenabled\" name=\"ntfyenabled\" <?= $notificationsNtfy['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"ntfyenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"ntfyhost\" id=\"ntfyhost\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('host', $i18n) ?>\" value=\"<?= $notificationsNtfy['host'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"ntfytopic\" id=\"ntfytopic\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('topic', $i18n) ?>\" value=\"<?= $notificationsNtfy['topic'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <textarea class=\"thin\" name=\"ntfyheaders\" id=\"ntfyheaders\"\r\n                            placeholder=\"<?= translate('custom_headers', $i18n) ?>\"><?= $notificationsNtfy['headers'] ?></textarea>\r\n                    </div>\r\n                    <div class=\"form-grpup-inline\">\r\n                        <input type=\"checkbox\" id=\"ntfyignoressl\" name=\"ntfyignoressl\"\r\n                            <?= $notificationsNtfy['ignore_ssl'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"ntfyignoressl\"><?= translate('ignore_ssl_errors', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsNtfy\"\r\n                            onClick=\"testNotificationsNtfyButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsNtfy\" onClick=\"saveNotificationsNtfyButton()\" />\r\n                    </div>\r\n            </section>\r\n\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('serverchan');\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-code\"></i>\r\n                        <?= translate('serverchan', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"serverchan\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"serverchanenabled\" name=\"serverchanenabled\"\r\n                            <?= $notificationsServerchan['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"serverchanenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"serverchansendkey\" id=\"serverchansendkey\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('serverchan_sendkey', $i18n) ?>\"\r\n                            value=\"<?= $notificationsServerchan['sendkey'] ? $notificationsServerchan['sendkey'] : '' ?>\" />\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsServerchan\"\r\n                            onClick=\"testNotificationsServerchanButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsServerchan\" onClick=\"saveNotificationsServerchanButton()\" />\r\n                    </div>\r\n                </div>\r\n            </section>\r\n\r\n            <section class=\"account-notifications-section\">\r\n                <header class=\"account-notification-section-header\" onclick=\"openNotificationsSettings('webhook');\">\r\n                    <h3>\r\n                        <i class=\"fa-solid fa-bolt\"></i>\r\n                        <?= translate('webhook', $i18n) ?>\r\n                    </h3>\r\n                </header>\r\n                <div class=\"account-notification-section-settings\" data-type=\"webhook\">\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"webhookenabled\" name=\"webhookenabled\"\r\n                            <?= $notificationsWebhook['enabled'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"webhookenabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n                    </div>\r\n                    <div>\r\n                        <label for=\"webhookrequestmethod\"\r\n                            class=\"capitalize\"><?= translate('request_method', $i18n) ?>:</label>\r\n                        <div class=\"form-group-inline\">\r\n                            <select name=\"webhookrequestmethod\" id=\"webhookrequestmethod\">\r\n                                <option value=\"GET\" <?= $notificationsWebhook['request_method'] == 'GET' ? 'selected' : '' ?>>GET</option>\r\n                                <option value=\"POST\" <?= $notificationsWebhook['request_method'] == 'POST' ? 'selected' : '' ?>>POST</option>\r\n                                <option value=\"PUT\" <?= $notificationsWebhook['request_method'] == 'PUT' ? 'selected' : '' ?>>PUT</option>\r\n                            </select>\r\n                        </div>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"text\" name=\"webhookurl\" id=\"webhookurl\" autocomplete=\"off\"\r\n                            placeholder=\"<?= translate('webhook_url', $i18n) ?>\"\r\n                            value=\"<?= $notificationsWebhook['url'] ?>\" />\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <textarea class=\"thin\" name=\"webhookcustomheaders\" id=\"webhookcustomheaders\"\r\n                            placeholder=\"<?= translate('custom_headers', $i18n) ?>\"><?= $notificationsWebhook['headers'] ?></textarea>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <textarea name=\"webhookpayload\" id=\"webhookpayload\"\r\n                            placeholder=\"<?= translate('payment_notifications_payload', $i18n) ?>\"><?= $notificationsWebhook['payload'] ?></textarea>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <textarea name=\"webhookcancelationpayload\" id=\"webhookcancelationpayload\"\r\n                            placeholder=\"<?= translate('cancelation_notification_payload', $i18n) ?>\"><?= $notificationsWebhook['cancelation_payload'] ?></textarea>\r\n                    </div>\r\n                    <div class=\"form-group-inline\">\r\n                        <input type=\"checkbox\" id=\"webhookignoressl\" name=\"webhookignoressl\"\r\n                            <?= $notificationsWebhook['ignore_ssl'] ? \"checked\" : \"\" ?>>\r\n                        <label for=\"webhookignoressl\"><?= translate('ignore_ssl_errors', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"buttons\">\r\n                        <input type=\"button\" class=\"secondary-button thin mobile-grow\"\r\n                            value=\"<?= translate('test', $i18n) ?>\" id=\"testNotificationsWebhook\"\r\n                            onClick=\"testNotificationsWebhookButton()\" />\r\n                        <input type=\"submit\" class=\"thin mobile-grow\" value=\"<?= translate('save', $i18n) ?>\"\r\n                            id=\"saveNotificationsWebhook\" onClick=\"saveNotificationsWebhookButton()\" />\r\n                    </div>\r\n                    <div class=\"settings-notes\">\r\n                        <p>\r\n                            <i class=\"fa-solid fa-circle-info\"></i> <?= translate('variables_available', $i18n) ?>:\r\n                            {{days_until}}, {{subscription_name}}, {{subscription_price}}, {{subscription_currency}},\r\n                            {{subscription_category}}, {{subscription_date}}, {{subscription_payer}},\r\n                            {{subscription_days_until_payment}}, {{subscription_notes}}, {{subscription_url}}\r\n                        </p>\r\n                        <p>\r\n                    </div>\r\n                </div>\r\n            </section>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    $sql = \"SELECT * FROM categories WHERE user_id = :userId ORDER BY `order` ASC\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $categories = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $categories[] = $row;\r\n        }\r\n    }\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('categories', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-categories\">\r\n            <div id=\"categories\" class=\"sortable-list\">\r\n                <?php\r\n                foreach ($categories as $category) {\r\n                    if ($category['id'] != 1) {\r\n                        $canDelete = true;\r\n\r\n                        $query = \"SELECT COUNT(*) as count FROM subscriptions WHERE category_id = :categoryId AND user_id = :userId\";\r\n                        $stmt = $db->prepare($query);\r\n                        $stmt->bindParam(':categoryId', $category['id'], SQLITE3_INTEGER);\r\n                        $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n                        $isUsed = $row['count'];\r\n\r\n                        if ($isUsed > 0) {\r\n                            $canDelete = false;\r\n                        }\r\n                        ?>\r\n                        <div class=\"form-group-inline\" data-categoryid=\"<?= $category['id'] ?>\">\r\n                            <div class=\" drag-icon\"><i class=\"fa-solid fa-grip-vertical\"></i></div>\r\n                            <input type=\"text\" name=\"category\" autocomplete=\"off\" value=\"<?= $category['name'] ?>\"\r\n                                placeholder=\"Category\">\r\n                            <button class=\"image-button medium\" onClick=\"editCategory(<?= $category['id'] ?>)\" name=\"save\"\r\n                                title=\"<?= translate('save_category', $i18n) ?>\">\r\n                                <?php include \"images/siteicons/svg/save.php\"; ?>\r\n                            </button>\r\n                            <?php\r\n                            if ($canDelete) {\r\n                                ?>\r\n                                <button class=\"image-button medium\" onClick=\"removeCategory(<?= $category['id'] ?>)\"\r\n                                    title=\"<?= translate('delete_category', $i18n) ?>\">\r\n                                    <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                                </button>\r\n                                <?php\r\n                            } else {\r\n                                ?>\r\n                                <button class=\"image-button medium disabled\"\r\n                                    title=\"<?= translate('cant_delete_category_in_use', $i18n) ?>\">\r\n                                    <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                                </button>\r\n                                <?php\r\n                            }\r\n                            ?>\r\n                        </div>\r\n                        <?php\r\n                    }\r\n                }\r\n                ?>\r\n            </div>\r\n            <div class=\"buttons\">\r\n                <input type=\"submit\" value=\"<?= translate('add', $i18n) ?>\" id=\"addCategory\"\r\n                    onClick=\"addCategoryButton()\" class=\"thin mobile-grow\" />\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    $sql = \"SELECT * FROM currencies WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $currencies = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $currencies[] = $row;\r\n        }\r\n    }\r\n\r\n    $query = \"SELECT main_currency FROM user WHERE id = :userId\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n    $mainCurrencyId = $row['main_currency'];\r\n\r\n    $query = \"SELECT date FROM last_exchange_update\";\r\n    $exchange_rates_last_updated = $db->querySingle($query);\r\n\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('currencies', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-currencies\">\r\n            <div id=\"currencies\">\r\n                <?php\r\n                foreach ($currencies as $currency) {\r\n                    $canDelete = true;\r\n                    $isMainCurrency = false;\r\n                    if ($currency['id'] === $mainCurrencyId) {\r\n                        $canDelete = false;\r\n                        $isMainCurrency = true;\r\n                    } else {\r\n                        $query = \"SELECT COUNT(*) as count FROM subscriptions WHERE currency_id = :currencyId\";\r\n                        $stmt = $db->prepare($query);\r\n                        $stmt->bindParam(':currencyId', $currency['id'], SQLITE3_INTEGER);\r\n                        $result = $stmt->execute();\r\n                        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n                        $isUsed = $row['count'];\r\n\r\n                        if ($isUsed > 0) {\r\n                            $canDelete = false;\r\n                        }\r\n                    }\r\n                    ?>\r\n\r\n                    <div class=\"form-group-inline\" data-currencyid=\"<?= $currency['id'] ?>\">\r\n                        <input type=\"text\" class=\"short\" name=\"symbol\" autocomplete=\"off\" value=\"<?= $currency['symbol'] ?>\"\r\n                            placeholder=\"$\">\r\n                        <input type=\"text\" name=\"currency\" autocomplete=\"off\" value=\"<?= $currency['name'] ?>\"\r\n                            placeholder=\"Currency Name\">\r\n                        <input type=\"text\" name=\"code\" autocomplete=\"off\" value=\"<?= $currency['code'] ?>\"\r\n                            placeholder=\"Currency Code\" <?= !$canDelete ? 'disabled' : '' ?>>\r\n                        <button class=\"image-button medium\" onClick=\"editCurrency(<?= $currency['id'] ?>)\" name=\"save\"\r\n                            title=\"<?= translate('save_currency', $i18n) ?>\">\r\n                            <?php include \"images/siteicons/svg/save.php\"; ?>\r\n                        </button>\r\n                        <?php\r\n                        if ($canDelete) {\r\n                            ?>\r\n                            <button class=\"image-button medium\" onClick=\"removeCurrency(<?= $currency['id'] ?>)\"\r\n                                title=\"<?= translate('delete_currency', $i18n) ?>\">\r\n                                <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                            </button>\r\n                            <?php\r\n                        } else {\r\n                            $cantDeleteMessage = $isMainCurrency ? translate('cant_delete_main_currency', $i18n) : translate('cant_delete_currency_in_use', $i18n);\r\n                            ?>\r\n                            <button class=\"image-button medium disabled\" title=\"<?= $cantDeleteMessage ?>\">\r\n                                <?php include \"images/siteicons/svg/delete.php\"; ?>\r\n                            </button>\r\n                            <?php\r\n                        }\r\n                        ?>\r\n\r\n                    </div>\r\n                    <?php\r\n                }\r\n                ?>\r\n            </div>\r\n            <div class=\"buttons\">\r\n                <input type=\"submit\" value=\"<?= translate('add', $i18n) ?>\" id=\"addCurrency\"\r\n                    onClick=\"addCurrencyButton()\" class=\"thin mobile-grow\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    <?= translate('exchange_update', $i18n) ?>\r\n                    <span>\r\n                        <?= $exchange_rates_last_updated ?>\r\n                    </span>\r\n                </p>\r\n                <p>\r\n                    <i class=\"fa-solid fa-circle-info\"></i>\r\n                    <?= translate('currency_info', $i18n) ?>\r\n                    <span>\r\n                        fixer.io\r\n                        <a href=\"https://fixer.io/symbols\" target=\"_blank\" title=\"Currency codes\">\r\n                            <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                        </a>\r\n                    </span>\r\n                </p>\r\n                <p>\r\n                    <?= translate('currency_performance', $i18n) ?>\r\n                </p>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    $apiKey = \"\";\r\n    $sql = \"SELECT api_key, provider FROM fixer WHERE user_id = :userId\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n    if ($result) {\r\n        $row = $result->fetchArray(SQLITE3_ASSOC);\r\n        if ($row) {\r\n            $apiKey = $row['api_key'];\r\n            $provider = $row['provider'];\r\n        } else {\r\n            $provider = 0;\r\n        }\r\n    }\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2>Fixer API Key</h2>\r\n        </header>\r\n        <div class=\"account-fixer\">\r\n            <div class=\"form-group\">\r\n                <input type=\"text\" name=\"fixer-key\" id=\"fixerKey\" autocomplete=\"off\" value=\"<?= $apiKey ?>\"\r\n                    placeholder=\"<?= translate('api_key', $i18n) ?>\" <?= $demoMode ? 'disabled title=\"Not available on Demo Mode\"' : '' ?>>\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <label for=\"fixerProvider\"><?= translate('provider', $i18n) ?>:</label>\r\n                <select name=\"fixer-provider\" id=\"fixerProvider\">\r\n                    <option value=\"0\" <?= $provider == 0 ? 'selected' : '' ?>>fixer.io</option>\r\n                    <option value=\"1\" <?= $provider == 1 ? 'selected' : '' ?>>apilayer.com</option>\r\n                </select>\r\n            </div>\r\n            <div class=\"buttons\">\r\n                <input type=\"submit\" value=\"<?= translate('save', $i18n) ?>\" id=\"addFixerKey\"\r\n                    onClick=\"addFixerKeyButton()\" class=\"thin mobile-grow\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p><i class=\"fa-solid fa-circle-info\"></i><?= translate('fixer_info', $i18n) ?></p>\r\n                <p><?= translate('get_key', $i18n) ?>:\r\n                    <span>\r\n                        https://fixer.io/\r\n                        <a href=\"https://fixer.io/#pricing_plan\"\r\n                            title=\"<?= translate(\"get_free_fixer_api_key\", $i18n) ?>\" target=\"_blank\">\r\n                            <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                        </a>\r\n                    </span>\r\n                </p>\r\n                <p>\r\n                    <?= translate(\"get_key_alternative\", $i18n) ?>\r\n                    <span>\r\n                        https://apilayer.com\r\n                        <a href=\"https://apilayer.com/marketplace/fixer-api\" title=\"Get free fixer api key\"\r\n                            target=\"_blank\">\r\n                            <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\r\n                        </a>\r\n                    </span>\r\n                </p>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    $sql = \"SELECT * FROM ai_settings WHERE user_id = :userId LIMIT 1\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    $aiSettings = [];\r\n    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n        $aiSettings = $row;\r\n    }\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('ai_recommendations', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-ai-settings\">\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"checkbox\" id=\"ai_enabled\" name=\"ai_enabled\" <?= isset($aiSettings['enabled']) && $aiSettings['enabled'] ? \"checked\" : \"\" ?>>\r\n                <label for=\"ai_enabled\" class=\"capitalize\"><?= translate('enabled', $i18n) ?></label>\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <label for=\"ai_type\"><?= translate('provider', $i18n) ?>:</label>\r\n                <select id=\"ai_type\" name=\"ai_type\" onchange=\"toggleAiInputs()\">\r\n                    <option value=\"chatgpt\" <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'chatgpt') ? 'selected' : '' ?>>ChatGPT</option>\r\n                    <option value=\"gemini\" <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'gemini') ? 'selected' : '' ?>>Gemini</option>\r\n                    <option value=\"openrouter\" <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'openrouter') ? 'selected' : '' ?>>OpenRouter</option>\r\n                    <option value=\"ollama\" <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'selected' : '' ?>>Local Ollama</option>\r\n                </select>\r\n            </div>\r\n            <div class=\"form-group-inline\">\r\n                <input type=\"password\" id=\"ai_api_key\" name=\"ai_api_key\" autocomplete=\"off\"\r\n                    class=\"<?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'hidden' : '' ?>\"\r\n                    placeholder=\"<?= translate('api_key', $i18n) ?>\"\r\n                    value=\"<?= isset($aiSettings['api_key']) ? htmlspecialchars($aiSettings['api_key']) : '' ?>\" />\r\n                <input type=\"text\" id=\"ai_ollama_host\" name=\"ai_ollama_host\" autocomplete=\"off\"\r\n                    class=\"<?= (!isset($aiSettings['type']) || $aiSettings['type'] != 'ollama') ? 'hidden' : '' ?>\"\r\n                    placeholder=\"<?= translate('host', $i18n) ?>\"\r\n                    value=\"<?= isset($aiSettings['url']) ? htmlspecialchars($aiSettings['url']) : '' ?>\" />\r\n                <button type=\"button\" id=\"toggleAiApiKey\" class=\"button secondary-button icon-button <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'hidden' : '' ?>\" onclick=\"toggleAiApiKeyVisibility()\" aria-label=\"Toggle API key visibility\">\r\n                    <i class=\"fa-solid fa-eye\"></i>\r\n                </button>\r\n                <button type=\"button\" id=\"fetchModelsButton\" class=\"button thin\" onclick=\"fetch_ai_models()\">\r\n                    <?= translate('test', $i18n) ?>\r\n                </button>\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <label for=\"ai_model\"><?= translate('ai_model', $i18n) ?>:</label>\r\n                <select id=\"ai_model\" name=\"ai_model\">\r\n                    <option value=\"\"><?= translate('select_ai_model', $i18n) ?></option>\r\n                    <?php if (!empty($aiSettings['model'])): ?>\r\n                        <option value=\"<?= htmlspecialchars($aiSettings['model']) ?>\" selected>\r\n                            <?= htmlspecialchars($aiSettings['model']) ?>\r\n                        </option>\r\n                    <?php endif; ?>\r\n                </select>\r\n            </div>\r\n            <div class=\"form-group\">\r\n                <label for=\"ai_run_schedule\" class=\"flex\"><?= translate('run_schedule', $i18n) ?>: <span\r\n                        class=\"info-badge\"><?= translate(\"coming_soon\", $i18n) ?></span></span></label>\r\n                <select id=\"ai_run_schedule\" name=\"ai_run_schedule\" disabled>\r\n                    <option value=\"manual\" <?= (isset($aiSettings['run_schedule']) && $aiSettings['run_schedule'] == 'manual') ? 'selected' : '' ?>><?= translate('manually', $i18n) ?>\r\n                    </option>\r\n                    <option value=\"weekly\" <?= (isset($aiSettings['run_schedule']) && $aiSettings['run_schedule'] == 'weekly') ? 'selected' : '' ?>><?= translate('Weekly', $i18n) ?>\r\n                    </option>\r\n                    <option value=\"monthly\" <?= (isset($aiSettings['run_schedule']) && $aiSettings['run_schedule'] == 'monthly') ? 'selected' : '' ?>><?= translate('Monthly', $i18n) ?>\r\n                    </option>\r\n                </select>\r\n            </div>\r\n            <div class=\"buttons wrap mobile-reverse\">\r\n                <?php\r\n                $canBeExecuted = !empty($aiSettings['model']) && !empty($aiSettings['enabled']) && $aiSettings['enabled'] == 1;\r\n                ?>\r\n                <input type=\"button\" id=\"runAiRecommendations\"\r\n                    class=\"secondary-button thin mobile-grow-force <?= !$canBeExecuted ? 'hidden' : '' ?>\"\r\n                    onclick=\"runAiRecommendations()\" value=\"<?= translate('generate_recommendations', $i18n) ?>\" />\r\n                <div id=\"aiSpinner\" class=\"spinner ai-spinner hidden\"></div>\r\n\r\n                <input type=\"submit\" class=\"thin mobile-grow-force\" value=\"<?= translate('save', $i18n) ?>\"\r\n                    id=\"saveAiSettings\" onClick=\"saveAiSettingsButton()\" />\r\n            </div>\r\n            <div class=\"settings-notes\">\r\n                <p><i class=\"fa-solid fa-circle-info\"></i><?= translate('ai_recommendations_info', $i18n) ?></p>\r\n                <p><i class=\"fa-solid fa-circle-info\"></i><?= translate('may_take_time', $i18n) ?></p>\r\n                <p><i\r\n                        class=\"fa-solid fa-circle-info\"></i><?= translate('recommendations_visible_on_dashboard', $i18n) ?>\r\n                </p>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <?php\r\n    $sql = \"SELECT * FROM payment_methods WHERE user_id = :userId ORDER BY `order` ASC\";\r\n    $stmt = $db->prepare($sql);\r\n    $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n    $result = $stmt->execute();\r\n\r\n    if ($result) {\r\n        $payments = array();\r\n        while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n            $payments[] = $row;\r\n        }\r\n    }\r\n    ?>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('payment_methods', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"payments-list\" id=\"payments-list\">\r\n            <?php\r\n            $paymentsInUseQuery = $db->prepare('SELECT id FROM payment_methods WHERE user_id = :userId AND id IN (SELECT DISTINCT payment_method_id FROM subscriptions WHERE user_id = :userId)');\r\n            $paymentsInUseQuery->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n            $result = $paymentsInUseQuery->execute();\r\n\r\n            $paymentsInUse = [];\r\n            while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n                $paymentsInUse[] = $row['id'];\r\n            }\r\n\r\n            foreach ($payments as $payment) {\r\n                $paymentIconFolder = (strpos($payment['icon'], 'images/uploads/icons/') !== false) ? \"\" : \"images/uploads/logos/\";\r\n\r\n                $inUse = in_array($payment['id'], $paymentsInUse);\r\n                ?>\r\n                <div class=\"payments-payment\" data-enabled=\"<?= $payment['enabled']; ?>\"\r\n                    data-in-use=\"<?= $inUse ? 'yes' : 'no' ?>\" data-paymentid=\"<?= $payment['id'] ?>\"\r\n                    title=\"<?= $inUse ? translate('cant_delete_payment_method_in_use', $i18n) : ($payment['enabled'] ? translate('disable', $i18n) : translate('enable', $i18n)) ?>\">\r\n                    <div class=\"drag-icon\" title=\"\">\r\n                        <i class=\"fa-solid fa-grip-vertical\"></i>\r\n                    </div>\r\n                    <img src=\"<?= $paymentIconFolder . $payment['icon'] ?>\" alt=\"Logo\" />\r\n                    <span class=\"payment-name\" contenteditable=\"true\"\r\n                        title=\"<?= translate(\"rename_payment_method\", $i18n) ?>\"><?= $payment['name'] ?></span>\r\n                    <?php\r\n                    if (!$inUse) {\r\n                        ?>\r\n                        <div class=\"delete-payment-method\" title=\"<?= translate('delete', $i18n) ?>\"\r\n                            data-paymentid=\"<?= $payment['id'] ?>\">x</div>\r\n                        <?php\r\n                    }\r\n                    ?>\r\n                </div>\r\n                <?php\r\n            }\r\n            ?>\r\n        </div>\r\n        <div class=\"settings-notes\">\r\n            <p>\r\n                <i class=\"fa-solid fa-circle-info\"></i>\r\n                <?= translate('payment_methods_info', $i18n) ?>\r\n            </p>\r\n            <p>\r\n                <i class=\"fa-solid fa-circle-info\"></i>\r\n                <?= translate('rename_payment_methods_info', $i18n) ?>\r\n            </p>\r\n        </div>\r\n        <header>\r\n            <h2 class=\"second-header\"><?= translate(\"add_custom_payment\", $i18n) ?></h2>\r\n        </header>\r\n        <div>\r\n            <form id=\"payments-form\">\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"text\" name=\"paymentname\" id=\"paymentname\" autocomplete=\"off\"\r\n                        placeholder=\"<?= translate('payment_method_name', $i18n) ?>\" onchange=\"setSearchButtonStatus()\"\r\n                        onkeypress=\"this.onchange();\" onpaste=\"this.onchange();\" oninput=\"this.onchange();\" />\r\n                    <label for=\"paymenticon\" class=\"icon-preview\">\r\n                        <img src=\"\" alt=\"<?= translate('logo_preview', $i18n) ?>\" id=\"form-icon\">\r\n                    </label>\r\n                    <div class=\"form-icon-search\">\r\n                        <input type=\"file\" id=\"paymenticon\" name=\"paymenticon\"\r\n                            accept=\"image/jpeg, image/png, image/gif, image/webp\" onchange=\"handleFileSelect(event)\"\r\n                            class=\"hidden-input\">\r\n                        <input type=\"hidden\" id=\"icon-url\" name=\"icon-url\">\r\n                        <div id=\"icon-search-button\" class=\"image-button medium disabled\"\r\n                            title=\"<?= translate('search_logo', $i18n) ?>\" onClick=\"searchPaymentIcon()\">\r\n                            <?php include \"images/siteicons/svg/websearch.php\"; ?>\r\n                        </div>\r\n                        <div id=\"icon-search-results\" class=\"icon-search\">\r\n                            <header>\r\n                                <span class=\"fa-solid fa-xmark close-icon-search\" onClick=\"closeIconSearch()\"></span>\r\n                            </header>\r\n                            <div id=\"icon-search-images\"></div>\r\n                        </div>\r\n                    </div>\r\n\r\n                    <input type=\"button\" class=\"button thin\" id=\"add-payment-button\" value=\"+\"\r\n                        title=\"<?= translate('add', $i18n) ?>\" id=\"addPayment\" onClick=\"addPaymentMethod()\" />\r\n                </div>\r\n            </form>\r\n        </div>\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('theme_settings', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-settings-theme\">\r\n            <div>\r\n                <h3><?= translate('theme', $i18n) ?></h3>\r\n                <div class=\"form-group-inline wrap\">\r\n                    <button type=\"button\"\r\n                        class=\"dark-theme-button capitalize <?= $settings['dark_theme'] == '0' ? 'selected' : '' ?>\"\r\n                        onClick=\"setDarkTheme('0')\" id=\"theme-light\">\r\n                        <i class=\"fa-solid fa-sun\"></i> <?= translate('light_theme', $i18n) ?>\r\n                    </button>\r\n                    <button type=\"button\"\r\n                        class=\"dark-theme-button capitalize <?= $settings['dark_theme'] == '1' ? 'selected' : '' ?>\"\r\n                        onClick=\"setDarkTheme('1')\" id=\"theme-dark\">\r\n                        <i class=\"fa-solid fa-moon\"></i> <?= translate('dark_theme', $i18n) ?>\r\n                    </button>\r\n                    <button type=\"button\"\r\n                        class=\"dark-theme-button capitalize <?= $settings['dark_theme'] == '2' ? 'selected' : '' ?>\"\r\n                        onClick=\"setDarkTheme('2')\" id=\"theme-automatic\">\r\n                        <i class=\"fa-solid fa-circle-half-stroke\"></i> <?= translate('automatic', $i18n) ?>\r\n                    </button>\r\n                </div>\r\n            </div>\r\n            <div>\r\n                <form class=\"theme-selector\">\r\n                    <h3><?= translate('colors', $i18n) ?></h3>\r\n                    <div class=\"form-group-inline wrap\">\r\n                        <div class=\"theme\">\r\n                            <input type=\"radio\" name=\"theme\" id=\"theme-blue\" value=\"blue\" onClick=\"setTheme('blue')\"\r\n                                <?= $settings['color_theme'] == 'blue' ? 'checked' : '' ?>>\r\n                            <label for=\"theme-blue\"\r\n                                class=\"theme-preview blue <?= $settings['color_theme'] == 'blue' ? 'is-selected' : '' ?>\">\r\n                                <span class=\"main-color\"></span>\r\n                                <span class=\"accent-color\"></span>\r\n                                <span class=\"hover-color\"></span>\r\n                            </label>\r\n                        </div>\r\n                        <div class=\"theme\">\r\n                            <input type=\"radio\" name=\"theme\" id=\"theme-green\" value=\"green\" onClick=\"setTheme('green')\"\r\n                                <?= $settings['color_theme'] == 'green' ? 'checked' : '' ?>>\r\n                            <label for=\"theme-green\"\r\n                                class=\"theme-preview green <?= $settings['color_theme'] == 'green' ? 'is-selected' : '' ?>\">\r\n                                <span class=\"main-color\"></span>\r\n                                <span class=\"accent-color\"></span>\r\n                                <span class=\"hover-color\"></span>\r\n                            </label>\r\n                        </div>\r\n                        <div class=\"theme\">\r\n                            <input type=\"radio\" name=\"theme\" id=\"theme-red\" value=\"red\" onClick=\"setTheme('red')\"\r\n                                <?= $settings['color_theme'] == 'red' ? 'checked' : '' ?>>\r\n                            <label for=\"theme-red\"\r\n                                class=\"theme-preview red <?= $settings['color_theme'] == 'red' ? 'is-selected' : '' ?>\">\r\n                                <span class=\"main-color\"></span>\r\n                                <span class=\"accent-color\"></span>\r\n                                <span class=\"hover-color\"></span>\r\n                            </label>\r\n                        </div>\r\n                        <div class=\"theme\">\r\n                            <input type=\"radio\" name=\"theme\" id=\"theme-yellow\" value=\"yellow\"\r\n                                onClick=\"setTheme('yellow')\" <?= $settings['color_theme'] == 'yellow' ? 'checked' : '' ?>>\r\n                            <label for=\"theme-yellow\"\r\n                                class=\"theme-preview yellow <?= $settings['color_theme'] == 'yellow' ? 'is-selected' : '' ?>\">\r\n                                <span class=\"main-color\"></span>\r\n                                <span class=\"accent-color\"></span>\r\n                                <span class=\"hover-color\"></span>\r\n                            </label>\r\n                        </div>\r\n                        <div class=\"theme\">\r\n                            <input type=\"radio\" name=\"theme\" id=\"theme-purple\" value=\"purple\"\r\n                                onClick=\"setTheme('purple')\" <?= $settings['color_theme'] == 'purple' ? 'checked' : '' ?>>\r\n                            <label for=\"theme-purple\"\r\n                                class=\"theme-preview purple <?= $settings['color_theme'] == 'purple' ? 'is-selected' : '' ?>\">\r\n                                <span class=\"main-color\"></span>\r\n                                <span class=\"accent-color\"></span>\r\n                                <span class=\"hover-color\"></span>\r\n                            </label>\r\n                        </div>\r\n                    </div>\r\n                </form>\r\n            </div>\r\n            <div>\r\n                <h3><?= translate('custom_colors', $i18n) ?></h3>\r\n                <div class=\"custom-colors wrap\">\r\n                    <div class=\"form-group-inline mobile-grow color-picker-button\">\r\n                        <input type=\"color\" id=\"mainColor\" name=\"mainColor\"\r\n                            value=\"<?= isset($settings['customColors']['main_color']) ? $settings['customColors']['main_color'] : '#FFFFFF' ?>\"\r\n                            class=\"color-picker fa-solid fa-eye-dropper\">\r\n                        <label for=\"mainColor\"><?= translate('main_color', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline mobile-grow color-picker-button\">\r\n                        <input type=\"color\" id=\"accentColor\" name=\"accentColor\"\r\n                            value=\"<?= isset($settings['customColors']['accent_color']) ? $settings['customColors']['accent_color'] : '#FFFFFF' ?>\"\r\n                            class=\"color-picker fa-solid fa-eye-dropper\">\r\n                        <label for=\"accentColor\"><?= translate('accent_color', $i18n) ?></label>\r\n                    </div>\r\n                    <div class=\"form-group-inline mobile-grow color-picker-button\">\r\n                        <input type=\"color\" id=\"hoverColor\" name=\"hoverColor\"\r\n                            value=\"<?= isset($settings['customColors']['hover_color']) ? $settings['customColors']['hover_color'] : '#FFFFFF' ?>\"\r\n                            class=\"color-picker fa-solid fa-eye-dropper\">\r\n                        <label for=\"hoverColor\"><?= translate('hover_color', $i18n) ?></label>\r\n                    </div>\r\n                </div>\r\n                <div class=\"custom-colors wrap\">\r\n                    <input type=\"button\" value=\"<?= translate('reset_custom_colors', $i18n) ?>\"\r\n                        onClick=\"resetCustomColors()\" class=\"secondary-button thin mobile-grow\" id=\"reset-colors\">\r\n                    <input type=\"button\" value=\"<?= translate('save_custom_colors', $i18n) ?>\"\r\n                        onClick=\"saveCustomColors()\" class=\"buton thin mobile-grow\" id=\"save-colors\">\r\n                </div>\r\n            </div>\r\n            <?php\r\n            if (!$demoMode) {\r\n                ?>\r\n                <div>\r\n                    <h3><?= translate('custom_css', $i18n) ?></h3>\r\n                    <div class=\"form-group\">\r\n                        <div class=\"form-group-inline\">\r\n                            <textarea name=\"customCss\" id=\"customCss\" placeholder=\"<?= translate('custom_css', $i18n) ?>\"\r\n                                class=\"thin\"><?= $settings['customCss'] ?? \"\" ?></textarea>\r\n                        </div>\r\n                        <div class=\"form-group-inline\">\r\n                            <input type=\"button\" value=\"<?= translate('save_custom_css', $i18n) ?>\"\r\n                                onClick=\"saveCustomCss()\" class=\"buton thin mobile-grow\" id=\"save-css\">\r\n                        </div>\r\n                    </div>\r\n                </div>\r\n                <?php\r\n            }\r\n            ?>\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('display_settings', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-settings-list\">\r\n            <h3><?= translate('price', $i18n) ?></h3>\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"monthlyprice\" name=\"monthlyprice\" onChange=\"setShowMonthlyPrice()\" <?php if ($settings['monthly_price'])\r\n                        echo 'checked'; ?>>\r\n                    <label for=\"monthlyprice\"><?= translate('calculate_monthly_price', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"convertcurrency\" name=\"convertcurrency\" onChange=\"setConvertCurrency()\"\r\n                        <?php\r\n                        if ($settings['convert_currency'])\r\n                            echo ' checked';\r\n                        if ($apiKey == \"\")\r\n                            echo ' disabled';\r\n                        ?>>\r\n                    <label for=\"convertcurrency\"><?= translate('convert_prices', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"showoriginalprice\" name=\"showoriginalprice\"\r\n                        onChange=\"setShowOriginalPrice()\" <?= $settings['show_original_price'] ? 'checked' : '' ?>>\r\n                    <label for=\"showoriginalprice\"><?= translate('show_original_price', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n            <h3><?= translate('experience', $i18n) ?></h3>\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"mobilenavigation\" name=\"mobilenavigation\"\r\n                        onChange=\"setMobileNavigation()\" <?= $settings['mobile_nav'] ? 'checked' : '' ?>>\r\n                    <label for=\"mobilenavigation\"><?= translate('use_mobile_navigation_bar', $i18n) ?></label>\r\n                </div>\r\n                <div class=\"mobile-nav-image\">\r\n                </div>\r\n            </div>\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"showsubscriptionprogress\" name=\"showsubscriptionprogress\"\r\n                        onChange=\"setShowSubscriptionProgress()\" <?= $settings['show_subscription_progress'] ? 'checked' : '' ?>>\r\n                    <label for=\"showsubscriptionprogress\"><?= translate('show_subscription_progress', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n            <h3><?= translate('disabled_subscriptions', $i18n) ?></h3>\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"disabledtobottom\" name=\"disabledtobottom\"\r\n                        onChange=\"setDisabledToBottom()\" <?= $settings['disabled_to_bottom'] ? 'checked' : '' ?>>\r\n                    <label\r\n                        for=\"disabledtobottom\"><?= translate('show_disabled_subscriptions_at_the_bottom', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"hidedisabled\" name=\"hidedisabled\" onChange=\"setHideDisabled()\"\r\n                        <?= $settings['hide_disabled'] ? 'checked' : '' ?>>\r\n                    <label for=\"hidedisabled\"><?= translate('hide_disabled_subscriptions', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n        </div>\r\n    </section>\r\n\r\n    <section class=\"account-section\">\r\n        <header>\r\n            <h2><?= translate('experimental_settings', $i18n) ?></h2>\r\n        </header>\r\n        <div class=\"account-settings-list\">\r\n            <div>\r\n                <div class=\"form-group-inline\">\r\n                    <input type=\"checkbox\" id=\"removebackground\" name=\"removebackground\"\r\n                        onChange=\"setRemoveBackground()\" <?= $settings['remove_background'] ? 'checked' : '' ?>>\r\n                    <label for=\"removebackground\"><?= translate('remove_background', $i18n) ?></label>\r\n                </div>\r\n            </div>\r\n        </div>\r\n        <div class=\"settings-notes\">\r\n            <p>\r\n                <i class=\"fa-solid fa-circle-info\"></i>\r\n                <?= translate('experimental_info', $i18n) ?>\r\n            </p>\r\n        </div>\r\n    </section>\r\n\r\n</section>\r\n<script src=\"scripts/settings.js?<?= $version ?>\"></script>\r\n<script src=\"scripts/theme.js?<?= $version ?>\"></script>\r\n<script src=\"scripts/notifications.js?<?= $version ?>\"></script>\r\n\r\n<?php\r\nrequire_once 'includes/footer.php';\r\n?>"
  },
  {
    "path": "startup.sh",
    "content": "#!/bin/sh\n\nset -euo pipefail\n\necho \"Startup script is running...\" > /var/log/startup.log\n\n# Default the PUID and PGID environment variables to 82, otherwise\n# set to the user defined ones.\nPUID=${PUID:-82}\nPGID=${PGID:-82}\n\n# Change the www-data user id and group id to be the user-specified ones\ngroupmod -o -g \"$PGID\" www-data\nusermod -o -u \"$PUID\" www-data\nchown -R www-data:www-data /var/www/html\nchown -R www-data:www-data /tmp\nchmod -R 770 /tmp\n\n# PIDs we’ll track\nPHP_FPM_PID=\nNGINX_PID=\nCROND_PID=\nshutdown_in_progress=0\n\nshutdown_once() {\n  exit_signal=$?\n  kill_signal=$(kill -l \"$exit_signal\" 2>/dev/null || echo \"$exit_signal\")\n\n  [ \"$shutdown_in_progress\" -eq 1 ] && return 0\n  shutdown_in_progress=1\n\n  echo \"Got signal: $kill_signal - Shutting down gracefully... \"\n  # nginx wants QUIT for graceful\n  nginx -s quit || true\n  # php-fpm graceful quit as well\n  [ -n \"${PHP_FPM_PID}\" ] && kill -QUIT \"${PHP_FPM_PID}\" 2>/dev/null || true\n  # cron can just get TERM\n  [ -n \"${CROND_PID}\" ] && kill -TERM \"${CROND_PID}\" 2>/dev/null || true\n  echo \"Graceful shutdown complete.\"\n}\n\n# Handle all common stop signals\ntrap 'shutdown_once' SIGTERM SIGINT SIGQUIT\n\n# Start both PHP-FPM and Nginx\necho \"Launching php-fpm\"\nphp-fpm -F &\nPHP_FPM_PID=$!\n\necho \"Launching crond\"\ncrond -f &\nCROND_PID=$!\n\necho \"Launching nginx\"\nnginx -g 'daemon off;' &\nNGINX_PID=$!\n\ntouch ~/startup.txt\n\n# Wait one second before running scripts\nsleep 1\n\n# Create database if it does not exist\n/usr/local/bin/php /var/www/html/endpoints/cronjobs/createdatabase.php\n\n# Perform any database migrations\n/usr/local/bin/php /var/www/html/endpoints/db/migrate.php\n\n# Change permissions on the database directory\nchmod -R 755 /var/www/html/db/\nchown -R www-data:www-data /var/www/html/db/\n\nmkdir -p /var/www/html/images/uploads/logos/avatars\n\n# Change permissions on the logos directory\nchmod -R 755 /var/www/html/images/uploads/logos\nchown -R www-data:www-data /var/www/html/images/uploads/logos\n\n# Remove crontab for the user\ncrontab -d -u root\n\n# Run updatenextpayment.php and wait for it to finish\n/usr/local/bin/php /var/www/html/endpoints/cronjobs/updatenextpayment.php\n\n# Run updateexchange.php\n/usr/local/bin/php /var/www/html/endpoints/cronjobs/updateexchange.php\n\n# Run checkforupdates.php\n/usr/local/bin/php /var/www/html/endpoints/cronjobs/checkforupdates.php\n\n# Essentially wait until all child processes exit\nwait\n"
  },
  {
    "path": "stats.php",
    "content": "<?php\r\nrequire_once 'includes/header.php';\r\n\r\n\r\n// Get code of main currency to display on statistics\r\n$query = \"SELECT c.code\r\n          FROM currencies c\r\n          INNER JOIN user u ON c.id = u.main_currency\r\n          WHERE u.id = :userId\";\r\n$stmt = $db->prepare($query);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n$result = $stmt->execute();\r\n$row = $result->fetchArray(SQLITE3_ASSOC);\r\n$code = $row['code'];\r\n\r\nrequire_once 'includes/stats_calculations.php';\r\n\r\n?>\r\n<section class=\"contain\">\r\n  <?php\r\n  if ($showCantConverErrorMessage) {\r\n    ?>\r\n    <div class=\"error-box\">\r\n      <div class=\"error-message\">\r\n        <i class=\"fa-solid fa-exclamation-circle\"></i>\r\n        <?= translate('cant_convert_currency', $i18n) ?>\r\n      </div>\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n  <div class=\"split-header\">\r\n    <h2>\r\n      <?= translate('general_statistics', $i18n) ?> <span class=\"header-subtitle\"><?= $statsSubtitle ?></span>\r\n    </h2>\r\n    <div class=\"filtermenu\">\r\n      <button class=\"button secondary-button\" id=\"filtermenu-button\">\r\n        <i class=\"fa-solid fa-filter\"></i>\r\n        <?= translate(\"filter\", $i18n) ?>\r\n      </button>\r\n      <div class=\"filtermenu-content\">\r\n        <?php\r\n        if (count($members) > 1) {\r\n          ?>\r\n          <div class=\"filtermenu-submenu\">\r\n            <div class=\"filter-title\" onClick=\"toggleSubMenu('member')\"><?= translate(\"member\", $i18n) ?></div>\r\n            <div class=\"filtermenu-submenu-content\" id=\"filter-member\">\r\n              <?php\r\n              foreach ($members as $member) {\r\n                if ($member['count'] == 0) {\r\n                  continue;\r\n                }\r\n                $selectedClass = '';\r\n                if (isset($_GET['member']) && $_GET['member'] == $member['id']) {\r\n                  $selectedClass = 'selected';\r\n                }\r\n                ?>\r\n                <div class=\"filter-item <?= $selectedClass ?>\" data-memberid=\"<?= $member['id'] ?>\"><?= $member['name'] ?>\r\n                </div>\r\n                <?php\r\n              }\r\n              ?>\r\n            </div>\r\n          </div>\r\n          <?php\r\n        }\r\n        ?>\r\n        <?php\r\n        if (count($categories) > 1) {\r\n          // sort categories by order\r\n          usort($categories, function ($a, $b) {\r\n            return $a['order'] - $b['order'];\r\n          });\r\n          ?>\r\n          <div class=\"filtermenu-submenu\">\r\n            <div class=\"filter-title\" onClick=\"toggleSubMenu('category')\"><?= translate(\"category\", $i18n) ?></div>\r\n            <div class=\"filtermenu-submenu-content\" id=\"filter-category\">\r\n              <?php\r\n              foreach ($categories as $category) {\r\n                if ($category['count'] > 0) {\r\n                  if ($category['name'] == \"No category\") {\r\n                    $category['name'] = translate(\"no_category\", $i18n);\r\n                  }\r\n                  $selectedClass = '';\r\n                  if (isset($_GET['category']) && $_GET['category'] == $category['id']) {\r\n                    $selectedClass = 'selected';\r\n                  }\r\n                  ?>\r\n                  <div class=\"filter-item <?= $selectedClass ?>\" data-categoryid=\"<?= $category['id'] ?>\">\r\n                    <?= $category['name'] ?>\r\n                  </div>\r\n                  <?php\r\n                }\r\n              }\r\n              ?>\r\n            </div>\r\n          </div>\r\n          <?php\r\n        }\r\n        ?>\r\n        <?php\r\n        if (count($paymentMethods) > 1) {\r\n\r\n          usort($paymentMethods, function ($a, $b) {\r\n            return $a['order'] <=> $b['order'];\r\n          });\r\n          ?>\r\n          <div class=\"filtermenu-submenu\">\r\n            <div class=\"filter-title\" onClick=\"toggleSubMenu('payment')\"><?= translate(\"payment_method\", $i18n) ?></div>\r\n            <div class=\"filtermenu-submenu-content\" id=\"filter-payment\">\r\n              <?php\r\n              foreach ($paymentMethods as $payment) {\r\n                if ($payment['count'] == 0) {\r\n                  continue;\r\n                }\r\n                $selectedClass = '';\r\n                if (isset($_GET['payment']) && $_GET['payment'] == $payment['id']) {\r\n                  $selectedClass = 'selected';\r\n                }\r\n                ?>\r\n                <div class=\"filter-item <?= $selectedClass ?>\" data-paymentid=\"<?= $payment['id'] ?>\">\r\n                  <?= $payment['name'] ?>\r\n                </div>\r\n                <?php\r\n              }\r\n              ?>\r\n            </div>\r\n          </div>\r\n          <?php\r\n        }\r\n        ?>\r\n        <?php\r\n        if (isset($_GET['member']) || isset($_GET['category']) || isset($_GET['payment'])) {\r\n          ?>\r\n          <div class=\"filtermenu-submenu\">\r\n            <div class=\"filter-title filter-clear\" onClick=\"clearFilters()\">\r\n              <i class=\"fa-solid fa-times-circle\"></i> <?= translate(\"clear\", $i18n) ?>\r\n            </div>\r\n          </div>\r\n          <?php\r\n        }\r\n        ?>\r\n      </div>\r\n    </div>\r\n  </div>\r\n  </div>\r\n  </div>\r\n  <div class=\"statistics\">\r\n    <div class=\"statistic\">\r\n      <span><?= $activeSubscriptions ?></span>\r\n      <div class=\"title\"><?= translate('active_subscriptions', $i18n) ?></div>\r\n    </div>\r\n    <div class=\"statistic\">\r\n      <span><?= CurrencyFormatter::format($totalCostPerMonth, $code) ?></span>\r\n      <div class=\"title\"><?= translate('monthly_cost', $i18n) ?></div>\r\n    </div>\r\n    <div class=\"statistic\">\r\n      <span><?= CurrencyFormatter::format($totalCostPerYear, $code) ?></span>\r\n      <div class=\"title\"><?= translate('yearly_cost', $i18n) ?></div>\r\n    </div>\r\n    <div class=\"statistic\">\r\n      <span><?= CurrencyFormatter::format($averageSubscriptionCost, $code) ?></span>\r\n      <div class=\"title\"><?= translate('average_monthly', $i18n) ?></div>\r\n    </div>\r\n    <div class=\"statistic short\">\r\n      <span><?= CurrencyFormatter::format($mostExpensiveSubscription['price'], $code) ?></span>\r\n      <div class=\"title\"><?= translate('most_expensive', $i18n) ?></div>\r\n      <?php\r\n      if (isset($mostExpensiveSubscription['logo']) && $mostExpensiveSubscription['logo'] != '') {\r\n        ?>\r\n        <div class=\"subtitle\">\r\n          <img src=\"images/uploads/logos/<?= $mostExpensiveSubscription['logo'] ?>\"\r\n            alt=\"<?= $mostExpensiveSubscription['name'] ?>\" title=\"<?= $mostExpensiveSubscription['name'] ?>\" />\r\n        </div>\r\n        <?php\r\n      } else if (isset($mostExpensiveSubscription['name']) && $mostExpensiveSubscription['name'] != '') {\r\n        ?>\r\n          <div class=\"subtitle\"><?= $mostExpensiveSubscription['name'] ?></div>\r\n        <?php\r\n      }\r\n      ?>\r\n    </div>\r\n    <div class=\"statistic\">\r\n      <span><?= CurrencyFormatter::format($amountDueThisMonth, $code) ?></span>\r\n      <div class=\"title\"><?= translate('amount_due', $i18n) ?></div>\r\n    </div>\r\n    <?php\r\n    if (isset($budgetUsed)) {\r\n      ?>\r\n      <div class=\"statistic\">\r\n        <span><?= number_format($budgetUsed, 2) ?>%</span>\r\n        <div class=\"title\"><?= translate('percentage_budget_used', $i18n) ?></div>\r\n      </div>\r\n      <?php\r\n    }\r\n    if (isset($budgetLeft)) {\r\n      ?>\r\n      <div class=\"statistic\">\r\n        <span><?= CurrencyFormatter::format($budgetLeft, $code) ?></span>\r\n        <div class=\"title\"><?= translate('budget_remaining', $i18n) ?></div>\r\n      </div>\r\n      <?php\r\n    }\r\n    if (isset($overBudgetAmount)) {\r\n      ?>\r\n      <div class=\"statistic\">\r\n        <span><?= CurrencyFormatter::format($overBudgetAmount, $code) ?></span>\r\n        <div class=\"title\"><?= translate('amount_over_budget', $i18n) ?></div>\r\n      </div>\r\n      <?php\r\n    }\r\n    if ($inactiveSubscriptions > 0) {\r\n      ?>\r\n      <div class=\"statistic\">\r\n        <span><?= $inactiveSubscriptions ?></span>\r\n        <div class=\"title\"><?= translate('inactive_subscriptions', $i18n) ?></div>\r\n      </div>\r\n      <?php\r\n      if ($totalSavingsPerMonth > 0) {\r\n        ?>\r\n        <div class=\"statistic\">\r\n          <span><?= CurrencyFormatter::format($totalSavingsPerMonth, $code) ?></span>\r\n          <div class=\"title\"><?= translate('monthly_savings', $i18n) ?></div>\r\n        </div>\r\n        <div class=\"statistic\">\r\n          <span><?= CurrencyFormatter::format($totalSavingsPerMonth * 12, $code) ?></span>\r\n          <div class=\"title\"><?= translate('yearly_savings', $i18n) ?></div>\r\n        </div>\r\n        <?php\r\n      }\r\n    }\r\n    ?>\r\n  </div>\r\n  <?php\r\n  $categoryDataPoints = [];\r\n  if (isset($categoryCost)) {\r\n    foreach ($categoryCost as $category) {\r\n      if ($category['cost'] != 0) {\r\n        $categoryDataPoints[] = [\r\n          \"label\" => html_entity_decode($category['name']),\r\n          \"y\" => $category[\"cost\"],\r\n        ];\r\n      }\r\n    }\r\n  }\r\n\r\n  $showCategoryCostGraph = count($categoryDataPoints) > 1;\r\n\r\n  $memberDataPoints = [];\r\n  if (isset($memberCost)) {\r\n    foreach ($memberCost as $member) {\r\n      if ($member['cost'] != 0) {\r\n        $memberDataPoints[] = [\r\n          \"label\" => html_entity_decode($member['name']),\r\n          \"y\" => $member[\"cost\"],\r\n        ];\r\n\r\n      }\r\n    }\r\n  }\r\n\r\n  $showMemberCostGraph = count($memberDataPoints) > 1;\r\n\r\n  $paymentMethodDataPoints = [];\r\n  foreach ($paymentMethodsCount as $paymentMethod) {\r\n    if ($paymentMethod['count'] != 0) {\r\n      $paymentMethodDataPoints[] = [\r\n        \"label\" => html_entity_decode($paymentMethod['name']),\r\n        \"y\" => $paymentMethod[\"count\"],\r\n      ];\r\n    }\r\n  }\r\n\r\n  $showPaymentMethodsGraph = count($paymentMethodDataPoints) > 1;\r\n  if ($showCategoryCostGraph || $showMemberCostGraph || $showPaymentMethodsGraph || $showTotalMonthlyCostGraph || $showVsBudgetGraph) {\r\n    ?>\r\n    <h2><?= translate('split_views', $i18n) ?></h2>\r\n    <div class=\"graphs\">\r\n      <?php\r\n\r\n      if ($showTotalMonthlyCostGraph) {\r\n        ?>\r\n        <section class=\"graph x2\">\r\n          <header>\r\n            <?= translate('total_cost_trend', $i18n) ?>\r\n            <div class=\"sub-header\">(<?= translate('monthly_cost', $i18n) ?>)</div>\r\n          </header>\r\n          <canvas id=\"totalMonthlyCostChart\" style=\"height: 370px; width: 100%; max-height: 370px;\"></canvas>\r\n        </section>\r\n        <?php\r\n      }\r\n\r\n      if ($showMemberCostGraph) {\r\n        ?>\r\n        <section class=\"graph\">\r\n          <header>\r\n            <?= translate('household_split', $i18n) ?>\r\n            <div class=\"sub-header\">(<?= translate('monthly_cost', $i18n) ?>)</div>\r\n          </header>\r\n          <canvas id=\"memberSplitChart\"></canvas>\r\n        </section>\r\n        <?php\r\n      }\r\n\r\n      if ($showCategoryCostGraph) {\r\n        ?>\r\n        <section class=\"graph\">\r\n          <header>\r\n            <?= translate('category_split', $i18n) ?>\r\n            <div class=\"sub-header\">(<?= translate('monthly_cost', $i18n) ?>)</div>\r\n          </header>\r\n          <canvas id=\"categorySplitChart\" style=\"height: 370px; width: 100%;\"></canvas>\r\n        </section>\r\n        <?php\r\n      }\r\n\r\n      if ($showPaymentMethodsGraph) {\r\n        ?>\r\n        <section class=\"graph\">\r\n          <header>\r\n            <?= translate('payment_method_split', $i18n) ?>\r\n          </header>\r\n          <canvas id=\"paymentMethidSplitChart\" style=\"height: 370px; width: 100%;\"></canvas>\r\n        </section>\r\n        <?php\r\n      }\r\n\r\n      if ($showVsBudgetGraph) {\r\n        ?>\r\n        <section class=\"graph\">\r\n          <header>\r\n            <?= translate('cost_vs_budget', $i18n) ?> (<?= CurrencyFormatter::format($budget, $code) ?>)\r\n          </header>\r\n          <canvas id=\"budgetVsCostChart\" style=\"height: 370px; width: 100%;\"></canvas>\r\n        </section>\r\n        <?php\r\n      }\r\n\r\n      ?>\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n\r\n</section>\r\n<?php\r\nif ($showCategoryCostGraph || $showMemberCostGraph || $showPaymentMethodsGraph || $showTotalMonthlyCostGraph || $showVsBudgetGraph) {\r\n  ?>\r\n  <script src=\"scripts/libs/chart.js\"></script>\r\n  <script type=\"text/javascript\">\r\n    window.onload = function () {\r\n      loadLineGraph(\"totalMonthlyCostChart\", <?php echo json_encode($totalMonthlyCostDataPoints, JSON_NUMERIC_CHECK); ?>, \"<?= $code ?>\", \"<?= $showTotalMonthlyCostGraph ?>\");\r\n      loadGraph(\"categorySplitChart\", <?php echo json_encode($categoryDataPoints, JSON_NUMERIC_CHECK); ?>, \"<?= $code ?>\", <?= $showCategoryCostGraph ?>);\r\n      loadGraph(\"memberSplitChart\", <?php echo json_encode($memberDataPoints, JSON_NUMERIC_CHECK); ?>, \"<?= $code ?>\", <?= $showMemberCostGraph ?>);\r\n      loadGraph(\"paymentMethidSplitChart\", <?php echo json_encode($paymentMethodDataPoints, JSON_NUMERIC_CHECK); ?>, \"\", <?= $showPaymentMethodsGraph ?>);\r\n      loadGraph(\"budgetVsCostChart\", <?php echo json_encode($vsBudgetDataPoints, JSON_NUMERIC_CHECK); ?>, \"<?= $code ?>\", <?= $showVsBudgetGraph ?>);\r\n    }\r\n  </script>\r\n  <?php\r\n}\r\n?>\r\n<script src=\"scripts/stats.js?<?= $version ?>\"></script>\r\n<?php\r\nrequire_once 'includes/footer.php';\r\n?>"
  },
  {
    "path": "styles/barlow.css",
    "content": "/* vietnamese */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 300;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3p-ks6FospT4.woff2) format('woff2');\n  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 300;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3p-ks6VospT4.woff2) format('woff2');\n  unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 300;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3p-ks51os.woff2) format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* vietnamese */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 400;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_A8s52Hs.woff2) format('woff2');\n  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 400;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_Ass52Hs.woff2) format('woff2');\n  unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 400;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_DMs5.woff2) format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* vietnamese */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 500;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3_-gs6FospT4.woff2) format('woff2');\n  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 500;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3_-gs6VospT4.woff2) format('woff2');\n  unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 500;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3_-gs51os.woff2) format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* vietnamese */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 600;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E30-8s6FospT4.woff2) format('woff2');\n  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 600;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E30-8s6VospT4.woff2) format('woff2');\n  unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 600;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E30-8s51os.woff2) format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* vietnamese */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 700;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3t-4s6FospT4.woff2) format('woff2');\n  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 700;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3t-4s6VospT4.woff2) format('woff2');\n  unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Barlow';\n  font-style: normal;\n  font-weight: 700;\n  src: url(https://fonts.gstatic.com/s/barlow/v12/7cHqv4kjgoGqM7E3t-4s51os.woff2) format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n"
  },
  {
    "path": "styles/brands.css",
    "content": "/*!\n * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com\n * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)\n * Copyright 2024 Fonticons, Inc.\n */\n:root, :host {\n  --fa-style-family-brands: 'Font Awesome 6 Brands';\n  --fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }\n\n@font-face {\n  font-family: 'Font Awesome 6 Brands';\n  font-style: normal;\n  font-weight: 400;\n  font-display: block;\n  src: url(\"../webfonts/fa-brands-400.woff2\") format(\"woff2\"), url(\"../webfonts/fa-brands-400.ttf\") format(\"truetype\"); }\n\n.fab,\n.fa-brands {\n  font-weight: 400; }\n\n.fa-monero:before {\n  content: \"\\f3d0\"; }\n\n.fa-hooli:before {\n  content: \"\\f427\"; }\n\n.fa-yelp:before {\n  content: \"\\f1e9\"; }\n\n.fa-cc-visa:before {\n  content: \"\\f1f0\"; }\n\n.fa-lastfm:before {\n  content: \"\\f202\"; }\n\n.fa-shopware:before {\n  content: \"\\f5b5\"; }\n\n.fa-creative-commons-nc:before {\n  content: \"\\f4e8\"; }\n\n.fa-aws:before {\n  content: \"\\f375\"; }\n\n.fa-redhat:before {\n  content: \"\\f7bc\"; }\n\n.fa-yoast:before {\n  content: \"\\f2b1\"; }\n\n.fa-cloudflare:before {\n  content: \"\\e07d\"; }\n\n.fa-ups:before {\n  content: \"\\f7e0\"; }\n\n.fa-pixiv:before {\n  content: \"\\e640\"; }\n\n.fa-wpexplorer:before {\n  content: \"\\f2de\"; }\n\n.fa-dyalog:before {\n  content: \"\\f399\"; }\n\n.fa-bity:before {\n  content: \"\\f37a\"; }\n\n.fa-stackpath:before {\n  content: \"\\f842\"; }\n\n.fa-buysellads:before {\n  content: \"\\f20d\"; }\n\n.fa-first-order:before {\n  content: \"\\f2b0\"; }\n\n.fa-modx:before {\n  content: \"\\f285\"; }\n\n.fa-guilded:before {\n  content: \"\\e07e\"; }\n\n.fa-vnv:before {\n  content: \"\\f40b\"; }\n\n.fa-square-js:before {\n  content: \"\\f3b9\"; }\n\n.fa-js-square:before {\n  content: \"\\f3b9\"; }\n\n.fa-microsoft:before {\n  content: \"\\f3ca\"; }\n\n.fa-qq:before {\n  content: \"\\f1d6\"; }\n\n.fa-orcid:before {\n  content: \"\\f8d2\"; }\n\n.fa-java:before {\n  content: \"\\f4e4\"; }\n\n.fa-invision:before {\n  content: \"\\f7b0\"; }\n\n.fa-creative-commons-pd-alt:before {\n  content: \"\\f4ed\"; }\n\n.fa-centercode:before {\n  content: \"\\f380\"; }\n\n.fa-glide-g:before {\n  content: \"\\f2a6\"; }\n\n.fa-drupal:before {\n  content: \"\\f1a9\"; }\n\n.fa-jxl:before {\n  content: \"\\e67b\"; }\n\n.fa-hire-a-helper:before {\n  content: \"\\f3b0\"; }\n\n.fa-creative-commons-by:before {\n  content: \"\\f4e7\"; }\n\n.fa-unity:before {\n  content: \"\\e049\"; }\n\n.fa-whmcs:before {\n  content: \"\\f40d\"; }\n\n.fa-rocketchat:before {\n  content: \"\\f3e8\"; }\n\n.fa-vk:before {\n  content: \"\\f189\"; }\n\n.fa-untappd:before {\n  content: \"\\f405\"; }\n\n.fa-mailchimp:before {\n  content: \"\\f59e\"; }\n\n.fa-css3-alt:before {\n  content: \"\\f38b\"; }\n\n.fa-square-reddit:before {\n  content: \"\\f1a2\"; }\n\n.fa-reddit-square:before {\n  content: \"\\f1a2\"; }\n\n.fa-vimeo-v:before {\n  content: \"\\f27d\"; }\n\n.fa-contao:before {\n  content: \"\\f26d\"; }\n\n.fa-square-font-awesome:before {\n  content: \"\\e5ad\"; }\n\n.fa-deskpro:before {\n  content: \"\\f38f\"; }\n\n.fa-brave:before {\n  content: \"\\e63c\"; }\n\n.fa-sistrix:before {\n  content: \"\\f3ee\"; }\n\n.fa-square-instagram:before {\n  content: \"\\e055\"; }\n\n.fa-instagram-square:before {\n  content: \"\\e055\"; }\n\n.fa-battle-net:before {\n  content: \"\\f835\"; }\n\n.fa-the-red-yeti:before {\n  content: \"\\f69d\"; }\n\n.fa-square-hacker-news:before {\n  content: \"\\f3af\"; }\n\n.fa-hacker-news-square:before {\n  content: \"\\f3af\"; }\n\n.fa-edge:before {\n  content: \"\\f282\"; }\n\n.fa-threads:before {\n  content: \"\\e618\"; }\n\n.fa-napster:before {\n  content: \"\\f3d2\"; }\n\n.fa-square-snapchat:before {\n  content: \"\\f2ad\"; }\n\n.fa-snapchat-square:before {\n  content: \"\\f2ad\"; }\n\n.fa-google-plus-g:before {\n  content: \"\\f0d5\"; }\n\n.fa-artstation:before {\n  content: \"\\f77a\"; }\n\n.fa-markdown:before {\n  content: \"\\f60f\"; }\n\n.fa-sourcetree:before {\n  content: \"\\f7d3\"; }\n\n.fa-google-plus:before {\n  content: \"\\f2b3\"; }\n\n.fa-diaspora:before {\n  content: \"\\f791\"; }\n\n.fa-foursquare:before {\n  content: \"\\f180\"; }\n\n.fa-stack-overflow:before {\n  content: \"\\f16c\"; }\n\n.fa-github-alt:before {\n  content: \"\\f113\"; }\n\n.fa-phoenix-squadron:before {\n  content: \"\\f511\"; }\n\n.fa-pagelines:before {\n  content: \"\\f18c\"; }\n\n.fa-algolia:before {\n  content: \"\\f36c\"; }\n\n.fa-red-river:before {\n  content: \"\\f3e3\"; }\n\n.fa-creative-commons-sa:before {\n  content: \"\\f4ef\"; }\n\n.fa-safari:before {\n  content: \"\\f267\"; }\n\n.fa-google:before {\n  content: \"\\f1a0\"; }\n\n.fa-square-font-awesome-stroke:before {\n  content: \"\\f35c\"; }\n\n.fa-font-awesome-alt:before {\n  content: \"\\f35c\"; }\n\n.fa-atlassian:before {\n  content: \"\\f77b\"; }\n\n.fa-linkedin-in:before {\n  content: \"\\f0e1\"; }\n\n.fa-digital-ocean:before {\n  content: \"\\f391\"; }\n\n.fa-nimblr:before {\n  content: \"\\f5a8\"; }\n\n.fa-chromecast:before {\n  content: \"\\f838\"; }\n\n.fa-evernote:before {\n  content: \"\\f839\"; }\n\n.fa-hacker-news:before {\n  content: \"\\f1d4\"; }\n\n.fa-creative-commons-sampling:before {\n  content: \"\\f4f0\"; }\n\n.fa-adversal:before {\n  content: \"\\f36a\"; }\n\n.fa-creative-commons:before {\n  content: \"\\f25e\"; }\n\n.fa-watchman-monitoring:before {\n  content: \"\\e087\"; }\n\n.fa-fonticons:before {\n  content: \"\\f280\"; }\n\n.fa-weixin:before {\n  content: \"\\f1d7\"; }\n\n.fa-shirtsinbulk:before {\n  content: \"\\f214\"; }\n\n.fa-codepen:before {\n  content: \"\\f1cb\"; }\n\n.fa-git-alt:before {\n  content: \"\\f841\"; }\n\n.fa-lyft:before {\n  content: \"\\f3c3\"; }\n\n.fa-rev:before {\n  content: \"\\f5b2\"; }\n\n.fa-windows:before {\n  content: \"\\f17a\"; }\n\n.fa-wizards-of-the-coast:before {\n  content: \"\\f730\"; }\n\n.fa-square-viadeo:before {\n  content: \"\\f2aa\"; }\n\n.fa-viadeo-square:before {\n  content: \"\\f2aa\"; }\n\n.fa-meetup:before {\n  content: \"\\f2e0\"; }\n\n.fa-centos:before {\n  content: \"\\f789\"; }\n\n.fa-adn:before {\n  content: \"\\f170\"; }\n\n.fa-cloudsmith:before {\n  content: \"\\f384\"; }\n\n.fa-opensuse:before {\n  content: \"\\e62b\"; }\n\n.fa-pied-piper-alt:before {\n  content: \"\\f1a8\"; }\n\n.fa-square-dribbble:before {\n  content: \"\\f397\"; }\n\n.fa-dribbble-square:before {\n  content: \"\\f397\"; }\n\n.fa-codiepie:before {\n  content: \"\\f284\"; }\n\n.fa-node:before {\n  content: \"\\f419\"; }\n\n.fa-mix:before {\n  content: \"\\f3cb\"; }\n\n.fa-steam:before {\n  content: \"\\f1b6\"; }\n\n.fa-cc-apple-pay:before {\n  content: \"\\f416\"; }\n\n.fa-scribd:before {\n  content: \"\\f28a\"; }\n\n.fa-debian:before {\n  content: \"\\e60b\"; }\n\n.fa-openid:before {\n  content: \"\\f19b\"; }\n\n.fa-instalod:before {\n  content: \"\\e081\"; }\n\n.fa-expeditedssl:before {\n  content: \"\\f23e\"; }\n\n.fa-sellcast:before {\n  content: \"\\f2da\"; }\n\n.fa-square-twitter:before {\n  content: \"\\f081\"; }\n\n.fa-twitter-square:before {\n  content: \"\\f081\"; }\n\n.fa-r-project:before {\n  content: \"\\f4f7\"; }\n\n.fa-delicious:before {\n  content: \"\\f1a5\"; }\n\n.fa-freebsd:before {\n  content: \"\\f3a4\"; }\n\n.fa-vuejs:before {\n  content: \"\\f41f\"; }\n\n.fa-accusoft:before {\n  content: \"\\f369\"; }\n\n.fa-ioxhost:before {\n  content: \"\\f208\"; }\n\n.fa-fonticons-fi:before {\n  content: \"\\f3a2\"; }\n\n.fa-app-store:before {\n  content: \"\\f36f\"; }\n\n.fa-cc-mastercard:before {\n  content: \"\\f1f1\"; }\n\n.fa-itunes-note:before {\n  content: \"\\f3b5\"; }\n\n.fa-golang:before {\n  content: \"\\e40f\"; }\n\n.fa-kickstarter:before {\n  content: \"\\f3bb\"; }\n\n.fa-square-kickstarter:before {\n  content: \"\\f3bb\"; }\n\n.fa-grav:before {\n  content: \"\\f2d6\"; }\n\n.fa-weibo:before {\n  content: \"\\f18a\"; }\n\n.fa-uncharted:before {\n  content: \"\\e084\"; }\n\n.fa-firstdraft:before {\n  content: \"\\f3a1\"; }\n\n.fa-square-youtube:before {\n  content: \"\\f431\"; }\n\n.fa-youtube-square:before {\n  content: \"\\f431\"; }\n\n.fa-wikipedia-w:before {\n  content: \"\\f266\"; }\n\n.fa-wpressr:before {\n  content: \"\\f3e4\"; }\n\n.fa-rendact:before {\n  content: \"\\f3e4\"; }\n\n.fa-angellist:before {\n  content: \"\\f209\"; }\n\n.fa-galactic-republic:before {\n  content: \"\\f50c\"; }\n\n.fa-nfc-directional:before {\n  content: \"\\e530\"; }\n\n.fa-skype:before {\n  content: \"\\f17e\"; }\n\n.fa-joget:before {\n  content: \"\\f3b7\"; }\n\n.fa-fedora:before {\n  content: \"\\f798\"; }\n\n.fa-stripe-s:before {\n  content: \"\\f42a\"; }\n\n.fa-meta:before {\n  content: \"\\e49b\"; }\n\n.fa-laravel:before {\n  content: \"\\f3bd\"; }\n\n.fa-hotjar:before {\n  content: \"\\f3b1\"; }\n\n.fa-bluetooth-b:before {\n  content: \"\\f294\"; }\n\n.fa-square-letterboxd:before {\n  content: \"\\e62e\"; }\n\n.fa-sticker-mule:before {\n  content: \"\\f3f7\"; }\n\n.fa-creative-commons-zero:before {\n  content: \"\\f4f3\"; }\n\n.fa-hips:before {\n  content: \"\\f452\"; }\n\n.fa-behance:before {\n  content: \"\\f1b4\"; }\n\n.fa-reddit:before {\n  content: \"\\f1a1\"; }\n\n.fa-discord:before {\n  content: \"\\f392\"; }\n\n.fa-chrome:before {\n  content: \"\\f268\"; }\n\n.fa-app-store-ios:before {\n  content: \"\\f370\"; }\n\n.fa-cc-discover:before {\n  content: \"\\f1f2\"; }\n\n.fa-wpbeginner:before {\n  content: \"\\f297\"; }\n\n.fa-confluence:before {\n  content: \"\\f78d\"; }\n\n.fa-shoelace:before {\n  content: \"\\e60c\"; }\n\n.fa-mdb:before {\n  content: \"\\f8ca\"; }\n\n.fa-dochub:before {\n  content: \"\\f394\"; }\n\n.fa-accessible-icon:before {\n  content: \"\\f368\"; }\n\n.fa-ebay:before {\n  content: \"\\f4f4\"; }\n\n.fa-amazon:before {\n  content: \"\\f270\"; }\n\n.fa-unsplash:before {\n  content: \"\\e07c\"; }\n\n.fa-yarn:before {\n  content: \"\\f7e3\"; }\n\n.fa-square-steam:before {\n  content: \"\\f1b7\"; }\n\n.fa-steam-square:before {\n  content: \"\\f1b7\"; }\n\n.fa-500px:before {\n  content: \"\\f26e\"; }\n\n.fa-square-vimeo:before {\n  content: \"\\f194\"; }\n\n.fa-vimeo-square:before {\n  content: \"\\f194\"; }\n\n.fa-asymmetrik:before {\n  content: \"\\f372\"; }\n\n.fa-font-awesome:before {\n  content: \"\\f2b4\"; }\n\n.fa-font-awesome-flag:before {\n  content: \"\\f2b4\"; }\n\n.fa-font-awesome-logo-full:before {\n  content: \"\\f2b4\"; }\n\n.fa-gratipay:before {\n  content: \"\\f184\"; }\n\n.fa-apple:before {\n  content: \"\\f179\"; }\n\n.fa-hive:before {\n  content: \"\\e07f\"; }\n\n.fa-gitkraken:before {\n  content: \"\\f3a6\"; }\n\n.fa-keybase:before {\n  content: \"\\f4f5\"; }\n\n.fa-apple-pay:before {\n  content: \"\\f415\"; }\n\n.fa-padlet:before {\n  content: \"\\e4a0\"; }\n\n.fa-amazon-pay:before {\n  content: \"\\f42c\"; }\n\n.fa-square-github:before {\n  content: \"\\f092\"; }\n\n.fa-github-square:before {\n  content: \"\\f092\"; }\n\n.fa-stumbleupon:before {\n  content: \"\\f1a4\"; }\n\n.fa-fedex:before {\n  content: \"\\f797\"; }\n\n.fa-phoenix-framework:before {\n  content: \"\\f3dc\"; }\n\n.fa-shopify:before {\n  content: \"\\e057\"; }\n\n.fa-neos:before {\n  content: \"\\f612\"; }\n\n.fa-square-threads:before {\n  content: \"\\e619\"; }\n\n.fa-hackerrank:before {\n  content: \"\\f5f7\"; }\n\n.fa-researchgate:before {\n  content: \"\\f4f8\"; }\n\n.fa-swift:before {\n  content: \"\\f8e1\"; }\n\n.fa-angular:before {\n  content: \"\\f420\"; }\n\n.fa-speakap:before {\n  content: \"\\f3f3\"; }\n\n.fa-angrycreative:before {\n  content: \"\\f36e\"; }\n\n.fa-y-combinator:before {\n  content: \"\\f23b\"; }\n\n.fa-empire:before {\n  content: \"\\f1d1\"; }\n\n.fa-envira:before {\n  content: \"\\f299\"; }\n\n.fa-google-scholar:before {\n  content: \"\\e63b\"; }\n\n.fa-square-gitlab:before {\n  content: \"\\e5ae\"; }\n\n.fa-gitlab-square:before {\n  content: \"\\e5ae\"; }\n\n.fa-studiovinari:before {\n  content: \"\\f3f8\"; }\n\n.fa-pied-piper:before {\n  content: \"\\f2ae\"; }\n\n.fa-wordpress:before {\n  content: \"\\f19a\"; }\n\n.fa-product-hunt:before {\n  content: \"\\f288\"; }\n\n.fa-firefox:before {\n  content: \"\\f269\"; }\n\n.fa-linode:before {\n  content: \"\\f2b8\"; }\n\n.fa-goodreads:before {\n  content: \"\\f3a8\"; }\n\n.fa-square-odnoklassniki:before {\n  content: \"\\f264\"; }\n\n.fa-odnoklassniki-square:before {\n  content: \"\\f264\"; }\n\n.fa-jsfiddle:before {\n  content: \"\\f1cc\"; }\n\n.fa-sith:before {\n  content: \"\\f512\"; }\n\n.fa-themeisle:before {\n  content: \"\\f2b2\"; }\n\n.fa-page4:before {\n  content: \"\\f3d7\"; }\n\n.fa-hashnode:before {\n  content: \"\\e499\"; }\n\n.fa-react:before {\n  content: \"\\f41b\"; }\n\n.fa-cc-paypal:before {\n  content: \"\\f1f4\"; }\n\n.fa-squarespace:before {\n  content: \"\\f5be\"; }\n\n.fa-cc-stripe:before {\n  content: \"\\f1f5\"; }\n\n.fa-creative-commons-share:before {\n  content: \"\\f4f2\"; }\n\n.fa-bitcoin:before {\n  content: \"\\f379\"; }\n\n.fa-keycdn:before {\n  content: \"\\f3ba\"; }\n\n.fa-opera:before {\n  content: \"\\f26a\"; }\n\n.fa-itch-io:before {\n  content: \"\\f83a\"; }\n\n.fa-umbraco:before {\n  content: \"\\f8e8\"; }\n\n.fa-galactic-senate:before {\n  content: \"\\f50d\"; }\n\n.fa-ubuntu:before {\n  content: \"\\f7df\"; }\n\n.fa-draft2digital:before {\n  content: \"\\f396\"; }\n\n.fa-stripe:before {\n  content: \"\\f429\"; }\n\n.fa-houzz:before {\n  content: \"\\f27c\"; }\n\n.fa-gg:before {\n  content: \"\\f260\"; }\n\n.fa-dhl:before {\n  content: \"\\f790\"; }\n\n.fa-square-pinterest:before {\n  content: \"\\f0d3\"; }\n\n.fa-pinterest-square:before {\n  content: \"\\f0d3\"; }\n\n.fa-xing:before {\n  content: \"\\f168\"; }\n\n.fa-blackberry:before {\n  content: \"\\f37b\"; }\n\n.fa-creative-commons-pd:before {\n  content: \"\\f4ec\"; }\n\n.fa-playstation:before {\n  content: \"\\f3df\"; }\n\n.fa-quinscape:before {\n  content: \"\\f459\"; }\n\n.fa-less:before {\n  content: \"\\f41d\"; }\n\n.fa-blogger-b:before {\n  content: \"\\f37d\"; }\n\n.fa-opencart:before {\n  content: \"\\f23d\"; }\n\n.fa-vine:before {\n  content: \"\\f1ca\"; }\n\n.fa-signal-messenger:before {\n  content: \"\\e663\"; }\n\n.fa-paypal:before {\n  content: \"\\f1ed\"; }\n\n.fa-gitlab:before {\n  content: \"\\f296\"; }\n\n.fa-typo3:before {\n  content: \"\\f42b\"; }\n\n.fa-reddit-alien:before {\n  content: \"\\f281\"; }\n\n.fa-yahoo:before {\n  content: \"\\f19e\"; }\n\n.fa-dailymotion:before {\n  content: \"\\e052\"; }\n\n.fa-affiliatetheme:before {\n  content: \"\\f36b\"; }\n\n.fa-pied-piper-pp:before {\n  content: \"\\f1a7\"; }\n\n.fa-bootstrap:before {\n  content: \"\\f836\"; }\n\n.fa-odnoklassniki:before {\n  content: \"\\f263\"; }\n\n.fa-nfc-symbol:before {\n  content: \"\\e531\"; }\n\n.fa-mintbit:before {\n  content: \"\\e62f\"; }\n\n.fa-ethereum:before {\n  content: \"\\f42e\"; }\n\n.fa-speaker-deck:before {\n  content: \"\\f83c\"; }\n\n.fa-creative-commons-nc-eu:before {\n  content: \"\\f4e9\"; }\n\n.fa-patreon:before {\n  content: \"\\f3d9\"; }\n\n.fa-avianex:before {\n  content: \"\\f374\"; }\n\n.fa-ello:before {\n  content: \"\\f5f1\"; }\n\n.fa-gofore:before {\n  content: \"\\f3a7\"; }\n\n.fa-bimobject:before {\n  content: \"\\f378\"; }\n\n.fa-brave-reverse:before {\n  content: \"\\e63d\"; }\n\n.fa-facebook-f:before {\n  content: \"\\f39e\"; }\n\n.fa-square-google-plus:before {\n  content: \"\\f0d4\"; }\n\n.fa-google-plus-square:before {\n  content: \"\\f0d4\"; }\n\n.fa-web-awesome:before {\n  content: \"\\e682\"; }\n\n.fa-mandalorian:before {\n  content: \"\\f50f\"; }\n\n.fa-first-order-alt:before {\n  content: \"\\f50a\"; }\n\n.fa-osi:before {\n  content: \"\\f41a\"; }\n\n.fa-google-wallet:before {\n  content: \"\\f1ee\"; }\n\n.fa-d-and-d-beyond:before {\n  content: \"\\f6ca\"; }\n\n.fa-periscope:before {\n  content: \"\\f3da\"; }\n\n.fa-fulcrum:before {\n  content: \"\\f50b\"; }\n\n.fa-cloudscale:before {\n  content: \"\\f383\"; }\n\n.fa-forumbee:before {\n  content: \"\\f211\"; }\n\n.fa-mizuni:before {\n  content: \"\\f3cc\"; }\n\n.fa-schlix:before {\n  content: \"\\f3ea\"; }\n\n.fa-square-xing:before {\n  content: \"\\f169\"; }\n\n.fa-xing-square:before {\n  content: \"\\f169\"; }\n\n.fa-bandcamp:before {\n  content: \"\\f2d5\"; }\n\n.fa-wpforms:before {\n  content: \"\\f298\"; }\n\n.fa-cloudversify:before {\n  content: \"\\f385\"; }\n\n.fa-usps:before {\n  content: \"\\f7e1\"; }\n\n.fa-megaport:before {\n  content: \"\\f5a3\"; }\n\n.fa-magento:before {\n  content: \"\\f3c4\"; }\n\n.fa-spotify:before {\n  content: \"\\f1bc\"; }\n\n.fa-optin-monster:before {\n  content: \"\\f23c\"; }\n\n.fa-fly:before {\n  content: \"\\f417\"; }\n\n.fa-aviato:before {\n  content: \"\\f421\"; }\n\n.fa-itunes:before {\n  content: \"\\f3b4\"; }\n\n.fa-cuttlefish:before {\n  content: \"\\f38c\"; }\n\n.fa-blogger:before {\n  content: \"\\f37c\"; }\n\n.fa-flickr:before {\n  content: \"\\f16e\"; }\n\n.fa-viber:before {\n  content: \"\\f409\"; }\n\n.fa-soundcloud:before {\n  content: \"\\f1be\"; }\n\n.fa-digg:before {\n  content: \"\\f1a6\"; }\n\n.fa-tencent-weibo:before {\n  content: \"\\f1d5\"; }\n\n.fa-letterboxd:before {\n  content: \"\\e62d\"; }\n\n.fa-symfony:before {\n  content: \"\\f83d\"; }\n\n.fa-maxcdn:before {\n  content: \"\\f136\"; }\n\n.fa-etsy:before {\n  content: \"\\f2d7\"; }\n\n.fa-facebook-messenger:before {\n  content: \"\\f39f\"; }\n\n.fa-audible:before {\n  content: \"\\f373\"; }\n\n.fa-think-peaks:before {\n  content: \"\\f731\"; }\n\n.fa-bilibili:before {\n  content: \"\\e3d9\"; }\n\n.fa-erlang:before {\n  content: \"\\f39d\"; }\n\n.fa-x-twitter:before {\n  content: \"\\e61b\"; }\n\n.fa-cotton-bureau:before {\n  content: \"\\f89e\"; }\n\n.fa-dashcube:before {\n  content: \"\\f210\"; }\n\n.fa-42-group:before {\n  content: \"\\e080\"; }\n\n.fa-innosoft:before {\n  content: \"\\e080\"; }\n\n.fa-stack-exchange:before {\n  content: \"\\f18d\"; }\n\n.fa-elementor:before {\n  content: \"\\f430\"; }\n\n.fa-square-pied-piper:before {\n  content: \"\\e01e\"; }\n\n.fa-pied-piper-square:before {\n  content: \"\\e01e\"; }\n\n.fa-creative-commons-nd:before {\n  content: \"\\f4eb\"; }\n\n.fa-palfed:before {\n  content: \"\\f3d8\"; }\n\n.fa-superpowers:before {\n  content: \"\\f2dd\"; }\n\n.fa-resolving:before {\n  content: \"\\f3e7\"; }\n\n.fa-xbox:before {\n  content: \"\\f412\"; }\n\n.fa-square-web-awesome-stroke:before {\n  content: \"\\e684\"; }\n\n.fa-searchengin:before {\n  content: \"\\f3eb\"; }\n\n.fa-tiktok:before {\n  content: \"\\e07b\"; }\n\n.fa-square-facebook:before {\n  content: \"\\f082\"; }\n\n.fa-facebook-square:before {\n  content: \"\\f082\"; }\n\n.fa-renren:before {\n  content: \"\\f18b\"; }\n\n.fa-linux:before {\n  content: \"\\f17c\"; }\n\n.fa-glide:before {\n  content: \"\\f2a5\"; }\n\n.fa-linkedin:before {\n  content: \"\\f08c\"; }\n\n.fa-hubspot:before {\n  content: \"\\f3b2\"; }\n\n.fa-deploydog:before {\n  content: \"\\f38e\"; }\n\n.fa-twitch:before {\n  content: \"\\f1e8\"; }\n\n.fa-ravelry:before {\n  content: \"\\f2d9\"; }\n\n.fa-mixer:before {\n  content: \"\\e056\"; }\n\n.fa-square-lastfm:before {\n  content: \"\\f203\"; }\n\n.fa-lastfm-square:before {\n  content: \"\\f203\"; }\n\n.fa-vimeo:before {\n  content: \"\\f40a\"; }\n\n.fa-mendeley:before {\n  content: \"\\f7b3\"; }\n\n.fa-uniregistry:before {\n  content: \"\\f404\"; }\n\n.fa-figma:before {\n  content: \"\\f799\"; }\n\n.fa-creative-commons-remix:before {\n  content: \"\\f4ee\"; }\n\n.fa-cc-amazon-pay:before {\n  content: \"\\f42d\"; }\n\n.fa-dropbox:before {\n  content: \"\\f16b\"; }\n\n.fa-instagram:before {\n  content: \"\\f16d\"; }\n\n.fa-cmplid:before {\n  content: \"\\e360\"; }\n\n.fa-upwork:before {\n  content: \"\\e641\"; }\n\n.fa-facebook:before {\n  content: \"\\f09a\"; }\n\n.fa-gripfire:before {\n  content: \"\\f3ac\"; }\n\n.fa-jedi-order:before {\n  content: \"\\f50e\"; }\n\n.fa-uikit:before {\n  content: \"\\f403\"; }\n\n.fa-fort-awesome-alt:before {\n  content: \"\\f3a3\"; }\n\n.fa-phabricator:before {\n  content: \"\\f3db\"; }\n\n.fa-ussunnah:before {\n  content: \"\\f407\"; }\n\n.fa-earlybirds:before {\n  content: \"\\f39a\"; }\n\n.fa-trade-federation:before {\n  content: \"\\f513\"; }\n\n.fa-autoprefixer:before {\n  content: \"\\f41c\"; }\n\n.fa-whatsapp:before {\n  content: \"\\f232\"; }\n\n.fa-square-upwork:before {\n  content: \"\\e67c\"; }\n\n.fa-slideshare:before {\n  content: \"\\f1e7\"; }\n\n.fa-google-play:before {\n  content: \"\\f3ab\"; }\n\n.fa-viadeo:before {\n  content: \"\\f2a9\"; }\n\n.fa-line:before {\n  content: \"\\f3c0\"; }\n\n.fa-google-drive:before {\n  content: \"\\f3aa\"; }\n\n.fa-servicestack:before {\n  content: \"\\f3ec\"; }\n\n.fa-simplybuilt:before {\n  content: \"\\f215\"; }\n\n.fa-bitbucket:before {\n  content: \"\\f171\"; }\n\n.fa-imdb:before {\n  content: \"\\f2d8\"; }\n\n.fa-deezer:before {\n  content: \"\\e077\"; }\n\n.fa-raspberry-pi:before {\n  content: \"\\f7bb\"; }\n\n.fa-jira:before {\n  content: \"\\f7b1\"; }\n\n.fa-docker:before {\n  content: \"\\f395\"; }\n\n.fa-screenpal:before {\n  content: \"\\e570\"; }\n\n.fa-bluetooth:before {\n  content: \"\\f293\"; }\n\n.fa-gitter:before {\n  content: \"\\f426\"; }\n\n.fa-d-and-d:before {\n  content: \"\\f38d\"; }\n\n.fa-microblog:before {\n  content: \"\\e01a\"; }\n\n.fa-cc-diners-club:before {\n  content: \"\\f24c\"; }\n\n.fa-gg-circle:before {\n  content: \"\\f261\"; }\n\n.fa-pied-piper-hat:before {\n  content: \"\\f4e5\"; }\n\n.fa-kickstarter-k:before {\n  content: \"\\f3bc\"; }\n\n.fa-yandex:before {\n  content: \"\\f413\"; }\n\n.fa-readme:before {\n  content: \"\\f4d5\"; }\n\n.fa-html5:before {\n  content: \"\\f13b\"; }\n\n.fa-sellsy:before {\n  content: \"\\f213\"; }\n\n.fa-square-web-awesome:before {\n  content: \"\\e683\"; }\n\n.fa-sass:before {\n  content: \"\\f41e\"; }\n\n.fa-wirsindhandwerk:before {\n  content: \"\\e2d0\"; }\n\n.fa-wsh:before {\n  content: \"\\e2d0\"; }\n\n.fa-buromobelexperte:before {\n  content: \"\\f37f\"; }\n\n.fa-salesforce:before {\n  content: \"\\f83b\"; }\n\n.fa-octopus-deploy:before {\n  content: \"\\e082\"; }\n\n.fa-medapps:before {\n  content: \"\\f3c6\"; }\n\n.fa-ns8:before {\n  content: \"\\f3d5\"; }\n\n.fa-pinterest-p:before {\n  content: \"\\f231\"; }\n\n.fa-apper:before {\n  content: \"\\f371\"; }\n\n.fa-fort-awesome:before {\n  content: \"\\f286\"; }\n\n.fa-waze:before {\n  content: \"\\f83f\"; }\n\n.fa-bluesky:before {\n  content: \"\\e671\"; }\n\n.fa-cc-jcb:before {\n  content: \"\\f24b\"; }\n\n.fa-snapchat:before {\n  content: \"\\f2ab\"; }\n\n.fa-snapchat-ghost:before {\n  content: \"\\f2ab\"; }\n\n.fa-fantasy-flight-games:before {\n  content: \"\\f6dc\"; }\n\n.fa-rust:before {\n  content: \"\\e07a\"; }\n\n.fa-wix:before {\n  content: \"\\f5cf\"; }\n\n.fa-square-behance:before {\n  content: \"\\f1b5\"; }\n\n.fa-behance-square:before {\n  content: \"\\f1b5\"; }\n\n.fa-supple:before {\n  content: \"\\f3f9\"; }\n\n.fa-webflow:before {\n  content: \"\\e65c\"; }\n\n.fa-rebel:before {\n  content: \"\\f1d0\"; }\n\n.fa-css3:before {\n  content: \"\\f13c\"; }\n\n.fa-staylinked:before {\n  content: \"\\f3f5\"; }\n\n.fa-kaggle:before {\n  content: \"\\f5fa\"; }\n\n.fa-space-awesome:before {\n  content: \"\\e5ac\"; }\n\n.fa-deviantart:before {\n  content: \"\\f1bd\"; }\n\n.fa-cpanel:before {\n  content: \"\\f388\"; }\n\n.fa-goodreads-g:before {\n  content: \"\\f3a9\"; }\n\n.fa-square-git:before {\n  content: \"\\f1d2\"; }\n\n.fa-git-square:before {\n  content: \"\\f1d2\"; }\n\n.fa-square-tumblr:before {\n  content: \"\\f174\"; }\n\n.fa-tumblr-square:before {\n  content: \"\\f174\"; }\n\n.fa-trello:before {\n  content: \"\\f181\"; }\n\n.fa-creative-commons-nc-jp:before {\n  content: \"\\f4ea\"; }\n\n.fa-get-pocket:before {\n  content: \"\\f265\"; }\n\n.fa-perbyte:before {\n  content: \"\\e083\"; }\n\n.fa-grunt:before {\n  content: \"\\f3ad\"; }\n\n.fa-weebly:before {\n  content: \"\\f5cc\"; }\n\n.fa-connectdevelop:before {\n  content: \"\\f20e\"; }\n\n.fa-leanpub:before {\n  content: \"\\f212\"; }\n\n.fa-black-tie:before {\n  content: \"\\f27e\"; }\n\n.fa-themeco:before {\n  content: \"\\f5c6\"; }\n\n.fa-python:before {\n  content: \"\\f3e2\"; }\n\n.fa-android:before {\n  content: \"\\f17b\"; }\n\n.fa-bots:before {\n  content: \"\\e340\"; }\n\n.fa-free-code-camp:before {\n  content: \"\\f2c5\"; }\n\n.fa-hornbill:before {\n  content: \"\\f592\"; }\n\n.fa-js:before {\n  content: \"\\f3b8\"; }\n\n.fa-ideal:before {\n  content: \"\\e013\"; }\n\n.fa-git:before {\n  content: \"\\f1d3\"; }\n\n.fa-dev:before {\n  content: \"\\f6cc\"; }\n\n.fa-sketch:before {\n  content: \"\\f7c6\"; }\n\n.fa-yandex-international:before {\n  content: \"\\f414\"; }\n\n.fa-cc-amex:before {\n  content: \"\\f1f3\"; }\n\n.fa-uber:before {\n  content: \"\\f402\"; }\n\n.fa-github:before {\n  content: \"\\f09b\"; }\n\n.fa-php:before {\n  content: \"\\f457\"; }\n\n.fa-alipay:before {\n  content: \"\\f642\"; }\n\n.fa-youtube:before {\n  content: \"\\f167\"; }\n\n.fa-skyatlas:before {\n  content: \"\\f216\"; }\n\n.fa-firefox-browser:before {\n  content: \"\\e007\"; }\n\n.fa-replyd:before {\n  content: \"\\f3e6\"; }\n\n.fa-suse:before {\n  content: \"\\f7d6\"; }\n\n.fa-jenkins:before {\n  content: \"\\f3b6\"; }\n\n.fa-twitter:before {\n  content: \"\\f099\"; }\n\n.fa-rockrms:before {\n  content: \"\\f3e9\"; }\n\n.fa-pinterest:before {\n  content: \"\\f0d2\"; }\n\n.fa-buffer:before {\n  content: \"\\f837\"; }\n\n.fa-npm:before {\n  content: \"\\f3d4\"; }\n\n.fa-yammer:before {\n  content: \"\\f840\"; }\n\n.fa-btc:before {\n  content: \"\\f15a\"; }\n\n.fa-dribbble:before {\n  content: \"\\f17d\"; }\n\n.fa-stumbleupon-circle:before {\n  content: \"\\f1a3\"; }\n\n.fa-internet-explorer:before {\n  content: \"\\f26b\"; }\n\n.fa-stubber:before {\n  content: \"\\e5c7\"; }\n\n.fa-telegram:before {\n  content: \"\\f2c6\"; }\n\n.fa-telegram-plane:before {\n  content: \"\\f2c6\"; }\n\n.fa-old-republic:before {\n  content: \"\\f510\"; }\n\n.fa-odysee:before {\n  content: \"\\e5c6\"; }\n\n.fa-square-whatsapp:before {\n  content: \"\\f40c\"; }\n\n.fa-whatsapp-square:before {\n  content: \"\\f40c\"; }\n\n.fa-node-js:before {\n  content: \"\\f3d3\"; }\n\n.fa-edge-legacy:before {\n  content: \"\\e078\"; }\n\n.fa-slack:before {\n  content: \"\\f198\"; }\n\n.fa-slack-hash:before {\n  content: \"\\f198\"; }\n\n.fa-medrt:before {\n  content: \"\\f3c8\"; }\n\n.fa-usb:before {\n  content: \"\\f287\"; }\n\n.fa-tumblr:before {\n  content: \"\\f173\"; }\n\n.fa-vaadin:before {\n  content: \"\\f408\"; }\n\n.fa-quora:before {\n  content: \"\\f2c4\"; }\n\n.fa-square-x-twitter:before {\n  content: \"\\e61a\"; }\n\n.fa-reacteurope:before {\n  content: \"\\f75d\"; }\n\n.fa-medium:before {\n  content: \"\\f23a\"; }\n\n.fa-medium-m:before {\n  content: \"\\f23a\"; }\n\n.fa-amilia:before {\n  content: \"\\f36d\"; }\n\n.fa-mixcloud:before {\n  content: \"\\f289\"; }\n\n.fa-flipboard:before {\n  content: \"\\f44d\"; }\n\n.fa-viacoin:before {\n  content: \"\\f237\"; }\n\n.fa-critical-role:before {\n  content: \"\\f6c9\"; }\n\n.fa-sitrox:before {\n  content: \"\\e44a\"; }\n\n.fa-discourse:before {\n  content: \"\\f393\"; }\n\n.fa-joomla:before {\n  content: \"\\f1aa\"; }\n\n.fa-mastodon:before {\n  content: \"\\f4f6\"; }\n\n.fa-airbnb:before {\n  content: \"\\f834\"; }\n\n.fa-wolf-pack-battalion:before {\n  content: \"\\f514\"; }\n\n.fa-buy-n-large:before {\n  content: \"\\f8a6\"; }\n\n.fa-gulp:before {\n  content: \"\\f3ae\"; }\n\n.fa-creative-commons-sampling-plus:before {\n  content: \"\\f4f1\"; }\n\n.fa-strava:before {\n  content: \"\\f428\"; }\n\n.fa-ember:before {\n  content: \"\\f423\"; }\n\n.fa-canadian-maple-leaf:before {\n  content: \"\\f785\"; }\n\n.fa-teamspeak:before {\n  content: \"\\f4f9\"; }\n\n.fa-pushed:before {\n  content: \"\\f3e1\"; }\n\n.fa-wordpress-simple:before {\n  content: \"\\f411\"; }\n\n.fa-nutritionix:before {\n  content: \"\\f3d6\"; }\n\n.fa-wodu:before {\n  content: \"\\e088\"; }\n\n.fa-google-pay:before {\n  content: \"\\e079\"; }\n\n.fa-intercom:before {\n  content: \"\\f7af\"; }\n\n.fa-zhihu:before {\n  content: \"\\f63f\"; }\n\n.fa-korvue:before {\n  content: \"\\f42f\"; }\n\n.fa-pix:before {\n  content: \"\\e43a\"; }\n\n.fa-steam-symbol:before {\n  content: \"\\f3f6\"; }\n"
  },
  {
    "path": "styles/dark-theme.css",
    "content": " :root {   \r\n    --background-color: #303030;\r\n    --background-color-rgb: 48, 48, 48;\r\n    --box-background-color: #222222;\r\n    --box-background-color-rgb: 34, 34, 34;\r\n    --box-border-color: #333;\r\n    --box-border-color-rgb: 51, 51, 51;\r\n    --header-background-color: #222222;\r\n    --header-background-color-rgb: 34, 34, 34;\r\n    --text-color: #E0E0E0;\r\n    --text-color-rgb: 224, 224, 224;\r\n    --input-border-color: #666;\r\n    --input-border-color-rgb: 102, 102, 102;\r\n    --input-background-color: #555;\r\n    --input-background-color-rgb: 85, 85, 85;\r\n    --input-disabled-background-color: #999999;\r\n    --input-disabled-background-color-rgb: 153, 153, 153;\r\n    --input-disabled-border-color: #666666;\r\n    --input-disabled-border-color-rgb: 102, 102, 102;\r\n    --box-shadow: 0 2px 5px rgba(120, 120, 120, 0.1);\r\n    --negative-box-shadow: 0 -2px 5px rgba(120, 120, 120, 0.1);\r\n }\r\n\r\nbody {\r\n    background-color: var(--background-color);\r\n    color: var(--text-color);\r\n}\r\n\r\nbody>header {\r\n    background-color: var(--header-background-color);\r\n}\r\n\r\nsvg .text-color {\r\n    fill: var(--text-color);\r\n}\r\n\r\n.split-header>h2 .header-subtitle {\r\n    color: var(--text-color);\r\n}\r\n\r\n.dropbtn {\r\n    color: #E0E0E0;\r\n}\r\n\r\n.dropdown-content a {\r\n    color: #E0E0E0;\r\n}\r\n\r\n.dropdown-content a:hover {\r\n    background-color: #333333;\r\n}\r\n\r\n.filtermenu-content .filter-item:hover,\r\n.filtermenu-content .filter-title:hover {\r\n    background-color: #333333;\r\n}\r\n\r\n.subscription-form h3,\r\n.subscription-modal h3 {\r\n    color: #FFF;\r\n}\r\n\r\n.subscription.inactive {\r\n    background-color: #222;\r\n    color: rgba(200, 200, 200, 0.6);\r\n    box-shadow: 0 2px 5px rgba(50, 50, 50, 0.1);\r\n}\r\n\r\n.subscription-main .actions {\r\n    color: #E0E0E0\r\n}\r\n\r\n.subscription-main .actions>li {\r\n    border-bottom: 1px solid #555;\r\n    border-color: #666;\r\n}\r\n\r\n.subscription-main .actions>li:hover {\r\n    background-color: #333;\r\n}\r\n\r\n.subscription-main .actions>li:last-of-type {\r\n    border: none;\r\n}\r\n\r\n.subscription-container {\r\n    background-color: #222;\r\n  }\r\n\r\n.close-form {\r\n    color: #EEE;\r\n}\r\n\r\ninput[type=\"text\"]::placeholder,\r\ninput[type=\"email\"]::placeholder,\r\ninput[type=\"password\"]::placeholder,\r\ninput[type=\"date\"]::placeholder,\r\ninput[type=\"number\"]::placeholder,\r\ntextarea::placeholder,\r\nselect::placeholder {\r\n    color: #BBB;\r\n}\r\n\r\nbutton.secondary-button,\r\nbutton.button.secondary-button,\r\ninput[type=\"button\"].secondary-button {\r\n    background-color: #222;\r\n}\r\n\r\nbutton.button.secondary-button:hover,\r\nbutton.secondary-button:hover,\r\ninput[type=\"button\"].secondary-button:hover {\r\n    background-color: #111;\r\n}\r\n\r\ninput[type=\"color\"] {\r\n    background-color: #F2F2F2;\r\n}\r\n\r\n.avatar-select .avatar-list .remove-avatar {\r\n    background-color: #222;\r\n}\r\n\r\n.avatar-select .avatar-list>img,\r\n.avatar-select .avatar-list .avatar-container>img {\r\n    border: 1px solid #999;\r\n}\r\n\r\n.avatar-select .avatar-list>img:hover,\r\n.avatar-select .avatar-list .avatar-container>img:hover {\r\n    border: 1px solid #EEE;\r\n}\r\n\r\n.avatar-select .avatar-list .remove-avatar:hover {\r\n    background-color: #666;\r\n}\r\n\r\n.account-notification-section-header:hover {\r\n    background-color: #444;\r\n}\r\n\r\n.toast {\r\n    box-shadow: 0 6px 20px -5px rgba(255, 255, 255, 0.1);\r\n}\r\n\r\n.toast .close-error {\r\n    color: #EEE;\r\n}\r\n\r\n.toast-content .message .text.text-1 {\r\n    color: #BBB;\r\n}\r\n\r\n.toast-content .message .text {\r\n    color: #999;\r\n}\r\n\r\n.logo-preview:after {\r\n    color: var(--main-color);\r\n}\r\n\r\n.sort-options>ul>li {\r\n    border-bottom: 1px solid #555;\r\n    color: #DDD;\r\n}\r\n\r\n.sort-options>ul>li:hover {\r\n    background-color: #444;\r\n}\r\n\r\n.payment-name {\r\n    color: #FFF;\r\n}\r\n\r\n.payments-list .payments-payment .delete-payment-method {\r\n    color: #FFF;\r\n}\r\n\r\n.calendar .calendar-body .calendar-cell .calendar-cell-header {\r\n    background-color: #111;\r\n}\r\n\r\n.calendar .calendar-body .calendar-cell {\r\n    border-right: 1px solid #111;\r\n}\r\n\r\nbutton.dark-theme-button {\r\n    color: #E0E0E0;\r\n}\r\n\r\nbutton.dark-theme-button:hover {\r\n    background-color: #111;\r\n}\r\n\r\n.account-notifications-section {\r\n    border: 1px solid #666;\r\n}\r\n\r\ninput[type=\"checkbox\"]+label::before,\r\ninput[type=\"radio\"]+label::before {\r\n    background: #555;\r\n    border: 1px solid #666;\r\n}\r\n\r\ninput[type=\"checkbox\"]:disabled+label::before,\r\ninput[type=\"radio\"]:disabled+label::before {\r\n    background-color: #333;\r\n    border-color: #222;\r\n    cursor: not-allowed;\r\n}\r\n\r\ninput {\r\n    color-scheme: dark;\r\n}\r\n\r\n.update-banner {\r\n    color: #FFF;\r\n}\r\n\r\n.update-banner>span>a {\r\n    color: #FFF;\r\n}\r\n\r\n.totp-qrcode-container {\r\n    padding: 14px;\r\n    border: 1px solid #DDD;\r\n    border-radius: 8px;\r\n}\r\n\r\n.totp-backup-codes {\r\n    background-color: #111;\r\n    border: 2px dashed #444;\r\n}\r\n\r\n.mobile-nav-image {\r\n    background-image: url(\"../images/siteimages/mobilenavdark.png\");\r\n}\r\n\r\n@media (max-width: 768px) {\r\n    .mobile-nav>a {\r\n        color: #909090;\r\n    }\r\n\r\n    .mobile-nav>a.active {\r\n        color: #f0f0F0;\r\n    }\r\n}"
  },
  {
    "path": "styles/login-dark-theme.css",
    "content": "body {\r\n    background-color: #303030;\r\n    color: #E0E0E0;\r\n}\r\n\r\nsvg .text-color {\r\n    fill: #E0E0E0;\r\n}\r\n\r\n.container {\r\n    background-color: #222;\r\n    border: 1px solid #333;\r\n    box-shadow: 0 2px 5px rgba(255, 255, 255, 0.1);\r\n}\r\n\r\n@media (max-width: 768px) {\r\n    .container {\r\n        background-color: transparent;\r\n        border: none;\r\n        box-shadow: none;\r\n    }\r\n}\r\n\r\ninput[type=\"text\"],\r\ninput[type=\"email\"],\r\ninput[type=\"password\"], \r\ninput[type=\"checkbox\"],\r\nselect {   \r\n    background-color: #555;\r\n    border: 1px solid #666;\r\n    color: #E0E0E0;\r\n}\r\n\r\ninput[type=\"text\"]::placeholder,\r\ninput[type=\"email\"]::placeholder,\r\ninput[type=\"password\"]::placeholder {\r\n    color: #BBB;\r\n}"
  },
  {
    "path": "styles/login.css",
    "content": "body, html {\r\n    font-family: Barlow, 'Helvetica Neue', Helvetica, sans-serif;\r\n    margin: 0;\r\n    padding: 0;\r\n    background-color: #f5f5f5;\r\n    font-size: 14px;\r\n    height: 100%;\r\n}\r\n\r\n.content {\r\n    display: flex;\r\n    flex-direction: column;\r\n    justify-content: center;\r\n    align-items: center;\r\n    height: 100%;\r\n}\r\n\r\n.container {\r\n    width: 400px;\r\n    margin: auto;\r\n    max-width: 100%;\r\n    padding: 20px;\r\n    background-color: #fff;\r\n    border-radius: 8px;\r\n    border: 1px solid #eee;\r\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\r\n    box-sizing: border-box;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n    .container {\r\n        background-color: transparent;\r\n        border: none;\r\n        box-shadow: none;\r\n    }\r\n}\r\n\r\n@media (max-height: 768px) {\r\n    .content {\r\n        height: auto;\r\n    }\r\n}\r\n\r\n.container > header {\r\n    text-align: center;\r\n}\r\n\r\nheader .logo-image {\r\n  height: 80px;\r\n  width: 215px;\r\n  margin: 0px auto;\r\n}\r\n\r\nheader .logo-image svg {\r\n  height: 80px;\r\n  width: 215px;\r\n}\r\n\r\n.container > .message {\r\n  text-align: center;\r\n  font-size: 18px;\r\n}\r\n\r\n.container > .message a {\r\n  color: var(--main-color);\r\n  text-decoration: none;\r\n}\r\n\r\nh2 {\r\n    text-align: center;\r\n    margin-bottom: 20px;\r\n}\r\n\r\n.form-group {\r\n    margin-bottom: 20px;\r\n}\r\n\r\n.form-group-inline {\r\n    display: flex;\r\n    flex-direction: row;\r\n    align-items: center;\r\n    margin-bottom: 20px;\r\n}\r\n\r\nlabel {\r\n    display: block;\r\n    margin-bottom: 5px;\r\n}\r\n\r\n.form-group-inline label {\r\n    font-weight: 300;\r\n    font-size: 13px;\r\n    margin-bottom: 0px;\r\n    margin-left: 8px;\r\n    cursor: pointer;\r\n}\r\n\r\n.rtl .form-group-inline label {\r\n    margin-right: 8px;\r\n    margin-left: 0px;\r\n}\r\n\r\ninput {\r\n    box-sizing: border-box;\r\n}\r\n\r\ninput[type=\"text\"],\r\ninput[type=\"email\"],\r\ninput[type=\"password\"],\r\nselect {\r\n    width: 100%;\r\n    padding: 15px;\r\n    font-size: 16px;\r\n    border: 1px solid #ccc;\r\n    border-radius: 8px;\r\n    outline: none;\r\n}\r\n\r\ninput[type=\"submit\"],\r\ninput[type=\"button\"],\r\na.button {\r\n    width: 100%;\r\n    padding: 15px;\r\n    font-size: 16px;\r\n    background-color: var(--main-color);\r\n    color: #fff;\r\n    border: none;\r\n    border-radius: 8px;\r\n    cursor: pointer;\r\n}\r\n\r\na.button {\r\n    text-decoration: none;\r\n    display: inline-block;\r\n    text-align: center;\r\n    box-sizing: border-box;\r\n}\r\n\r\ninput[type=\"submit\"]:hover,\r\na.button:hover {\r\n    background-color: var(--hover-color);\r\n}\r\n\r\ninput[type=\"button\"].secondary-button,\r\nbutton.button.secondary-button,\r\na.button.secondary-button {\r\n  background-color: #FFFFFF;\r\n  color: var(--main-color);\r\n  border: 2px solid var(--main-color);\r\n}\r\n\r\ninput[type=\"button\"].secondary-button:hover,\r\nbutton.button.secondary-button:hover,\r\na.button.secondary-button:hover {\r\n  background-color: #EEEEEE;\r\n  color: var(--hover-color);\r\n  border-color: var(--hover-color);\r\n}\r\n\r\ninput[type=\"checkbox\"] {\r\n    cursor: pointer;\r\n    width: 25px;\r\n    height: 25px;\r\n    padding: 0px;\r\n    margin: 0px;\r\n    background-color: #fff;\r\n    border: 1px solid #ccc;\r\n    border-radius: 8px;\r\n    display: grid;\r\n    place-content: center;\r\n}\r\n\r\n.or-separator {\r\n    text-align: center;\r\n    display: block;\r\n    margin: 3px 0px 16px;\r\n    font-size: 16px;\r\n}\r\n\r\n.error {\r\n    display: block;\r\n    color: var(--error-color);\r\n    margin-bottom: 20px;\r\n}\r\n\r\n.error-box,\r\n.success-box {\r\n    display: block;\r\n    color: #FFFFFF;\r\n    margin-bottom: 20px;\r\n    padding: 14px 14px 16px 14px;\r\n    border: 1px solid var(--error-color);\r\n    background-color: rgba(var(--error-color-rgb), 0.8);\r\n    border-radius: 8px;\r\n}\r\n\r\n.success-box {\r\n    border: 1px solid var(--success-color);\r\n    background-color: rgba(var(--success-color-rgb), 0.5);\r\n}\r\n\r\n.error-box li,\r\n.success-box li {\r\n  list-style: none;\r\n  font-size: 15px;\r\n  display: flex;\r\n  gap: 8px;\r\n  align-items: baseline;\r\n  margin-bottom: 5px;\r\n}\r\n\r\n.error-box li:last-of-type,\r\n.success-box li:last-of-type {\r\n  margin-bottom: 0px;\r\n}\r\n\r\n.separator {\r\n    border-top: 1px solid #ccc;\r\n    padding-top: 20px;\r\n}\r\n\r\n.login-form-link {\r\n    text-align: center;\r\n    margin: 20px 0px;\r\n}\r\n\r\n.login-form-link a {\r\n    color: var(--main-color);\r\n    text-decoration: none;\r\n    font-size: 16px;\r\n}\r\n\r\n/* TOAST MESSAGE */\r\n\r\n.toast {\r\n    position: fixed;\r\n    bottom: 25px;\r\n    right: 30px;\r\n    border-radius: 12px;\r\n    border: 1px solid #eeeeee;\r\n    background: #fff;\r\n    padding: 20px 35px 20px 25px;\r\n    box-shadow: 0 6px 20px -5px rgba(0, 0, 0, 0.1);\r\n    overflow: hidden;\r\n    transform: translateX(calc(100% + 30px));\r\n    transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.35);\r\n    box-sizing: border-box;\r\n  }\r\n  \r\n  @media (max-width: 768px) {\r\n    .toast {\r\n      bottom: 0px;\r\n      right: 0px;\r\n      left: 0px;\r\n      width: 100%;\r\n    }\r\n  }\r\n  \r\n  .toast.active {\r\n    transform: translateX(0%);\r\n  }\r\n  \r\n  .toast .toast-content {\r\n    display: flex;\r\n    align-items: center;\r\n  }\r\n  \r\n  .toast-content .toast-icon {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    height: 35px;\r\n    min-width: 35px;\r\n    color: #fff;\r\n    font-size: 20px;\r\n    border-radius: 50%;\r\n  }\r\n  \r\n  .toast-content .toast-icon.error {\r\n    background-color: var(--error-color);\r\n  }\r\n  \r\n  .toast-content .toast-icon.success {\r\n    background-color: var(--success-color);\r\n  }\r\n  \r\n  \r\n  .toast-content .message {\r\n    display: flex;\r\n    flex-direction: column;\r\n    margin: 0 20px;\r\n  }\r\n  \r\n  .toast-content .message .text {\r\n    font-size: 16px;\r\n    font-weight: 400;\r\n    color: #666666;\r\n  }\r\n  \r\n  .toast-content .message .text.text-1 {\r\n    font-weight: 600;\r\n    color: #333;\r\n  }\r\n  \r\n  .toast .close {\r\n    position: absolute;\r\n    top: 10px;\r\n    right: 15px;\r\n    padding: 5px;\r\n    cursor: pointer;\r\n    opacity: 0.7;\r\n  }\r\n  \r\n  .toast .close:hover {\r\n    opacity: 1;\r\n  }\r\n  \r\n  .toast .progress {\r\n    position: absolute;\r\n    bottom: 0;\r\n    left: 0;\r\n    height: 3px;\r\n    width: 100%;\r\n  \r\n  }\r\n  \r\n  .toast .progress:before {\r\n    content: \"\";\r\n    position: absolute;\r\n    bottom: 0;\r\n    right: 0;\r\n    height: 100%;\r\n    width: 100%;\r\n  }\r\n  \r\n  .toast .progress.error:before {\r\n    background-color: var(--error-color);\r\n  }\r\n  \r\n  .toast .progress.success:before {\r\n    background-color: var(--success-color);\r\n  }\r\n  \r\n  .progress.active:before {\r\n    animation: progress 5s linear forwards;\r\n  }\r\n  \r\n  @keyframes progress {\r\n    100% {\r\n      right: 100%;\r\n    }\r\n  }\r\n  \r\n  /* TOAST END */"
  },
  {
    "path": "styles/styles.css",
    "content": ":root {\r\n  --logo-flex-basis: 17%;\r\n}\r\n\r\nbody {\r\n  font-family: Barlow, 'Helvetica Neue', Helvetica, sans-serif;\r\n  margin: 0;\r\n  padding: 0;\r\n  background-color: var(--background-color);\r\n  color: var(--text-color);\r\n}\r\n\r\nbody.no-scroll {\r\n  overflow-y: hidden;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .mobile-navigation main {\r\n    margin-bottom: 70px;\r\n  }\r\n}\r\n\r\ninput,\r\nbutton,\r\nselect,\r\ntextarea {\r\n  font-family: Barlow, 'Helvetica Neue', Helvetica, sans-serif;\r\n  font-weight: 400;\r\n}\r\n\r\nbutton.hidden,\r\ninput.hidden {\r\n  display: none;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  body.no-scroll section.contain {\r\n    display: none;\r\n  }\r\n}\r\n\r\na:hover>i {\r\n  color: var(--hover-color);\r\n}\r\n\r\nh2,\r\nh3 {\r\n  font-weight: 500;\r\n}\r\n\r\n.contain {\r\n  width: 100%;\r\n  max-width: 970px;\r\n  margin: 0px auto;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.error-box {\r\n  padding: 20px 16px;\r\n  background-color: rgba(var(--error-color-rgb), 0.3);\r\n  border: 1px solid var(--error-color);\r\n  border-radius: 8px;\r\n  margin-bottom: 20px;\r\n  font-size: 16px;\r\n}\r\n\r\n.error-box .error-message i {\r\n  color: var(--error-color);\r\n  margin-right: 10px;\r\n}\r\n\r\n.split-header {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  justify-content: space-between;\r\n  margin-bottom: 10px;\r\n}\r\n\r\n.split-header h2 {\r\n  margin-right: 20px;\r\n  display: flex;\r\n}\r\n\r\n.split-header>h2 .header-subtitle {\r\n  font-size: 22px;\r\n  font-weight: 400;\r\n  color: #666666;\r\n  margin-left: 10px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .contain.settings {\r\n    padding: 20px 0px;\r\n  }\r\n\r\n  .split-header>h2 .header-subtitle {\r\n    margin-left: 0px;\r\n    font-size: 18px;\r\n  }\r\n}\r\n\r\nbody>header {\r\n  border-bottom: 7px solid var(--main-color);\r\n  background-color: var(--header-background-color);\r\n}\r\n\r\nbody>header>.contain {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  padding: 10px 20px;\r\n}\r\n\r\nheader .logo .logo-image {\r\n  height: 50px;\r\n  width: 134px;\r\n  margin-right: 10px;\r\n}\r\n\r\nheader .logo .logo-image svg {\r\n  height: 50px;\r\n  width: 134px;\r\n}\r\n\r\n.button-icon {\r\n  width: 16px;\r\n  height: 16px;\r\n}\r\n\r\n.dropdown {\r\n  position: relative;\r\n  display: inline-block;\r\n}\r\n\r\n.dropbtn:after {\r\n  content: \" \\25BC\";\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .mobile-navigation .dropbtn:after {\r\n    content: \"\";\r\n    display: none;\r\n  }\r\n}\r\n\r\n.dropbtn {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  gap: 8px;\r\n  background-color: transparent;\r\n  color: var(--text-color);\r\n  padding: 7px 12px;\r\n  font-size: 16px;\r\n  border: none;\r\n  cursor: pointer;\r\n  -webkit-tap-highlight-color: transparent;\r\n  -moz-tap-highlight-color: transparent;\r\n  -ms-tap-highlight-color: transparent;\r\n}\r\n\r\n.dropbtn>img {\r\n  width: 35px;\r\n  height: 35px;\r\n  object-fit: cover;\r\n}\r\n\r\n.dropdown-content {\r\n  display: none;\r\n  position: absolute;\r\n  right: 0px;\r\n  background-color: var(--header-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  min-width: 130px;\r\n  box-shadow: var(--box-shadow);\r\n  z-index: 5;\r\n  width: max-content;\r\n  border-top: none;\r\n  border-radius: 8px;\r\n}\r\n\r\n.dropdown-content a {\r\n  color: var(--text-color);\r\n  padding: 14px 18px;\r\n  text-decoration: none;\r\n  display: flex;\r\n  flex-direction: row;\r\n  gap: 12px;\r\n  align-items: center;\r\n}\r\n\r\n.dropdown-content a:hover {\r\n  background-color: #f9f9f9;\r\n}\r\n\r\n.dropdown:hover .dropdown-content {\r\n  display: block;\r\n}\r\n\r\n.dropdown-content a>svg {\r\n  width: 20px;\r\n  height: 20px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .dropdown:hover .dropdown-content {\r\n    display: none;\r\n  }\r\n\r\n  .dropdown.is-open .dropdown-content {\r\n    display: block !important;\r\n  }\r\n}\r\n\r\nmain>.contain {\r\n  display: flex;\r\n  flex-direction: column;\r\n  padding: 20px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .main>.contain {\r\n    padding: 0px 10px;\r\n  }\r\n}\r\n\r\n.main-actions {\r\n  margin: 0px 0px 20px 0px;\r\n  display: flex;\r\n  flex-direction: row;\r\n  justify-content: space-between;\r\n  gap: 16px;\r\n  flex-wrap: wrap;\r\n  position: relative;\r\n}\r\n\r\n.main-actions.hidden {\r\n  display: none;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .main-actions {\r\n    justify-content: space-between;\r\n    flex-direction: column-reverse;\r\n  }\r\n}\r\n\r\n.button {\r\n  display: flex;\r\n  flex-direction: row;\r\n  gap: 8px;\r\n  align-items: center;\r\n  font-weight: 500;\r\n  text-align: center;\r\n  vertical-align: middle;\r\n  justify-content: center;\r\n  cursor: pointer;\r\n  user-select: none;\r\n  color: #fff;\r\n  border: 1px solid var(--main-color);\r\n  background-color: var(--main-color);\r\n  padding: 15px 30px;\r\n  font-size: 1rem;\r\n  border-radius: 8px;\r\n  transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;\r\n  text-decoration: none;\r\n}\r\n\r\n.button:hover {\r\n  background-color: var(--hover-color);\r\n  border-color: var(--hover-color);\r\n}\r\n\r\n.button.thin {\r\n  padding: 14px 20px;\r\n}\r\n\r\n.button.tiny,\r\n.button-secondary.tiny {\r\n  padding: 7px 14px;\r\n  font-size: 12px;\r\n}\r\n\r\n.button.icon-button,\r\n.button-secondary.icon-button {\r\n  padding: 15px;\r\n}\r\n\r\nbutton:hover svg .main-color {\r\n  fill: var(--hover-color);\r\n}\r\n\r\n.actions li:hover svg .main-color {\r\n  fill: var(--hover-color);\r\n}\r\n\r\n.image-button:hover svg .main-color {\r\n  fill: var(--hover-color);\r\n}\r\n\r\n.top-actions {\r\n  display: flex;\r\n  flex-direction: row;\r\n  gap: 16px;\r\n  align-items: center;\r\n  width: auto;\r\n}\r\n\r\n.top-actions .search {\r\n  flex-grow: 1;\r\n}\r\n\r\n.top-actions .search>input[type=\"text\"] {\r\n  padding-right: 40px;\r\n}\r\n\r\n.top-actions>.search>.search-icon,\r\n.top-actions>.search>.clear-search {\r\n  float: right;\r\n  right: 15px;\r\n  margin-top: -35px;\r\n  position: relative;\r\n  z-index: 2;\r\n  color: var(--main-color);\r\n  font-size: 20px;\r\n}\r\n\r\n.top-actions>.search>.clear-search {\r\n  display: none;\r\n  cursor: pointer;\r\n}\r\n\r\n.top-actions>.search.has-text>.search-icon {\r\n  display: none;\r\n}\r\n\r\n.top-actions>.search.has-text>.clear-search {\r\n  display: block;\r\n}\r\n\r\n.rtl .top-actions>.search>.search-icon,\r\n.rtl .top-actions>.search>.clear-search {\r\n  float: left;\r\n  right: -15px;\r\n}\r\n\r\n.subscriptions {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 15px;\r\n  justify-content: center;\r\n  font-size: 17px;\r\n}\r\n\r\n.subscription-container {\r\n  position: relative;\r\n  background-color: var(--box-background-color);\r\n  box-shadow: var(--box-shadow);\r\n  border-radius: 16px;\r\n}\r\n\r\n.subscription-container>.mobile-actions {\r\n  display: flex;\r\n  flex-direction: row;\r\n  position: absolute;\r\n  right: 0px;\r\n  top: 0px;\r\n  height: 100%;\r\n  overflow: hidden;\r\n  border-top-right-radius: 16px;\r\n  border-bottom-right-radius: 16px;\r\n}\r\n\r\n.subscription-container>.mobile-actions>button {\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  padding: 10px;\r\n  border: none;\r\n  cursor: pointer;\r\n  height: 100%;\r\n  width: 60px;\r\n  justify-content: center;\r\n  color: #f1f1f1;\r\n}\r\n\r\nbutton.mobile-action-edit {\r\n  background-color: #ffbf15;\r\n  /* #f3e22d; */\r\n}\r\n\r\nbutton.mobile-action-delete {\r\n  background-color: #f45a40;\r\n}\r\n\r\nbutton.mobile-action-clone {\r\n  background-color: #2da7f3\r\n}\r\n\r\nbutton.mobile-action-renew {\r\n  background-color: #188823;\r\n}\r\n\r\n.subscription-container>.mobile-actions>button>svg {\r\n  width: 25px;\r\n  height: 25px;\r\n  min-height: 25px;\r\n}\r\n\r\n.subscription {\r\n  display: flex;\r\n  flex-direction: column;\r\n  height: auto;\r\n  justify-content: flex-start;\r\n  gap: 12px;\r\n  background-color: var(--box-background-color);\r\n  box-shadow: var(--box-shadow);\r\n  padding: 12px 15px;\r\n  border-radius: 16px;\r\n  cursor: pointer;\r\n  position: relative;\r\n  transition: transform 0.2s;\r\n  box-sizing: border-box;\r\n  justify-content: center;\r\n}\r\n\r\n.subscription-container.hide {\r\n  display: none;\r\n}\r\n\r\n.subscription-container>.subscription-progress-container {\r\n  position: absolute;\r\n  bottom: 0px;\r\n  left: 8px;\r\n  right: 8px;\r\n  height: 3px;\r\n  z-index: 1;\r\n}\r\n\r\n.subscription-container>.subscription-progress-container>.subscription-progress {\r\n  height: 3px;\r\n  background-color: var(--accent-color);\r\n  display: block;\r\n  position: absolute;\r\n}\r\n\r\n.subscription.inactive {\r\n  background-color: var(--box-background-color);\r\n  color: rgba(100, 100, 100, 0.6);\r\n  box-shadow: 0 2px 5px rgba(100, 100, 100, 0.1);\r\n}\r\n\r\n.subscription.inactive span.price {\r\n  text-decoration: line-through;\r\n}\r\n\r\n.subscription.inactive .payment_method img {\r\n  opacity: 0.4;\r\n}\r\n\r\n.subscription-main {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  gap: 12px;\r\n  position: relative;\r\n  min-height: 40px;\r\n}\r\n\r\n.subscription-main .actions-expand {\r\n  font-size: 21px;\r\n  padding: 8px 16px;\r\n  color: var(--main-color);\r\n  background-color: transparent;\r\n  border: none;\r\n  cursor: pointer;\r\n}\r\n\r\n.subscription-main .actions-expand:hover {\r\n  color: var(--hover-color);\r\n}\r\n\r\n.subscription-main .actions {\r\n  display: none;\r\n  position: absolute;\r\n  right: -16px;\r\n  top: 60px;\r\n  z-index: 2;\r\n  flex-direction: column;\r\n  color: var(--text-color);\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  box-shadow: var(--box-shadow);\r\n  border-radius: 16px;\r\n  padding: 0px;\r\n  margin: 0px;\r\n}\r\n\r\n.rtl .subscription-main .actions {\r\n  left: -16px;\r\n  right: auto;\r\n}\r\n\r\n.subscription-main .actions.is-open {\r\n  display: flex;\r\n}\r\n\r\n.subscription-main .actions>li {\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: flex-start;\r\n  padding: 14px 35px 14px 18px;\r\n  gap: 12px;\r\n  cursor: pointer;\r\n  border-bottom: 1px solid var(--box-border-color);\r\n}\r\n\r\n.rtl .subscription-main .actions>li {\r\n  padding: 14px 18px 14px 35px;\r\n}\r\n\r\n.subscription-main .actions>li:hover {\r\n  background-color: #f9f9f9;\r\n}\r\n\r\n.subscription-main .actions>li>i {\r\n  color: var(--main-color);\r\n}\r\n\r\n.subscription-main .actions>li:hover>i {\r\n  color: var(--hover-color);\r\n}\r\n\r\n.subscription-secondary {\r\n  display: none;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  gap: 12px;\r\n  padding: 6px 5px;\r\n  overflow: hidden;\r\n}\r\n\r\n.subscription-notes {\r\n  display: none;\r\n  flex-direction: row;\r\n  padding: 6px 5px;\r\n  overflow: hidden;\r\n}\r\n\r\n.subscription-main .actions svg {\r\n  width: 32px;\r\n  height: 32px;\r\n}\r\n\r\n.subscription-secondary svg,\r\n.subscription-notes svg {\r\n  width: 20px;\r\n  height: 20px;\r\n}\r\n\r\n.subscription-main>span,\r\n.subscription-secondary>span {\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  text-align: center;\r\n  box-sizing: border-box;\r\n  margin: 0px;\r\n}\r\n\r\n.subscription .logo {\r\n  flex-basis: var(--logo-flex-basis);\r\n}\r\n\r\n.subscription .logo img {\r\n  width: 100%;\r\n  height: 100%;\r\n  max-height: 42px;\r\n  object-fit: contain;\r\n  min-width: 32px;\r\n}\r\n\r\n.subscription .logo svg {\r\n  max-width: 100%;\r\n  height: 42px;\r\n}\r\n\r\n.subscription .name {\r\n  flex-basis: 25%;\r\n  font-weight: 600;\r\n}\r\n\r\n.subscription .cycle {\r\n  flex-basis: 16%;\r\n  flex-grow: 1;\r\n  flex-direction: row;\r\n  align-items: center;\r\n}\r\n\r\n.subscription .cycle>svg {\r\n  width: 15px;\r\n  height: 15px;\r\n  margin-right: 3px;\r\n  margin-top: 2px;\r\n}\r\n\r\n.subscription .next {\r\n  flex-basis: 16%;\r\n  flex-grow: 1;\r\n  text-transform: capitalize;\r\n}\r\n\r\n.subscription .payment_method {\r\n  margin-left: 10px;\r\n  display: flex;\r\n  justify-content: center;\r\n  align-items: center;\r\n}\r\n\r\n.subscription .payment_method img {\r\n  width: 44px;\r\n  height: 30px;\r\n  aspect-ratio: 3 / 2;\r\n  object-fit: contain;\r\n}\r\n\r\n.rtl .subscription .payment_method img {\r\n  margin-right: 10px;\r\n  margin-left: 0px;\r\n\r\n}\r\n\r\n.subscription .price {\r\n  flex-basis: 8%;\r\n  justify-content: center;\r\n  flex-direction: row;\r\n}\r\n\r\n.subscription .price .original_price {\r\n  font-size: 14px;\r\n  color: #888;\r\n}\r\n\r\n.subscription .actions {\r\n  flex-basis: auto;\r\n}\r\n\r\n.subscription .actions img {\r\n  width: 25px;\r\n  height: 25px;\r\n  cursor: pointer;\r\n}\r\n\r\n.subscription-secondary>.name {\r\n  display: none;\r\n  justify-content: flex-start;\r\n  flex-basis: 33%;\r\n}\r\n\r\n.subscription-secondary>span {\r\n  justify-content: flex-start;\r\n  flex-basis: 33%;\r\n  gap: 10px;\r\n}\r\n\r\n.subscription-secondary>.url {\r\n  flex-basis: 20px;\r\n  margin-left: auto;\r\n  cursor: pointer;\r\n}\r\n\r\n.rtl .subscription-secondary>.url {\r\n  margin-left: 0px;\r\n  margin-right: auto;\r\n}\r\n\r\n.subscription-notes>span {\r\n  display: flex;\r\n  align-items: center;\r\n  font-size: 14px;\r\n  gap: 10px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n\r\n  .subscription-main>.hideOnMobile {\r\n    display: none;\r\n  }\r\n\r\n  .subscription-main>.name {\r\n    flex-basis: var(--logo-flex-basis);\r\n    font-size: 14px;\r\n    font-weight: normal;\r\n    max-height: 38px;\r\n    overflow: hidden;\r\n    align-items: baseline;\r\n  }\r\n\r\n  .subscription-secondary>.name {\r\n    display: flex;\r\n  }\r\n\r\n  .subscription-secondary>span {\r\n    flex-grow: 1;\r\n    flex-shrink: 1;\r\n    font-size: 14px;\r\n  }\r\n}\r\n\r\n@media (max-width: 375px) {\r\n  .subscription-main>.cycle {\r\n    display: none;\r\n  }\r\n}\r\n\r\n.subscription.is-open .subscription-secondary,\r\n.subscription.is-open .subscription-notes {\r\n  display: flex;\r\n}\r\n\r\n.subscription-secondary img,\r\n.subscription-notes img {\r\n  height: 20px;\r\n}\r\n\r\n.subscription-secondary .url img {\r\n  margin-right: 0px;\r\n  ;\r\n}\r\n\r\n.empty-page,\r\n.no-matching-subscriptions {\r\n  display: block;\r\n  max-width: 90%;\r\n  margin: auto;\r\n  text-align: center;\r\n  font-size: 20px;\r\n}\r\n\r\n.empty-page>img,\r\n.no-matching-subscriptions>img {\r\n  max-width: 100%;\r\n}\r\n\r\n.no-matching-subscriptions>img {\r\n  margin-top: 30px;\r\n}\r\n\r\n.empty-page>p {\r\n  margin: 5px 0px 40px 0px;\r\n}\r\n\r\n.no-matching-subscriptions>p {\r\n  margin: 30px 0px 40px 0px;\r\n}\r\n\r\n.empty-page>button,\r\n.no-matching-subscriptions>button {\r\n  margin: 0px auto;\r\n}\r\n\r\n.account-section {\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  padding: 20px;\r\n  box-shadow: var(--box-shadow);\r\n  border-radius: 16px;\r\n}\r\n\r\n.account-section header h2 {\r\n  margin-top: 0px;\r\n  margin-bottom: 34px;\r\n}\r\n\r\n.account-section header h2.second-header {\r\n  margin-top: 34px;\r\n}\r\n\r\n.account-section+.account-section {\r\n  margin-top: 34px;\r\n}\r\n\r\n.account-section .account-settings-list {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 16px;\r\n}\r\n\r\n.account-section .account-settings-list .form-group-inline {\r\n  margin-bottom: 0px;\r\n}\r\n\r\n.account-section .account-settings-list h3 {\r\n  margin: 0px;\r\n}\r\n\r\n.account-section .account-settings-theme h3 {\r\n  margin-bottom: 24px;\r\n}\r\n\r\n.user-form {\r\n  display: flex;\r\n  flex-direction: column;\r\n}\r\n\r\n.user-form .fields {\r\n  display: flex;\r\n  flex-direction: row;\r\n  gap: 34px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .user-form .fields {\r\n    flex-direction: column;\r\n    align-items: center;\r\n    gap: 20px;\r\n  }\r\n\r\n  .grow {\r\n    width: 100%;\r\n  }\r\n}\r\n\r\nheader #avatar {\r\n  border-radius: 50%;\r\n}\r\n\r\n.user-form .user-avatar {\r\n  position: relative;\r\n}\r\n\r\n.user-form .user-avatar>img {\r\n  cursor: pointer;\r\n  width: 80px;\r\n  height: 80px;\r\n  object-fit: cover;\r\n  max-width: 80px;\r\n  border-radius: 50%;\r\n  border: 1px solid #ccc;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.user-form .user-avatar .edit-avatar {\r\n  display: none;\r\n  align-items: center;\r\n  justify-content: center;\r\n  width: 80px;\r\n  height: 80px;\r\n  position: absolute;\r\n  top: 0px;\r\n  left: 0px;\r\n  background-color: rgba(0, 0, 0, 0.6);\r\n  border-radius: 39px;\r\n  cursor: pointer;\r\n  color: #FFFFFF;\r\n  font-size: 30px;\r\n}\r\n\r\n.user-form .user-avatar:hover>.edit-avatar {\r\n  display: flex;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .user-form .user-avatar:hover>.edit-avatar {\r\n    display: none;\r\n  }\r\n}\r\n\r\n.avatar-select {\r\n  display: none;\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  position: absolute;\r\n  padding: 20px;\r\n  box-sizing: border-box;\r\n  width: 336px;\r\n  max-width: 100%;\r\n  box-shadow: var(--box-shadow);\r\n  z-index: 3;\r\n}\r\n\r\n.avatar-option {\r\n  border-radius: 50%;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .avatar-select {\r\n    left: 50%;\r\n    transform: translateX(-50%);\r\n  }\r\n}\r\n\r\n.avatar-select.is-open {\r\n  display: block;\r\n}\r\n\r\n.avatar-select .avatar-list {\r\n  display: flex;\r\n  gap: 18px;\r\n  flex-wrap: wrap;\r\n}\r\n\r\n.avatar-select .avatar-list>img,\r\n.avatar-select .avatar-list .avatar-container>img {\r\n  width: 60px;\r\n  height: 60px;\r\n  object-fit: cover;\r\n  cursor: pointer;\r\n  border: 1px solid #ccc;\r\n  box-sizing: border-box\r\n}\r\n\r\n.avatar-select .avatar-list>img:hover,\r\n.avatar-select .avatar-list .avatar-container>img:hover {\r\n  border: 1px solid #222;\r\n}\r\n\r\n.avatar-select .avatar-list .avatar-container {\r\n  position: relative;\r\n  height: 60px;\r\n}\r\n\r\n.avatar-select label.add-avatar {\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  justify-content: center;\r\n  width: 60px;\r\n  height: 60px;\r\n  border: 1px solid var(--main-color);\r\n  border-radius: 50%;\r\n  cursor: pointer;\r\n  margin: 0px;\r\n  box-sizing: border-box;\r\n  color: var(--main-color);\r\n}\r\n\r\n.avatar-select label.add-avatar:hover {\r\n  border-color: var(--accent-color);\r\n  color: var(--accent-color);\r\n}\r\n\r\n.avatar-select .avatar-list .remove-avatar {\r\n  position: absolute;\r\n  top: -4px;\r\n  right: -11px;\r\n  background-color: var(--box-background-color);\r\n  border-radius: 50%;\r\n  cursor: pointer;\r\n  display: flex;\r\n  font-weight: 600;\r\n  align-items: center;\r\n  justify-content: center;\r\n  border: 1px solid #ccc;\r\n  width: 25px;\r\n  height: 25px;\r\n  box-sizing: border-box;\r\n  font-size: 8px;\r\n}\r\n\r\n.avatar-select .avatar-list .remove-avatar:hover {\r\n  background-color: #eee;\r\n}\r\n\r\n.user-form .fields .grow {\r\n  flex: 1;\r\n}\r\n\r\n.user-form .buttons,\r\n.account-members .buttons,\r\n.account-currencies .buttons,\r\n.account-fixer .buttons,\r\n.account-ai-settings .buttons,\r\n.account-categories .buttons,\r\n.account-notifications .buttons,\r\n.admin-form .buttons,\r\n.account-2fa .buttons {\r\n  display: flex;\r\n  justify-content: flex-end;\r\n  align-items: center;\r\n  gap: 20px;\r\n}\r\n\r\n.account-2fa .buttons {\r\n  justify-content: flex-start;\r\n}\r\n\r\n.admin-form hr {\r\n  margin: 20px 0px;\r\n  color: var(--main-color);\r\n  border-color: var(--main-color);\r\n  background-color: var(--main-color);\r\n}\r\n\r\n.account-notifications-section {\r\n  border: 1px solid #ccc;\r\n  border-radius: 8px;\r\n  margin-bottom: 10px;\r\n  overflow: hidden;\r\n}\r\n\r\n.account-notification-section-header {\r\n  padding: 16px;\r\n  cursor: pointer;\r\n}\r\n\r\n.account-notification-section-header:hover {\r\n  background-color: #EEE;\r\n}\r\n\r\n.account-notification-section-header h3 {\r\n  margin: 0px;\r\n}\r\n\r\n.account-notification-section-header h3 i {\r\n  margin-right: 10px;\r\n}\r\n\r\n.account-notification-section-settings {\r\n  max-height: 0px;\r\n  overflow: hidden;\r\n  /* Hide content that goes beyond the height */\r\n  transition: max-height 0.3s ease-in-out;\r\n  /* Animate max-height changes */\r\n  padding: 0px 16px;\r\n}\r\n\r\n.account-notification-section-settings.is-open {\r\n  max-height: 1500px;\r\n  /* Set to a value larger than the content's natural height */\r\n}\r\n\r\n.account-notification-section-settings>div:first-of-type {\r\n  margin-top: 20px;\r\n}\r\n\r\n.account-notification-section-settings>div:last-of-type {\r\n  margin-bottom: 20px;\r\n}\r\n\r\n.account-notifications .buttons {\r\n  gap: 15px;\r\n}\r\n\r\n.image-button {\r\n  box-sizing: border-box;\r\n  border: none;\r\n  background: transparent;\r\n  cursor: pointer;\r\n  padding: 0px;\r\n}\r\n\r\n.image-button>i {\r\n  color: var(--hover-color);\r\n  font-size: 28px;\r\n  padding: 2px;\r\n}\r\n\r\n.image-button>svg {\r\n  width: 32px;\r\n  height: 32px;\r\n}\r\n\r\n.image-button.disabled>img,\r\n.image-button.disabled>svg {\r\n  -webkit-filter: grayscale(100%);\r\n  filter: grayscale(100%);\r\n}\r\n\r\n.image-button.success>img {\r\n  filter: hue-rotate(262deg);\r\n}\r\n\r\n.image-button.error>img {\r\n  filter: hue-rotate(141deg);\r\n}\r\n\r\n.image-button.small>img {\r\n  width: 25px;\r\n  height: 25px;\r\n  object-fit: contain;\r\n}\r\n\r\n.image-button.medium>img {\r\n  width: 32px;\r\n  height: 32px;\r\n  object-fit: contain;\r\n}\r\n\r\n.payments-list {\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n  gap: 16px;\r\n}\r\n\r\n.payments-list .payments-payment {\r\n  cursor: pointer;\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  gap: 8px;\r\n  background-color: var(--accent-color);\r\n  padding: 6px 12px;\r\n  border-radius: 8px;\r\n  transition: filter 300ms;\r\n}\r\n\r\n.payments-list .payments-payment[data-enabled=\"0\"] {\r\n  filter: grayscale(100%);\r\n}\r\n\r\n.payments-list .payments-payment[data-in-use=\"yes\"] {\r\n  cursor: not-allowed;\r\n}\r\n\r\n.payments-list .payments-payment .drag-icon {\r\n  height: 20px;\r\n  width: 14px;\r\n  font-size: 14px;\r\n}\r\n\r\n.payments-list .payments-payment>img {\r\n  width: 32px;\r\n  height: 32px;\r\n  object-fit: contain;\r\n}\r\n\r\n.payments-list .payments-payment>.payment-name {\r\n  cursor: text;\r\n}\r\n\r\n.payments-list .payments-payment .delete-payment-method {\r\n  padding: 5px;\r\n  font-weight: bold;\r\n  color: var(--text-color);\r\n}\r\n\r\n.credits-list {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 16px;\r\n  line-break: anywhere;\r\n}\r\n\r\n.credits-list>div {\r\n  margin: 0px;\r\n  font-size: 18px;\r\n  display: flex;\r\n  flex-direction: column;\r\n}\r\n\r\n.updates-list>div {\r\n  margin: 0px;\r\n}\r\n\r\n.credits-list>div>h3 {\r\n  margin: 2px 0px 0px 0px;\r\n  font-size: 20px;\r\n}\r\n\r\n.credits-list>div>h3>i {\r\n  color: var(--accent-color);\r\n  font-size: 18px;\r\n}\r\n\r\n.settings-notes {\r\n  margin-bottom: 1.5em;\r\n}\r\n\r\n.settings-notes>p {\r\n  margin-bottom: 0px;\r\n}\r\n\r\n.credits-list>div>span,\r\n.updates-list>p>span,\r\n.settings-notes>p>span {\r\n  color: #AAA;\r\n  font-size: 14px;\r\n}\r\n\r\n.credits-list>div>span,\r\n.updates-list>p>span {\r\n  font-size: 16px;\r\n}\r\n\r\n.credits-list>div>span>a,\r\n.updates-list>p>span>a,\r\n.settings-notes>p>span>a {\r\n  margin-left: 5px;\r\n  font-size: 13px;\r\n  color: var(--accent-color);\r\n}\r\n\r\n.rtl .credits-list>div>span>a,\r\n.rtl .updates-list>p>span>a,\r\n.rtl .settings-notes>p>span>a {\r\n  margin-left: 0px;\r\n  margin-right: 5px;\r\n}\r\n\r\n.credits-list>div>span>a:visited,\r\n.updates-list>p>span>a:visited,\r\n.settings-notes>p>span>a:visited {\r\n  color: var(--accent-color);\r\n}\r\n\r\n.settings-notes>p>i,\r\n.account-section .notes>p>i {\r\n  color: var(--main-color);\r\n  margin-right: 5px;\r\n}\r\n\r\n.rtl .settings-notes>p>i,\r\n.rtl .account-section .notes>p>i {\r\n  margin-right: 0px;\r\n  margin-left: 5px;\r\n}\r\n\r\n.form-group {\r\n  margin-bottom: 20px;\r\n}\r\n\r\n.form-group-inline {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  margin-bottom: 20px;\r\n  gap: 15px;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.form-group .inline {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  gap: 15px;\r\n  justify-content: space-between;\r\n}\r\n\r\n.form-group .inline .split33 {\r\n  flex-basis: 33.34%;\r\n}\r\n\r\n.form-group .inline .split66 {\r\n  flex-basis: 66.66%;\r\n}\r\n\r\n.form-group .inline .split50 {\r\n  flex-basis: 50%;\r\n  display: flex;\r\n  flex-direction: column;\r\n}\r\n\r\n.form-group.hide,\r\n.form-group-inline.hide {\r\n  display: none;\r\n}\r\n\r\n.height50 {\r\n  height: 50px;\r\n}\r\n\r\n.inline-row {\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-wrap: wrap;\r\n  gap: 15px;\r\n  margin-bottom: 20px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .form-group .inline .mobile-split-50 {\r\n    flex-basis: 50%;\r\n  }\r\n\r\n  select#frequency {\r\n    width: 100px;\r\n    padding: 0px 10px;\r\n  }\r\n}\r\n\r\nlabel {\r\n  display: block;\r\n  margin-bottom: 5px;\r\n}\r\n\r\n.form-group-inline label {\r\n  font-size: 16px;\r\n  margin-bottom: 0px;\r\n  margin-left: 0px;\r\n  cursor: pointer;\r\n}\r\n\r\nlabel.split-label {\r\n  display: flex;\r\n  flex-direction: row;\r\n  justify-content: space-between;\r\n}\r\n\r\ninput {\r\n  box-sizing: border-box;\r\n}\r\n\r\ninput[type=\"text\"],\r\ninput[type=\"email\"],\r\ninput[type=\"password\"],\r\ninput[type=\"date\"],\r\ninput[type=\"number\"],\r\nselect {\r\n  width: 100%;\r\n  padding: 0px 15px;\r\n  height: 50px;\r\n  font-size: 16px;\r\n  background-color: var(--input-background-color);\r\n  border: 1px solid var(--input-border-color);\r\n  border-radius: 8px;\r\n  outline: none;\r\n  color: var(--text-color);\r\n  box-sizing: border-box;\r\n}\r\n\r\ninput[type=\"color\"] {\r\n  height: 46px;\r\n  width: 46px;\r\n  background-color: #222;\r\n  border: 1px solid var(--hover-color);\r\n  outline: none;\r\n  box-sizing: border-box;\r\n  cursor: pointer;\r\n  font-size: 16px;\r\n  position: relative;\r\n  border-radius: 5px;\r\n}\r\n\r\n.one-third {\r\n  max-width: 33%;\r\n}\r\n\r\nselect {\r\n  cursor: pointer;\r\n  height: 50px;\r\n}\r\n\r\n.date-wrapper {\r\n  display: flex;\r\n  flex-grow: 0;\r\n  flex-direction: row;\r\n  flex-basis: 100%;\r\n  box-sizing: border-box;\r\n}\r\n\r\ninput[type=\"date\"] {\r\n  display: flex;\r\n  flex-grow: 1;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  flex-basis: 100%;\r\n  box-sizing: border-box;\r\n}\r\n\r\ninput[type=\"text\"].short {\r\n  flex-basis: 55px;\r\n  min-width: 55px;\r\n  text-align: center;\r\n}\r\n\r\ninput[type=\"submit\"],\r\ninput[type=\"button\"],\r\nbutton.button {\r\n  padding: 15px 30px;\r\n  font-size: 16px;\r\n  background-color: var(--main-color);\r\n  color: var(--text-color-inverted);\r\n  border: none;\r\n  border-radius: 8px;\r\n  cursor: pointer;\r\n  box-sizing: border-box;\r\n  border: 2px solid var(--main-color);\r\n}\r\n\r\ninput[type=\"submit\"].thin,\r\ninput[type=\"button\"].thin,\r\nbutton.button.thin {\r\n  padding: 13px 30px;\r\n}\r\n\r\ninput[type=\"button\"].secondary-button,\r\nbutton.button.secondary-button {\r\n  background-color: #FFFFFF;\r\n  color: var(--main-color);\r\n}\r\n\r\ninput[type=\"button\"].secondary-button:hover,\r\nbutton.button.secondary-button:hover {\r\n  background-color: #EEEEEE;\r\n  color: var(--hover-color);\r\n  border-color: var(--hover-color);\r\n}\r\n\r\ninput[type=\"button\"].warning-button {\r\n  background-color: #f45a40;\r\n  border-color: #f45a40;\r\n}\r\n\r\ninput[type=\"button\"].warning-button:hover,\r\nbutton.button.warning-button:hover {\r\n  background-color: #ef8674;\r\n  border-color: #ef8674;\r\n}\r\n\r\ninput[type=\"submit\"]:hover,\r\ninput[type=\"button\"]:hover,\r\nbutton.button:hover {\r\n  background-color: var(--hover-color);\r\n  border-color: var(--hover-color);\r\n}\r\n\r\ninput[type=\"submit\"]:disabled,\r\ninput[type=\"button\"]:disabled,\r\nbutton.button:disabled {\r\n  background-color: #ccc;\r\n  border-color: #ccc;\r\n}\r\n\r\ninput[type=\"button\"].left button.button.left {\r\n  margin-right: auto;\r\n}\r\n\r\ninput[type=\"checkbox\"] {\r\n  cursor: pointer;\r\n  width: 25px;\r\n  height: 25px;\r\n  padding: 0px;\r\n  margin: 0px;\r\n  background-color: var(--input-background-color);\r\n  border: 1px solid var(--input-border-color);\r\n  border-radius: 8px;\r\n  display: grid;\r\n  place-content: center;\r\n}\r\n\r\nbutton.disabled {\r\n  cursor: not-allowed;\r\n}\r\n\r\ninput[type=\"text\"]:disabled,\r\ninput[type=\"password\"]:disabled,\r\ninput[type=\"email\"]:disabled {\r\n  background-color: var(--input-disabled-background-color);\r\n  border-color: var(--input-disabled-border-color);\r\n  cursor: not-allowed;\r\n}\r\n\r\ntextarea {\r\n  font-size: 16px;\r\n  background-color: var(--input-background-color);\r\n  border: 1px solid var(--input-border-color);\r\n  border-radius: 8px;\r\n  padding: 5px 14px;\r\n  color: var(--text-color);\r\n  width: 100%;\r\n  height: 245px;\r\n}\r\n\r\ntextarea:focus {\r\n  outline: none;\r\n}\r\n\r\ntextarea.thin {\r\n  height: 80px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  input[type=\"checkbox\"] {\r\n    width: 20px;\r\n    height: 20px;\r\n    flex-shrink: 0;\r\n  }\r\n}\r\n\r\n.form-icon-search {\r\n  position: relative;\r\n}\r\n\r\n.logo-search,\r\n.icon-search {\r\n  position: absolute;\r\n  width: 165px;\r\n  height: 298px;\r\n  top: 130px;\r\n  right: 32px;\r\n  overflow-y: auto;\r\n  overflow-x: hidden;\r\n  padding: 10px;\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 8px;\r\n  box-shadow: var(--box-shadow);\r\n  background-color: var(--box-background-color);\r\n  box-sizing: border-box;\r\n  z-index: 1;\r\n  display: none;\r\n}\r\n\r\n.icon-search {\r\n  width: 156px;\r\n  height: 224px;\r\n  top: 50px;\r\n  right: 0px;\r\n}\r\n\r\n.logo-search.is-open,\r\n.icon-search.is-open {\r\n  display: block;\r\n}\r\n\r\n.logo-search>header,\r\n.icon-search>header {\r\n  padding: 0px 5px 5px;\r\n  border-bottom: 1px solid #CCC;\r\n  display: flex;\r\n  flex-direction: row;\r\n  justify-content: space-between;\r\n  margin-bottom: 10px;\r\n}\r\n\r\n.icon-search>header>span {\r\n  margin-left: auto;\r\n}\r\n\r\n.logo-search .close-logo-search,\r\n.icon-search .close-icon-search {\r\n  cursor: pointer;\r\n}\r\n\r\n.logo-search img,\r\n.icon-search img {\r\n  max-width: 100%;\r\n  cursor: pointer;\r\n  border-bottom: 1px solid #ccc;\r\n  padding: 10px 0px;\r\n}\r\n\r\n.icon-search img {\r\n  padding: 8px 0px;\r\n  aspect-ratio: 16 / 5;\r\n  object-fit: contain;\r\n}\r\n\r\n.logo-search img:last-of-type,\r\n.icon-search img:last-of-type {\r\n  border-bottom: none;\r\n  padding-bottom: 0px;\r\n}\r\n\r\n.capitalize {\r\n  text-transform: capitalize;\r\n}\r\n\r\nbutton.dark-theme-button {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  justify-content: center;\r\n  font-size: 18px !important;\r\n  border: 1px solid #ccc;\r\n  border-radius: 8px;\r\n  padding: 16px 20px 14px;\r\n  background-color: transparent;\r\n  color: var(--text-color);\r\n  cursor: pointer;\r\n  flex-grow: 1;\r\n}\r\n\r\nbutton.dark-theme-button:hover {\r\n  background-color: #EEE;\r\n}\r\n\r\nbutton.dark-theme-button.selected {\r\n  border: 1px solid var(--main-color);\r\n}\r\n\r\nbutton.dark-theme-button i {\r\n  margin-right: 12px;\r\n}\r\n\r\n.error {\r\n  display: block;\r\n  color: #f45a40;\r\n}\r\n\r\n.success {\r\n  display: block;\r\n  color: #188823;\r\n}\r\n\r\n.user-error,\r\n.user-success {\r\n  display: none;\r\n}\r\n\r\n.user-error.show,\r\n.user-success.show {\r\n  display: block;\r\n}\r\n\r\n.subscription-form,\r\n.subscription-modal {\r\n  background-color: var(--box-background-color);\r\n  padding: 22px;\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 16px;\r\n  box-shadow: var(--box-shadow);\r\n  box-sizing: border-box;\r\n  position: fixed;\r\n  left: 50%;\r\n  top: 50%;\r\n  transform: translateX(-50%) translateY(-50%);\r\n  z-index: 3;\r\n  max-width: 800px;\r\n  max-height: calc(100vh - 34px);\r\n  overflow: auto;\r\n  overflow-y: auto;\r\n  width: 90%;\r\n  display: none;\r\n}\r\n\r\n.subscription-form.is-open,\r\n.subscription-modal.is-open {\r\n  display: block;\r\n}\r\n\r\n.subscription-form h3,\r\n.subscription-modal h3 {\r\n  border-bottom: 1px solid var(--main-color);\r\n  padding-bottom: 15px;\r\n  margin-top: 0px;\r\n}\r\n\r\n.subscription-form .buttons {\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-wrap: wrap;\r\n  justify-content: flex-end;\r\n  gap: 16px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n\r\n  .subscription-form,\r\n  .subscription-modal {\r\n    width: 100%;\r\n    max-height: 100vh;\r\n  }\r\n\r\n  .subscription-form .buttons input {\r\n    flex: 1;\r\n  }\r\n}\r\n\r\n.logo-preview {\r\n  padding: 2px 0px;\r\n  height: 49px;\r\n  box-sizing: border-box;\r\n  aspect-ratio: 3.55/1;\r\n  display: block;\r\n  cursor: pointer;\r\n  overflow: hidden;\r\n}\r\n\r\n.logo-preview:after {\r\n  content: \"Upload Logo\";\r\n  display: flex;\r\n  justify-content: center;\r\n  align-items: center;\r\n  height: 100%;\r\n  color: var(--main-color);\r\n  font-size: 16px;\r\n  text-align: center;\r\n}\r\n\r\n.logo-preview:hover::after,\r\n.icon-preview:hover::after {\r\n  color: var(--hover-color);\r\n}\r\n\r\n.logo-preview img {\r\n  width: 100%;\r\n  height: 100%;\r\n  object-fit: contain;\r\n  display: none;\r\n}\r\n\r\n.icon-preview {\r\n  padding: 2px 0px;\r\n  height: 49px;\r\n  box-sizing: border-box;\r\n  aspect-ratio: 3/2;\r\n  display: block;\r\n  cursor: pointer;\r\n  overflow: hidden;\r\n  flex-shrink: 0;\r\n}\r\n\r\n.icon-preview:after {\r\n  content: \"Upload Icon\";\r\n  display: flex;\r\n  justify-content: center;\r\n  align-items: center;\r\n  height: 100%;\r\n  color: var(--main-color);\r\n  font-size: 16px;\r\n  text-align: center;\r\n}\r\n\r\n.icon-preview img {\r\n  width: 100%;\r\n  height: 100%;\r\n  object-fit: contain;\r\n  display: none;\r\n}\r\n\r\n.hidden-input {\r\n  display: none;\r\n}\r\n\r\n.close-form {\r\n  display: block;\r\n  position: absolute;\r\n  top: 15px;\r\n  right: 15px;\r\n  padding: 10px;\r\n  font-size: 20px;\r\n  cursor: pointer;\r\n  color: gray;\r\n}\r\n\r\n.rtl .close-form {\r\n  right: auto;\r\n  left: 15px;\r\n}\r\n\r\n.sort-container {\r\n  position: relative;\r\n}\r\n\r\n.sort-options {\r\n  position: absolute;\r\n  color: var(--text-color);\r\n  font-size: 16px;\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 8px;\r\n  box-shadow: 0 2px 5px rgba(163, 100, 100, 0.1);\r\n  box-sizing: border-box;\r\n  top: 52px;\r\n  right: 0px;\r\n  display: none;\r\n  width: 144px;\r\n  width: max-content;\r\n  z-index: 2;\r\n}\r\n\r\n.rtl .sort-options {\r\n  left: 0px;\r\n  right: auto;\r\n}\r\n\r\n@media (max-width: 380px) {\r\n  .sort-container {\r\n    flex-grow: 1;\r\n    max-width: 144px;\r\n  }\r\n}\r\n\r\n.sort-options.is-open {\r\n  display: block;\r\n}\r\n\r\n.sort-options>ul {\r\n  padding: 0px;\r\n  margin: 0px;\r\n}\r\n\r\n.sort-options>ul>li {\r\n  position: relative;\r\n  list-style: none;\r\n  padding: 14px 35px 14px 18px;\r\n  border-bottom: 1px solid #DDD;\r\n  cursor: pointer;\r\n}\r\n\r\n.rtl .sort-options>ul>li {\r\n  padding: 14px 18px 14px 35px;\r\n}\r\n\r\n.sort-options>ul>li:last-of-type {\r\n  border-bottom: none;\r\n}\r\n\r\n.sort-options>ul>li:hover {\r\n  background-color: #EEE;\r\n}\r\n\r\n.sort-options>ul>li.selected::after {\r\n  content: \"\";\r\n  position: absolute;\r\n  right: 10px;\r\n  top: 50%;\r\n  transform: translateY(-50%);\r\n  width: 16px;\r\n  height: 16px;\r\n  background-color: var(--main-color);\r\n  -webkit-mask: url(\"../images/siteicons/svg/check.php\") no-repeat center;\r\n  mask: url(\"../images/siteicons/svg/check.php\") no-repeat center;\r\n  background-size: 100% 100%;\r\n}\r\n\r\n.rtl .sort-options>ul>li.selected {\r\n  background-position: center left 10px;\r\n}\r\n\r\n.subscription-list-title {\r\n  font-size: 18px;\r\n  padding: 4px;\r\n  font-weight: 500;\r\n}\r\n\r\n\r\n/* TOAST MESSAGE */\r\n\r\n.toast {\r\n  position: fixed;\r\n  bottom: 25px;\r\n  right: 30px;\r\n  border-radius: 12px;\r\n  border: 1px solid var(--box-border-color);\r\n  background-color: var(--box-background-color);\r\n  padding: 20px 35px 20px 25px;\r\n  box-shadow: 0 6px 20px -5px rgba(0, 0, 0, 0.1);\r\n  overflow: hidden;\r\n  transform: translateX(calc(100% + 30px));\r\n  transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.35);\r\n  box-sizing: border-box;\r\n  z-index: 5;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .toast {\r\n    bottom: 0px;\r\n    right: 0px;\r\n    left: 0px;\r\n    width: 100%;\r\n  }\r\n\r\n  .mobile-navigation .toast {\r\n    bottom: 70px;\r\n  }\r\n}\r\n\r\n.toast.active {\r\n  transform: translateX(0%);\r\n}\r\n\r\n.toast .toast-content {\r\n  display: flex;\r\n  align-items: center;\r\n}\r\n\r\n.toast-content .toast-icon {\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  height: 35px;\r\n  min-width: 35px;\r\n  color: #fff;\r\n  font-size: 20px;\r\n  border-radius: 50%;\r\n}\r\n\r\n.toast-content .toast-icon.error {\r\n  background-color: var(--error-color);\r\n}\r\n\r\n.toast-content .toast-icon.success {\r\n  background-color: var(--success-color);\r\n}\r\n\r\n\r\n.toast-content .message {\r\n  display: flex;\r\n  flex-direction: column;\r\n  margin: 0 20px;\r\n}\r\n\r\n.toast-content .message .text {\r\n  font-size: 16px;\r\n  font-weight: 400;\r\n  color: #666666;\r\n}\r\n\r\n.toast-content .message .text.text-1 {\r\n  font-weight: 600;\r\n  color: #333;\r\n}\r\n\r\n.toast .close {\r\n  position: absolute;\r\n  top: 10px;\r\n  right: 15px;\r\n  padding: 5px;\r\n  cursor: pointer;\r\n  opacity: 0.7;\r\n}\r\n\r\n.toast .close:hover {\r\n  opacity: 1;\r\n}\r\n\r\n.toast .progress {\r\n  position: absolute;\r\n  bottom: 0;\r\n  left: 0;\r\n  height: 3px;\r\n  width: 100%;\r\n\r\n}\r\n\r\n.toast .progress:before {\r\n  content: \"\";\r\n  position: absolute;\r\n  bottom: 0;\r\n  right: 0;\r\n  height: 100%;\r\n  width: 100%;\r\n}\r\n\r\n.toast .progress.error:before {\r\n  background-color: var(--error-color);\r\n}\r\n\r\n.toast .progress.success:before {\r\n  background-color: var(--success-color);\r\n}\r\n\r\n.progress.active:before {\r\n  animation: progress 5s linear forwards;\r\n}\r\n\r\n@keyframes progress {\r\n  100% {\r\n    right: 100%;\r\n  }\r\n}\r\n\r\n/* TOAST END */\r\n\r\n.statistics {\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-wrap: wrap;\r\n  gap: 20px;\r\n  justify-content: flex-start;\r\n}\r\n\r\n.statistic {\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 16px;\r\n  box-shadow: var(--box-shadow);\r\n  padding: 20px 24px 30px;\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  flex-basis: calc(33.333% - (20px * 2 / 3));\r\n  flex-shrink: 0;\r\n  box-sizing: border-box;\r\n  flex-grow: 0;\r\n  overflow: hidden;\r\n}\r\n\r\n.statistic.short {\r\n  padding-bottom: 15px;\r\n}\r\n\r\n.statistic.empty {\r\n  background-color: transparent;\r\n  border: none;\r\n  box-shadow: none;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .statistic {\r\n    flex-basis: 100%;\r\n  }\r\n\r\n  .statistic.empty {\r\n    display: none;\r\n  }\r\n}\r\n\r\n.statistic>span {\r\n  font-size: 42px;\r\n  color: var(--main-color);\r\n}\r\n\r\n.statistic>.title {\r\n  margin-top: 5px;\r\n  text-align: center;\r\n}\r\n\r\n.statistic>.subtitle {\r\n  font-size: 25px;\r\n  color: var(--accent-color);\r\n  margin-top: 10px;\r\n  text-align: center;\r\n}\r\n\r\n.statistic>.subtitle>img {\r\n  width: 100px;\r\n  max-height: 40px;\r\n  object-fit: contain;\r\n}\r\n\r\n.graphs {\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-wrap: wrap;\r\n  gap: 20px;\r\n  justify-content: space-between;\r\n}\r\n\r\n.graph {\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 16px;\r\n  box-shadow: var(--box-shadow);\r\n  flex-basis: 48%;\r\n  align-items: center;\r\n  justify-content: center;\r\n  display: flex;\r\n  flex-direction: column;\r\n  padding: 20px 10px;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.graph>header {\r\n  font-size: 18px;\r\n  font-weight: 500;\r\n  margin-bottom: 15px;\r\n  text-align: center;\r\n}\r\n\r\n.graph>header>.sub-header {\r\n  font-size: 13px;\r\n  font-weight: normal;\r\n}\r\n\r\n.graph.x2 {\r\n  flex-basis: 100%;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .graph {\r\n    flex-basis: 100%;\r\n    max-width: 100%;\r\n  }\r\n}\r\n\r\n/* Settings sort category */\r\n\r\n.sortable-list {\r\n  margin: 0px;\r\n  padding: 0px;\r\n  -webkit-touch-callout: none;\r\n  -webkit-user-select: none;\r\n  -khtml-user-select: none;\r\n  -moz-user-select: none;\r\n  -ms-user-select: none;\r\n  user-select: none;\r\n}\r\n\r\n.drag-icon {\r\n  width: 28px;\r\n  height: 50px;\r\n  cursor: grab;\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  font-size: 20px;\r\n}\r\n\r\n.sortable-list .sortable-ghost {\r\n  border-radius: 8px;\r\n  background-color: rgba(var(--accent-color-rgb), 0.6);\r\n  border: 1px solid var(--accent-color);\r\n  padding: 5px;\r\n}\r\n\r\n/* Fitler dropdown */\r\n\r\n.filtermenu {\r\n  position: relative;\r\n  display: inline-block;\r\n}\r\n\r\n.filtermenu-content {\r\n  display: none;\r\n  position: absolute;\r\n  left: auto;\r\n  right: 0;\r\n  width: 220px;\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 8px;\r\n  box-shadow: var(--box-shadow);\r\n  z-index: 3;\r\n  overflow: hidden;\r\n  margin-top: 6px;\r\n}\r\n\r\n.rtl .filtermenu-content {\r\n  left: 0;\r\n  right: auto;\r\n\r\n}\r\n\r\n@media (max-width: 354px) {\r\n  .on-dashboard .filtermenu-content {\r\n    right: -94px;\r\n  }\r\n}\r\n\r\n.filtermenu-content.is-open {\r\n  display: block;\r\n}\r\n\r\n.filtermenu-content .filter-title {\r\n  padding: 14px 18px;\r\n  text-decoration: none;\r\n  display: block;\r\n  cursor: pointer;\r\n  font-weight: 500;\r\n  border-bottom: 1px solid #DDD;\r\n  user-select: none;\r\n}\r\n\r\n.filtermenu-content .filtermenu-submenu.hide {\r\n  display: none;\r\n}\r\n\r\n.filtermenu-content .filtermenu-submenu:last-of-type .filter-title {\r\n  border-bottom: none;\r\n}\r\n\r\n.filtermenu-content .filtermenu-submenu:last-of-type .filter-item:first-of-type {\r\n  border-top: 1px solid #DDD;\r\n}\r\n\r\n.filtermenu-content .filtermenu-submenu:last-of-type .filter-item:last-of-type {\r\n  border-bottom: none;\r\n}\r\n\r\n.filtermenu-content .filter-item {\r\n  position: relative;\r\n  padding: 14px 24px;\r\n  text-decoration: none;\r\n  display: block;\r\n  cursor: pointer;\r\n  border-bottom: 1px solid #DDD;\r\n  user-select: none;\r\n  font-size: 16px;\r\n}\r\n\r\n.filtermenu-content .filter-item.selected::after {\r\n  content: \"\";\r\n  position: absolute;\r\n  right: 10px;\r\n  top: 50%;\r\n  transform: translateY(-50%);\r\n  width: 16px;\r\n  /* Explicitly set the size */\r\n  height: 16px;\r\n  /* Explicitly set the size */\r\n  background-color: var(--main-color);\r\n  /* Set your desired color here */\r\n  -webkit-mask: url(\"../images/siteicons/svg/check.php\") no-repeat center;\r\n  mask: url(\"../images/siteicons/svg/check.php\") no-repeat center;\r\n  background-size: 100% 100%;\r\n  /* Ensure the icon scales correctly */\r\n}\r\n\r\n.rtl .filtermenu-content .filter-item.selected {\r\n  background-position: center left 10px;\r\n}\r\n\r\n.filtermenu-content .filter-title.filter-clear {\r\n  color: var(--hover-color);\r\n  font-weight: normal;\r\n  border-bottom: none;\r\n}\r\n\r\n.filtermenu-content .filter-title.filter-clear>i {\r\n  margin-right: 8px;\r\n}\r\n\r\n.rtl .filtermenu-content .filter-title.filter-clear>i {\r\n  margin-left: 8px;\r\n  margin-right: 0px;\r\n}\r\n\r\n.filtermenu-content .filter-item:hover,\r\n.filtermenu-content .filter-title:hover {\r\n  background-color: #f1f1f1;\r\n}\r\n\r\n.filtermenu-submenu-content {\r\n  display: none;\r\n}\r\n\r\n.filtermenu-submenu-content.is-open {\r\n  display: block;\r\n}\r\n\r\n/* Theme Selector */\r\n\r\n.theme {\r\n  flex-grow: 1;\r\n}\r\n\r\n.theme-preview {\r\n  cursor: pointer;\r\n  border: 1px solid #ccc;\r\n  border-radius: 8px;\r\n  padding: 20px 15px 20px 10px;\r\n  display: flex;\r\n  gap: 15px;\r\n  flex-direction: row;\r\n  align-items: center;\r\n}\r\n\r\n.theme-preview.is-selected {\r\n  border: 1px solid var(--main-color);\r\n}\r\n\r\n.theme-preview:hover {\r\n  background-color: rgba(var(--accent-color-rgb), 0.6);\r\n}\r\n\r\n.theme-preview .main-color,\r\n.theme-preview .accent-color,\r\n.theme-preview .hover-color {\r\n  display: inline-block;\r\n  width: 24px;\r\n  height: 24px;\r\n  border: 1px solid #FFF;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.theme-preview.blue .main-color {\r\n  background-color: #007bff;\r\n}\r\n\r\n.theme-preview.blue .accent-color {\r\n  background-color: #8fbffa;\r\n}\r\n\r\n.theme-preview.blue .hover-color {\r\n  background-color: #0056b3;\r\n}\r\n\r\n.theme-preview.green .main-color {\r\n  background-color: #6B8E23;\r\n}\r\n\r\n.theme-preview.green .accent-color {\r\n  background-color: #9ACD32;\r\n}\r\n\r\n.theme-preview.green .hover-color {\r\n  background-color: #556B2F;\r\n}\r\n\r\n.theme-preview.red .main-color {\r\n  background-color: #f45a40;\r\n}\r\n\r\n.theme-preview.red .accent-color {\r\n  background-color: #f79988;\r\n}\r\n\r\n.theme-preview.red .hover-color {\r\n  background-color: #c73f29;\r\n}\r\n\r\n.theme-preview.yellow .main-color {\r\n  background-color: #ffae00;\r\n}\r\n\r\n.theme-preview.yellow .accent-color {\r\n  background-color: #faea8f;\r\n}\r\n\r\n.theme-preview.yellow .hover-color {\r\n  background-color: #cd930c;\r\n}\r\n\r\n.theme-preview.purple .main-color {\r\n  background-color: #6d4aff;\r\n}\r\n\r\n.theme-preview.purple .accent-color {\r\n  background-color: #b086ff;\r\n}\r\n\r\n.theme-preview.purple .hover-color {\r\n  background-color: #5e42cd;\r\n}\r\n\r\n.custom-colors {\r\n  display: flex;\r\n  flex-direction: row;\r\n  gap: 12px;\r\n}\r\n\r\n.color-picker {\r\n  flex-shrink: 0;\r\n}\r\n\r\n.color-picker::before {\r\n  color: var(--hover-color);\r\n  position: absolute;\r\n  top: -5px;\r\n  right: -5px;\r\n  border: 1px solid;\r\n  border-radius: 15px;\r\n  background-color: var(--box-background-color);\r\n  padding: 4px;\r\n}\r\n\r\n.color-picker-button:hover label {\r\n  color: var(--hover-color);\r\n}\r\n\r\n.wrap {\r\n  flex-wrap: wrap;\r\n}\r\n\r\n.user-list {\r\n  display: flex;\r\n  flex-direction: column;\r\n  flex-wrap: wrap;\r\n  justify-content: space-between;\r\n}\r\n\r\n.user-list>div {\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-grow: 1;\r\n  flex-wrap: wrap;\r\n}\r\n\r\n.user-list .user-list-row {\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-grow: 1;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .user-list .user-list-row {\r\n    flex-direction: column;\r\n  }\r\n}\r\n\r\n.user-list .user-list-row:last-of-type {\r\n  flex-grow: 0;\r\n}\r\n\r\n.user-list .user-list-row>div {\r\n  display: flex;\r\n  flex-basis: 50%;\r\n  gap: 12px;\r\n  align-items: baseline;\r\n}\r\n\r\n.user-list a {\r\n  color: var(--main-color);\r\n  text-decoration: none;\r\n}\r\n\r\n.user-list a:hover {\r\n  color: var(--hover-color);\r\n}\r\n\r\n.user-list .user-list-icon {\r\n  width: 16px;\r\n  text-align: center;\r\n}\r\n\r\n.calendar-nav {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  gap: 16px;\r\n  font-size: 18px;\r\n}\r\n\r\n.calendar-nav .month {\r\n  text-align: center;\r\n}\r\n\r\n.calendar {\r\n  display: flex;\r\n  flex-direction: column;\r\n  width: 100%;\r\n  background-color: var(--box-background-color);\r\n  border-collapse: collapse;\r\n  border-radius: 16px;\r\n  box-shadow: var(--box-shadow);\r\n  box-sizing: border-box;\r\n}\r\n\r\n.calendar .calendar-header,\r\n.calendar .calendar-row {\r\n  display: flex;\r\n}\r\n\r\n.calendar .calendar-header {\r\n  border-bottom: 6px solid var(--main-color);\r\n}\r\n\r\n.calendar .calendar-row:last-of-type {\r\n  border-bottom: none;\r\n}\r\n\r\n.calendar .calendar-cell {\r\n  display: flex;\r\n  flex: 0 0 14.2857%;\r\n  flex-direction: column;\r\n  gap: 5px;\r\n  overflow: hidden;\r\n}\r\n\r\n.calendar .calendar-header .calendar-cell {\r\n  padding: 26px 0px;\r\n  font-size: 16px;\r\n  font-weight: 500;\r\n  text-align: center;\r\n  box-sizing: border-box;\r\n  min-height: 45px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .calendar .calendar-header .calendar-cell {\r\n    font-size: 12px;\r\n  }\r\n}\r\n\r\n.calendar .calendar-body .calendar-cell {\r\n  font-size: 13px;\r\n  text-align: center;\r\n  box-sizing: border-box;\r\n  min-height: 92px;\r\n  border-right: 1px solid var(--box-border-color);\r\n}\r\n\r\n.calendar .calendar-body .calendar-cell:last-of-type {\r\n  border-right: none;\r\n}\r\n\r\n.calendar .calendar-body .calendar-cell .calendar-cell-header {\r\n  background-color: #EEE;\r\n  padding: 5px 0px;\r\n}\r\n\r\n.calendar .calendar-body .calendar-cell .calendar-cell-content {\r\n  padding: 6px 0px;\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 6px;\r\n  box-sizing: border-box;\r\n  padding: 0px 6px;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .calendar .calendar-body .calendar-cell .calendar-cell-content {\r\n    padding: 0px 1px;\r\n  }\r\n}\r\n\r\n.calendar .calendar-subscription-title {\r\n  border: 1px solid var(--main-color);\r\n  border-radius: 8px;\r\n  padding: 4px 2px;\r\n  cursor: pointer;\r\n  box-sizing: border-box;\r\n  white-space: normal;\r\n  overflow-wrap: break-word;\r\n  word-wrap: break-word;\r\n  user-select: none;\r\n}\r\n\r\n.calendar .day {\r\n  font-size: 14px;\r\n}\r\n\r\n.calendar .today .day {\r\n  color: var(--main-color);\r\n  font-weight: 700;\r\n}\r\n\r\n.over-budget {\r\n  background-color: rgba(var(--error-color-rgb), 0.2);\r\n  border: 1px solid var(--error-color);\r\n  border-radius: 8px;\r\n  padding: 10px;\r\n  margin-top: 20px;\r\n  text-align: center;\r\n  font-size: 16px;\r\n}\r\n\r\n.over-budget>i {\r\n  color: var(--error-color);\r\n}\r\n\r\n.subscription-modal {\r\n  max-width: 400px;\r\n}\r\n\r\n.subscription-modal .modal-header {\r\n  position: relative;\r\n}\r\n\r\n.subscription-modal .close-modal {\r\n  position: absolute;\r\n  top: -5px;\r\n  right: -5px;\r\n  padding: 5px;\r\n  font-size: 20px;\r\n  cursor: pointer;\r\n}\r\n\r\n.subscription-modal img {\r\n  max-width: 135px;\r\n}\r\n\r\n.grow {\r\n  flex-grow: 1;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .mobile-grow {\r\n    flex-grow: 1;\r\n  }\r\n\r\n  .mobile-reverse {\r\n    flex-direction: column-reverse;\r\n  }\r\n\r\n  .mobile-grow-force {\r\n    flex-grow: 1;\r\n    width: 100%;\r\n  }\r\n\r\n}\r\n\r\n.bold {\r\n  font-weight: 700;\r\n}\r\n\r\n/* Checkbox */\r\ninput[type=\"checkbox\"] {\r\n  opacity: 0;\r\n  position: absolute;\r\n}\r\n\r\ninput[type=\"checkbox\"]+label {\r\n  position: relative;\r\n  padding-left: 54px;\r\n  cursor: pointer;\r\n  display: inline-flex;\r\n  align-items: center;\r\n}\r\n\r\ninput[type=\"checkbox\"]+label::before {\r\n  content: '';\r\n  position: absolute;\r\n  left: 0;\r\n  top: 50%;\r\n  transform: translateY(-50%);\r\n  width: 44px;\r\n  height: 24px;\r\n  background-color: #e9e9eb;\r\n  border-radius: 12px;\r\n  transition: background-color 0.3s ease;\r\n  border: 1px solid #ccc;\r\n}\r\n\r\ninput[type=\"checkbox\"]+label::after {\r\n  content: '';\r\n  position: absolute;\r\n  left: 3px;\r\n  top: 50%;\r\n  transform: translateY(-50%);\r\n  width: 20px;\r\n  height: 20px;\r\n  background-color: #fff;\r\n  border-radius: 50%;\r\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\r\n  transition: transform 0.3s ease;\r\n}\r\n\r\ninput[type=\"checkbox\"]:focus+label::before {\r\n  box-shadow: 0 0 0 2px rgba(var(--main-color-rgb, 100, 149, 237), 0.5);\r\n}\r\n\r\ninput[type=\"checkbox\"]:disabled+label::before {\r\n  background-color: #d3d3d3;\r\n}\r\n\r\ninput[type=\"checkbox\"]:disabled+label::after {\r\n  background-color: #f0f0f0;\r\n}\r\n\r\ninput[type=\"checkbox\"]:checked+label::before {\r\n  background-color: var(--main-color);\r\n}\r\n\r\ninput[type=\"checkbox\"]:checked+label::after {\r\n  transform: translate(20px, -50%);\r\n}\r\n\r\ninput[type=\"checkbox\"]:focus+label::after {\r\n  outline: 2px solid rgba(var(--main-color-rgb, 100, 149, 237), 0.5);\r\n}\r\n\r\n/* Radio */\r\ninput[type=\"radio\"] {\r\n  opacity: 0;\r\n  position: absolute;\r\n}\r\n\r\ninput[type=\"radio\"]+label {\r\n  position: relative;\r\n  padding-left: 35px;\r\n  cursor: pointer;\r\n  display: flex;\r\n  line-height: 22px;\r\n}\r\n\r\ninput[type=\"radio\"]+label::before {\r\n  content: '';\r\n  position: absolute;\r\n  left: 0;\r\n  top: 0;\r\n  width: 24px;\r\n  height: 24px;\r\n  background: #fafafa;\r\n  border: 1px solid #ccc;\r\n  border-radius: 50%;\r\n  box-sizing: border-box;\r\n}\r\n\r\ninput[type=\"radio\"]:focus+label::before {\r\n  border-color: var(--main-color);\r\n  box-shadow: 0 0 0 2px rgba(var(--main-color-rgb), 0.5);\r\n}\r\n\r\ninput[type=\"radio\"]:disabled+label::before {\r\n  background-color: #F5F5F5;\r\n  border-color: #F5F5F5;\r\n  cursor: not-allowed;\r\n}\r\n\r\ninput[type=\"radio\"]:checked+label::after {\r\n  content: '';\r\n  position: absolute;\r\n  left: 5px;\r\n  top: 5px;\r\n  width: 14px;\r\n  height: 14px;\r\n  background: var(--main-color);\r\n  border-radius: 50%;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.theme input[type=\"radio\"]+label {\r\n  padding-left: 44px;\r\n}\r\n\r\n.theme input[type=\"radio\"]+label::before {\r\n  left: 11px;\r\n  top: 20px;\r\n}\r\n\r\n.theme input[type=\"radio\"]:checked+label::after {\r\n  left: 16px;\r\n  top: 25px;\r\n}\r\n\r\n.update-banner {\r\n  padding: 15px 20px;\r\n  background-color: var(--accent-color);\r\n  border: 1px solid var(--main-color);\r\n  border-radius: 12px;\r\n  margin-bottom: 20px;\r\n  text-align: center;\r\n  color: var(--text-color);\r\n}\r\n\r\n.update-banner>span {\r\n  font-weight: 500;\r\n}\r\n\r\n.update-banner>span>a {\r\n  color: var(--text-color);\r\n  text-decoration: underline;\r\n}\r\n\r\n.demo-banner {\r\n  padding: 15px 20px;\r\n  background-color: rgba(var(--error-color-rgb), 0.2);\r\n  border: 1px solid #f45a40;\r\n  border-radius: 12px;\r\n  margin-bottom: 20px;\r\n  text-align: center;\r\n}\r\n\r\n.totp-popup {\r\n  display: none;\r\n  position: fixed;\r\n  width: 380px;\r\n  max-width: 90%;\r\n  top: 50%;\r\n  left: 50%;\r\n  transform: translate(-50%, -50%);\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 16px;\r\n  box-shadow: var(--box-shadow);\r\n  box-sizing: border-box;\r\n  padding: 20px;\r\n  flex-direction: column;\r\n  gap: 20px;\r\n  z-index: 2;\r\n}\r\n\r\n.totp-popup h3,\r\n.totp-popup h4 {\r\n  margin: 4px 0px;\r\n  text-align: center;\r\n}\r\n\r\n.totp-popup.is-open {\r\n  display: flex;\r\n}\r\n\r\n.totp-popup-content {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 20px;\r\n  align-items: center;\r\n  margin-top: 20px;\r\n}\r\n\r\n.totp-setup {\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n}\r\n\r\n.totp-setup.hide {\r\n  display: none;\r\n}\r\n\r\n.totp-qrcode-container {\r\n  padding: 14px;\r\n  border: 1px solid #333;\r\n  border-radius: 8px;\r\n}\r\n\r\n.totp-backup-codes {\r\n  background-color: #EEE;\r\n  border: 2px dashed #ccc;\r\n  border-radius: 8px;\r\n  padding: 10px;\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-wrap: wrap;\r\n  gap: 10px 18px;\r\n  justify-content: space-evenly;\r\n}\r\n\r\n.totp-backup-codes li {\r\n  list-style: none;\r\n  padding: 5px 10px;\r\n  font-weight: 500;\r\n}\r\n\r\n.mobile-nav {\r\n  display: none;\r\n}\r\n\r\n.mobile-nav-image {\r\n  width: 100%;\r\n  max-width: 440px;\r\n  margin-top: 15px;\r\n  background-image: url(\"../images/siteimages/mobilenav.png\");\r\n  background-size: contain;\r\n  background-position: center;\r\n  background-repeat: no-repeat;\r\n  aspect-ratio: 16/9;\r\n}\r\n\r\n.button.export-ical {\r\n  padding: 0px;\r\n  width: 30px;\r\n  height: 30px;\r\n  margin-left: 10px;\r\n  color: white;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .mobile-nav {\r\n    position: fixed;\r\n    bottom: 0px;\r\n    width: 100%;\r\n    background-color: var(--box-background-color);\r\n    border-top: 1px solid var(--box-border-color);\r\n    display: flex;\r\n    flex-direction: row;\r\n    justify-content: space-around;\r\n    z-index: 2;\r\n    padding: 7px 0px;\r\n    box-shadow: var(--negative-box-shadow);\r\n    box-sizing: border-box;\r\n    align-items: center;\r\n  }\r\n\r\n  .mobile-nav>a {\r\n    flex-grow: 1;\r\n    flex-shrink: 0;\r\n    text-align: center;\r\n    padding: 5px 0px 10px 0px;\r\n    color: #AAA;\r\n    font-size: 10px;\r\n    text-decoration: none;\r\n\r\n    display: flex;\r\n    flex-direction: column;\r\n    align-items: center;\r\n  }\r\n\r\n  .mobile-nav>a>svg {\r\n    width: 30px;\r\n    height: 30px;\r\n    max-width: 85%;\r\n  }\r\n\r\n  .mobile-nav>a.active {\r\n    color: #202020;\r\n  }\r\n\r\n  .mobile-navigation .mobileNavigationHideOnMobile {\r\n    display: none !important;\r\n  }\r\n}\r\n\r\n.button.autofill-next-payment {\r\n  padding: 15px 15px !important;\r\n  margin-top: 22px;\r\n}\r\n\r\n.autofill-next-payment {\r\n  color: var(--main-color);\r\n  cursor: pointer;\r\n}\r\n\r\n.autofill-next-payment.hideOnDesktop {\r\n  display: none;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .button.autofill-next-payment.hideOnMobile {\r\n    display: none !important;\r\n  }\r\n\r\n\r\n  .autofill-next-payment.hideOnDesktop {\r\n    display: block;\r\n  }\r\n}\r\n\r\n.dashboard h1 {\r\n  margin: 0px;\r\n}\r\n\r\n\r\n.dashboard-subscriptions-container {\r\n  overflow-x: auto;\r\n  overflow-y: hidden;\r\n  max-width: 100%;\r\n  padding-bottom: 10px;\r\n}\r\n\r\n.dashboard-subscriptions-list {\r\n  display: flex;\r\n  flex-direction: row;\r\n  gap: 10px;\r\n  min-width: fit-content;\r\n  /* prevent collapsing */\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item {\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 16px;\r\n  box-shadow: var(--box-shadow);\r\n  padding: 20px;\r\n  width: 155px;\r\n  height: 145px;\r\n  flex: 0 0 auto;\r\n  /* prevent flex resizing */\r\n  box-sizing: border-box;\r\n  display: flex;\r\n  flex-direction: column;\r\n  justify-content: space-between;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item.thin {\r\n  height: 115px;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item .subscription-item-title {\r\n  font-size: 19px;\r\n  font-weight: 500;\r\n  margin: 0px;\r\n  display: -webkit-box;\r\n  -webkit-line-clamp: 2;     /* maximum number of lines */\r\n  line-clamp: 2;\r\n  -webkit-box-orient: vertical;\r\n  overflow: hidden;\r\n  text-overflow: ellipsis;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item.thin .subscription-item-title {\r\n  font-size: 16px;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item .subscription-item-logo {\r\n  max-width: 100%;\r\n  height: 42px;\r\n  object-fit: contain;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item .subscription-item-date {\r\n  font-size: 16px;\r\n  margin: 0px;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item .subscription-item-price {\r\n  font-size: 18px;\r\n  font-weight: 600;\r\n  margin: 0px;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item .subscription-item-value {\r\n  font-size: 24px;\r\n  font-weight: 600;\r\n  margin: 0px;\r\n}\r\n\r\n.dashboard-subscriptions-list>.subscription-item.thin .subscription-item-value {\r\n  font-size: 20px;;\r\n}\r\n\r\n.ai-recommendations-container {\r\n  width: 100%;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.ai-recommendations-list {\r\n  list-style: none;\r\n  padding: 0;\r\n  margin: 0;\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 16px;\r\n}\r\n\r\n.ai-recommendation-item {\r\n  background-color: var(--box-background-color);\r\n  border: 1px solid var(--box-border-color);\r\n  border-radius: 16px;\r\n  box-shadow: var(--box-shadow);\r\n  padding: 18px 20px;\r\n  width: 100%;\r\n  box-sizing: border-box;\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 8px;\r\n  margin: 0;\r\n  cursor: pointer;\r\n}\r\n\r\n.ai-recommendation-item .ai-recommendation-header h3 {\r\n  font-size: 18px;\r\n  font-weight: 600;\r\n  margin: 0px;\r\n  line-height: 1.2;\r\n}\r\n\r\n.ai-recommendation-item .ai-recommendation-header {\r\n  display: flex;\r\n  flex-direction: row;\r\n  align-items: center;\r\n  justify-content: space-between;\r\n  gap: 8px;\r\n}\r\n\r\n.ai-recommendation-item .ai-recommendation-header .item-arrow-down {\r\n  color: var(--main-color);\r\n}\r\n\r\n.ai-recommendation-item.expanded .ai-recommendation-header .item-arrow-down {\r\n  transform: rotate(180deg);\r\n}\r\n\r\n.ai-recommendation-item .ai-recommendation-header h3 > span {\r\n  color: var(--main-color);\r\n}\r\n\r\n.ai-recommendation-item p {\r\n  display: none;\r\n  font-size: 15px;\r\n  margin: 0 0 4px 0;\r\n  line-height: 1.5;\r\n  margin-top: 8px;\r\n}\r\n\r\n.ai-recommendation-item.expanded p {\r\n  display: block;\r\n}\r\n\r\n.ai-recommendation-item p:last-child {\r\n  font-size: 16px;\r\n  font-weight: 600;\r\n  color: var(--accent-color);\r\n}\r\n\r\n.ai-recommendation-item p.ai-recommendation-savings {\r\n  justify-content: space-between;\r\n}\r\n\r\n.ai-recommendation-item p.ai-recommendation-savings a {\r\n  color: var(--main-color);\r\n  text-decoration: none;\r\n}\r\n\r\n.ai-recommendation-item.expanded p.ai-recommendation-savings {\r\n  display: flex;\r\n}\r\n\r\n.flex {\r\n  display: flex;\r\n}\r\n\r\n.info-badge {\r\n    background-color: orange;\r\n    border-radius: 5px;\r\n    font-size: 10px;\r\n    padding: 2px 6px;\r\n    color: #FFFFFF;\r\n    margin-bottom: auto;\r\n    margin-left: 10px;\r\n}\r\n\r\n.spinner {\r\n  width: 38px;\r\n  height: 38px;\r\n  border: 4px solid rgba(0, 0, 0, 0.1);\r\n  border-left-color: var(--main-color);\r\n  border-radius: 50%;\r\n  animation: spin 1s linear infinite;\r\n  margin: auto;\r\n}\r\n\r\n.spinner.ai-spinner {\r\n  margin: 0px 0px 0px auto;\r\n}\r\n\r\n.spinner.hidden {\r\n  display: none;\r\n}\r\n\r\n@keyframes spin {\r\n  to {\r\n    transform: rotate(360deg);\r\n  }\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .spinner.ai-spinner {\r\n    margin: auto;\r\n  }\r\n}\r\n\r\n.dashboard-subscriptions-container {\r\n    scrollbar-width: none;\r\n    -ms-overflow-style: none;\r\n}\r\n\r\n.dashboard-subscriptions-container::-webkit-scrollbar {\r\n    display: none;\r\n}"
  },
  {
    "path": "styles/theme.css",
    "content": ":root {\r\n    --main-color: #007BFF;\r\n    --main-color-rgb: 0, 123, 255;\r\n    --accent-color: #8FBFFA;\r\n    --accent-color-rgb: 143, 191, 250;\r\n    --hover-color: #0056B3;\r\n    --hover-color-rgb: 0, 86, 179;\r\n    --error-color: #F45A40;\r\n    --error-color-rgb: 244, 90, 64;\r\n    --success-color: #188823;\r\n    --success-color-rgb: 24, 136, 35;\r\n    --background-color: #F5F5F5;\r\n    --background-color-rgb: 245, 245, 245;\r\n    --header-background-color: #FFFFFF;\r\n    --header-background-color-rgb: 255, 255, 255;\r\n    --box-background-color: #FFFFFF;\r\n    --box-background-color-rgb: 255, 255, 255;\r\n    --box-border-color: #EEEEEE;\r\n    --box-border-color-rgb: 238, 238, 238;\r\n    --box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\r\n    --text-color: #202020;\r\n    --text-color-rgb: 32, 32, 32;\r\n    --text-color-inverted: #FFFFFF;\r\n    --text-color-inverted-rgb: 255, 255, 255;\r\n    --input-background-color: #FFFFFF;\r\n    --input-background-color-rgb: 255, 255, 255;\r\n    --input-border-color: #CCCCCC;\r\n    --input-border-color-rgb: 204, 204, 204;\r\n    --input-disabled-background-color: #F5F5F5;\r\n    --input-disabled-background-color-rgb: 245, 245, 245;\r\n    --input-disabled-border-color: #F5F5F5;\r\n    --input-disabled-border-color-rgb: 245, 245, 245;\r\n    --negative-box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);\r\n}\r\n\r\nsvg .main-color {\r\n    fill: var(--main-color);\r\n}\r\n\r\nsvg .accent-color {\r\n    fill: var(--accent-color);\r\n}\r\n\r\nsvg .text-color {\r\n    fill: #202020;\r\n}"
  },
  {
    "path": "styles/themes/green.css",
    "content": ":root {\r\n    --main-color: #6B8E23; /* Dark Olive Green */\r\n    --main-color-rgb: 107, 142, 35;\r\n    --accent-color: #9ACD32; /* Yellow-Green */\r\n    --accent-color-rgb: 154, 205, 50;\r\n    --hover-color: #556B2F; /* Olive Drab */\r\n    --hover-color-rgb: 85, 107, 47;\r\n}"
  },
  {
    "path": "styles/themes/purple.css",
    "content": ":root {\r\n    --main-color: #6d4aff;\r\n    --main-color-rgb: 109, 74, 255;\r\n    --accent-color: #b086ff;\r\n    --accent-color-rgb: 176, 134, 255;\r\n    --hover-color: #5e42cd;\r\n    --hover-color-rgb: 50, 48, 108;\r\n}\r\n"
  },
  {
    "path": "styles/themes/red.css",
    "content": ":root {\r\n    --main-color: #f45a40;\r\n    --main-color-rgb: 244, 90, 64;\r\n    --accent-color: #f79988;\r\n    --accent-color-rgb: 239, 134, 116;\r\n    --hover-color: #c73f29;\r\n    --hover-color-rgb: 199, 63, 41;\r\n}\r\n"
  },
  {
    "path": "styles/themes/yellow.css",
    "content": ":root {\r\n    --main-color: #ffae00;\r\n    --main-color-rgb: 255, 174, 0;\r\n    --accent-color: #faea8f;\r\n    --accent-color-rgb: 250, 234, 143;\r\n    --hover-color: #cd930c;\r\n    --hover-color-rgb: 179, 124, 0;\r\n}"
  },
  {
    "path": "subscriptions.php",
    "content": "<?php\r\n\r\nrequire_once 'includes/header.php';\r\nrequire_once 'includes/getdbkeys.php';\r\n\r\ninclude_once 'includes/list_subscriptions.php';\r\n\r\n$sort = \"next_payment\";\r\n$sortOrder = $sort;\r\n\r\nif ($settings['disabledToBottom'] === 'true') {\r\n  $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId ORDER BY inactive ASC, next_payment ASC\";\r\n} else {\r\n  $sql = \"SELECT * FROM subscriptions WHERE user_id = :userId ORDER BY next_payment ASC, inactive ASC\";\r\n}\r\n\r\n$params = array();\r\n\r\nif (isset($_COOKIE['sortOrder']) && $_COOKIE['sortOrder'] != \"\") {\r\n  $sort = $_COOKIE['sortOrder'] ?? 'next_payment';\r\n}\r\n\r\n$sortOrder = $sort;\r\n$allowedSortCriteria = ['name', 'id', 'next_payment', 'price', 'payer_user_id', 'category_id', 'payment_method_id', 'inactive', 'alphanumeric', 'renewal_type'];\r\n$order = ($sort == \"price\" || $sort == \"id\") ? \"DESC\" : \"ASC\";\r\n\r\nif ($sort == \"alphanumeric\") {\r\n  $sort = \"name\";\r\n}\r\n\r\nif (!in_array($sort, $allowedSortCriteria)) {\r\n  $sort = \"next_payment\";\r\n}\r\n\r\nif ($sort == \"renewal_type\") {\r\n  $sort = \"auto_renew\";\r\n}\r\n\r\n$sql = \"SELECT * FROM subscriptions WHERE user_id = :userId\";\r\n\r\nif (isset($_GET['member'])) {\r\n  $memberIds = explode(',', $_GET['member']);\r\n  $placeholders = array_map(function ($key) {\r\n    return \":member{$key}\";\r\n  }, array_keys($memberIds));\r\n\r\n  $sql .= \" AND payer_user_id IN (\" . implode(',', $placeholders) . \")\";\r\n\r\n  foreach ($memberIds as $key => $memberId) {\r\n    $params[\":member{$key}\"] = $memberId;\r\n  }\r\n}\r\n\r\nif (isset($_GET['category'])) {\r\n  $categoryIds = explode(',', $_GET['category']);\r\n  $placeholders = array_map(function ($key) {\r\n    return \":category{$key}\";\r\n  }, array_keys($categoryIds));\r\n\r\n  $sql .= \" AND category_id IN (\" . implode(',', $placeholders) . \")\";\r\n\r\n  foreach ($categoryIds as $key => $categoryId) {\r\n    $params[\":category{$key}\"] = $categoryId;\r\n  }\r\n}\r\n\r\nif (isset($_GET['payment'])) {\r\n  $paymentIds = explode(',', $_GET['payment']);\r\n  $placeholders = array_map(function ($key) {\r\n    return \":payment{$key}\";\r\n  }, array_keys($paymentIds));\r\n\r\n  $sql .= \" AND payment_method_id IN (\" . implode(',', $placeholders) . \")\";\r\n\r\n  foreach ($paymentIds as $key => $paymentId) {\r\n    $params[\":payment{$key}\"] = $paymentId;\r\n  }\r\n}\r\n\r\nif (!isset($settings['hideDisabledSubscriptions']) || $settings['hideDisabledSubscriptions'] !== 'true') {\r\n  if (isset($_GET['state']) && $_GET['state'] != \"\") {\r\n    $sql .= \" AND inactive = :inactive\";\r\n    $params[':inactive'] = $_GET['state'];\r\n  }\r\n}\r\n\r\n$orderByClauses = [];\r\n\r\nif ($settings['disabledToBottom'] === 'true') {\r\n  if (in_array($sort, [\"payer_user_id\", \"category_id\", \"payment_method_id\"])) {\r\n    $orderByClauses[] = \"$sort $order\";\r\n    $orderByClauses[] = \"inactive ASC\";\r\n  } else {\r\n    $orderByClauses[] = \"inactive ASC\";\r\n    $orderByClauses[] = \"$sort $order\";\r\n  }\r\n} else {\r\n  $orderByClauses[] = \"$sort $order\";\r\n  if ($sort != \"inactive\") {\r\n    $orderByClauses[] = \"inactive ASC\";\r\n  }\r\n}\r\n\r\nif ($sort != \"next_payment\") {\r\n  $orderByClauses[] = \"next_payment ASC\";\r\n}\r\n\r\n$sql .= \" ORDER BY \" . implode(\", \", $orderByClauses);\r\n\r\n$stmt = $db->prepare($sql);\r\n$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);\r\n\r\nif (!empty($params)) {\r\n  foreach ($params as $key => $value) {\r\n    $stmt->bindValue($key, $value, SQLITE3_INTEGER);\r\n  }\r\n}\r\n\r\n$result = $stmt->execute();\r\nif ($result) {\r\n  $subscriptions = array();\r\n  while ($row = $result->fetchArray(SQLITE3_ASSOC)) {\r\n    $subscriptions[] = $row;\r\n  }\r\n}\r\n\r\nforeach ($subscriptions as $subscription) {\r\n  $memberId = $subscription['payer_user_id'];\r\n  $members[$memberId]['count']++;\r\n  $categoryId = $subscription['category_id'];\r\n  $categories[$categoryId]['count']++;\r\n  $paymentMethodId = $subscription['payment_method_id'];\r\n  $payment_methods[$paymentMethodId]['count']++;\r\n}\r\n\r\nif ($sortOrder == \"category_id\") {\r\n  usort($subscriptions, function ($a, $b) use ($categories) {\r\n    return $categories[$a['category_id']]['order'] - $categories[$b['category_id']]['order'];\r\n  });\r\n}\r\n\r\nif ($sortOrder == \"payment_method_id\") {\r\n  usort($subscriptions, function ($a, $b) use ($payment_methods) {\r\n    return $payment_methods[$a['payment_method_id']]['order'] - $payment_methods[$b['payment_method_id']]['order'];\r\n  });\r\n}\r\n\r\n$headerClass = count($subscriptions) > 0 ? \"main-actions\" : \"main-actions hidden\";\r\n?>\r\n<style>\r\n  .logo-preview:after {\r\n    content: '<?= translate('upload_logo', $i18n) ?>';\r\n  }\r\n</style>\r\n\r\n<section class=\"contain\">\r\n  <?php\r\n  if ($isAdmin && $settings['update_notification']) {\r\n    if (!is_null($settings['latest_version'])) {\r\n      $latestVersion = $settings['latest_version'];\r\n      if (version_compare($version, $latestVersion) == -1) {\r\n        ?>\r\n        <div class=\"update-banner\">\r\n          <?= translate('new_version_available', $i18n) ?>:\r\n          <span><a href=\"https://github.com/ellite/Wallos/releases/tag/<?= htmlspecialchars($latestVersion) ?>\"\r\n              target=\"_blank\" rel=\"noreferer\">\r\n              <?= htmlspecialchars($latestVersion) ?>\r\n            </a></span>\r\n        </div>\r\n        <?php\r\n      }\r\n    }\r\n  }\r\n\r\n  if ($demoMode) {\r\n    ?>\r\n    <div class=\"demo-banner\">\r\n      Running in <b>Demo Mode</b>, certain actions and settings are disabled.<br>\r\n      The database will be reset every 120 minutes.\r\n    </div>\r\n    <?php\r\n  }\r\n  ?>\r\n\r\n  <header class=\"<?= $headerClass ?>\" id=\"main-actions\">\r\n    <button class=\"button\" onClick=\"addSubscription()\">\r\n      <i class=\"fa-solid fa-circle-plus\"></i>\r\n      <?= translate('new_subscription', $i18n) ?>\r\n    </button>\r\n    <div class=\"top-actions\">\r\n      <div class=\"search\">\r\n        <input type=\"text\" autocomplete=\"off\" name=\"search\" id=\"search\" placeholder=\"<?= translate('search', $i18n) ?>\"\r\n          onkeyup=\"searchSubscriptions()\" />\r\n        <span class=\"fa-solid fa-magnifying-glass search-icon\"></span>\r\n        <span class=\"fa-solid fa-xmark clear-search\" onClick=\"clearSearch()\"></span>\r\n      </div>\r\n\r\n      <div class=\"filtermenu on-dashboard\">\r\n        <button class=\"button secondary-button\" id=\"filtermenu-button\" title=\"<?= translate(\"filter\", $i18n) ?>\">\r\n          <i class=\"fa-solid fa-filter\"></i>\r\n        </button>\r\n        <?php include 'includes/filters_menu.php'; ?>\r\n      </div>\r\n\r\n      <div class=\"sort-container\">\r\n        <button class=\"button secondary-button\" value=\"Sort\" onClick=\"toggleSortOptions()\" id=\"sort-button\"\r\n          title=\"<?= translate('sort', $i18n) ?>\">\r\n          <i class=\"fa-solid fa-arrow-down-wide-short\"></i>\r\n        </button>\r\n        <?php include 'includes/sort_options.php'; ?>\r\n      </div>\r\n    </div>\r\n  </header>\r\n  <div class=\"subscriptions\" id=\"subscriptions\">\r\n    <?php\r\n    $formatter = new IntlDateFormatter(\r\n      'en', // Force English locale\r\n      IntlDateFormatter::SHORT,\r\n      IntlDateFormatter::NONE,\r\n      null,\r\n      null,\r\n      'MMM d, yyyy'\r\n    );\r\n\r\n    foreach ($subscriptions as $subscription) {\r\n      if ($subscription['inactive'] == 1 && isset($settings['hideDisabledSubscriptions']) && $settings['hideDisabledSubscriptions'] === 'true') {\r\n        continue;\r\n      }\r\n      $id = $subscription['id'];\r\n      $print[$id]['id'] = $id;\r\n      $print[$id]['logo'] = $subscription['logo'] != \"\" ? \"images/uploads/logos/\" . $subscription['logo'] : \"\";\r\n      $print[$id]['name'] = $subscription['name'];\r\n      $cycle = $subscription['cycle'];\r\n      $frequency = $subscription['frequency'];\r\n      $print[$id]['billing_cycle'] = getBillingCycle($cycle, $frequency, $i18n);\r\n      $paymentMethodId = $subscription['payment_method_id'];\r\n      $print[$id]['currency_code'] = $currencies[$subscription['currency_id']]['code'];\r\n      $currencyId = $subscription['currency_id'];\r\n      $print[$id]['auto_renew'] = $subscription['auto_renew'];\r\n      $next_payment_timestamp = strtotime($subscription['next_payment']);\r\n      $formatted_date = $formatter->format($next_payment_timestamp);\r\n      $print[$id]['next_payment'] = $formatted_date;\r\n      $paymentIconFolder = (strpos($payment_methods[$paymentMethodId]['icon'], 'images/uploads/icons/') !== false) ? \"\" : \"images/uploads/logos/\";\r\n      $print[$id]['payment_method_icon'] = $paymentIconFolder . $payment_methods[$paymentMethodId]['icon'];\r\n      $print[$id]['payment_method_name'] = $payment_methods[$paymentMethodId]['name'];\r\n      $print[$id]['payment_method_id'] = $paymentMethodId;\r\n      $print[$id]['category_id'] = $subscription['category_id'];\r\n      $print[$id]['payer_user_id'] = $subscription['payer_user_id'];\r\n      $print[$id]['price'] = floatval($subscription['price']);\r\n      $print[$id]['progress'] = getSubscriptionProgress($cycle, $frequency, $subscription['next_payment']);\r\n      $print[$id]['inactive'] = $subscription['inactive'];\r\n      $print[$id]['url'] = $subscription['url'];\r\n      $print[$id]['notes'] = $subscription['notes'];\r\n      $print[$id]['replacement_subscription_id'] = $subscription['replacement_subscription_id'];\r\n\r\n      if (isset($settings['convertCurrency']) && $settings['convertCurrency'] === 'true' && $currencyId != $mainCurrencyId) {\r\n        $print[$id]['price'] = getPriceConverted($print[$id]['price'], $currencyId, $db);\r\n        $print[$id]['currency_code'] = $currencies[$mainCurrencyId]['code'];\r\n      }\r\n      if (isset($settings['showMonthlyPrice']) && $settings['showMonthlyPrice'] === 'true') {\r\n        $print[$id]['price'] = getPricePerMonth($cycle, $frequency, $print[$id]['price']);\r\n      }\r\n      if (isset($settings['showOriginalPrice']) && $settings['showOriginalPrice'] === 'true') {\r\n        $print[$id]['original_price'] = floatval($subscription['price']);\r\n        $print[$id]['original_currency_code'] = $currencies[$subscription['currency_id']]['code'];\r\n      }\r\n    }\r\n\r\n    if ($sortOrder == \"alphanumeric\") {\r\n      usort($print, function ($a, $b) {\r\n        return strnatcmp(strtolower($a['name']), strtolower($b['name']));\r\n      });\r\n      if ($settings['disabledToBottom'] === 'true') {\r\n        usort($print, function ($a, $b) {\r\n          return $a['inactive'] - $b['inactive'];\r\n        });\r\n      }\r\n    }\r\n\r\n    if (isset($print)) {\r\n      printSubscriptions($print, $sort, $categories, $members, $i18n, $colorTheme, \"\", $settings['disabledToBottom'], $settings['mobileNavigation'], $settings['showSubscriptionProgress'], $currencies, $lang);\r\n    }\r\n    $db->close();\r\n\r\n    if (count($subscriptions) == 0) {\r\n      ?>\r\n      <div class=\"empty-page\">\r\n        <img src=\"images/siteimages/empty.png\" alt=\"<?= translate('empty_page', $i18n) ?>\" />\r\n        <p>\r\n          <?= translate('no_subscriptions_yet', $i18n) ?>\r\n        </p>\r\n        <button class=\"button\" onClick=\"addSubscription()\">\r\n          <i class=\"fa-solid fa-circle-plus\"></i>\r\n          <?= translate('add_first_subscription', $i18n) ?>\r\n        </button>\r\n      </div>\r\n      <?php\r\n    }\r\n    ?>\r\n  </div>\r\n</section>\r\n<section class=\"subscription-form\" id=\"subscription-form\">\r\n  <header>\r\n    <h3 id=\"form-title\"><?= translate('add_subscription', $i18n) ?></h3>\r\n    <span class=\"fa-solid fa-xmark close-form\" onClick=\"closeAddSubscription()\"></span>\r\n  </header>\r\n  <form action=\"endpoints/subscription/add.php\" method=\"post\" id=\"subs-form\">\r\n\r\n    <div class=\"form-group-inline\">\r\n      <input type=\"text\" id=\"name\" name=\"name\" autocomplete=\"off\"\r\n        placeholder=\"<?= translate('subscription_name', $i18n) ?>\"\r\n        onchange=\"setSearchButtonStatus()\" onkeypress=\"this.onchange();\" onpaste=\"this.onchange();\"\r\n        oninput=\"this.onchange();\" required>\r\n      <label for=\"logo\" class=\"logo-preview\">\r\n        <img src=\"\" alt=\"<?= translate('logo_preview', $i18n) ?>\" id=\"form-logo\">\r\n      </label>\r\n      <input type=\"file\" id=\"logo\" name=\"logo\" accept=\"image/jpeg, image/png, image/gif, image/webp, image/svg+xml\"\r\n        onchange=\"handleFileSelect(event)\" class=\"hidden-input\">\r\n      <input type=\"hidden\" id=\"logo-url\" name=\"logo-url\">\r\n      <div id=\"logo-search-button\" class=\"image-button medium disabled\" title=\"<?= translate('search_logo', $i18n) ?>\"\r\n        onClick=\"searchLogo()\">\r\n        <?php include \"images/siteicons/svg/websearch.php\"; ?>\r\n      </div>\r\n      <input type=\"hidden\" id=\"id\" name=\"id\">\r\n      <div id=\"logo-search-results\" class=\"logo-search\">\r\n        <header>\r\n          <?= translate('web_search', $i18n) ?>\r\n          <span class=\"fa-solid fa-xmark close-logo-search\" onClick=\"closeLogoSearch()\"></span>\r\n        </header>\r\n        <div id=\"logo-search-images\"></div>\r\n      </div>\r\n    </div>\r\n\r\n    <div class=\"form-group-inline\">\r\n      <input type=\"number\" step=\"0.01\" id=\"price\" name=\"price\" autocomplete=\"off\"\r\n        placeholder=\"<?= translate('price', $i18n) ?>\" required>\r\n      <select id=\"currency\" name=\"currency_id\" placeholder=\"<?= translate('add_subscription', $i18n) ?>\">\r\n        <?php\r\n        foreach ($currencies as $currency) {\r\n          $selected = ($currency['id'] == $main_currency) ? 'selected' : '';\r\n          ?>\r\n          <option value=\"<?= $currency['id'] ?>\" <?= $selected ?>><?= $currency['name'] ?></option>\r\n          <?php\r\n        }\r\n        ?>\r\n      </select>\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <div class=\"inline\">\r\n        <div class=\"split66\">\r\n          <label for=\"cycle\"><?= translate('payment_every', $i18n) ?></label>\r\n          <div class=\"inline\">\r\n            <select id=\"frequency\" name=\"frequency\" placeholder=\"<?= translate('frequency', $i18n) ?>\">\r\n              <?php\r\n              for ($i = 1; $i <= 366; $i++) {\r\n                ?>\r\n                <option value=\"<?= $i ?>\"><?= $i ?></option>\r\n                <?php\r\n              }\r\n              ?>\r\n            </select>\r\n            <select id=\"cycle\" name=\"cycle\" placeholder=\"Cycle\">\r\n              <?php\r\n              foreach ($cycles as $cycle) {\r\n                ?>\r\n                <option value=\"<?= $cycle['id'] ?>\" <?= $cycle['id'] == 3 ? \"selected\" : \"\" ?>>\r\n                  <?= translate(strtolower($cycle['name']), $i18n) ?>\r\n                </option>\r\n                <?php\r\n              }\r\n              ?>\r\n            </select>\r\n          </div>\r\n        </div>\r\n        <div class=\"split33\">\r\n          <label><?= translate('auto_renewal', $i18n) ?></label>\r\n          <div class=\"inline height50\">\r\n            <input type=\"checkbox\" id=\"auto_renew\" name=\"auto_renew\" checked>\r\n            <label for=\"auto_renew\"><?= translate('automatically_renews', $i18n) ?></label>\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <div class=\"inline\">\r\n        <div class=\"split50\">\r\n          <label for=\"start_date\"><?= translate('start_date', $i18n) ?></label>\r\n          <div class=\"date-wrapper\">\r\n            <input type=\"date\" id=\"start_date\" name=\"start_date\" autocomplete=\"off\">\r\n          </div>\r\n        </div>\r\n        <button type=\"button\" id=\"autofill-next-payment-button\"\r\n          class=\"button secondary-button autofill-next-payment hideOnMobile\"\r\n          title=\"<?= translate('calculate_next_payment_date', $i18n) ?>\" onClick=\"autoFillNextPaymentDate(event)\">\r\n          <i class=\"fa-solid fa-wand-magic-sparkles\"></i>\r\n        </button>\r\n        <div class=\"split50\">\r\n          <label for=\"next_payment\" class=\"split-label\">\r\n            <?= translate('next_payment', $i18n) ?>\r\n            <div id=\"autofill-next-payment-button\" class=\"autofill-next-payment hideOnDesktop\"\r\n              title=\"<?= translate('calculate_next_payment_date', $i18n) ?>\" onClick=\"autoFillNextPaymentDate(event)\">\r\n              <i class=\"fa-solid fa-wand-magic-sparkles\"></i>\r\n            </div>\r\n          </label>\r\n          <div class=\"date-wrapper\">\r\n            <input type=\"date\" id=\"next_payment\" name=\"next_payment\" autocomplete=\"off\" required>\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <div class=\"inline\">\r\n        <div class=\"split50\">\r\n          <label for=\"payment_method\"><?= translate('payment_method', $i18n) ?></label>\r\n          <select id=\"payment_method\" name=\"payment_method_id\">\r\n            <?php\r\n            foreach ($payment_methods as $payment) {\r\n              ?>\r\n              <option value=\"<?= $payment['id'] ?>\">\r\n                <?= $payment['name'] ?>\r\n              </option>\r\n              <?php\r\n            }\r\n            ?>\r\n          </select>\r\n        </div>\r\n        <div class=\"split50\">\r\n          <label for=\"payer_user\"><?= translate('paid_by', $i18n) ?></label>\r\n          <select id=\"payer_user\" name=\"payer_user_id\">\r\n            <?php\r\n            foreach ($members as $member) {\r\n              ?>\r\n              <option value=\"<?= $member['id'] ?>\"><?= $member['name'] ?></option>\r\n              <?php\r\n            }\r\n            ?>\r\n          </select>\r\n        </div>\r\n      </div>\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <label for=\"category\"><?= translate('category', $i18n) ?></label>\r\n      <select id=\"category\" name=\"category_id\">\r\n        <?php\r\n        foreach ($categories as $category) {\r\n          ?>\r\n          <option value=\"<?= $category['id'] ?>\">\r\n            <?= $category['name'] ?>\r\n          </option>\r\n          <?php\r\n        }\r\n        ?>\r\n      </select>\r\n    </div>\r\n\r\n    <div class=\"form-group-inline grow\">\r\n      <input type=\"checkbox\" id=\"notifications\" name=\"notifications\" onchange=\"toggleNotificationDays()\">\r\n      <label for=\"notifications\" class=\"grow\"><?= translate('enable_notifications', $i18n) ?></label>\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <div class=\"inline\">\r\n        <div class=\"split66 mobile-split-50\">\r\n          <label for=\"notify_days_before\"><?= translate('notify_me', $i18n) ?></label>\r\n          <select id=\"notify_days_before\" name=\"notify_days_before\" disabled>\r\n            <option value=\"-1\"><?= translate('default_value_from_settings', $i18n) ?></option>\r\n            <option value=\"0\"><?= translate('on_due_date', $i18n) ?></option>\r\n            <option value=\"1\">1 <?= translate('day_before', $i18n) ?></option>\r\n            <?php\r\n            for ($i = 2; $i <= 180; $i++) {\r\n              ?>\r\n              <option value=\"<?= $i ?>\"><?= $i ?>   <?= translate('days_before', $i18n) ?></option>\r\n              <?php\r\n            }\r\n            ?>\r\n          </select>\r\n        </div>\r\n        <div class=\"split33 mobile-split-50\">\r\n          <label for=\"cancellation_date\"><?= translate('cancellation_notification', $i18n) ?></label>\r\n          <div class=\"date-wrapper\">\r\n            <input type=\"date\" id=\"cancellation_date\" name=\"cancellation_date\" autocomplete=\"off\">\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <input type=\"text\" id=\"url\" name=\"url\" autocomplete=\"off\" placeholder=\"<?= translate('url', $i18n) ?>\">\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <input type=\"text\" id=\"notes\" name=\"notes\" autocomplete=\"off\" placeholder=\"<?= translate('notes', $i18n) ?>\">\r\n    </div>\r\n\r\n    <div class=\"form-group\">\r\n      <div class=\"inline grow\">\r\n        <input type=\"checkbox\" id=\"inactive\" name=\"inactive\" onchange=\"toggleReplacementSub()\">\r\n        <label for=\"inactive\" class=\"grow\"><?= translate('inactive', $i18n) ?></label>\r\n      </div>\r\n    </div>\r\n\r\n    <?php\r\n    $orderedSubscriptions = $subscriptions;\r\n    usort($orderedSubscriptions, function ($a, $b) {\r\n      return strnatcmp(strtolower($a['name']), strtolower($b['name']));\r\n    });\r\n    ?>\r\n\r\n    <div class=\"form-group hide\" id=\"replacement_subscritpion\">\r\n      <label for=\"replacement_subscription_id\"><?= translate('replaced_with', $i18n) ?>:</label>\r\n      <select id=\"replacement_subscription_id\" name=\"replacement_subscription_id\">\r\n        <option value=\"0\"><?= translate('none', $i18n) ?></option>\r\n        <?php\r\n        foreach ($orderedSubscriptions as $sub) {\r\n          if ($sub['inactive'] == 0) {\r\n            ?>\r\n            <option value=\"<?= htmlspecialchars($sub['id']) ?>\"><?= htmlspecialchars($sub['name']) ?>\r\n            </option>\r\n            <?php\r\n          }\r\n        }\r\n        ?>\r\n      </select>\r\n    </div>\r\n\r\n    <div class=\"buttons\">\r\n      <input type=\"button\" value=\"<?= translate('delete', $i18n) ?>\" class=\"warning-button left thin\" id=\"deletesub\"\r\n        style=\"display: none\">\r\n      <input type=\"button\" value=\"<?= translate('cancel', $i18n) ?>\" class=\"secondary-button thin\"\r\n        onClick=\"closeAddSubscription()\">\r\n      <input type=\"submit\" value=\"<?= translate('save', $i18n) ?>\" class=\"thin\" id=\"save-button\">\r\n    </div>\r\n  </form>\r\n</section>\r\n<script src=\"scripts/subscriptions.js?<?= $version ?>\"></script>\r\n<?php\r\nif (isset($_GET['add'])) {\r\n  ?>\r\n  <script>\r\n    addSubscription();\r\n  </script>\r\n  <?php\r\n}\r\n\r\nrequire_once 'includes/footer.php';\r\n?>"
  },
  {
    "path": "totp.php",
    "content": "<?php\r\nrequire_once 'includes/connect.php';\r\nrequire_once 'includes/checkuser.php';\r\n\r\nrequire_once 'includes/i18n/languages.php';\r\nrequire_once 'includes/i18n/getlang.php';\r\nrequire_once 'includes/i18n/' . $lang . '.php';\r\n\r\nrequire_once 'includes/version.php';\r\n\r\nif ($userCount == 0) {\r\n    header(\"Location: registration.php\");\r\n    exit();\r\n}\r\n\r\nsession_start();\r\n\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    $db->close();\r\n    header(\"Location: .\");\r\n    exit();\r\n}\r\n\r\nif (!isset($_SESSION['totp_user_id'])) {\r\n    $db->close();\r\n    header(\"Location: login.php\");\r\n    exit();\r\n}\r\n\r\n$theme = \"light\";\r\n$updateThemeSettings = false;\r\nif (isset($_COOKIE['theme'])) {\r\n    $theme = $_COOKIE['theme'];\r\n} else {\r\n    $updateThemeSettings = true;\r\n}\r\n\r\n$colorTheme = \"blue\";\r\nif (isset($_COOKIE['colorTheme'])) {\r\n    $colorTheme = $_COOKIE['colorTheme'];\r\n}\r\n\r\n$demoMode = getenv('DEMO_MODE');\r\n\r\n$cookieExpire = time() + (30 * 24 * 60 * 60);\r\n$invalidTotp = false;\r\n\r\nif (isset($_POST['one-time-code'])) {\r\n    $totp_code = $_POST['one-time-code'];\r\n\r\n    $statement = $db->prepare('SELECT totp_secret, backup_codes FROM totp WHERE user_id = :id');\r\n    $statement->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);\r\n    $result = $statement->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n    $totp_secret = $row['totp_secret'];\r\n    $backupCodes = json_decode($row['backup_codes'], true);\r\n\r\n    require_once 'libs/OTPHP/FactoryInterface.php';\r\n    require_once 'libs/OTPHP/Factory.php';\r\n    require_once 'libs/OTPHP/ParameterTrait.php';\r\n    require_once 'libs/OTPHP/OTPInterface.php';\r\n    require_once 'libs/OTPHP/OTP.php';\r\n    require_once 'libs/OTPHP/TOTPInterface.php';\r\n    require_once 'libs/OTPHP/TOTP.php';\r\n    require_once 'libs/Psr/Clock/ClockInterface.php';\r\n    require_once 'libs/OTPHP/InternalClock.php';\r\n    require_once 'libs/constant_time_encoding/Binary.php';\r\n    require_once 'libs/constant_time_encoding/EncoderInterface.php';\r\n    require_once 'libs/constant_time_encoding/Base32.php';\r\n\r\n    $clock = new OTPHP\\InternalClock();\r\n\r\n    $totp = OTPHP\\TOTP::createFromSecret($totp_secret, $clock);\r\n    $totp->setPeriod(30);\r\n    $valid = $totp->verify($totp_code, null, 15);\r\n\r\n    // If totp is not valid check backup codes\r\n    if (!$valid) {\r\n        if (in_array($totp_code, $backupCodes)) {\r\n            $key = array_search($totp_code, $backupCodes);\r\n            unset($backupCodes[$key]);\r\n            $backupCodes = array_values($backupCodes);\r\n\r\n            $statement = $db->prepare('UPDATE totp SET backup_codes = :backup_codes WHERE user_id = :id');\r\n            $statement->bindValue(':backup_codes', json_encode($backupCodes), SQLITE3_TEXT);\r\n            $statement->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);\r\n            $statement->execute();\r\n\r\n            $valid = true;\r\n        } else {\r\n            $invalidTotp = true;\r\n        }\r\n    } else {\r\n        $statement = $db->prepare('UPDATE totp SET last_totp_used = :last_totp_used WHERE user_id = :id');\r\n        $statement->bindValue(':last_totp_used', time(), SQLITE3_INTEGER);\r\n        $statement->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);\r\n        $statement->execute();\r\n    }\r\n\r\n    if ($valid) {\r\n        $query = \"SELECT id, username, main_currency, language FROM user WHERE id = :id\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $user = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n        $_SESSION['username'] = $user['username'];\r\n        $_SESSION['loggedin'] = true;\r\n        $_SESSION['main_currency'] = $user['main_currency'];\r\n        $_SESSION['userId'] = $user['id'];\r\n\r\n        if (!empty($_SESSION['pending_remember_me'])) {\r\n            $token = bin2hex(random_bytes(32));\r\n            $addLoginTokens = \"INSERT INTO login_tokens (user_id, token) VALUES (:userId, :token)\";\r\n            $addLoginTokensStmt = $db->prepare($addLoginTokens);\r\n            $addLoginTokensStmt->bindParam(':userId', $user['id'], SQLITE3_INTEGER);\r\n            $addLoginTokensStmt->bindParam(':token', $token, SQLITE3_TEXT);\r\n            $addLoginTokensStmt->execute();\r\n            $cookieExpire = time() + (30 * 24 * 60 * 60);\r\n            $cookieValue = $user['username'] . \"|\" . $token . \"|\" . $user['main_currency'];\r\n            setcookie('wallos_login', $cookieValue, [\r\n                'expires'  => $cookieExpire,\r\n                'samesite' => 'Strict',\r\n                'httponly' => true,\r\n            ]);\r\n            unset($_SESSION['pending_remember_me']);\r\n        }\r\n\r\n        setcookie('language', $user['language'], [\r\n            'expires' => $cookieExpire,\r\n            'samesite' => 'Strict'\r\n        ]);\r\n\r\n        if (!isset($_COOKIE['sortOrder'])) {\r\n            setcookie('sortOrder', 'next_payment', [\r\n                'expires' => $cookieExpire,\r\n                'samesite' => 'Strict'\r\n            ]);\r\n        }\r\n\r\n        $query = \"SELECT color_theme FROM settings WHERE user_id = :id\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);\r\n        $result = $stmt->execute();\r\n        $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n        setcookie('colorTheme', $settings['color_theme'], [\r\n            'expires' => $cookieExpire,\r\n            'samesite' => 'Strict'\r\n        ]);\r\n\r\n        unset($_SESSION['totp_user_id']);\r\n\r\n        $db->close();\r\n        header(\"Location: .\");\r\n        exit();\r\n    }\r\n\r\n}\r\n\r\n?>\r\n<!DOCTYPE html>\r\n<html dir=\"<?= $languages[$lang]['dir'] ?>\">\r\n\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\r\n    <meta name=\"theme-color\" content=\"<?= $theme == \"light\" ? \"#FFFFFF\" : \"#222222\" ?>\" id=\"theme-color\" />\r\n    <meta name=\"apple-mobile-web-app-title\" content=\"Wallos\">\r\n    <title>Wallos - Subscription Tracker</title>\r\n    <link rel=\"icon\" type=\"image/png\" href=\"images/icon/favicon.ico\" sizes=\"16x16\">\r\n    <link rel=\"apple-touch-icon\" href=\"images/icon/apple-touch-icon.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"images/icon/apple-touch-icon-152.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"images/icon/apple-touch-icon-180.png\">\r\n    <link rel=\"manifest\" href=\"manifest.json\">\r\n    <link rel=\"stylesheet\" href=\"styles/theme.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/login.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/themes/red.css?<?= $version ?>\" id=\"red-theme\" <?= $colorTheme != \"red\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/green.css?<?= $version ?>\" id=\"green-theme\" <?= $colorTheme != \"green\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/yellow.css?<?= $version ?>\" id=\"yellow-theme\" <?= $colorTheme != \"yellow\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/purple.css?<?= $version ?>\" id=\"purple-theme\" <?= $colorTheme != \"purple\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/font-awesome.min.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/barlow.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/login-dark-theme.css?<?= $version ?>\" id=\"dark-theme\" <?= $theme == \"light\" ? \"disabled\" : \"\" ?>>\r\n    <script type=\"text/javascript\">\r\n        window.update_theme_settings = \"<?= $updateThemeSettings ?>\";\r\n        window.color_theme = \"<?= $colorTheme ?>\";\r\n    </script>\r\n    <script type=\"text/javascript\" src=\"scripts/login.js?<?= $version ?>\"></script>\r\n</head>\r\n\r\n<body class=\"<?= $languages[$lang]['dir'] ?>\">\r\n    <div class=\"content\">\r\n        <section class=\"container\">\r\n            <header>\r\n                <div class=\"logo-image\" title=\"Wallos - Subscription Tracker\">\r\n                    <?php include \"images/siteicons/svg/logo.php\"; ?>\r\n                </div>\r\n                <p>\r\n                    <?= translate('insert_totp_code', $i18n) ?>\r\n                </p>\r\n            </header>\r\n            <form action=\"totp.php\" method=\"post\">\r\n                <div class=\"form-group\">\r\n                    <label for=\"one-time-code\"><?= translate('totp_code', $i18n) ?>:</label>\r\n                    <input type=\"text\" id=\"one-time-code\" name=\"one-time-code\" autocomplete=\"one-time-code\" required>\r\n                </div>\r\n                <div class=\"form-group\">\r\n                    <input type=\"submit\" value=\"<?= translate('login', $i18n) ?>\">\r\n                </div>\r\n                <?php\r\n                if ($invalidTotp) {\r\n                    ?>\r\n                    <ul class=\"error-box\">\r\n                        <li>\r\n                            <i class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('totp_code_incorrect', $i18n) ?>\r\n                        </li>\r\n                    </ul>\r\n                    <?php\r\n                }\r\n                ?>\r\n\r\n            </form>\r\n        </section>\r\n    </div>\r\n</body>\r\n\r\n</html>"
  },
  {
    "path": "verifyemail.php",
    "content": "<?php\r\n\r\nrequire_once 'includes/connect.php';\r\nrequire_once 'includes/checkuser.php';\r\n\r\nrequire_once 'includes/i18n/languages.php';\r\nrequire_once 'includes/i18n/getlang.php';\r\nrequire_once 'includes/i18n/' . $lang . '.php';\r\n\r\nrequire_once 'includes/version.php';\r\n\r\nif ($userCount == 0) {\r\n    header(\"Location: registration.php\");\r\n    exit();\r\n}\r\n\r\nsession_start();\r\nif (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {\r\n    $db->close();\r\n    header(\"Location: .\");\r\n    exit();\r\n}\r\n\r\n$theme = \"light\";\r\nif (isset($_COOKIE['theme'])) {\r\n    $theme = $_COOKIE['theme'];\r\n}\r\n\r\n$colorTheme = \"blue\";\r\nif (isset($_COOKIE['colorTheme'])) {\r\n    $colorTheme = $_COOKIE['colorTheme'];\r\n}\r\n\r\n$validated = false;\r\n\r\nif (isset($_GET['email']) && isset($_GET['token'])) {\r\n    $email = $_GET['email'];\r\n    $token = $_GET['token'];\r\n\r\n    $query = \"SELECT * FROM email_verification WHERE email = :email AND token = :token\";\r\n    $stmt = $db->prepare($query);\r\n    $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n    $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n    $result = $stmt->execute();\r\n    $row = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n    if ($row) {\r\n        $query = \"DELETE FROM email_verification WHERE email = :email AND token = :token\";\r\n        $stmt = $db->prepare($query);\r\n        $stmt->bindValue(':email', $email, SQLITE3_TEXT);\r\n        $stmt->bindValue(':token', $token, SQLITE3_TEXT);\r\n        $stmt->execute();\r\n\r\n        $validated = true;\r\n\r\n        header(\"Location: login.php?validated=true\");\r\n        exit;\r\n\r\n    } else {\r\n        $query = \"SELECT require_email_verification FROM admin\";\r\n        $stmt = $db->prepare($query);\r\n        $result = $stmt->execute();\r\n        $settings = $result->fetchArray(SQLITE3_ASSOC);\r\n\r\n        if ($settings['require_email_verification'] != 1) {\r\n            header(\"Location: .\");\r\n            exit;\r\n        }\r\n    }\r\n}\r\n\r\n?>\r\n<!DOCTYPE html>\r\n<html dir=\"<?= $languages[$lang]['dir'] ?>\">\r\n\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\r\n    <meta name=\"theme-color\" content=\"<?= $theme == \"light\" ? \"#FFFFFF\" : \"#222222\" ?>\" />\r\n    <meta name=\"apple-mobile-web-app-title\" content=\"Wallos\">\r\n    <title>Wallos - Subscription Tracker</title>\r\n    <link rel=\"icon\" type=\"image/png\" href=\"images/icon/favicon.ico\" sizes=\"16x16\">\r\n    <link rel=\"apple-touch-icon\" href=\"images/icon/apple-touch-icon.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"images/icon/apple-touch-icon-152.png\">\r\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"images/icon/apple-touch-icon-180.png\">\r\n    <link rel=\"manifest\" href=\"manifest.json\">\r\n    <link rel=\"stylesheet\" href=\"styles/theme.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/login.css?<?= $version ?>\">\r\n    <link rel=\"stylesheet\" href=\"styles/themes/red.css?<?= $version ?>\" id=\"red-theme\" <?= $colorTheme != \"red\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/green.css?<?= $version ?>\" id=\"green-theme\" <?= $colorTheme != \"green\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/yellow.css?<?= $version ?>\" id=\"yellow-theme\" <?= $colorTheme != \"yellow\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/themes/purple.css?<?= $version ?>\" id=\"purple-theme\" <?= $colorTheme != \"purple\" ? \"disabled\" : \"\" ?>>\r\n    <link rel=\"stylesheet\" href=\"styles/font-awesome.min.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/barlow.css\">\r\n    <link rel=\"stylesheet\" href=\"styles/login-dark-theme.css?<?= $version ?>\" id=\"dark-theme\" <?= $theme == \"light\" ? \"disabled\" : \"\" ?>>\r\n</head>\r\n\r\n<body class=\"<?= $languages[$lang]['dir'] ?>\">\r\n    <div class=\"content\">\r\n        <section class=\"container\">\r\n            <header>\r\n                <div class=\"logo-image\" title=\"Wallos - Subscription Tracker\">\r\n                    <?php include \"images/siteicons/svg/logo.php\"; ?>\r\n                </div>\r\n            </header>\r\n            <div class=\"message\">\r\n                <?php\r\n                if ($validated == false) {\r\n                    ?>\r\n                    <ul class=\"error-box\">\r\n                        <li><i\r\n                                class=\"fa-solid fa-triangle-exclamation\"></i><?= translate('email_verification_failed', $i18n) ?>\r\n                        </li>\r\n                    </ul>\r\n                    <?php\r\n                }\r\n                ?>\r\n            </div>\r\n            <div class=\"separator\"></div>\r\n            <input type=\"button\" class=\"button\" onclick=\"window.location.href='login.php'\"\r\n                value=\"<?= translate('login', $i18n) ?>\"></input>\r\n        </section>\r\n    </div>\r\n</body>\r\n\r\n</html>"
  }
]