feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,17 @@
import { Value } from './index';
declare global {
interface Window {
ExperimentalBadge?: {
set: (value?: number) => void;
clear: () => void;
};
}
interface Navigator {
setExperimentalAppBadge?: (value?: number) => void;
clearExperimentalAppBadge?: () => void;
}
}
export declare function isAvailable(): boolean;
export declare function set(value: Value): boolean;
export declare function clear(): void;
//# sourceMappingURL=badging.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"badging.d.ts","sourceRoot":"","sources":["src/badging.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAI/B,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,iBAAiB,CAAC,EAAE;YAClB,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;YAC7B,KAAK,EAAE,MAAM,IAAI,CAAA;SAClB,CAAA;KACF;IAED,UAAU,SAAS;QACjB,uBAAuB,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;QAClD,yBAAyB,CAAC,EAAE,MAAM,IAAI,CAAA;KACvC;CACF;AAqCD,wBAAgB,WAAW,YAc1B;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,KAAK,WAmB/B;AAED,wBAAgB,KAAK,SAUpB"}

View File

@@ -0,0 +1,2 @@
export default function deepMerge(target: Record<string, any>, source: Record<string, any>): Record<string, any>;
//# sourceMappingURL=deepMerge.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"deepMerge.d.ts","sourceRoot":"","sources":["src/deepMerge.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,uBAW5B"}

View File

@@ -0,0 +1,18 @@
import { Value } from './index';
export declare type Options = {
backgroundColor: string;
color: string;
indicator: string;
radius: number;
size: number;
horizontalMargin: number;
verticalMargin: number;
horizontalPadding: number;
verticalPadding: number;
};
export declare const DefaultValue: Value;
export declare const DefaultOptions: Options;
export declare function isAvailable(): boolean;
export declare function set(value: Value, options?: Partial<Options>): boolean;
export declare function clear(): void;
//# sourceMappingURL=favicon.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"favicon.d.ts","sourceRoot":"","sources":["src/favicon.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAG/B,oBAAY,OAAO,GAAG;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,gBAAgB,EAAE,MAAM,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,eAAe,EAAE,MAAM,CAAA;CACxB,CAAA;AAMD,eAAO,MAAM,YAAY,EAAE,KAAS,CAAA;AAEpC,eAAO,MAAM,cAAc,EAAE,OAU5B,CAAA;AAwPD,wBAAgB,WAAW,YAE1B;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,WAqD3D;AAED,wBAAgB,KAAK,SA4BpB"}

View File

@@ -0,0 +1,22 @@
import * as favicon from './favicon';
import * as title from './title';
export declare type Value = number | undefined;
export interface Interface {
set: (value: Value) => void;
clear: () => void;
}
export declare type Method = 'Badging' | 'Favicon' | 'Title';
export interface Options {
method: Method;
favicon: Partial<favicon.Options>;
title: Partial<title.Options>;
}
/**
* Sets badge
*/
export declare function set(value: Value, options?: Partial<Options>): void;
/**
* Clears badge
*/
export declare function clear(): void;
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,OAAO,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,KAAK,MAAM,SAAS,CAAA;AAEhC,oBAAY,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;AAEtC,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,oBAAY,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAEpD,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACjC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED;;GAEG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,GAAE,OAAO,CAAC,OAAO,CAAM,QAsB/D;AAED;;GAEG;AACH,wBAAgB,KAAK,SAIpB"}

View File

