Repository: dani3l0/honey Branch: main Commit: 0ae1ea569beb Files: 38 Total size: 34.8 KB Directory structure: gitextract_y0s9qw7f/ ├── .github/ │ └── workflows/ │ └── docker-image.yml ├── .gitignore ├── Dockerfile ├── README.md ├── css/ │ ├── Background.css │ ├── Flags/ │ │ ├── Dark.css │ │ ├── Flags.css │ │ └── Loaded.css │ ├── Pages/ │ │ ├── Home.css │ │ ├── More/ │ │ │ ├── More.css │ │ │ ├── Overview.css │ │ │ └── Settings.css │ │ ├── Pages.css │ │ └── Services.css │ └── main.css ├── docker-compose.yaml ├── entrypoint.sh ├── fonts/ │ ├── MaterialSymbolsRounded/ │ │ └── MaterialSymbolsRounded.css │ ├── Quicksand/ │ │ └── Quicksand.css │ └── fonts.css ├── index.html ├── js/ │ ├── App.js │ ├── UI/ │ │ ├── Drawer/ │ │ │ └── Drawer.js │ │ ├── Home/ │ │ │ └── Home.js │ │ ├── Main/ │ │ │ └── Main.js │ │ └── More/ │ │ ├── More.js │ │ ├── Overview/ │ │ │ ├── Overview.js │ │ │ ├── analyzer.js │ │ │ └── tiles.js │ │ └── Settings/ │ │ ├── Settings.js │ │ ├── events.js │ │ └── tiles.js │ ├── Utils/ │ │ ├── Config.js │ │ ├── DOMUtils.js │ │ ├── PingService.js │ │ └── StringUtils.js │ └── main.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/docker-image.yml ================================================ # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images name: Create and publish a Docker image # Build Docker image on new release on: push: tags: ['v*'] # Set registry to GitHub Container Registry env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push-image: runs-on: ubuntu-latest permissions: contents: read packages: write # Build steps steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to the Container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v4 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 provenance: false push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .gitignore ================================================ dist node_modules config ================================================ FILE: Dockerfile ================================================ FROM node:alpine # Path to assets in container WORKDIR /app COPY . . # Build honey RUN npm install RUN npm run build # Run a built-in webserver CMD ["/app/entrypoint.sh"] # Expose port EXPOSE 4173 # Health check HEALTHCHECK CMD wget -nv --spider --tries=1 http://127.0.0.1:4173 ================================================ FILE: README.md ================================================ # honey _A sweet dashboard I use on my homeserver with some self-hosted stuff..._ honey is written in **pure** `HTML` `CSS` `JS` so dynamic backend or special webserver configuration is not required. It works out-of-the-box as all operations are done client-side. **[📺 Live demo](https://honeyy.vercel.app/)** ## 🚀 Installation ### 🕸️ On existing webserver 1. Download latest prebuilt archive from **[Releases](https://github.com/dani3l0/honey/releases)**. 2. Extract downloaded archive to your webserver root. 3. You're done! ### 🐋 via Docker ``` docker run -p 4173:4173 -v /path/to/config:/app/dist/config ghcr.io/dani3l0/honey:latest ``` - `-p 4173:4173` - exposes HTTP port to your machine - `-v /path/to/config:/app/dist/config` - mounts config directory to your local filesystem, missing config files will be created automatically If you have custom icons or background images, you can freely put them in `config` dir. Just remember to provide valid URLs (with `/config` prefix). _alternatively, use a `docker-compose.yml` file_ ## ⚙️ Configuration Configuration file is located at `config/config.json`. ### 📱 Tweaking the user interface The following keys are available under `ui` section. Some of them are listed in _Settings_ page and can be customized by end-user. | Key name | Description | in Settings | |-----------------------|-----------------------------------------------------------------------------------------------------------|----------------| | `name` | Name shown at the main screen and the tab title. | ❌ | | `desc` | Short description shown under title at the main screen. | ❌ | | `icon` | Icon shown at the main screen and as site's favicon. | ❌ | | `wallpaper` | Background image visible when dark mode is disabled. | ❌ | | `wallpaper_dark` | Background image visible when dark mode is enabled. | ❌ | | `dark_mode` | Tells whether dark mode is enabled by default. (Available values: `Auto`,`Off`,`On`) | ✅ | | `open_new_tab` | Tells whether clicking on a service will open it in new tab by default. | ✅ | | `ping_dots` | Enables small dot before service name indicating whether is it available or not. | ✅ | | `blur` | Tells whether card background blur is enabled by default. | ✅ | | `animations` | Tells whether UI animations are enabled by default. | ✅ | | `trusted_domains` | Array of domains (or IP addresses) to no longer be considered as 3rd-parties. RegExp is fully supported. | ✅ | ### 🔗 Adding custom services `services` section is an array containing objects. Object's structure looks like this: | Key name | Description | |-------------------|-------------------------------------------------------------------------------| | `name` | Your service's name. | | `desc` | Short description shown under service's name. | | `href` | URL address of your service. It is directly passed to `` tag. | | `icon` | Path to an icon of your service. | Example: ``` ... { "name": "CalDav", "desc": "Simple CalDav server for calendar sync between various devices.", "href": "caldav", "icon": "img/preview/caldav.png" }, ... ``` ## 🛠️ Development honey is built on top of [Vite.js](https://vitejs.dev/). This tool allows faster development and offers various optimizations. How to prepare a development environment: ``` # Download the source code git clone https://github.com/dani3l0/honey && cd honey # Install required modules npm i ``` ### 🗼 Live server **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`. ``` npm run dev ``` ### 🏗️ Build **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. ``` npm run build ``` ## 🤝 Credits Of course, some third-party resources are used in this project. I kanged them for self-hosting, easier development and to avoid compatibility issues. - **[Material Icons](https://github.com/materialos/android-icon-pack/)**, for app icons at _Services_ page - **[Google Fonts](https://fonts.google.com/)**, for material icons on buttons and Quicksand font - **honey icon** - random icon found in DuckDuckGo Images - **Wallpapers** - very nice background images kanged from [wallhaven](https://wallhaven.cc/) ================================================ FILE: css/Background.css ================================================ #background img { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; object-fit: cover; transition: transform .4s, opacity .4s !important; } #background.scaled img { transform: scale(var(--scale-factor)); } body #background img { opacity: 1; } body:not(.dark) #background img:last-child, body.dark #background img:first-child { opacity: 0; } body.dark #background:not(.scaled) img:first-child, body:not(.dark) #background:not(.scaled) img:last-child { transform: scale(var(--scale-factor)); } body.dark #background.scaled img:first-child, body:not(.dark) #background.scaled img:last-child { transform: none; } #background .notloaded { transform: scale(1) !important; opacity: 0 !important; } ================================================ FILE: css/Flags/Dark.css ================================================ body.dark { --color: #EEE; --color2: #EEE6; --background: #1118; --bg2: #0008; --hover: #FFF1; } body.dark #theme-switcher i::before { --hidden: 1; } body.dark #theme-switcher i::after { --hidden: 0; } ================================================ FILE: css/Flags/Flags.css ================================================ @import url(Loaded.css); @import url(Dark.css); body { --color: #222; --color2: #2229; --background: #EEE8; --bg2: #FFF8; --hover: #0001; --scale-factor: 1.15; --blur: blur(16px); } body.noblur { --blur: 0; } body.noanim * { transition: none !important; } ================================================ FILE: css/Flags/Loaded.css ================================================ body { transition: opacity .3s; } body:not(.loaded) { opacity: 0; transition: none; } body:not(.loaded) * { transition: none !important; } body:not(.loaded) .page .wrapper { transform: scale(.8) translateY(50%); } ================================================ FILE: css/Pages/Home.css ================================================ .main { position: fixed; top: 0; left: 0; width: 100%; height: 100%; } .appicon { width: 192px; height: 192px; object-fit: cover; transition: transform .4s, opacity .4s; } .appicon.notloaded { transform: scale(.8); opacity: 0; } #theme-switcher i { position: relative; overflow: hidden; } #theme-switcher i::before, #theme-switcher i::after { position: absolute; top: 50%; left: 50%; --hidden: 1; opacity: calc(1 - var(--hidden)); transform: translate(-50%, -50%) rotateZ(calc(var(--hidden) * 360deg)) scale(calc(1 - var(--hidden) / 2)); transition: transform .3s, opacity .3s; } #theme-switcher i::before { --hidden: 0; --dark: 0; content: "light_mode"; } #theme-switcher i::after { --dark: 1; content: "dark_mode"; } .home.page { top: 50%; left: 50%; width: 100%; height: auto; overflow: hidden; transform: translate(-50%, -50%); } .home.page:not(.current) { top: calc(50% - 64px); } .home .wrapper { box-shadow: none; background: transparent; backdrop-filter: none; } .appname { font-size: 48px; } .appdesc { opacity: .6; margin: 2px 12px; } .buttons { box-shadow: 2px 2px 8px #0002; display: flex; margin: 16px auto 0; backdrop-filter: var(--blur); border-radius: 24px; max-width: 480px; background: var(--background); padding: 2px; justify-content: space-between; transition: background .3s; } .buttons > div { padding: 16px; margin: 2px; cursor: pointer; border-radius: 20px; width: 100%; transition: background .2s; } .buttons > div:hover { background: var(--hover); } .buttons .text { margin-top: -2px; } ================================================ FILE: css/Pages/More/More.css ================================================ @import url(Overview.css); @import url(Settings.css); .subpages { position: relative; transform: translateX(calc(var(--id) * -100%)); transition: transform .4s, height .4s; } .subpages > div { position: absolute; left: calc(var(--n) * 100%); width: 100%; } ================================================ FILE: css/Pages/More/Overview.css ================================================ .overview { display: flex; align-items: center; justify-content: space-between; max-width: 640px; padding: 0 20px; margin: 128px auto 88px; text-align: right; } .overview > i { font-size: 80px; } @property --value { syntax: ''; initial-value: 0; inherits: false; } .overview .big { font-size: 64px; margin-bottom: -4px; counter-reset: value var(--value); transition: --value 1.2s; } .page:not(.current) .overview .big { --value: 0 !important; transition: --value 0s .5s; } .overview .big::after { content: counter(value); } .overview .small { opacity: .5; } .privacy-boxes { display: flex; text-align: left; flex-wrap: wrap; margin-bottom: 10px; } .privacy-boxes > div { display: flex; width: 50%; min-width: 256px; flex: 1; padding: 8px; align-items: center; } .privacy-boxes i { color: var(--color); text-shadow: 0 0 48px var(--color); padding: 16px; font-size: 28px; border-radius: 32px; } .privacy-boxes .title { font-size: 16px; } .privacy-boxes .subtitle { opacity: .5; } ================================================ FILE: css/Pages/More/Settings.css ================================================ #settings { margin: 32px auto; padding: 0 16px; } .setting { background: var(--bg2); margin: 8px; padding: 20px; display: flex; border-radius: 16px; align-items: center; text-align: left; transition: background .3s; } .setting.pointer { cursor: pointer; } .setting i { margin-right: 14px; font-size: 28px; } .setting .name { font-size: 16px; } .setting .desc { opacity: .6; margin-right: 16px; } .setting .switch { position: relative; width: 44px; min-width: 44px; height: 24px; background: #8886; border-radius: 100px; margin: 0 4px 0 auto; transition: border .4s, background .3s; } .setting .switch:after { content: ""; position: absolute; width: 16px; height: 16px; background: var(--color); left: 4px; top: 50%; border-radius: 10px; transform: translateY(-50%); transition: left .2s, background .3s; } .setting.checked .switch { background-color: #68F; border-color: #68F; } .setting.checked .switch:after { left: calc(100% - 20px); } #no-cookies { margin: 24px 0 -8px; color: #F60; } #no-cookies.hidden { display: none; } .options { position: relative; margin-left: auto; background: #8883; border-radius: 16px; border: 4px solid transparent; display: flex; align-items: center; text-align: center; overflow: hidden; } .options::before { content: " "; position: absolute; width: calc(100% / var(--items)); left: calc(100% / var(--items) * var(--item)); height: 100%; border-radius: 12px; background: #68F; transition: left .3s; } .options div { padding: 12px 0; width: 64px; flex: 1; cursor: pointer; z-index: 1; } ================================================ FILE: css/Pages/Pages.css ================================================ @import url(Home.css); @import url(Services.css); @import url(More/More.css); .page { position: fixed; top: 0; left: 50%; width: 100%; max-width: 920px; height: 100vh; transform: translateX(-50%); overflow-y: scroll; transition: top .4s, opacity .4s, visibility .4s, color .3s; } .page:not(.current) { top: 240px; visibility: hidden; opacity: 0; } .wrapper { box-shadow: 2px 2px 8px #0003; background: var(--background); padding: 3px; backdrop-filter: var(--blur); border-radius: 24px; margin: -4px 12px 16px; text-align: center; overflow: hidden; min-width: 340px; transition: background .2s, transform .4s, backdrop-filter .6s; } .back { box-shadow: 2px 2px 8px #0002; position: relative; width: 64px; height: 64px; border-radius: 24px; background: var(--background); margin: 20px; backdrop-filter: var(--blur); transition: background .2s; } .back i { margin: 4px; width: 56px; height: 56px; cursor: pointer; border-radius: 20px; transition: background .2s; } .back i:hover { background: var(--hover); } .back i:after { content: "chevron_left"; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .header { display: flex; align-items: center; margin: 20px 20px 16px; } .header i { margin-top: 1px; margin-right: 10px; } .header .text { font-size: 26px; } .subswitch { display: flex; background: var(--bg2); transition: background .2s; margin: 16px 24px 0; z-index: 1; padding: 4px; border-radius: 16px; position: relative; overflow: hidden; --id: inherit; } .subswitch::before { content: " "; z-index: -1; position: absolute; top: 4px; left: calc(var(--id) / var(--switches) * 100% + 4px - 4px * var(--id)); width: calc(100% / var(--switches) - 4px); height: calc(100% - 8px); opacity: .25; background: var(--color2); border-radius: 12px; transition: left .3s, background .3s; } .subswitch div { padding: 12px; width: 50%; cursor: pointer; } ================================================ FILE: css/Pages/Services.css ================================================ .boxes { display: flex; flex: 1 1 0; flex-wrap: wrap; } .box { min-width: 292px; flex: 1; margin: 2px; border-radius: 20px; padding: 8px; display: flex; align-items: center; text-align: left; text-decoration: none; color: inherit; transition: background .2s; } .box:hover { background: var(--hover); } .box i { font-size: 24px; padding: 20px; background: hsl(var(--color), 100%, 89%); color: hsl(var(--color), 100%, 35%); border-radius: 100px; margin: 2px 12px 2px 2px; } a.box { cursor: pointer; padding: 14px; } .box img { width: 64px; height: 64px; object-fit: cover; margin-right: 12px; } .box .name { position: relative; font-size: 18px; } .box.pingdot .name { padding-left: 16px; } .box.pingdot .name::before { content: " "; position: absolute; width: 6px; height: 6px; border-radius: 6px; top: 50%; left: 0; transform: translateY(-50%); background: #888; transition: background .2s, box-shadow .8s; } .box.pingdot.up .name::before { background: #0F8; box-shadow: 0 0 12px #0F8; } .box.pingdot.down .name::before { background: #F35; box-shadow: 0 0 12px #F35; } .box.pingdot.error .name::before { background: #F82; box-shadow: 0 0 12px #F82; } .box .desc { opacity: .6; } ================================================ FILE: css/main.css ================================================ @import url(../fonts/fonts.css); @import url(Flags/Flags.css); @import url(Background.css); @import url(Pages/Pages.css); body { background: #000; color: var(--color); margin: 0; font-family: Quicksand; font-weight: bold; user-select: none; font-size: 14px; -webkit-tap-highlight-color: transparent; } * { scrollbar-width: none; } ::-webkit-scrollbar { display: none; } ================================================ FILE: docker-compose.yaml ================================================ services: honey: image: ghcr.io/dani3l0/honey:latest container_name: honey restart: unless-stopped volumes: - ./config:/app/dist/config ports: - "4173:4173" ================================================ FILE: entrypoint.sh ================================================ #!/bin/sh cp -rnv /app/public/config/* /app/dist/config npm run preview exit $? ================================================ FILE: fonts/MaterialSymbolsRounded/MaterialSymbolsRounded.css ================================================ @font-face { font-family: 'Material Symbols Rounded'; font-style: normal; font-weight: 500; src: url(MaterialSymbolsRounded.woff2) format('woff2'); } i { font-family: 'Material Symbols Rounded'; font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -webkit-font-feature-settings: 'liga'; -webkit-font-smoothing: antialiased; } i { display: inline-block; } ================================================ FILE: fonts/Quicksand/Quicksand.css ================================================ /* quicksand-regular-latin */ @font-face { font-family: Quicksand; font-style: normal; font-weight: 400; src: local("Quicksand Regular"), local("Quicksand-Regular"), url(quicksand-regular-latin.woff2) format("woff2"); unicode-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; } /* quicksand-regular-latin-ext */ @font-face { font-family: Quicksand; font-style: normal; font-weight: 400; src: local("Quicksand Regular"), local("Quicksand-Regular"), url(quicksand-regular-latin-ext.woff2) format("woff2"); unicode-range: U+0100-024F,U+0259,U+1E00-1EFF,U+20A0-20CF,U+2C60-2C7F,U+A720-A7FF; } /* quicksand-bold-latin */ @font-face { font-family: Quicksand; font-style: normal; font-weight: 700; src: local("Quicksand Bold"), local("Quicksand-Bold"), url(quicksand-bold-latin.woff2) format("woff2"); unicode-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; } /* quicksand-bold-latin-ext */ @font-face { font-family: Quicksand; font-style: normal; font-weight: 700; src: local("Quicksand Bold"), local("Quicksand-Bold"), url(quicksand-bold-latin-ext.woff2) format("woff2"); unicode-range: U+0100-024F,U+0259,U+1E00-1EFF,U+20A0-20CF,U+2C60-2C7F,U+A720-A7FF; } ================================================ FILE: fonts/fonts.css ================================================ @import url(Quicksand/Quicksand.css); @import url(MaterialSymbolsRounded/MaterialSymbolsRounded.css); ================================================ FILE: index.html ================================================
 
