├── .devcontainer ├── configuration.yaml ├── devcontainer.json └── ui-lovelace.yaml ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── favicon-counter.js ├── hacs.json ├── info.md ├── media ├── img.png └── notification_counter.gif ├── package.json ├── rollup.config.js ├── src └── favicon-counter.ts └── tsconfig.json /.devcontainer/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | lovelace: 3 | mode: yaml 4 | demo: -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "name": "Favicon counter Development", 4 | "image": "ludeeus/devcontainer:monster-stable", 5 | "context": "..", 6 | "appPort": ["5000:5000", "9123:8123"], 7 | "postCreateCommand": "npm install", 8 | "runArgs": [ 9 | "-v", 10 | "${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh" // This is added so you can push from inside the container 11 | ], 12 | "extensions": [ 13 | "github.vscode-pull-request-github", 14 | "eamodio.gitlens", 15 | "dbaeumer.vscode-eslint", 16 | "esbenp.prettier-vscode", 17 | "bierner.lit-html", 18 | "runem.lit-plugin", 19 | "auchenberg.vscode-browser-preview", 20 | "davidanson.vscode-markdownlint", 21 | "redhat.vscode-yaml" 22 | ], 23 | "settings": { 24 | "files.eol": "\n", 25 | "editor.tabSize": 2, 26 | "terminal.integrated.shell.linux": "/bin/bash", 27 | "editor.formatOnPaste": false, 28 | "editor.formatOnSave": true, 29 | "editor.formatOnType": true, 30 | "files.trimTrailingWhitespace": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.devcontainer/ui-lovelace.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - url: http://127.0.0.1:5000/favicon-counter.js 3 | type: module 4 | views: 5 | - cards: [] 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | experimentalDecorators: true, 12 | }, 13 | rules: { 14 | "@typescript-eslint/camelcase": 0 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ludeeus] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | 12 | 13 | **Checklist:** 14 | 15 | - [ ] I updated to the latest version available 16 | - [ ] I cleared the cache of my browser 17 | 18 | **Release with the issue:** 19 | 20 | **Last working release (if known):** 21 | 22 | **Browser and Operating System:** 23 | 24 | 27 | 28 | **Description of problem:** 29 | 30 | 33 | 34 | **Javascript errors shown in the web inspector (if applicable):** 35 | 36 | ``` 37 | 38 | ``` 39 | 40 | **Additional information:** 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Test build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Build 18 | run: | 19 | npm install 20 | npm run build -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Prepare release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | # Build 15 | - name: Build the file 16 | run: | 17 | cd /home/runner/work/favicon-counter/favicon-counter 18 | npm install 19 | npm run build 20 | 21 | # Upload build file to the releas as an asset. 22 | - name: Upload zip to release 23 | uses: svenstaro/upload-release-action@v1-release 24 | 25 | with: 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} 27 | file: /home/runner/work/favicon-counter/favicon-counter/dist/favicon-counter.js 28 | asset_name: favicon-counter.js 29 | tag: ${{ github.ref }} 30 | overwrite: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.rpt2_cache/ 3 | package-lock.json 4 | /dist -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "github.vscode-pull-request-github", 4 | "eamodio.gitlens", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "bierner.lit-html", 8 | "runem.lit-plugin", 9 | "auchenberg.vscode-browser-preview", 10 | "davidanson.vscode-markdownlint", 11 | "redhat.vscode-yaml" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ludeeus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Favicon counter 2 | 3 | Show a counter for your pending notifications as a bage on the favicon. 4 | 5 | ![img](media/img.png) 6 | 7 | ## Installation 8 | 9 | If you use []() it's there. 10 | 11 | if not, download the `favicon-counter.js` from this repo, and place it underyou `www` folder with all other lovelace plugins. 12 | 13 | ## Configuration 14 | 15 | ``` 16 | resources: 17 | - url: /community_plugin/favicon-counter/favicon-counter.js 18 | type: module 19 | ``` 20 | 21 | ## Demo 22 | 23 | ![demo](media/notification_counter.gif) 24 | 25 | 26 | ### Notes 27 | 28 | - It's not perfect. 29 | - It _may_ contain bugs. 30 | - It's probably the wrong way to do it. 31 | - But hey! it's something :D 32 | -------------------------------------------------------------------------------- /favicon-counter.js: -------------------------------------------------------------------------------- 1 | let t,e;function n(){let t=document.querySelector("home-assistant");if(null===t)return 0;t=t.hass;let e=0;if(null===t)return 0;for(const n in t.states)n.startsWith("persistent_notification.")&&(e+=1);return e}window.setInterval((function(){e!==n()&&(function(){const e=n(),l=parent.document.querySelector("link[rel=icon]");void 0===t&&(t=l.href);const i=16,r=document.createElement("canvas");r.width=16,r.height=16;const o=r.getContext("2d"),a=document.createElement("img");null===l||(a.src=l.href,a.onload=()=>{null===o||(o.drawImage(a,0,0,i,i),0", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@babel/core": "^7.6.4", 15 | "@babel/plugin-proposal-class-properties": "^7.5.5", 16 | "@babel/plugin-proposal-decorators": "^7.4.0", 17 | "@typescript-eslint/eslint-plugin": "^2.6.0", 18 | "@typescript-eslint/parser": "^2.6.0", 19 | "eslint-config-airbnb-base": "^14.0.0", 20 | "eslint-config-prettier": "^6.5.0", 21 | "eslint-plugin-import": "^2.18.2", 22 | "eslint-plugin-prettier": "^3.1.1", 23 | "eslint": "^6.6.0", 24 | "prettier": "^1.18.2", 25 | "rollup-plugin-babel-minify": "^9.1.1", 26 | "rollup-plugin-babel": "^4.3.2", 27 | "rollup-plugin-cleanup": "^3.1.1", 28 | "rollup-plugin-commonjs": "^10.1.0", 29 | "rollup-plugin-gzip": "^2.2.0", 30 | "rollup-plugin-node-resolve": "^5.2.0", 31 | "rollup-plugin-serve": "^1.0.1", 32 | "rollup-plugin-progress": "^1.1.1", 33 | "rollup-plugin-sizes": "^1.0.1", 34 | "rollup-plugin-terser": "^5.1.2", 35 | "rollup-plugin-typescript2": "^0.24.3", 36 | "rollup": "^1.26.0", 37 | "typescript": "^3.6.4" 38 | }, 39 | "scripts": { 40 | "start": "rollup -c --watch", 41 | "build": "npm run lint && npm run rollup", 42 | "lint": "eslint src/*.ts", 43 | "rollup": "rollup -c" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from 'rollup-plugin-terser'; 2 | import babel from 'rollup-plugin-babel'; 3 | import cleanup from 'rollup-plugin-cleanup'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | import serve from 'rollup-plugin-serve'; 6 | import minify from 'rollup-plugin-babel-minify'; 7 | import nodeResolve from 'rollup-plugin-node-resolve'; 8 | import progress from 'rollup-plugin-progress'; 9 | import sizes from 'rollup-plugin-sizes'; 10 | import typescript from 'rollup-plugin-typescript2'; 11 | 12 | const isdev = process.env.ROLLUP_WATCH; 13 | 14 | const opts_babel = { 15 | exclude: 'node_modules/**', 16 | }; 17 | 18 | const opts_serve = { 19 | contentBase: ['./dist'], 20 | host: '0.0.0.0', 21 | port: 5000, 22 | allowCrossOrigin: true, 23 | headers: { 24 | 'Access-Control-Allow-Origin': '*', 25 | }, 26 | }; 27 | 28 | const opts_cleanup = { 29 | comments: 'none', 30 | }; 31 | 32 | const opts_sizes = { 33 | details: true, 34 | }; 35 | 36 | const AwesomePlugins = [ 37 | progress(), 38 | nodeResolve(), 39 | commonjs(), 40 | typescript(), 41 | babel(opts_babel), 42 | !isdev && minify(), 43 | !isdev && terser(), 44 | !isdev && cleanup(opts_cleanup), 45 | isdev && sizes(opts_sizes), 46 | isdev && serve(opts_serve), 47 | ]; 48 | 49 | export default [ 50 | { 51 | input: ['src/favicon-counter.ts'], 52 | output: { 53 | file: `dist/favicon-counter.js`, 54 | format: 'es', 55 | }, 56 | plugins: [...AwesomePlugins], 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /src/favicon-counter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | // Modified from https://medium.com/@alperen.talaslioglu/building-dynamic-favicon-with-javascript-223ad7999661 3 | let favurl: string; 4 | function GetNotifyCount(): number { 5 | let hass = document.querySelector('home-assistant'); 6 | if (hass === null) return 0; 7 | hass = (hass as any).hass; 8 | let notifications = 0; 9 | 10 | if (hass === null) return 0; 11 | 12 | for (const state in (hass as any).states) { 13 | if (state.startsWith('persistent_notification.')) notifications += 1; 14 | } 15 | return notifications; 16 | } 17 | 18 | function UpdateFavicon(): void { 19 | const count = GetNotifyCount(); 20 | const favicon = parent.document.querySelector('link[rel=icon]'); 21 | 22 | if (favurl === undefined) favurl = (favicon as any).href; 23 | 24 | const faviconSize = 16; 25 | const canvas = document.createElement('canvas'); 26 | canvas.width = faviconSize; 27 | canvas.height = faviconSize; 28 | const context = canvas.getContext('2d'); 29 | const img = document.createElement('img'); 30 | 31 | if (favicon === null) return; 32 | img.src = (favicon as any).href; 33 | 34 | img.onload = (): void => { 35 | // Draw Original Favicon as Background 36 | if (context === null) return; 37 | context.drawImage(img, 0, 0, faviconSize, faviconSize); 38 | if (count > 0) { 39 | // Draw Notification Circle 40 | context.beginPath(); 41 | context.arc(canvas.width - faviconSize / 3, faviconSize / 3, faviconSize / 3, 0, 2 * Math.PI); 42 | context.fillStyle = '#FF0000'; 43 | context.fill(); 44 | 45 | // Draw Notification Number 46 | context.font = '10px "helvetica", sans-serif'; 47 | context.textAlign = 'center'; 48 | context.textBaseline = 'middle'; 49 | context.fillStyle = '#FFFFFF'; 50 | context.fillText(String(count), canvas.width - faviconSize / 3, faviconSize / 3); 51 | // Replace favicon 52 | (favicon as any).href = canvas.toDataURL('image/png'); 53 | } else { 54 | (favicon as any).href = favurl; 55 | } 56 | }; 57 | } 58 | 59 | let prevCount: number; 60 | window.setInterval(function() { 61 | if (prevCount !== GetNotifyCount()) { 62 | UpdateFavicon(); 63 | prevCount = GetNotifyCount(); 64 | } 65 | }, 1000); 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["es2017", "dom", "dom.iterable"], 7 | "noEmit": true, 8 | "noUnusedParameters": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "experimentalDecorators": true 16 | } 17 | } 18 | --------------------------------------------------------------------------------