@@ -0,0 +1,445 @@
let warnedBefore = false;
const warn = () => {
if (warnedBefore) {
return;
}
// We will only warn the user if the Badging API is not available at all
if ('ExperimentalBadge' in window || 'setExperimentalAppBadge' in navigator) {
return;
}
console.warn('Badging API must be enabled. Please check here how you can enable it: https://developers.google.com/web/updates/2018/12/badging-api#use');
warnedBefore = true;
};
const current = {
mediaQuery: null,
value: 0,
};
function isVersion1Available() {
return 'ExperimentalBadge' in window && !!window.ExperimentalBadge;
}
function isVersion2Available() {
return ('setExperimentalAppBadge' in navigator &&
!!navigator.setExperimentalAppBadge &&
'clearExperimentalAppBadge' in navigator &&
!!navigator.clearExperimentalAppBadge);
}
function isAvailable() {
if (!current.mediaQuery) {
current.mediaQuery = window.matchMedia('(display-mode: standalone)');
// Get notified once app is installed
current.mediaQuery.onchange = event => {
set(current.value);
};
}
return (current.mediaQuery.matches &&
(isVersion1Available() || isVersion2Available()));
}
function set(value) {
current.value = value;
if (!isAvailable()) {
warn();
return false;
}
// Sets the badge to contents (an integer), or to "flag" if contents is omitted. If contents is 0, clears the badge for the matching app(s).
// See details here: https://github.com/WICG/badging/blob/master/explainer.md#the-api
if (isVersion1Available()) {
window.ExperimentalBadge.set(value);
return true;
}
else if (isVersion2Available()) {
navigator.setExperimentalAppBadge(value);
return true;
}
return false;
}
function clear() {
if (!isAvailable()) {
return;
}
if (isVersion1Available()) {
window.ExperimentalBadge.clear();
}
else if (isVersion2Available()) {
navigator.clearExperimentalAppBadge();
}
}
function deepMerge(target, source) {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (source[key] instanceof Object)
Object.assign(source[key], deepMerge(target[key], source[key]));
}
// Join `target` and modified `source`
Object.assign(target || {}, source);
return target;
}
function isPositiveNumber(value) {
return (typeof value !== 'undefined' && Number.isInteger(value) && value >= 0);
}
const DefaultValue = 0;
const DefaultOptions = {
backgroundColor: '#424242',
color: '#ffffff',
indicator: '!',
radius: 3,
size: 7,
horizontalMargin: 0,
verticalMargin: 0,
horizontalPadding: 1,
verticalPadding: 1,
};
const isFirefox = navigator.userAgent.indexOf('Firefox') > -1;
// Get all favicons of the page
const getFavicons = () => {
const links = document.head.getElementsByTagName('link');
const favicons = [];
for (let i = 0; i < links.length; i++) {
const link = links[i];
const href = link.getAttribute('href');
const rel = link.getAttribute('rel');
if (!href) {
continue;
}
if (!rel) {
continue;
}
if (rel.split(' ').indexOf('icon') === -1) {
continue;
}
favicons.push(link);
}
return favicons;
};
// Get the favicon with the best quality of the document
const getBestFavicon = () => {
const favicons = getFavicons();
let bestFavicon = null;
let bestSize = 0;
for (let i = 0; i < favicons.length; i++) {
const favicon = favicons[i];
const href = favicon.getAttribute('href');
const sizes = favicon.getAttribute('sizes');
// If the href looks like it's an SVG, it's the best we can get
if (href === null || href === void 0 ? void 0 : href.endsWith('.svg')) {
return favicon;
}
// If the link does not have a "sizes" attribute, we use it only if we haven't found anything else yet
if (!sizes) {
if (!bestFavicon) {
bestFavicon = favicon;
bestSize = 0;
}
continue;
}
// If we find an icon with sizes "any", it's the best we can get
if (sizes === 'any') {
return favicon;
}
// Otherwise we will try to find the maximum size
const size = parseInt(sizes.split('x')[0], 10);
if (Number.isNaN(size)) {
if (!bestFavicon) {
bestFavicon = favicon;
bestSize = 0;
}
continue;
}
if (size > bestSize) {
bestFavicon = favicon;
bestSize = size;
continue;
}
}
return bestFavicon;
};
// References to the favicons that we need to track in order to reset and update the counters
const current$1 = {
favicons: null,
bestFavicon: null,
bestFaviconImage: null,
value: DefaultValue,
options: DefaultOptions,
};
// Get size depending on screen density
const devicePixelRatioListener = window.matchMedia('screen and (min-resolution: 2dppx)');
const getRatio = () => {
return Math.ceil(window.devicePixelRatio) || 1;
};
const handleRatioChange = () => {
set$1(current$1.value, current$1.options);
};
const getIconSize = () => {
return 16 * getRatio();
};
// Update favicon
const setFavicon = (url) => {
if (!url) {
return;
}
// Remove previous favicons
for (const favicon of getFavicons()) {
if (favicon.parentNode) {
favicon.parentNode.removeChild(favicon);
}
}
// Create new favicon
const newFavicon = document.createElement('link');
newFavicon.id = 'badgin';
newFavicon.type = 'image/x-icon';
newFavicon.rel = 'icon favicon';
newFavicon.href = url;
document.getElementsByTagName('head')[0].appendChild(newFavicon);
};
// Draw the favicon
const drawFavicon = (image, value, options) => {
const iconSize = getIconSize();
const canvas = document.createElement('canvas');
canvas.width = iconSize;
canvas.height = iconSize;
const context = canvas.getContext('2d');
if (!context) {
return;
}
// Draw new image
image.width = iconSize;
image.height = iconSize;
context.drawImage(image, 0, 0, image.width, image.height);
// Draw bubble on the top
drawBubble(context, value, options);
// Refresh tag in page
setFavicon(canvas.toDataURL());
};
// Draws the bubble on the canvas
const drawBubble = (context, value, options) => {
const ratio = getRatio();
const iconSize = getIconSize();
// Do we need to render the bubble at all?
let finalValue = '';
if (isPositiveNumber(value)) {
if (value === 0) {
finalValue = '';
}
else if (value < 100) {
finalValue = String(value);
}
else {
finalValue = '99+';
}
}
else {
finalValue = options.indicator;
}
// Return early
if (!finalValue) {
return;
}
// Calculate text width initially
const textHeight = options.size - 2;
const font = `${options.size * ratio}px Arial`;
context.font = font;
const { width: textWidth } = context.measureText(finalValue);
context.restore();
// Calculate position etc.
const width = textWidth + 2 * options.horizontalPadding;
const height = textHeight * ratio + 2 * options.verticalPadding;
const top = iconSize - height - options.verticalMargin;
const left = iconSize - width - options.horizontalMargin;
const bottom = 16 * ratio - options.verticalMargin;
const right = 16 * ratio - options.horizontalMargin;
const radius = options.radius;
// Bubble
context.globalAlpha = 1;
context.fillStyle = options.backgroundColor;
context.strokeStyle = options.backgroundColor;
context.lineWidth = 0;
context.beginPath();
context.moveTo(left + radius, top);
context.quadraticCurveTo(left, top, left, top + radius);
context.lineTo(left, bottom - radius);
context.quadraticCurveTo(left, bottom, left + radius, bottom);
context.lineTo(right - radius, bottom);
context.quadraticCurveTo(right, bottom, right, bottom - radius);
context.lineTo(right, top + radius);
context.quadraticCurveTo(right, top, right - radius, top);
context.closePath();
context.fill();
context.save();
// Value
context.font = font;
context.fillStyle = options.color;
context.textAlign = 'center';
context.textBaseline = 'hanging';
context.fillText(finalValue, left + width / 2, top + options.verticalPadding + (isFirefox ? 1 : 0));
context.save();
/*
// Helper line
context.restore()
context.strokeStyle = '#ff0000'
context.moveTo(0, top + height / 2)
context.lineTo(iconSize, top + height / 2)
context.stroke()
context.save()
*/
};
function isAvailable$1() {
return !!getBestFavicon();
}
function set$1(value, options) {
// Remember options
current$1.value = value;
deepMerge(current$1.options, options || {});
if (!isAvailable$1()) {
return false;
}
// Remember favicons
if (!current$1.bestFavicon) {
const bestFavicon = getBestFavicon();
if (bestFavicon) {
const bestFaviconImage = document.createElement('img');
// Allow cross origin resource requests if the image is not a data:uri
if (!bestFavicon.href.match(/^data/)) {
bestFaviconImage.crossOrigin = 'anonymous';
}
// Load image
bestFaviconImage.src = bestFavicon.href;
// Store for next time
current$1.bestFavicon = bestFavicon;
current$1.bestFaviconImage = bestFaviconImage;
}
// Once the device pixel ratio changes we set the value again
devicePixelRatioListener.addEventListener('change', handleRatioChange);
}
if (!current$1.favicons) {
current$1.favicons = getFavicons();
}
// The image is required for setting the badge
if (!current$1.bestFaviconImage) {
return false;
}
// If we have the image, we can draw immediately
if (current$1.bestFaviconImage.complete) {
drawFavicon(current$1.bestFaviconImage, current$1.value, current$1.options);
return true;
}
// Otherwise we will wait for the load event
current$1.bestFaviconImage.addEventListener('load', function () {
drawFavicon(this, current$1.value, current$1.options);
});
return true;
}
function clear$1() {
if (!isAvailable$1()) {
return;
}
// Reset value and options
current$1.value = DefaultValue;
current$1.options = DefaultOptions;
// Remove old listener
devicePixelRatioListener.removeEventListener('change', handleRatioChange);
if (current$1.favicons) {
// Remove current favicons
for (const favicon of getFavicons()) {
if (favicon.parentNode) {
favicon.parentNode.removeChild(favicon);
}
}
// Recreate old favicons
for (const favicon of current$1.favicons) {
document.head.appendChild(favicon);
}
current$1.favicons = null;
current$1.bestFavicon = null;
current$1.bestFaviconImage = null;
}
}
const defaultOptions = {
indicator: '!',
};
const current$2 = {
title: null,
value: 0,
options: defaultOptions,
};
function changeTitle(title, value, options) {
let newTitle = title;
if (isPositiveNumber(value)) {
if (value === 0) {
newTitle = title;
}
else {
newTitle = `(${value}) ${title}`;
}
}
else {
newTitle = `(${options.indicator}) ${title}`;
}
const element = document.querySelector('title');
if (element) {
element.childNodes[0].nodeValue = newTitle;
}
}
function set$2(value, options) {
if (current$2.title === null) {
current$2.title = document.title;
// Watch changes of title
Object.defineProperty(document, 'title', {
get: () => {
return current$2.title;
},
set: title => {
current$2.title = title;
changeTitle(current$2.title, current$2.value, current$2.options);
},
});
}
// Remember value and options
current$2.value = value;
deepMerge(current$2.options, options || {});
// Trigger change
document.title = document.title;
return true;
}
function clear$2() {
current$2.value = 0;
// Trigger change
document.title = document.title;
}
/**
* Sets badge
*/
function set$3(value, options = {}) {
switch (options.method) {
case undefined:
case 'Badging': {
if (set(value)) {
// Break only if method is explicitly requested
if (options.method === 'Badging') {
break;
}
}
}
case 'Favicon': {
if (set$1(value, options.favicon)) {
break;
}
}
default: {
set$2(value, options.title);
}
}
}
/**
* Clears badge
*/
function clear$3() {
clear();
clear$1();
clear$2();
}
export { clear$3 as clear, set$3 as set };

