[
  {
    "path": ".github/workflows/docker-image.yml",
    "content": "# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images\n\n\nname: Create and publish a Docker image\n\n\n# Build Docker image on new release\non:\n  push:\n    tags: ['v*']\n\n\n# Set registry to GitHub Container Registry\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n\n  build-and-push-image:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n      packages: write\n\n    # Build steps\n    steps:\n\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64\n          provenance: false\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".gitignore",
    "content": "dist\nnode_modules\nconfig\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:alpine\n\n# Path to assets in container\nWORKDIR /app\nCOPY . .\n\n# Build honey\nRUN npm install\nRUN npm run build\n\n# Run a built-in webserver\nCMD [\"/app/entrypoint.sh\"]\n\n# Expose port\nEXPOSE 4173\n\n# Health check\nHEALTHCHECK CMD wget -nv --spider --tries=1 http://127.0.0.1:4173\n"
  },
  {
    "path": "README.md",
    "content": "# honey\n\n_A sweet dashboard I use on my homeserver with some self-hosted stuff..._\n\nhoney is written in **pure** `HTML` `CSS` `JS` so dynamic backend or special webserver configuration is not required.\nIt works out-of-the-box as all operations are done client-side.\n\n<font size=\"4\">**[📺 Live demo](https://honeyy.vercel.app/)**</font>\n\n<img src=\"screenshot.jpg\" style=\"width: 720px\">\n\n\n## 🚀 Installation\n\n### 🕸️ On existing webserver\n\n1. Download latest prebuilt archive from **[Releases](https://github.com/dani3l0/honey/releases)**.\n\n2. Extract downloaded archive to your webserver root.\n\n3. You're done!\n\n\n### 🐋 via Docker\n\n```\ndocker run -p 4173:4173 -v /path/to/config:/app/dist/config ghcr.io/dani3l0/honey:latest\n```\n\n- `-p 4173:4173` - exposes HTTP port to your machine\n- `-v /path/to/config:/app/dist/config` - mounts config directory to your local filesystem, missing config files will be created automatically\n\nIf you have custom icons or background images, you can freely put them in `config` dir.\nJust remember to provide valid URLs (with `/config` prefix).\n\n_alternatively, use a `docker-compose.yml` file_\n\n\n## ⚙️ Configuration\n\nConfiguration file is located at `config/config.json`.\n\n\n### 📱 Tweaking the user interface\n\nThe following keys are available under `ui` section.\nSome of them are listed in _Settings_ page and can be customized by end-user.\n\n| Key name\t\t\t\t| Description\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t| in Settings\t |\n|-----------------------|-----------------------------------------------------------------------------------------------------------|----------------|\n| `name`\t\t\t\t| Name shown at the main screen and the tab title.\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\t\t❌\t\t|\n| `desc`\t\t\t\t| Short description shown under title at the main screen.\t\t\t\t\t\t\t\t\t\t\t\t\t|\t\t❌\t\t|\n| `icon`\t\t\t\t| Icon shown at the main screen and as site's favicon.\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\t\t❌\t\t|\n| `wallpaper`\t\t\t| Background image visible when dark mode is disabled.\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\t\t❌\t\t|\n| `wallpaper_dark`\t\t| Background image visible when dark mode is enabled.\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\t\t❌\t\t|\n| `dark_mode`\t\t\t| Tells whether dark mode is enabled by default. (Available values: `Auto`,`Off`,`On`)\t\t\t\t\t\t|\t\t✅\t\t|\n| `open_new_tab`\t\t| Tells whether clicking on a service will open it in new tab by default.\t\t\t\t\t\t\t\t\t|\t\t✅\t\t|\n| `ping_dots`\t\t\t| Enables small dot before service name indicating whether is it available or not.\t\t\t\t\t\t\t|\t\t✅\t\t|\n| `blur`\t\t\t\t| Tells whether card background blur is enabled by default.\t\t\t\t\t\t\t\t\t\t\t\t\t|\t\t✅\t\t|\n| `animations`\t\t\t| Tells whether UI animations are enabled by default.\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\t\t✅\t\t|\n| `trusted_domains`\t\t| Array of domains (or IP addresses) to no longer be considered as 3rd-parties. RegExp is fully supported.\t|\t\t✅\t\t|\n\n\n### 🔗 Adding custom services\n\n`services` section is an array containing objects. Object's structure looks like this:\n\n| Key name\t\t\t| Description\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n|-------------------|-------------------------------------------------------------------------------|\n| `name`\t\t\t| Your service's name.\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| `desc`\t\t\t| Short description shown under service's name.\t\t\t\t\t\t\t\t\t|\n| `href`\t\t\t| URL address of your service. It is directly passed to `<a>` tag.\t\t\t\t|\n| `icon`\t\t\t| Path to an icon of your service.\t\t\t\t\t\t\t\t\t\t\t\t|\n\nExample:\n```\n...\n{\n\t\"name\": \"CalDav\",\n\t\"desc\": \"Simple CalDav server for calendar sync between various devices.\",\n\t\"href\": \"caldav\",\n\t\"icon\": \"img/preview/caldav.png\"\n},\n...\n```\n\n\n## 🛠️ Development\n\nhoney is built on top of [Vite.js](https://vitejs.dev/). This tool allows faster development and offers various optimizations.\n\nHow to prepare a development environment:\n\n```\n# Download the source code\ngit clone https://github.com/dani3l0/honey && cd honey\n\n# Install required modules\nnpm i\n```\n\n\n### 🗼 Live server\n\n**For coding.** This will spin up a HTTP server on **[localhost:5173](http://localhost:5173/)**. Each time source file is saved, UI will automatically hot-reload so there is no need for `ALT+TAB` and `F5`.\n\n```\nnpm run dev\n```\n\n\n### 🏗️ Build\n\n**Prepare project for production.** This command will link and optimize project assets to take less space and require less bandwith. Prebuilt assets will be stored in `dist` directory and are ready to be put in a webserver root.\n\n```\nnpm run build\n```\n\n\n## 🤝 Credits\n\nOf course, some third-party resources are used in this project. I kanged them for self-hosting, easier development and to avoid compatibility issues.\n\n- **[Material Icons](https://github.com/materialos/android-icon-pack/)**, for app icons at _Services_ page\n\n- **[Google Fonts](https://fonts.google.com/)**, for material icons on buttons and Quicksand font\n\n- **honey icon** - random icon found in DuckDuckGo Images\n\n- **Wallpapers** - very nice background images kanged from [wallhaven](https://wallhaven.cc/)\n"
  },
  {
    "path": "css/Background.css",
    "content": "#background img {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\toverflow: hidden;\n\tobject-fit: cover;\n\ttransition: transform .4s, opacity .4s !important;\n}\n\n#background.scaled img {\n\ttransform: scale(var(--scale-factor));\n}\n\nbody #background img {\n\topacity: 1;\n}\n\nbody:not(.dark) #background img:last-child,\nbody.dark #background img:first-child {\n\topacity: 0;\n}\n\nbody.dark #background:not(.scaled) img:first-child,\nbody:not(.dark) #background:not(.scaled) img:last-child {\n\ttransform: scale(var(--scale-factor));\n}\n\nbody.dark #background.scaled img:first-child,\nbody:not(.dark) #background.scaled img:last-child {\n\ttransform: none;\n}\n\n#background .notloaded {\n\ttransform: scale(1) !important;\n\topacity: 0 !important;\n}\n"
  },
  {
    "path": "css/Flags/Dark.css",
    "content": "body.dark {\n\t--color: #EEE;\n\t--color2: #EEE6;\n\t--background: #1118;\n\t--bg2: #0008;\n\t--hover: #FFF1;\n}\n\nbody.dark #theme-switcher i::before {\n\t--hidden: 1;\n}\n\nbody.dark #theme-switcher i::after {\n\t--hidden: 0;\n}\n"
  },
  {
    "path": "css/Flags/Flags.css",
    "content": "@import url(Loaded.css);\n@import url(Dark.css);\n\nbody {\n\t--color: #222;\n\t--color2: #2229;\n\t--background: #EEE8;\n\t--bg2: #FFF8;\n\t--hover: #0001;\n\t--scale-factor: 1.15;\n\t--blur: blur(16px);\n}\n\nbody.noblur {\n\t--blur: 0;\n}\n\nbody.noanim * {\n\ttransition: none !important;\n}\n"
  },
  {
    "path": "css/Flags/Loaded.css",
    "content": "body {\n\ttransition: opacity .3s;\n}\n\nbody:not(.loaded) {\n\topacity: 0;\n\ttransition: none;\n}\n\nbody:not(.loaded) * {\n\ttransition: none !important;\n}\n\nbody:not(.loaded) .page .wrapper {\n\ttransform: scale(.8) translateY(50%);\n}\n"
  },
  {
    "path": "css/Pages/Home.css",
    "content": ".main {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.appicon {\n\twidth: 192px;\n\theight: 192px;\n\tobject-fit: cover;\n\ttransition: transform .4s, opacity .4s;\n}\n\n.appicon.notloaded {\n\ttransform: scale(.8);\n\topacity: 0;\n}\n\n#theme-switcher i {\n\tposition: relative;\n\toverflow: hidden;\n}\n\n#theme-switcher i::before, #theme-switcher i::after {\n\tposition: absolute;\n\ttop: 50%;\n\tleft: 50%;\n\t--hidden: 1;\n\topacity: calc(1 - var(--hidden));\n\ttransform: translate(-50%, -50%) rotateZ(calc(var(--hidden) * 360deg)) scale(calc(1 - var(--hidden) / 2));\n\ttransition: transform .3s, opacity .3s;\n}\n\n#theme-switcher i::before {\n\t--hidden: 0;\n\t--dark: 0;\n\tcontent: \"light_mode\";\n}\n\n#theme-switcher i::after {\n\t--dark: 1;\n\tcontent: \"dark_mode\";\n}\n\n.home.page {\n\ttop: 50%;\n\tleft: 50%;\n\twidth: 100%;\n\theight: auto;\n\toverflow: hidden;\n\ttransform: translate(-50%, -50%);\n}\n\n.home.page:not(.current) {\n\ttop: calc(50% - 64px);\n}\n\n.home .wrapper {\n\tbox-shadow: none;\n\tbackground: transparent;\n\tbackdrop-filter: none;\n}\n\n.appname {\n\tfont-size: 48px;\n}\n\n.appdesc {\n\topacity: .6;\n\tmargin: 2px 12px;\n}\n\n.buttons {\n\tbox-shadow: 2px 2px 8px #0002;\n\tdisplay: flex;\n\tmargin: 16px auto 0;\n\tbackdrop-filter: var(--blur);\n\tborder-radius: 24px;\n\tmax-width: 480px;\n\tbackground: var(--background);\n\tpadding: 2px;\n\tjustify-content: space-between;\n\ttransition: background .3s;\n}\n\n.buttons > div {\n\tpadding: 16px;\n\tmargin: 2px;\n\tcursor: pointer;\n\tborder-radius: 20px;\n\twidth: 100%;\n\ttransition: background .2s;\n}\n\n.buttons > div:hover {\n\tbackground: var(--hover);\n}\n\n.buttons .text {\n\tmargin-top: -2px;\n}\n"
  },
  {
    "path": "css/Pages/More/More.css",
    "content": "@import url(Overview.css);\n@import url(Settings.css);\n\n.subpages {\n\tposition: relative;\n\ttransform: translateX(calc(var(--id) * -100%));\n\ttransition: transform .4s, height .4s;\n}\n\n.subpages > div {\n\tposition: absolute;\n\tleft: calc(var(--n) * 100%);\n\twidth: 100%;\n}\n"
  },
  {
    "path": "css/Pages/More/Overview.css",
    "content": ".overview {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tmax-width: 640px;\n\tpadding: 0 20px;\n\tmargin: 128px auto 88px;\n\ttext-align: right;\n}\n\n.overview > i {\n\tfont-size: 80px;\n}\n\n@property --value {\n\tsyntax: '<integer>';\n\tinitial-value: 0;\n\tinherits: false;\n}\n\n.overview .big {\n\tfont-size: 64px;\n\tmargin-bottom: -4px;\n\tcounter-reset: value var(--value);\n\ttransition: --value 1.2s;\n}\n\n.page:not(.current) .overview .big {\n\t--value: 0 !important;\n\ttransition: --value 0s .5s;\n}\n\n.overview .big::after {\n\tcontent: counter(value);\n}\n\n.overview .small {\n\topacity: .5;\n}\n\n.privacy-boxes {\n\tdisplay: flex;\n\ttext-align: left;\n\tflex-wrap: wrap;\n\tmargin-bottom: 10px;\n}\n\n.privacy-boxes > div {\n\tdisplay: flex;\n\twidth: 50%;\n\tmin-width: 256px;\n\tflex: 1;\n\tpadding: 8px;\n\talign-items: center;\n}\n\n.privacy-boxes i {\n\tcolor: var(--color);\n\ttext-shadow: 0 0 48px var(--color);\n\tpadding: 16px;\n\tfont-size: 28px;\n\tborder-radius: 32px;\n}\n\n.privacy-boxes .title {\n\tfont-size: 16px;\n}\n\n.privacy-boxes .subtitle {\n\topacity: .5;\n}\n"
  },
  {
    "path": "css/Pages/More/Settings.css",
    "content": "#settings {\n\tmargin: 32px auto;\n\tpadding: 0 16px;\n}\n\n.setting {\n\tbackground: var(--bg2);\n\tmargin: 8px;\n\tpadding: 20px;\n\tdisplay: flex;\n\tborder-radius: 16px;\n\talign-items: center;\n\ttext-align: left;\n\ttransition: background .3s;\n}\n.setting.pointer {\n\tcursor: pointer;\n}\n\n.setting i {\n\tmargin-right: 14px;\n\tfont-size: 28px;\n}\n\n.setting .name {\n\tfont-size: 16px;\n}\n\n.setting .desc {\n\topacity: .6;\n\tmargin-right: 16px;\n}\n\n.setting .switch {\n\tposition: relative;\n\twidth: 44px;\n\tmin-width: 44px;\n\theight: 24px;\n\tbackground: #8886;\n\tborder-radius: 100px;\n\tmargin: 0 4px 0 auto;\n\ttransition: border .4s, background .3s;\n}\n\n.setting .switch:after {\n\tcontent: \"\";\n\tposition: absolute;\n\twidth: 16px;\n\theight: 16px;\n\tbackground: var(--color);\n\tleft: 4px;\n\ttop: 50%;\n\tborder-radius: 10px;\n\ttransform: translateY(-50%);\n\ttransition: left .2s, background .3s;\n}\n\n.setting.checked .switch {\n\tbackground-color: #68F;\n\tborder-color: #68F;\n}\n\n.setting.checked .switch:after {\n\tleft: calc(100% - 20px);\n}\n\n#no-cookies {\n\tmargin: 24px 0 -8px;\n\tcolor: #F60;\n}\n\n#no-cookies.hidden {\n\tdisplay: none;\n}\n\n.options {\n\tposition: relative;\n\tmargin-left: auto;\n\tbackground: #8883;\n\tborder-radius: 16px;\n\tborder: 4px solid transparent;\n\tdisplay: flex;\n\talign-items: center;\n\ttext-align: center;\n\toverflow: hidden;\n}\n\n.options::before {\n\tcontent: \" \";\n\tposition: absolute;\n\twidth: calc(100% / var(--items));\n\tleft: calc(100% / var(--items) * var(--item));\n\theight: 100%;\n\tborder-radius: 12px;\n\tbackground: #68F;\n\ttransition: left .3s;\n}\n\n.options div {\n\tpadding: 12px 0;\n\twidth: 64px;\n\tflex: 1;\n\tcursor: pointer;\n\tz-index: 1;\n}\n"
  },
  {
    "path": "css/Pages/Pages.css",
    "content": "@import url(Home.css);\n@import url(Services.css);\n@import url(More/More.css);\n\n.page {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 50%;\n\twidth: 100%;\n\tmax-width: 920px;\n\theight: 100vh;\n\ttransform: translateX(-50%);\n\toverflow-y: scroll;\n\ttransition: top .4s, opacity .4s, visibility .4s, color .3s;\n}\n\n.page:not(.current) {\n\ttop: 240px;\n\tvisibility: hidden;\n\topacity: 0;\n}\n\n.wrapper {\n\tbox-shadow: 2px 2px 8px #0003;\n\tbackground: var(--background);\n\tpadding: 3px;\n\tbackdrop-filter: var(--blur);\n\tborder-radius: 24px;\n\tmargin: -4px 12px 16px;\n\ttext-align: center;\n\toverflow: hidden;\n\tmin-width: 340px;\n\ttransition: background .2s, transform .4s, backdrop-filter .6s;\n}\n\n.back {\n\tbox-shadow: 2px 2px 8px #0002;\n\tposition: relative;\n\twidth: 64px;\n\theight: 64px;\n\tborder-radius: 24px;\n\tbackground: var(--background);\n\tmargin: 20px;\n\tbackdrop-filter: var(--blur);\n\ttransition: background .2s;\n}\n\n.back i {\n\tmargin: 4px;\n\twidth: 56px;\n\theight: 56px;\n\tcursor: pointer;\n\tborder-radius: 20px;\n\ttransition: background .2s;\n}\n\n.back i:hover {\n\tbackground: var(--hover);\n}\n\n.back i:after {\n\tcontent: \"chevron_left\";\n\tposition: absolute;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%, -50%);\n}\n\n.header {\n\tdisplay: flex;\n\talign-items: center;\n\tmargin: 20px 20px 16px;\n}\n\n.header i {\n\tmargin-top: 1px;\n\tmargin-right: 10px;\n}\n\n.header .text {\n\tfont-size: 26px;\n}\n\n.subswitch {\n\tdisplay: flex;\n\tbackground: var(--bg2);\n\ttransition: background .2s;\n\tmargin: 16px 24px 0;\n\tz-index: 1;\n\tpadding: 4px;\n\tborder-radius: 16px;\n\tposition: relative;\n\toverflow: hidden;\n\t--id: inherit;\n}\n\n.subswitch::before {\n\tcontent: \" \";\n\tz-index: -1;\n\tposition: absolute;\n\ttop: 4px;\n\tleft: calc(var(--id) / var(--switches) * 100% + 4px - 4px * var(--id));\n\twidth: calc(100% / var(--switches) - 4px);\n\theight: calc(100% - 8px);\n\topacity: .25;\n\tbackground: var(--color2);\n\tborder-radius: 12px;\n\ttransition: left .3s, background .3s;\n}\n\n.subswitch div {\n\tpadding: 12px;\n\twidth: 50%;\n\tcursor: pointer;\n}\n"
  },
  {
    "path": "css/Pages/Services.css",
    "content": ".boxes {\n\tdisplay: flex;\n\tflex: 1 1 0;\n\tflex-wrap: wrap;\n}\n\n.box {\n\tmin-width: 292px;\n\tflex: 1;\n\tmargin: 2px;\n\tborder-radius: 20px;\n\tpadding: 8px;\n\tdisplay: flex;\n\talign-items: center;\n\ttext-align: left;\n\ttext-decoration: none;\n\tcolor: inherit;\n\ttransition: background .2s;\n}\n\n.box:hover {\n\tbackground: var(--hover);\n}\n\n.box i {\n\tfont-size: 24px;\n\tpadding: 20px;\n\tbackground: hsl(var(--color), 100%, 89%);\n\tcolor: hsl(var(--color), 100%, 35%);\n\tborder-radius: 100px;\n\tmargin: 2px 12px 2px 2px;\n}\n\na.box {\n\tcursor: pointer;\n\tpadding: 14px;\n}\n\n.box img {\n\twidth: 64px;\n\theight: 64px;\n\tobject-fit: cover;\n\tmargin-right: 12px;\n}\n\n.box .name {\n\tposition: relative;\n\tfont-size: 18px;\n}\n\n.box.pingdot .name {\n\tpadding-left: 16px;\n}\n\n.box.pingdot .name::before {\n\tcontent: \" \";\n\tposition: absolute;\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 6px;\n\ttop: 50%;\n\tleft: 0;\n\ttransform: translateY(-50%);\n\tbackground: #888;\n\ttransition: background .2s, box-shadow .8s;\n}\n.box.pingdot.up .name::before {\n\tbackground: #0F8;\n\tbox-shadow: 0 0 12px #0F8;\n}\n.box.pingdot.down .name::before {\n\tbackground: #F35;\n\tbox-shadow: 0 0 12px #F35;\n}\n.box.pingdot.error .name::before {\n\tbackground: #F82;\n\tbox-shadow: 0 0 12px #F82;\n}\n\n.box .desc {\n\topacity: .6;\n}\n"
  },
  {
    "path": "css/main.css",
    "content": "@import url(../fonts/fonts.css);\n\n@import url(Flags/Flags.css);\n@import url(Background.css);\n@import url(Pages/Pages.css);\n\n\nbody {\n\tbackground: #000;\n\tcolor: var(--color);\n\tmargin: 0;\n\tfont-family: Quicksand;\n\tfont-weight: bold;\n\tuser-select: none;\n\tfont-size: 14px;\n\t-webkit-tap-highlight-color: transparent;\n}\n\n* {\n\tscrollbar-width: none;\n}\n\n::-webkit-scrollbar {\n\tdisplay: none;\n}"
  },
  {
    "path": "docker-compose.yaml",
    "content": "services:\n  honey:\n    image: ghcr.io/dani3l0/honey:latest\n    container_name: honey\n    restart: unless-stopped\n    volumes:\n      - ./config:/app/dist/config\n    ports:\n      - \"4173:4173\"\n"
  },
  {
    "path": "entrypoint.sh",
    "content": "#!/bin/sh\ncp -rnv /app/public/config/* /app/dist/config\nnpm run preview\nexit $?\n"
  },
  {
    "path": "fonts/MaterialSymbolsRounded/MaterialSymbolsRounded.css",
    "content": "@font-face {\n\tfont-family: 'Material Symbols Rounded';\n\tfont-style: normal;\n\tfont-weight: 500;\n\tsrc: url(MaterialSymbolsRounded.woff2) format('woff2');\n}\n\ni {\n\tfont-family: 'Material Symbols Rounded';\n\tfont-weight: normal;\n\tfont-style: normal;\n\tfont-size: 24px;\n\tline-height: 1;\n\tletter-spacing: normal;\n\ttext-transform: none;\n\tdisplay: inline-block;\n\twhite-space: nowrap;\n\tword-wrap: normal;\n\tdirection: ltr;\n\t-webkit-font-feature-settings: 'liga';\n\t-webkit-font-smoothing: antialiased;\n}\n\ni {\n\tdisplay: inline-block;\n}\n"
  },
  {
    "path": "fonts/Quicksand/Quicksand.css",
    "content": "/* quicksand-regular-latin */\n@font-face {\n\tfont-family: Quicksand;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tsrc: local(\"Quicksand Regular\"), local(\"Quicksand-Regular\"), url(quicksand-regular-latin.woff2) format(\"woff2\");\n\tunicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2212,U+2215;\n}\n/* quicksand-regular-latin-ext */\n@font-face {\n\tfont-family: Quicksand;\n\tfont-style: normal;\n\tfont-weight: 400;\n\tsrc: local(\"Quicksand Regular\"), local(\"Quicksand-Regular\"), url(quicksand-regular-latin-ext.woff2) format(\"woff2\");\n\tunicode-range: U+0100-024F,U+0259,U+1E00-1EFF,U+20A0-20CF,U+2C60-2C7F,U+A720-A7FF;\n}\n/* quicksand-bold-latin */\n@font-face {\n\tfont-family: Quicksand;\n\tfont-style: normal;\n\tfont-weight: 700;\n\tsrc: local(\"Quicksand Bold\"), local(\"Quicksand-Bold\"), url(quicksand-bold-latin.woff2) format(\"woff2\");\n\tunicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2212,U+2215;\n}\n/* quicksand-bold-latin-ext */\n@font-face {\n\tfont-family: Quicksand;\n\tfont-style: normal;\n\tfont-weight: 700;\n\tsrc: local(\"Quicksand Bold\"), local(\"Quicksand-Bold\"), url(quicksand-bold-latin-ext.woff2) format(\"woff2\");\n\tunicode-range: U+0100-024F,U+0259,U+1E00-1EFF,U+20A0-20CF,U+2C60-2C7F,U+A720-A7FF;\n}\n\n"
  },
  {
    "path": "fonts/fonts.css",
    "content": "@import url(Quicksand/Quicksand.css);\n@import url(MaterialSymbolsRounded/MaterialSymbolsRounded.css);\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\t<link rel=\"icon\" id=\"favicon\">\n\t<link rel=\"apple-touch-icon\" id=\"apple-touch-icon\">\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css\">\n\t<script src=\"js/main.js\" type=\"module\"></script>\n</head>\n<body>\n\t<div id=\"background\"></div>\n\t<div class=\"main\">\n\n\t\t<!-- Home page -->\n\t\t<div class=\"page home\" p=\"home\">\n\t\t\t<div class=\"wrapper\">\n\t\t\t\t<div class=\"home\">\n\t\t\t\t\t<img class=\"appicon\">\n\t\t\t\t\t<div class=\"appname\"></div>\n\t\t\t\t\t<div class=\"appdesc\"></div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"buttons\">\n\t\t\t\t\t<div id=\"theme-switcher\">\n\t\t\t\t\t\t<i>&nbsp;</i>\n\t\t\t\t\t\t<div class=\"text\">Theme</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div t=\"services\">\n\t\t\t\t\t\t<i>apps</i>\n\t\t\t\t\t\t<div class=\"text\">Services</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div t=\"more\">\n\t\t\t\t\t\t<i>more</i>\n\t\t\t\t\t\t<div class=\"text\">More</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- App list -->\n\t\t<div class=\"page\" p=\"services\">\n\t\t\t<div class=\"back\"><i></i></div>\n\t\t\t<div class=\"wrapper\">\n\t\t\t\t<div class=\"header\">\n\t\t\t\t\t<i>apps</i>\n\t\t\t\t\t<div class=\"text\">Services</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"boxes\" id=\"app-list\"></div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- More -->\n\t\t<div class=\"page\" p=\"more\">\n\t\t\t<div class=\"back\"><i></i></div>\n\t\t\t<div class=\"wrapper\">\n\t\t\t\t<div class=\"subswitch\">\n\t\t\t\t\t<div>Overview</div>\n\t\t\t\t\t<div>Settings</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"subpages\">\n\n\t\t\t\t\t<!-- Overview -->\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"overview\">\n\t\t\t\t\t\t\t<i>rocket_launch</i>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<div class=\"big\"></div>\n\t\t\t\t\t\t\t\t<div class=\"small\"></div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"privacy-boxes\"></div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<!-- Settings -->\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div id=\"no-cookies\">WARNING: due to blocked cookies, all settings will be lost after page reload</div>\n\t\t\t\t\t\t<div id=\"settings\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</body>\n</html>\n"
  },
  {
    "path": "js/App.js",
    "content": "import Drawer from \"./UI/Drawer/Drawer\"\nimport Home from \"./UI/Home/Home\"\nimport Main from \"./UI/Main/Main\"\nimport More from \"./UI/More/More\"\nimport Config from \"./Utils/Config\"\nimport { showPage } from \"./Utils/DOMUtils\"\n\n\nexport default class App {\n\tstatic instance\n\n\tconstructor(config) {\n\t\tif (App.instance) return App.instance\n\t\tApp.instance = this\n\t\tthis.config = new Config(config)\n\t\tthis.init()\n\t}\n\n\tinit() {\n\t\tthis.main = new Main()\n\t\tthis.home = new Home()\n\t\tthis.drawer = new Drawer()\n\t\tthis.more = new More()\n\n\t\tshowPage(\"home\")\n\t\tsetTimeout(() => {\n\t\t\tdocument.body.classList.add(\"loaded\")\n\t\t}, 100)\n\t}\n}\n"
  },
  {
    "path": "js/UI/Drawer/Drawer.js",
    "content": "import App from \"../../App\";\nimport PingService from \"../../Utils/PingService\";\n\nexport default class Drawer {\n\tconstructor() {\n\t\tthis.app = new App()\n\t\tthis.config = this.app.config\n\t\tthis.init()\n\t}\n\n\tinit() {\n\t\tthis.importApps()\n\t}\n\n\timportApps() {\n\t\tlet apps = this.config.getServices()\n\t\tlet enablePingDots = this.config.get(\"ping_dots\")\n\t\tlet openNewTab = this.config.get(\"open_new_tab\")\n\t\tlet applist = document.querySelector(\"#app-list\")\n\t\tapplist.innerHTML = \"\"\n\t\tfor (let app of apps) {\n\t\t\tlet a = document.createElement(\"a\")\n\t\t\ta.classList.add(\"box\")\n\t\t\ta.href = app.href\n\t\t\tif (openNewTab) a.setAttribute(\"target\", \"_blank\")\n\t\t\ta.innerHTML = `\n\t\t\t\t<img src=\"${app.icon}\">\n\t\t\t\t<div>\n\t\t\t\t\t<div class=\"name\">${app.name}</div>\n\t\t\t\t\t<div class=\"desc\">${app.desc}</div>\n\t\t\t\t</div>`\n\n\t\t\tif (enablePingDots) {\n\t\t\t\ta.classList.add(\"pingdot\")\n\t\t\t\tPingService(app.href, status => {\n\t\t\t\t\tif (!status) return\n\t\t\t\t\tlet resp = \"down\"\n\t\t\t\t\tif (status >= 200 && status < 400) resp = \"up\"\n\t\t\t\t\telse if (status >= 400) resp = \"error\"\n\t\t\t\t\ta.classList.add(resp)\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tapplist.appendChild(a)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "js/UI/Home/Home.js",
    "content": "import App from \"../../App\"\nimport { showPage } from \"../../Utils/DOMUtils\"\n\n\nexport default class Home {\n\tconstructor() {\n\t\tthis.app = new App()\n\t\tthis.config = this.app.config\n\t\tthis.init()\n\t}\n\n\tinit() {\n\t\tthis.initButtons()\n\t\tthis.initHomeUI()\n\t\tthis.initBackButtons()\n\t}\n\n\tinitButtons() {\n\t\tlet buttons = document.querySelector(\".buttons\").children\n\t\tfor (let button of buttons) {\n\t\t\tlet target = button.getAttribute(\"t\")\n\t\t\tif (target) {\n\t\t\t\tbutton.addEventListener(\"click\", () => {\n\t\t\t\t\tshowPage(target)\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tinitBackButtons() {\n\t\tlet backButtons = document.querySelectorAll(\".back\")\n\t\tfor (let button of backButtons) {\n\t\t\tbutton.addEventListener(\"click\", () => {\n\t\t\t\tshowPage(\"home\")\n\t\t\t})\n\t\t}\n\t}\n\n\tinitHomeUI() {\n\t\tlet logo = document.querySelector(\".appicon\")\n\t\tlogo.src = this.config.get(\"icon\")\n\t\tlogo.classList.add(\"notloaded\")\n\t\tlogo.addEventListener(\"load\", () => {\n\t\t\tlogo.classList.remove(\"notloaded\")\n\t\t})\n\n\t\tlet name = document.querySelector(\".appname\")\n\t\tname.innerText = this.config.get(\"name\")\n\n\t\tlet desc = document.querySelector(\".appdesc\")\n\t\tdesc.innerText = this.config.get(\"desc\")\n\t}\n}"
  },
  {
    "path": "js/UI/Main/Main.js",
    "content": "import App from \"../../App\"\n\nexport default class Main {\n\tconstructor() {\n\t\tthis.app = new App()\n\t\tthis.config = this.app.config\n\t\tthis.init()\n\t}\n\n\tinit() {\n\t\tdocument.title = this.config.get(\"name\")\n\t\tdocument.querySelector(\"#favicon\").href = this.config.get(\"icon\")\n\t\tdocument.querySelector(\"#apple-touch-icon\").href = this.config.get(\"icon\")\n\t\tthis.initBackgrounds()\n\t}\n\n\tinitBackgrounds() {\n\t\tthis.backgrounds = document.querySelector(\"#background\")\n\t\tfor (let i = 0; i < 2; i++) {\n\t\t\tlet img = document.createElement(\"img\")\n\t\t\timg.classList.add(\"notloaded\")\n\t\t\timg.addEventListener(\"load\", () => {\n\t\t\t\timg.classList.remove(\"notloaded\")\n\t\t\t})\n\t\t\tthis.backgrounds.appendChild(img)\n\t\t}\n\n\t\tthis.backgrounds = this.backgrounds.children\n\t\tthis.backgrounds[0].src = this.config.get(\"wallpaper\")\n\t\tthis.backgrounds[1].src = this.config.get(\"wallpaper_dark\")\n\t}\n}\n"
  },
  {
    "path": "js/UI/More/More.js",
    "content": "import App from \"../../App\";\nimport Overview from \"./Overview/Overview\";\nimport Settings from \"./Settings/Settings\";\n\n\nexport default class More {\n\tconstructor() {\n\t\tthis.app = new App()\n\t\tthis.config = this.app.config\n\t\tthis.overview = new Overview()\n\t\tthis.settings = new Settings()\n\t\tthis.init()\n\t}\n\n\tinit() {\n\t\tthis.overview.init()\n\t\tthis.settings.init()\n\t\tthis.initPager()\n\t}\n\n\tinitPager() {\n\t\tlet switcher = document.querySelector(\".subswitch\")\n\t\tlet buttons = switcher.children\n\t\tlet subsettings = document.querySelector(\".subpages\")\n\n\t\tfor (let i = 0; i < buttons.length; i++) {\n\t\t\tlet button = buttons[i]\n\t\t\tsubsettings.children[i].setAttribute(\"style\", `--n: ${i}`)\n\n\t\t\tbutton.addEventListener(\"click\", () => {\n\t\t\t\tlet calculatedHeight = subsettings.children[i].offsetHeight\n\t\t\t\tsubsettings.style.height = `${calculatedHeight}px`\n\t\t\t\tsubsettings.parentNode.setAttribute(\"style\", `--id: ${i}`)\n\t\t\t\tswitcher.setAttribute(\"style\", `--switches: ${buttons.length}`)\n\t\t\t})\n\t\t}\n\n\t\tbuttons[0].click()\n\t}\n}\n"
  },
  {
    "path": "js/UI/More/Overview/Overview.js",
    "content": "import App from \"../../../App\";\nimport { analyzeService } from \"./analyzer\";\nimport { privacyBox } from \"./tiles\";\nimport { s, isare } from \"../../../Utils/StringUtils\";\n\n\nexport default class Overview {\n\tconstructor() {\n\t\tthis.app = new App()\n\t\tthis.config = this.app.config\n\t\tthis.div = document.querySelector(\".overview\").parentNode\n\t}\n\n\tinit() {\n\t\tthis.initPrivacyBoxes()\n\t}\n\n\tinitPrivacyBoxes() {\n\t\tlet stats = {\n\t\t\ttotal: 0,\n\t\t\tsecure: 0,\n\t\t\tthirdParties: 0\n\t\t}\n\t\tfor (let service of this.config.getServices()) {\n\t\t\tlet analysis = analyzeService(service.href, this.config.get(\"trusted_domains\"))\n\t\t\tstats.total++\n\t\t\tstats.secure += analysis.isSecure\n\t\t\tstats.thirdParties += analysis.isThirdParty\n\t\t}\n\t\tthis.div.querySelector(\".big\").setAttribute(\"style\", `--value: ${stats.total}`)\n\t\tthis.div.querySelector(\".small\").innerText = `Available service${s(stats.total)}`\n\n\t\tlet encryption_t, encryption_d\n\t\tif (stats.secure == stats.total) {\n\t\t\tencryption_t = \"Full encryption\"\n\t\t\tencryption_d = \"All services use secure connections (HTTPS).\"\n\t\t}\n\t\telse if (stats.secure == 0) {\n\t\t\tencryption_t = \"No encryption\"\n\t\t\tencryption_d = \"It seems server does not support HTTPS.\"\n\n\t\t}\n\t\telse {\n\t\t\tlet insecure = stats.total - stats.secure\n\t\t\tencryption_t = \"Partial encryption\"\n\t\t\tencryption_d = `${insecure} service${s(insecure)} do not use secure connections.`\n\n\t\t}\n\n\t\tlet indepencence_t, indepencence_d\n\t\tif (stats.thirdParties == 0) {\n\t\t\tindepencence_t = \"Independence\"\n\t\t\tindepencence_d = \"This server is free of 3rd party services.\"\n\t\t}\n\t\telse if (stats.thirdParties == stats.total) {\n\t\t\tindepencence_t = \"Something is wrong...\"\n\t\t\tindepencence_d = \"It seems only 3rd-party services are listed.\"\n\t\t}\n\t\telse {\n\t\t\tindepencence_t = \"Partial independence\"\n\t\t\tindepencence_d = `${stats.thirdParties} service${s(stats.thirdParties)} ${isare(stats.thirdParties)} provided by 3rd-parties.`\n\t\t}\n\n\t\tprivacyBox(\"lock\", \"#0D6\", encryption_t, encryption_d, stats.secure / stats.total)\n\t\tprivacyBox(\"home\", \"#68F\", indepencence_t, indepencence_d, 1 - stats.thirdParties / stats.total)\n\t}\n}\n"
  },
  {
    "path": "js/UI/More/Overview/analyzer.js",
    "content": "export function analyzeService(url, whitelist) {\n\tlet isSiteSecure = (\n\t\twindow.location.protocol == \"https:\" ||\n\t\twindow.location.hostname == \"localhost\"\n\t)\n\n\tlet isSecure = false\n\tif (url.startsWith(\"https://\")) {\n\t\tisSecure = true\n\t}\n\telse if (![\"http\", \"https\"].includes(url.split(\"://\")[0])) {\n\t\tisSecure = isSiteSecure\n\t}\n\n\tlet isThirdParty = true\n\tlet domain_base = window.location.hostname\n\tlet domain = url.split(\"://\")\n\tif (domain.length > 1) {\n\t\tdomain = domain[1]\n\t\tdomain = domain.split(\"/\")[0]\n\t\tisThirdParty = !domain.includes(domain_base)\n\t\tif (isThirdParty) {\n\t\t\tfor (let entry of whitelist) {\n\t\t\t\tlet re = RegExp(entry)\n\t\t\t\tif (re.exec(domain)) {\n\t\t\t\t\tisThirdParty = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\telse {\n\t\tisThirdParty = false\n\t}\n\n\treturn {isSecure, isThirdParty}\n}"
  },
  {
    "path": "js/UI/More/Overview/tiles.js",
    "content": "export function privacyBox(icon, color, name, desc, pp) {\n\tif (pp == 1) {}\n\telse if (pp > 0.7) color = \"#EA0\"\n\telse if (pp > 0.25) color = \"#F72\"\n\telse color = \"#F33\"\n\n\tlet item = document.createElement(\"div\")\n\titem.setAttribute(\"style\", `--color: ${color}`)\n\n\titem.innerHTML = `<i>${icon}</i>\n\t\t<div>\n\t\t\t<div class=\"title\">${name}</div>\n\t\t\t<div class=\"subtitle\">${desc}</div>\n\t\t</div>`\n\n\tdocument.querySelector(\".privacy-boxes\").appendChild(item)\n\treturn item\n}\n"
  },
  {
    "path": "js/UI/More/Settings/Settings.js",
    "content": "import App from \"../../../App\";\nimport { addOnOffTile, addOptionsTile } from \"./tiles\";\nimport * as EVENTS from \"./events\"\n\n\nexport default class Settings {\n\tconstructor() {\n\t\tthis.app = new App()\n\t\tthis.config = this.app.config\n\t}\n\n\tinit() {\n\t\tthis.checkLocalStorage()\n\t\tthis.initSettings()\n\t}\n\n\tinitSettings() {\n\t\tlet darkMode = addOptionsTile(this.config,\n\t\t\t\"dark_mode\", \"Dark mode\",\n\t\t\t\"Make the colors more appropriate for low-light environments\",\n\t\t\t\"dark_mode\", EVENTS.onThemeChange\n\t\t)\n\n\t\taddOnOffTile(this.config,\n\t\t\t\"open_in_new\", \"Open in new tab\",\n\t\t\t\"Clicking on application will open it in a new browser tab\",\n\t\t\t\"open_new_tab\", EVENTS.onNewTabChange\n\t\t)\n\n\t\taddOnOffTile(this.config,\n\t\t\t\"sensors\", \"Ping dots\",\n\t\t\t\"Shows small dots before titles indicating whether service is up or not\",\n\t\t\t\"ping_dots\", EVENTS.onPingDotsChange\n\t\t)\n\n\t\taddOnOffTile(this.config,\n\t\t\t\"blur_on\", \"Enable blur\",\n\t\t\t\"Improves UI sweetness but may have a huge impact on performance\",\n\t\t\t\"blur\", EVENTS.onBlurChange\n\t\t)\n\n\t\taddOnOffTile(this.config,\n\t\t\t\"animation\", \"Animations\",\n\t\t\t\"Show nice and fancy page transitions for improved experience\",\n\t\t\t\"animations\", EVENTS.onAnimationChange\n\t\t)\n\n\t\tdocument.querySelector(\"#theme-switcher\").addEventListener(\"click\", () => {\n\t\t\tlet targetButtons = darkMode.querySelector(\".options\").children\n\t\t\tlet storedValue = this.config.get(\"dark_mode\")\n\t\t\tlet target;\n\t\t\tif (storedValue == \"Auto\") {\n\t\t\t\tlet isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches\n\t\t\t\ttarget = 2 - isSystemDark\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlet isEnforcedDark = storedValue == \"On\"\n\t\t\t\ttarget = !isEnforcedDark + 1\n\t\t\t}\n\t\t\ttargetButtons[target].click()\n\t\t})\n\t}\n\n\tcheckLocalStorage() {\n\t\tlet warn = document.querySelector(\"#no-cookies\").classList\n\t\tif (this.config.storageAvailable) warn.add(\"hidden\")\n\t}\n}"
  },
  {
    "path": "js/UI/More/Settings/events.js",
    "content": "import App from \"../../../App\";\n\nconst CL = document.body.classList\n\n// Switch between light & dark themes\nvar onThemeChange_SystemListener = false;\nexport function onThemeChange(config) {\n\tlet value = config.get(\"dark_mode\")\n\tlet mm = window.matchMedia('(prefers-color-scheme: dark)')\n\tlet isDark = value == \"Auto\" ? mm.matches : value == \"On\"\n\tisDark ? CL.add(\"dark\") : CL.remove(\"dark\")\n\n\t// Listen for system theme changes\n\tif (!onThemeChange_SystemListener) {\n\t\tonThemeChange_SystemListener = true\n\t\tmm.addEventListener('change', event => {\n\t\t\tif (config.get(\"dark_mode\") == \"Auto\") {\n\t\t\t\tlet isDark = event.matches;\n\t\t\t\tisDark ? CL.add(\"dark\") : CL.remove(\"dark\")\n\t\t\t}\n\t\t});\n\t}\n}\n\n// Open apps in new tab\nexport function onNewTabChange(config) {\n\tlet openNewTab = config.get(\"open_new_tab\")\n\tlet appList = document.querySelector(\"#app-list\").children\n\n\tfor (let app of appList) {\n\t\tif (openNewTab) app.setAttribute(\"target\", \"_blank\")\n\t\telse app.removeAttribute(\"target\") \n\t}\n}\n\n// Enable/disable ping dots\nexport function onPingDotsChange(config) {\n\tlet app = new App()\n\tapp.drawer.importApps()\n}\n\n// Enable/disable background blur\nexport function onBlurChange(config) {\n\tlet blur = config.get(\"blur\")\n\tblur ? CL.remove(\"noblur\") : CL.add(\"noblur\")\n}\n\n// Enable/disable animations\nexport function onAnimationChange(config) {\n\tlet animations = config.get(\"animations\")\n\tanimations ? CL.remove(\"noanim\") : CL.add(\"noanim\")\n}\n"
  },
  {
    "path": "js/UI/More/Settings/tiles.js",
    "content": "export function addOnOffTile(conf, icon, name, desc, key, func) {\n\tlet item = document.createElement(\"div\")\n\titem.classList.add(\"setting\")\n\titem.classList.add(\"pointer\")\n\titem.innerHTML = `\n\t\t<i>${icon}</i>\n\t\t<div class=\"text\">\n\t\t\t<div class=\"name\">${name}</div>\n\t\t\t<div class=\"desc\">${desc}</div>\n\t\t</div>\n\t\t<div class=\"switch\"></div>`\n\n\tlet handleState = () => {\n\t\tlet c = item.classList\n\t\tif (conf.get(key)) c.add(\"checked\")\n\t\telse c.remove(\"checked\")\n\t}\n\n\tlet write = () => {\n\t\tlet target_value = !conf.get(key)\n\t\tconf.set(key, target_value)\n\t}\n\n\tlet f = () => {func(conf)}\n\n\titem.addEventListener(\"click\", write)\n\titem.addEventListener(\"click\", handleState)\n\tif (func) item.addEventListener(\"click\", f)\n\n\thandleState()\n\tif (func) f()\n\n\tdocument.querySelector(\"#settings\").appendChild(item)\n\treturn item\n}\n\nexport function addOptionsTile(conf, icon, name, desc, key, func) {\n\tlet options = [\"Auto\", \"Off\", \"On\"]\n\tlet optionsHtml = document.createElement(\"div\")\n\toptionsHtml.classList.add(\"options\")\n\n\tlet handleState = () => {\n\t\tlet c = optionsHtml\n\t\tlet value = conf.get(key)\n\t\tlet n = options.indexOf(value)\n\t\tfor (let i = 0; i < options.length; i++) {\n\t\t\tlet cl = c.children[i].classList\n\t\t\tif (i == n) cl.add(\"active\")\n\t\t\telse cl.remove(\"active\")\n\t\t}\n\t\tc.setAttribute(\"style\", `--item: ${n}; --items: ${options.length}`)\n\t}\n\n\tlet write = (val) => {\n\t\tconf.set(key, val)\n\t}\n\n\tlet f = () => {func(conf)}\n\n\toptions.forEach(e => {\n\t\tlet node = document.createElement(\"div\")\n\t\tnode.innerText = e\n\t\tnode.addEventListener(\"click\", () => {\n\t\t\twrite(e)\n\t\t\thandleState()\n\t\t\tif (func) f()\n\t\t})\n\t\toptionsHtml.appendChild(node)\n\t})\n\n\thandleState()\n\tif (func) f()\n\n\tlet item = document.createElement(\"div\")\n\titem.classList.add(\"setting\")\n\titem.innerHTML = `\n\t\t<i>${icon}</i>\n\t\t<div class=\"text\">\n\t\t\t<div class=\"name\">${name}</div>\n\t\t\t<div class=\"desc\">${desc}</div>\n\t\t</div>`\n\titem.appendChild(optionsHtml)\n\n\tdocument.querySelector(\"#settings\").appendChild(item)\n\treturn item\n}\n"
  },
  {
    "path": "js/Utils/Config.js",
    "content": "export default class Config {\n\n\t// Initialization & localStorage availability check\n\tconstructor(config) {\n\t\tthis.config = config\n\t\ttry {\n\t\t\twindow.localStorage\n\t\t\tthis.storageAvailable = true\n\t\t}\n\t\tcatch (e) {\n\t\t\tthis.storageAvailable = false\n\t\t}\n\t}\n\n\t// Get value from config or localStorage (if set)\n\tget(key) {\n\t\tlet value = this.config[\"ui\"][key]\n\n\t\tif (this.storageAvailable) {\n\t\t\tlet type = typeof(value)\n\t\t\tlet stored_value = window.localStorage.getItem(key)\n\n\t\t\tif (stored_value != null) {\n\t\t\t\tvalue = stored_value\n\t\t\t\tif (type == \"number\") value = Number(value)\n\t\t\t\telse if (type == \"boolean\") value = value == \"true\"\n\t\t\t}\n\t\t}\n\n\t\treturn value\n\t}\n\n\t// Save value to localStorage\n\tset(key, value) {\n\t\tkey = key.toLowerCase()\n\t\tthis.config[\"ui\"][key] = value\n\n\t\tif (this.storageAvailable) {\n\t\t\twindow.localStorage.setItem(key, value)\n\t\t}\n\t}\n\n\t// Get services from config.json file\n\tgetServices() {\n\t\treturn this.config[\"services\"]\n\t}\n}\n"
  },
  {
    "path": "js/Utils/DOMUtils.js",
    "content": "export function showPage(target) {\n\tlet bg = document.querySelector(\"#background\").classList\n\tif (target == \"home\") bg.add(\"scaled\")\n\telse bg.remove(\"scaled\")\n\tlet pages = document.querySelectorAll(\".page\")\n\n\tfor (let page of pages) {\n\t\tlet p = page.getAttribute(\"p\")\n\t\tif (p == target) page.classList.add(\"current\")\n\t\telse page.classList.remove(\"current\")\n\t}\n\n}\n"
  },
  {
    "path": "js/Utils/PingService.js",
    "content": "export default function PingService(uri, callback) {\n    let xhr = new XMLHttpRequest()\n    xhr.open(\"GET\", uri)\n    xhr.onreadystatechange = function() {\n        if (this.readyState < 4) return\n        let code = this.status\n        setTimeout(() => callback(code), 3000)\n    }\n    xhr.send()\n}\n"
  },
  {
    "path": "js/Utils/StringUtils.js",
    "content": "export function s(number) {\n\tif (number == 1) return \"\"\n\treturn \"s\"\n}\n\nexport function isare(number) {\n\tif (number == 1) return \"is\"\n\treturn \"are\"\n}\n"
  },
  {
    "path": "js/main.js",
    "content": "import App from \"./App\"\n\nwindow.addEventListener(\"DOMContentLoaded\", () => {\n\tlet xhr = new XMLHttpRequest()\n\txhr.open(\"GET\", \"config/config.json\")\n\txhr.onload = function() {\n\t\tlet config = JSON.parse(this.responseText)\n\t\twindow.app = new App(config)\n\t}\n\txhr.send()\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"honey\",\n  \"private\": true,\n  \"version\": \"2\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview --host\"\n  },\n  \"devDependencies\": {\n    \"vite\": \"^5.4.9\"\n  }\n}\n"
  }
]