Theme
apps
Services
more
More
apps
Services
Overview
Settings
rocket_launch
WARNING: due to blocked cookies, all settings will be lost after page reload
================================================ FILE: js/App.js ================================================ import Drawer from "./UI/Drawer/Drawer" import Home from "./UI/Home/Home" import Main from "./UI/Main/Main" import More from "./UI/More/More" import Config from "./Utils/Config" import { showPage } from "./Utils/DOMUtils" export default class App { static instance constructor(config) { if (App.instance) return App.instance App.instance = this this.config = new Config(config) this.init() } init() { this.main = new Main() this.home = new Home() this.drawer = new Drawer() this.more = new More() showPage("home") setTimeout(() => { document.body.classList.add("loaded") }, 100) } } ================================================ FILE: js/UI/Drawer/Drawer.js ================================================ import App from "../../App"; import PingService from "../../Utils/PingService"; export default class Drawer { constructor() { this.app = new App() this.config = this.app.config this.init() } init() { this.importApps() } importApps() { let apps = this.config.getServices() let enablePingDots = this.config.get("ping_dots") let openNewTab = this.config.get("open_new_tab") let applist = document.querySelector("#app-list") applist.innerHTML = "" for (let app of apps) { let a = document.createElement("a") a.classList.add("box") a.href = app.href if (openNewTab) a.setAttribute("target", "_blank") a.innerHTML = `
${app.name}
${app.desc}
` if (enablePingDots) { a.classList.add("pingdot") PingService(app.href, status => { if (!status) return let resp = "down" if (status >= 200 && status < 400) resp = "up" else if (status >= 400) resp = "error" a.classList.add(resp) }) } applist.appendChild(a) } } } ================================================ FILE: js/UI/Home/Home.js ================================================ import App from "../../App" import { showPage } from "../../Utils/DOMUtils" export default class Home { constructor() { this.app = new App() this.config = this.app.config this.init() } init() { this.initButtons() this.initHomeUI() this.initBackButtons() } initButtons() { let buttons = document.querySelector(".buttons").children for (let button of buttons) { let target = button.getAttribute("t") if (target) { button.addEventListener("click", () => { showPage(target) }) } } } initBackButtons() { let backButtons = document.querySelectorAll(".back") for (let button of backButtons) { button.addEventListener("click", () => { showPage("home") }) } } initHomeUI() { let logo = document.querySelector(".appicon") logo.src = this.config.get("icon") logo.classList.add("notloaded") logo.addEventListener("load", () => { logo.classList.remove("notloaded") }) let name = document.querySelector(".appname") name.innerText = this.config.get("name") let desc = document.querySelector(".appdesc") desc.innerText = this.config.get("desc") } } ================================================ FILE: js/UI/Main/Main.js ================================================ import App from "../../App" export default class Main { constructor() { this.app = new App() this.config = this.app.config this.init() } init() { document.title = this.config.get("name") document.querySelector("#favicon").href = this.config.get("icon") document.querySelector("#apple-touch-icon").href = this.config.get("icon") this.initBackgrounds() } initBackgrounds() { this.backgrounds = document.querySelector("#background") for (let i = 0; i < 2; i++) { let img = document.createElement("img") img.classList.add("notloaded") img.addEventListener("load", () => { img.classList.remove("notloaded") }) this.backgrounds.appendChild(img) } this.backgrounds = this.backgrounds.children this.backgrounds[0].src = this.config.get("wallpaper") this.backgrounds[1].src = this.config.get("wallpaper_dark") } } ================================================ FILE: js/UI/More/More.js ================================================ import App from "../../App"; import Overview from "./Overview/Overview"; import Settings from "./Settings/Settings"; export default class More { constructor() { this.app = new App() this.config = this.app.config this.overview = new Overview() this.settings = new Settings() this.init() } init() { this.overview.init() this.settings.init() this.initPager() } initPager() { let switcher = document.querySelector(".subswitch") let buttons = switcher.children let subsettings = document.querySelector(".subpages") for (let i = 0; i < buttons.length; i++) { let button = buttons[i] subsettings.children[i].setAttribute("style", `--n: ${i}`) button.addEventListener("click", () => { let calculatedHeight = subsettings.children[i].offsetHeight subsettings.style.height = `${calculatedHeight}px` subsettings.parentNode.setAttribute("style", `--id: ${i}`) switcher.setAttribute("style", `--switches: ${buttons.length}`) }) } buttons[0].click() } } ================================================ FILE: js/UI/More/Overview/Overview.js ================================================ import App from "../../../App"; import { analyzeService } from "./analyzer"; import { privacyBox } from "./tiles"; import { s, isare } from "../../../Utils/StringUtils"; export default class Overview { constructor() { this.app = new App() this.config = this.app.config this.div = document.querySelector(".overview").parentNode } init() { this.initPrivacyBoxes() } initPrivacyBoxes() { let stats = { total: 0, secure: 0, thirdParties: 0 } for (let service of this.config.getServices()) { let analysis = analyzeService(service.href, this.config.get("trusted_domains")) stats.total++ stats.secure += analysis.isSecure stats.thirdParties += analysis.isThirdParty } this.div.querySelector(".big").setAttribute("style", `--value: ${stats.total}`) this.div.querySelector(".small").innerText = `Available service${s(stats.total)}` let encryption_t, encryption_d if (stats.secure == stats.total) { encryption_t = "Full encryption" encryption_d = "All services use secure connections (HTTPS)." } else if (stats.secure == 0) { encryption_t = "No encryption" encryption_d = "It seems server does not support HTTPS." } else { let insecure = stats.total - stats.secure encryption_t = "Partial encryption" encryption_d = `${insecure} service${s(insecure)} do not use secure connections.` } let indepencence_t, indepencence_d if (stats.thirdParties == 0) { indepencence_t = "Independence" indepencence_d = "This server is free of 3rd party services." } else if (stats.thirdParties == stats.total) { indepencence_t = "Something is wrong..." indepencence_d = "It seems only 3rd-party services are listed." } else { indepencence_t = "Partial independence" indepencence_d = `${stats.thirdParties} service${s(stats.thirdParties)} ${isare(stats.thirdParties)} provided by 3rd-parties.` } privacyBox("lock", "#0D6", encryption_t, encryption_d, stats.secure / stats.total) privacyBox("home", "#68F", indepencence_t, indepencence_d, 1 - stats.thirdParties / stats.total) } } ================================================ FILE: js/UI/More/Overview/analyzer.js ================================================ export function analyzeService(url, whitelist) { let isSiteSecure = ( window.location.protocol == "https:" || window.location.hostname == "localhost" ) let isSecure = false if (url.startsWith("https://")) { isSecure = true } else if (!["http", "https"].includes(url.split("://")[0])) { isSecure = isSiteSecure } let isThirdParty = true let domain_base = window.location.hostname let domain = url.split("://") if (domain.length > 1) { domain = domain[1] domain = domain.split("/")[0] isThirdParty = !domain.includes(domain_base) if (isThirdParty) { for (let entry of whitelist) { let re = RegExp(entry) if (re.exec(domain)) { isThirdParty = false break } } } } else { isThirdParty = false } return {isSecure, isThirdParty} } ================================================ FILE: js/UI/More/Overview/tiles.js ================================================ export function privacyBox(icon, color, name, desc, pp) { if (pp == 1) {} else if (pp > 0.7) color = "#EA0" else if (pp > 0.25) color = "#F72" else color = "#F33" let item = document.createElement("div") item.setAttribute("style", `--color: ${color}`) item.innerHTML = `${icon}
${name}
${desc}
` document.querySelector(".privacy-boxes").appendChild(item) return item } ================================================ FILE: js/UI/More/Settings/Settings.js ================================================ import App from "../../../App"; import { addOnOffTile, addOptionsTile } from "./tiles"; import * as EVENTS from "./events" export default class Settings { constructor() { this.app = new App() this.config = this.app.config } init() { this.checkLocalStorage() this.initSettings() } initSettings() { let darkMode = addOptionsTile(this.config, "dark_mode", "Dark mode", "Make the colors more appropriate for low-light environments", "dark_mode", EVENTS.onThemeChange ) addOnOffTile(this.config, "open_in_new", "Open in new tab", "Clicking on application will open it in a new browser tab", "open_new_tab", EVENTS.onNewTabChange ) addOnOffTile(this.config, "sensors", "Ping dots", "Shows small dots before titles indicating whether service is up or not", "ping_dots", EVENTS.onPingDotsChange ) addOnOffTile(this.config, "blur_on", "Enable blur", "Improves UI sweetness but may have a huge impact on performance", "blur", EVENTS.onBlurChange ) addOnOffTile(this.config, "animation", "Animations", "Show nice and fancy page transitions for improved experience", "animations", EVENTS.onAnimationChange ) document.querySelector("#theme-switcher").addEventListener("click", () => { let targetButtons = darkMode.querySelector(".options").children let storedValue = this.config.get("dark_mode") let target; if (storedValue == "Auto") { let isSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches target = 2 - isSystemDark } else { let isEnforcedDark = storedValue == "On" target = !isEnforcedDark + 1 } targetButtons[target].click() }) } checkLocalStorage() { let warn = document.querySelector("#no-cookies").classList if (this.config.storageAvailable) warn.add("hidden") } } ================================================ FILE: js/UI/More/Settings/events.js ================================================ import App from "../../../App"; const CL = document.body.classList // Switch between light & dark themes var onThemeChange_SystemListener = false; export function onThemeChange(config) { let value = config.get("dark_mode") let mm = window.matchMedia('(prefers-color-scheme: dark)') let isDark = value == "Auto" ? mm.matches : value == "On" isDark ? CL.add("dark") : CL.remove("dark") // Listen for system theme changes if (!onThemeChange_SystemListener) { onThemeChange_SystemListener = true mm.addEventListener('change', event => { if (config.get("dark_mode") == "Auto") { let isDark = event.matches; isDark ? CL.add("dark") : CL.remove("dark") } }); } } // Open apps in new tab export function onNewTabChange(config) { let openNewTab = config.get("open_new_tab") let appList = document.querySelector("#app-list").children for (let app of appList) { if (openNewTab) app.setAttribute("target", "_blank") else app.removeAttribute("target") } } // Enable/disable ping dots export function onPingDotsChange(config) { let app = new App() app.drawer.importApps() } // Enable/disable background blur export function onBlurChange(config) { let blur = config.get("blur") blur ? CL.remove("noblur") : CL.add("noblur") } // Enable/disable animations export function onAnimationChange(config) { let animations = config.get("animations") animations ? CL.remove("noanim") : CL.add("noanim") } ================================================ FILE: js/UI/More/Settings/tiles.js ================================================ export function addOnOffTile(conf, icon, name, desc, key, func) { let item = document.createElement("div") item.classList.add("setting") item.classList.add("pointer") item.innerHTML = ` ${icon}
${name}
${desc}
` let handleState = () => { let c = item.classList if (conf.get(key)) c.add("checked") else c.remove("checked") } let write = () => { let target_value = !conf.get(key) conf.set(key, target_value) } let f = () => {func(conf)} item.addEventListener("click", write) item.addEventListener("click", handleState) if (func) item.addEventListener("click", f) handleState() if (func) f() document.querySelector("#settings").appendChild(item) return item } export function addOptionsTile(conf, icon, name, desc, key, func) { let options = ["Auto", "Off", "On"] let optionsHtml = document.createElement("div") optionsHtml.classList.add("options") let handleState = () => { let c = optionsHtml let value = conf.get(key) let n = options.indexOf(value) for (let i = 0; i < options.length; i++) { let cl = c.children[i].classList if (i == n) cl.add("active") else cl.remove("active") } c.setAttribute("style", `--item: ${n}; --items: ${options.length}`) } let write = (val) => { conf.set(key, val) } let f = () => {func(conf)} options.forEach(e => { let node = document.createElement("div") node.innerText = e node.addEventListener("click", () => { write(e) handleState() if (func) f() }) optionsHtml.appendChild(node) }) handleState() if (func) f() let item = document.createElement("div") item.classList.add("setting") item.innerHTML = ` ${icon}
${name}
${desc}
` item.appendChild(optionsHtml) document.querySelector("#settings").appendChild(item) return item } ================================================ FILE: js/Utils/Config.js ================================================ export default class Config { // Initialization & localStorage availability check constructor(config) { this.config = config try { window.localStorage this.storageAvailable = true } catch (e) { this.storageAvailable = false } } // Get value from config or localStorage (if set) get(key) { let value = this.config["ui"][key] if (this.storageAvailable) { let type = typeof(value) let stored_value = window.localStorage.getItem(key) if (stored_value != null) { value = stored_value if (type == "number") value = Number(value) else if (type == "boolean") value = value == "true" } } return value } // Save value to localStorage set(key, value) { key = key.toLowerCase() this.config["ui"][key] = value if (this.storageAvailable) { window.localStorage.setItem(key, value) } } // Get services from config.json file getServices() { return this.config["services"] } } ================================================ FILE: js/Utils/DOMUtils.js ================================================ export function showPage(target) { let bg = document.querySelector("#background").classList if (target == "home") bg.add("scaled") else bg.remove("scaled") let pages = document.querySelectorAll(".page") for (let page of pages) { let p = page.getAttribute("p") if (p == target) page.classList.add("current") else page.classList.remove("current") } } ================================================ FILE: js/Utils/PingService.js ================================================ export default function PingService(uri, callback) { let xhr = new XMLHttpRequest() xhr.open("GET", uri) xhr.onreadystatechange = function() { if (this.readyState < 4) return let code = this.status setTimeout(() => callback(code), 3000) } xhr.send() } ================================================ FILE: js/Utils/StringUtils.js ================================================ export function s(number) { if (number == 1) return "" return "s" } export function isare(number) { if (number == 1) return "is" return "are" } ================================================ FILE: js/main.js ================================================ import App from "./App" window.addEventListener("DOMContentLoaded", () => { let xhr = new XMLHttpRequest() xhr.open("GET", "config/config.json") xhr.onload = function() { let config = JSON.parse(this.responseText) window.app = new App(config) } xhr.send() }) ================================================ FILE: package.json ================================================ { "name": "honey", "private": true, "version": "2", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview --host" }, "devDependencies": { "vite": "^5.4.9" } }