View File

@@ -0,0 +1 @@
var badgin=function(e){"use strict";let t=!1;const n={mediaQuery:null,value:0};function i(){return"ExperimentalBadge"in window&&!!window.ExperimentalBadge}function a(){return"setExperimentalAppBadge"in navigator&&!!navigator.setExperimentalAppBadge&&"clearExperimentalAppBadge"in navigator&&!!navigator.clearExperimentalAppBadge}function o(){return n.mediaQuery||(n.mediaQuery=window.matchMedia("(display-mode: standalone)"),n.mediaQuery.onchange=e=>{r(n.value)}),n.mediaQuery.matches&&(i()||a())}function r(e){return n.value=e,o()?i()?(window.ExperimentalBadge.set(e),!0):!!a()&&(navigator.setExperimentalAppBadge(e),!0):(t||"ExperimentalBadge"in window||"setExperimentalAppBadge"in navigator||(console.warn("Badging API must be enabled. Please check here how you can enable it: https://developers.google.com/web/updates/2018/12/badging-api#use"),t=!0),!1)}function l(e,t){for(const n of Object.keys(t))t[n]instanceof Object&&Object.assign(t[n],l(e[n],t[n]));return Object.assign(e||{},t),e}function c(e){return void 0!==e&&Number.isInteger(e)&&e>=0}const d={backgroundColor:"#424242",color:"#ffffff",indicator:"!",radius:3,size:7,horizontalMargin:0,verticalMargin:0,horizontalPadding:1,verticalPadding:1},s=navigator.userAgent.indexOf("Firefox")>-1,u=()=>{const e=document.head.getElementsByTagName("link"),t=[];for(let n=0;n<e.length;n++){const i=e[n],a=i.getAttribute("href"),o=i.getAttribute("rel");a&&(o&&-1!==o.split(" ").indexOf("icon")&&t.push(i))}return t},g=()=>{const e=u();let t=null,n=0;for(let i=0;i<e.length;i++){const a=e[i],o=a.getAttribute("href"),r=a.getAttribute("sizes");if(null==o?void 0:o.endsWith(".svg"))return a;if(!r){t||(t=a,n=0);continue}if("any"===r)return a;const l=parseInt(r.split("x")[0],10);Number.isNaN(l)?t||(t=a,n=0):l>n&&(t=a,n=l)}return t},v={favicons:null,bestFavicon:null,bestFaviconImage:null,value:0,options:d},f=window.matchMedia("screen and (min-resolution: 2dppx)"),m=()=>Math.ceil(window.devicePixelRatio)||1,p=()=>{E(v.value,v.options)},h=()=>16*m(),b=(e,t,n)=>{const i=h(),a=document.createElement("canvas");a.width=i,a.height=i;const o=a.getContext("2d");o&&(e.width=i,e.height=i,o.drawImage(e,0,0,e.width,e.height),w(o,t,n),(e=>{if(!e)return;for(const e of u())e.parentNode&&e.parentNode.removeChild(e);const t=document.createElement("link");t.id="badgin",t.type="image/x-icon",t.rel="icon favicon",t.href=e,document.getElementsByTagName("head")[0].appendChild(t)})(a.toDataURL()))},w=(e,t,n)=>{const i=m(),a=h();let o="";if(o=c(t)?0===t?"":t<100?String(t):"99+":n.indicator,!o)return;const r=n.size-2,l=n.size*i+"px Arial";e.font=l;const{width:d}=e.measureText(o);e.restore();const u=d+2*n.horizontalPadding,g=a-(r*i+2*n.verticalPadding)-n.verticalMargin,v=a-u-n.horizontalMargin,f=16*i-n.verticalMargin,p=16*i-n.horizontalMargin,b=n.radius;e.globalAlpha=1,e.fillStyle=n.backgroundColor,e.strokeStyle=n.backgroundColor,e.lineWidth=0,e.beginPath(),e.moveTo(v+b,g),e.quadraticCurveTo(v,g,v,g+b),e.lineTo(v,f-b),e.quadraticCurveTo(v,f,v+b,f),e.lineTo(p-b,f),e.quadraticCurveTo(p,f,p,f-b),e.lineTo(p,g+b),e.quadraticCurveTo(p,g,p-b,g),e.closePath(),e.fill(),e.save(),e.font=l,e.fillStyle=n.color,e.textAlign="center",e.textBaseline="hanging",e.fillText(o,v+u/2,g+n.verticalPadding+(s?1:0)),e.save()};function x(){return!!g()}function E(e,t){if(v.value=e,l(v.options,t||{}),!x())return!1;if(!v.bestFavicon){const e=g();if(e){const t=document.createElement("img");e.href.match(/^data/)||(t.crossOrigin="anonymous"),t.src=e.href,v.bestFavicon=e,v.bestFaviconImage=t}f.addEventListener("change",p)}return v.favicons||(v.favicons=u()),!!v.bestFaviconImage&&(v.bestFaviconImage.complete?(b(v.bestFaviconImage,v.value,v.options),!0):(v.bestFaviconImage.addEventListener("load",(function(){b(this,v.value,v.options)})),!0))}const y={title:null,value:0,options:{indicator:"!"}};function B(e,t){return null===y.title&&(y.title=document.title,Object.defineProperty(document,"title",{get:()=>y.title,set:e=>{y.title=e,function(e,t,n){let i=e;i=c(t)?0===t?e:`(${t}) ${e}`:`(${n.indicator}) ${e}`;const a=document.querySelector("title");a&&(a.childNodes[0].nodeValue=i)}(y.title,y.value,y.options)}})),y.value=e,l(y.options,t||{}),document.title=document.title,!0}return e.clear=function(){o()&&(i()?window.ExperimentalBadge.clear():a()&&navigator.clearExperimentalAppBadge()),function(){if(x()&&(v.value=0,v.options=d,f.removeEventListener("change",p),v.favicons)){for(const e of u())e.parentNode&&e.parentNode.removeChild(e);for(const e of v.favicons)document.head.appendChild(e);v.favicons=null,v.bestFavicon=null,v.bestFaviconImage=null}}(),y.value=0,document.title=document.title},e.set=function(e,t={}){switch(t.method){case void 0:case"Badging":if(r(e)&&"Badging"===t.method)break;case"Favicon":if(E(e,t.favicon))break;default:B(e,t.title)}},e}({});

View File

@@ -0,0 +1,450 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
let warnedBefore = false;
const warn = () => {
if (warnedBefore) {
return;
}
// We will only warn the user if the Badging API is not available at all
if ('ExperimentalBadge' in window || 'setExperimentalAppBadge' in navigator) {
return;
}
console.warn('Badging API must be enabled. Please check here how you can enable it: https://developers.google.com/web/updates/2018/12/badging-api#use');
warnedBefore = true;
};
const current = {
mediaQuery: null,
value: 0,
};
function isVersion1Available() {
return 'ExperimentalBadge' in window && !!window.ExperimentalBadge;
}
function isVersion2Available() {
return ('setExperimentalAppBadge' in navigator &&
!!navigator.setExperimentalAppBadge &&
'clearExperimentalAppBadge' in navigator &&
!!navigator.clearExperimentalAppBadge);
}
function isAvailable() {
if (!current.mediaQuery) {
current.mediaQuery = window.matchMedia('(display-mode: standalone)');
// Get notified once app is installed
current.mediaQuery.onchange = event => {
set(current.value);
};
}
return (current.mediaQuery.matches &&
(isVersion1Available() || isVersion2Available()));
}
function set(value) {
current.value = value;
if (!isAvailable()) {
warn();
return false;
}
// Sets the badge to contents (an integer), or to "flag" if contents is omitted. If contents is 0, clears the badge for the matching app(s).
// See details here: https://github.com/WICG/badging/blob/master/explainer.md#the-api
if (isVersion1Available()) {
window.ExperimentalBadge.set(value);
return true;
}
else if (isVersion2Available()) {
navigator.setExperimentalAppBadge(value);
return true;
}
return false;
}
function clear() {
if (!isAvailable()) {
return;
}
if (isVersion1Available()) {
window.ExperimentalBadge.clear();
}
else if (isVersion2Available()) {
navigator.clearExperimentalAppBadge();
}
}
function deepMerge(target, source) {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (source[key] instanceof Object)
Object.assign(source[key], deepMerge(target[key], source[key]));
}
// Join `target` and modified `source`
Object.assign(target || {}, source);
return target;
}
function isPositiveNumber(value) {
return (typeof value !== 'undefined' && Number.isInteger(value) && value >= 0);
}
const DefaultValue = 0;
const DefaultOptions = {
backgroundColor: '#424242',
color: '#ffffff',
indicator: '!',
radius: 3,
size: 7,
horizontalMargin: 0,
verticalMargin: 0,
horizontalPadding: 1,
verticalPadding: 1,
};
const isFirefox = navigator.userAgent.indexOf('Firefox') > -1;
// Get all favicons of the page
const getFavicons = () => {
const links = document.head.getElementsByTagName('link');
const favicons = [];
for (let i = 0; i < links.length; i++) {
const link = links[i];
const href = link.getAttribute('href');
const rel = link.getAttribute('rel');
if (!href) {
continue;
}
if (!rel) {
continue;
}
if (rel.split(' ').indexOf('icon') === -1) {
continue;
}
favicons.push(link);
}
return favicons;
};
// Get the favicon with the best quality of the document
const getBestFavicon = () => {
const favicons = getFavicons();
let bestFavicon = null;
let bestSize = 0;
for (let i = 0; i < favicons.length; i++) {
const favicon = favicons[i];
const href = favicon.getAttribute('href');
const sizes = favicon.getAttribute('sizes');
// If the href looks like it's an SVG, it's the best we can get
if (href === null || href === void 0 ? void 0 : href.endsWith('.svg')) {
return favicon;
}
// If the link does not have a "sizes" attribute, we use it only if we haven't found anything else yet
if (!sizes) {
if (!bestFavicon) {
bestFavicon = favicon;
bestSize = 0;
}
continue;
}
// If we find an icon with sizes "any", it's the best we can get
if (sizes === 'any') {
return favicon;
}
// Otherwise we will try to find the maximum size
const size = parseInt(sizes.split('x')[0], 10);
if (Number.isNaN(size)) {
if (!bestFavicon) {
bestFavicon = favicon;
bestSize = 0;
}
continue;
}
if (size > bestSize) {
bestFavicon = favicon;
bestSize = size;
continue;
}
}
return bestFavicon;
};
// References to the favicons that we need to track in order to reset and update the counters
const current$1 = {
favicons: null,
bestFavicon: null,
bestFaviconImage: null,
value: DefaultValue,
options: DefaultOptions,
};
// Get size depending on screen density
const devicePixelRatioListener = window.matchMedia('screen and (min-resolution: 2dppx)');
const getRatio = () => {
return Math.ceil(window.devicePixelRatio) || 1;
};
const handleRatioChange = () => {
set$1(current$1.value, current$1.options);
};
const getIconSize = () => {
return 16 * getRatio();
};
// Update favicon
const setFavicon = (url) => {
if (!url) {
return;
}
// Remove previous favicons
for (const favicon of getFavicons()) {
if (favicon.parentNode) {
favicon.parentNode.removeChild(favicon);
}
}
// Create new favicon
const newFavicon = document.createElement('link');
newFavicon.id = 'badgin';
newFavicon.type = 'image/x-icon';
newFavicon.rel = 'icon favicon';
newFavicon.href = url;
document.getElementsByTagName('head')[0].appendChild(newFavicon);
};
// Draw the favicon
const drawFavicon = (image, value, options) => {
const iconSize = getIconSize();
const canvas = document.createElement('canvas');
canvas.width = iconSize;
canvas.height = iconSize;
const context = canvas.getContext('2d');
if (!context) {
return;
}
// Draw new image
image.width = iconSize;
image.height = iconSize;
context.drawImage(image, 0, 0, image.width, image.height);
// Draw bubble on the top
drawBubble(context, value, options);
// Refresh tag in page
setFavicon(canvas.toDataURL());
};
// Draws the bubble on the canvas
const drawBubble = (context, value, options) => {
const ratio = getRatio();
const iconSize = getIconSize();
// Do we need to render the bubble at all?
let finalValue = '';
if (isPositiveNumber(value)) {
if (value === 0) {
finalValue = '';
}
else if (value < 100) {
finalValue = String(value);
}
else {
finalValue = '99+';
}
}
else {
finalValue = options.indicator;
}
// Return early
if (!finalValue) {
return;
}
// Calculate text width initially
const textHeight = options.size - 2;
const font = `${options.size * ratio}px Arial`;
context.font = font;
const { width: textWidth } = context.measureText(finalValue);
context.restore();
// Calculate position etc.
const width = textWidth + 2 * options.horizontalPadding;
const height = textHeight * ratio + 2 * options.verticalPadding;
const top = iconSize - height - options.verticalMargin;
const left = iconSize - width - options.horizontalMargin;
const bottom = 16 * ratio - options.verticalMargin;
const right = 16 * ratio - options.horizontalMargin;
const radius = options.radius;
// Bubble
context.globalAlpha = 1;
context.fillStyle = options.backgroundColor;
context.strokeStyle = options.backgroundColor;
context.lineWidth = 0;
context.beginPath();
context.moveTo(left + radius, top);
context.quadraticCurveTo(left, top, left, top + radius);
context.lineTo(left, bottom - radius);
context.quadraticCurveTo(left, bottom, left + radius, bottom);
context.lineTo(right - radius, bottom);
context.quadraticCurveTo(right, bottom, right, bottom - radius);
context.lineTo(right, top + radius);
context.quadraticCurveTo(right, top, right - radius, top);
context.closePath();
context.fill();
context.save();
// Value
context.font = font;
context.fillStyle = options.color;
context.textAlign = 'center';
context.textBaseline = 'hanging';
context.fillText(finalValue, left + width / 2, top + options.verticalPadding + (isFirefox ? 1 : 0));
context.save();
/*
// Helper line
context.restore()
context.strokeStyle = '#ff0000'
context.moveTo(0, top + height / 2)
context.lineTo(iconSize, top + height / 2)
context.stroke()
context.save()
*/
};
function isAvailable$1() {
return !!getBestFavicon();
}
function set$1(value, options) {
// Remember options
current$1.value = value;
deepMerge(current$1.options, options || {});
if (!isAvailable$1()) {
return false;
}
// Remember favicons
if (!current$1.bestFavicon) {
const bestFavicon = getBestFavicon();
if (bestFavicon) {
const bestFaviconImage = document.createElement('img');
// Allow cross origin resource requests if the image is not a data:uri
if (!bestFavicon.href.match(/^data/)) {
bestFaviconImage.crossOrigin = 'anonymous';
}
// Load image
bestFaviconImage.src = bestFavicon.href;
// Store for next time
current$1.bestFavicon = bestFavicon;
current$1.bestFaviconImage = bestFaviconImage;
}
// Once the device pixel ratio changes we set the value again
devicePixelRatioListener.addEventListener('change', handleRatioChange);
}
if (!current$1.favicons) {
current$1.favicons = getFavicons();
}
// The image is required for setting the badge
if (!current$1.bestFaviconImage) {
return false;
}
// If we have the image, we can draw immediately
if (current$1.bestFaviconImage.complete) {
drawFavicon(current$1.bestFaviconImage, current$1.value, current$1.options);
return true;
}
// Otherwise we will wait for the load event
current$1.bestFaviconImage.addEventListener('load', function () {
drawFavicon(this, current$1.value, current$1.options);
});
return true;
}
function clear$1() {
if (!isAvailable$1()) {
return;
}
// Reset value and options
current$1.value = DefaultValue;
current$1.options = DefaultOptions;
// Remove old listener
devicePixelRatioListener.removeEventListener('change', handleRatioChange);
if (current$1.favicons) {
// Remove current favicons
for (const favicon of getFavicons()) {
if (favicon.parentNode) {
favicon.parentNode.removeChild(favicon);
}
}
// Recreate old favicons
for (const favicon of current$1.favicons) {
document.head.appendChild(favicon);
}
current$1.favicons = null;
current$1.bestFavicon = null;
current$1.bestFaviconImage = null;
}
}
const defaultOptions = {
indicator: '!',
};
const current$2 = {
title: null,
value: 0,
options: defaultOptions,
};
function changeTitle(title, value, options) {
let newTitle = title;
if (isPositiveNumber(value)) {
if (value === 0) {
newTitle = title;
}
else {
newTitle = `(${value}) ${title}`;
}
}
else {
newTitle = `(${options.indicator}) ${title}`;
}
const element = document.querySelector('title');
if (element) {
element.childNodes[0].nodeValue = newTitle;
}
}
function set$2(value, options) {
if (current$2.title === null) {
current$2.title = document.title;
// Watch changes of title
Object.defineProperty(document, 'title', {
get: () => {
return current$2.title;
},
set: title => {
current$2.title = title;
changeTitle(current$2.title, current$2.value, current$2.options);
},
});
}
// Remember value and options
current$2.value = value;
deepMerge(current$2.options, options || {});
// Trigger change
document.title = document.title;
return true;
}
function clear$2() {
current$2.value = 0;
// Trigger change
document.title = document.title;
}
/**
* Sets badge
*/
function set$3(value, options = {}) {
switch (options.method) {
case undefined:
case 'Badging': {
if (set(value)) {
// Break only if method is explicitly requested
if (options.method === 'Badging') {
break;
}
}
}
case 'Favicon': {
if (set$1(value, options.favicon)) {
break;
}
}
default: {
set$2(value, options.title);
}
}
}
/**
* Clears badge
*/
function clear$3() {
clear();
clear$1();
clear$2();
}
exports.clear = clear$3;
exports.set = set$3;

View File

@@ -0,0 +1,3 @@
import { Value } from './index';
export default function isPositiveNumber(value: Value): value is number;
//# sourceMappingURL=isPositiveNumber.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"isPositiveNumber.d.ts","sourceRoot":"","sources":["src/isPositiveNumber.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,MAAM,CAItE"}

View File

@@ -0,0 +1,11 @@
import { Value } from './index';
export declare type Options = {
indicator: string;
};
declare type Title = string | null;
export declare const defaultOptions: Options;
export declare function changeTitle(title: Title, value: Value, options: Options): void;
export declare function set(value: Value, options?: Partial<Options>): boolean;
export declare function clear(): void;
export {};
//# sourceMappingURL=title.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"title.d.ts","sourceRoot":"","sources":["src/title.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAG/B,oBAAY,OAAO,GAAG;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,aAAK,KAAK,GAAG,MAAM,GAAG,IAAI,CAAA;AAE1B,eAAO,MAAM,cAAc,EAAE,OAE5B,CAAA;AAQD,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,QAiBvE;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,WAwB3D;AAED,wBAAgB,KAAK,SAKpB"}