diff --git a/package.json b/package.json index 5359408..b15a9f8 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,13 @@ }, "devDependencies": { "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.2.3", "rollup": "^3.28.1", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-ts": "^3.4.4", "typescript": "^5.2.2" + }, + "dependencies": { + "home-assistant-query-selector": "^1.1.3" } } diff --git a/rollup.config.js b/rollup.config.js index eb4abad..1d240de 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,9 +1,11 @@ import ts from 'rollup-plugin-ts'; import json from '@rollup/plugin-json'; import { terser } from 'rollup-plugin-terser'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; export default { plugins: [ + nodeResolve(), json(), ts({ browserslist: false diff --git a/src/constants/index.ts b/src/constants/index.ts index 01db2c3..2e0bc2b 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,48 +1,15 @@ export const NAMESPACE = 'keep-texts-in-tabs'; -export const MAX_ATTEMPTS = 500; -export const RETRY_DELAY = 50; -export const SHADOW_ROOT_SUFFIX = ':shadowRoot'; export const ARIA_LABEL_ATTRIBUTE = 'aria-label'; export const DEFAULT_MOBILE_WIDTH = 640; export const WINDOW_RESIZE_DELAY = 100; export enum ELEMENT { - HOME_ASSISTANT = 'home-assistant', - HOME_ASSISTANT_MAIN = 'home-assistant-main', - PARTIAL_PANEL_RESOLVER = 'partial-panel-resolver', - HA_PANEL_LOVELACE = 'ha-panel-lovelace', - HUI_ROOT = 'hui-root', TOOLBAR = '.toolbar', HA_TABS = 'ha-tabs', PAPER_TABS = 'paper-tabs', PAPER_TAB = 'paper-tab', HUI_VIEW = 'hui-view', HA_ICON = 'ha-icon' - /* - MENU_ITEM = 'ha-icon-button', - MENU_ITEM_ICON = 'mwc-icon-button', - BUTTON_MENU = 'ha-button-menu', - OVERLAY_MENU_ITEM = 'mwc-list-item', - HA_SIDEBAR = 'ha-sidebar', - HA_DRAWER = 'ha-drawer', - - ACTION_ITEMS = '.action-items', - HA_MORE_INFO_DIALOG = 'ha-more-info-dialog', - HA_DIALOG = 'ha-dialog', - HA_DIALOG_HEADER = 'ha-dialog-header', - HA_DIALOG_CONTENT = '.content', - HA_DIALOG_MORE_INFO = 'ha-more-info-info', - HA_DIALOG_HISTORY = 'ha-more-info-history', - HA_DIALOG_LOGBOOK = 'ha-more-info-logbook', - HA_DIALOG_MORE_INFO_CONTENT = 'more-info-content', - HA_DIALOG_MORE_INFO_HISTORY_AND_LOGBOOK = 'ha-more-info-history-and-logbook', - HA_DIALOG_DEFAULT = 'more-info-default', - HA_DIALOG_TIMER = 'more-info-timer', - HA_DIALOG_VACUUM = 'more-info-vacuum', - HA_DIALOG_MEDIA_PLAYER = 'more-info-media_player', - HA_DIALOG_UPDATE = 'more-info-update', - HA_DIALOG_CLIMATE = 'more-info-climate', - HA_DIALOG_ATTRIBUTES = 'ha-attributes'*/ } export enum NODE_TYPE { diff --git a/src/keep-texts-in-tabs.ts b/src/keep-texts-in-tabs.ts index c981626..f38d905 100644 --- a/src/keep-texts-in-tabs.ts +++ b/src/keep-texts-in-tabs.ts @@ -1,3 +1,4 @@ +import { HAQuerySelector } from 'home-assistant-query-selector'; import { Lovelace, KeepTextsInTabsConfig, @@ -8,14 +9,12 @@ import { import { NAMESPACE, ELEMENT, - SHADOW_ROOT_SUFFIX, ARIA_LABEL_ATTRIBUTE, DEFAULT_MOBILE_WIDTH, NODE_TYPE, WINDOW_RESIZE_DELAY } from '@constants'; import { - getPromisableElement, getSpan, addStyle, logVersionToConsole @@ -24,103 +23,41 @@ import { class KeepTextsInTabs { constructor() { - this.resizeWindowBinded = this.resizeWindow.bind(this); - this.start(); + const selector = new HAQuerySelector(); + selector.addEventListener('onLovelacePanelLoad', async (event) => { + const { + HA_PANEL_LOVELACE, + HUI_ROOT, + HEADER + } = event.detail; + + this.lovelace = await HA_PANEL_LOVELACE.element as Lovelace; + this.huiRoot = await HUI_ROOT.shadowRootQuerySelector('$'); + this.appToolbar = await HEADER.querySelector(ELEMENT.TOOLBAR); + this.run(); + }); + selector.listen(); + this.resizeWindowBinded = this.resizeWindow.bind(this); + window.addEventListener('resize', this.resizeWindowBinded); } - private ha: HTMLElement; - private main: ShadowRoot; - private partialPanelResolver: HTMLElement; private lovelace: Lovelace; private huiRoot: ShadowRoot; - private appToolbar: HTMLElement; + private appToolbar: Element; - private panelResolverObserver: MutationObserver; - private lovelaceResolver: MutationObserver; private toolBarObserver: MutationObserver; private paperTabsEditionResolver: MutationObserver; private resizeDelay: number; private resizeWindowBinded: () => void; - protected async start() { - - this.ha = await getPromisableElement( - (): HTMLElement => document.querySelector(ELEMENT.HOME_ASSISTANT), - (ha: HTMLElement) => !!(ha && ha.shadowRoot), - ELEMENT.HOME_ASSISTANT - ); - - this.main = await getPromisableElement( - (): ShadowRoot => this.ha.shadowRoot.querySelector(ELEMENT.HOME_ASSISTANT_MAIN)?.shadowRoot, - (main: ShadowRoot) => !!main, - `${ELEMENT.HOME_ASSISTANT_MAIN}${SHADOW_ROOT_SUFFIX}` - ); - - this.partialPanelResolver = await getPromisableElement( - (): HTMLElement => this.main.querySelector(ELEMENT.PARTIAL_PANEL_RESOLVER), - (partialPanelResolver: HTMLElement) => !!partialPanelResolver, - `${ELEMENT.HOME_ASSISTANT_MAIN} > ${ELEMENT.PARTIAL_PANEL_RESOLVER}` - ); - - this.lovelace = await getPromisableElement( - (): Lovelace => this.main.querySelector(ELEMENT.HA_PANEL_LOVELACE), - (lovelace: Lovelace) => !!lovelace, - `${ELEMENT.HOME_ASSISTANT_MAIN} > ${ELEMENT.HA_PANEL_LOVELACE}` - ); - - if (this.panelResolverObserver) { - this.panelResolverObserver.disconnect(); - } - - this.panelResolverObserver = new MutationObserver(this.dashboardChanged.bind(this)); - - this.panelResolverObserver.observe(this.partialPanelResolver, { - childList: true, - }); - - window.removeEventListener('resize', this.resizeWindowBinded); - window.addEventListener('resize', this.resizeWindowBinded); - - this.run(); - - } - protected async run() { - - if (this.lovelaceResolver) { - this.lovelaceResolver.disconnect(); - } - - if (this.toolBarObserver) { - this.toolBarObserver.disconnect(); - } - - this.huiRoot = await getPromisableElement( - (): ShadowRoot => this.lovelace?.shadowRoot?.querySelector(ELEMENT.HUI_ROOT)?.shadowRoot, - (huiRoot: ShadowRoot) => !!huiRoot, - `${ELEMENT.HOME_ASSISTANT_MAIN} > ${ELEMENT.HA_PANEL_LOVELACE} > ${ELEMENT.HUI_ROOT}${SHADOW_ROOT_SUFFIX}` - ); - - this.appToolbar = await getPromisableElement( - (): HTMLElement => this.huiRoot.querySelector(ELEMENT.TOOLBAR), - (appToolbar: HTMLElement) => !!appToolbar, - `${ELEMENT.HOME_ASSISTANT_MAIN} > ${ELEMENT.HA_PANEL_LOVELACE} > ${ELEMENT.HUI_ROOT}${SHADOW_ROOT_SUFFIX} > ${ELEMENT.TOOLBAR}` - ); - - // Get the configuration and process it - const config = await getPromisableElement( - () => this.lovelace?.lovelace?.config, - (config: Lovelace['lovelace']['config']) => !!config, - 'Lovelace config' - ); + this.toolBarObserver?.disconnect(); addStyle(this.appToolbar); - this.lovelaceResolver = new MutationObserver(this.lovelaceChanged.bind(this)); - this.lovelaceResolver.observe(this.lovelace.shadowRoot, { - childList: true, - }); + // Get the configuration and process it + const config = this.lovelace.lovelace.config; this.toolBarObserver = new MutationObserver(this.process.bind(this, config.keep_texts_in_tabs)); this.toolBarObserver.observe(this.appToolbar, { @@ -133,9 +70,7 @@ class KeepTextsInTabs { protected process(config: KeepTextsInTabsConfig | undefined) { - if (this.paperTabsEditionResolver) { - this.paperTabsEditionResolver.disconnect(); - } + this.paperTabsEditionResolver?.disconnect(); if (!config) return; @@ -243,31 +178,10 @@ class KeepTextsInTabs { } } - protected dashboardChanged(mutations: MutationRecord[]) { - mutations.forEach(({ addedNodes }): void => { - addedNodes.forEach((node: Element): void => { - if (node.localName === ELEMENT.HA_PANEL_LOVELACE) { - this.lovelace = node as Lovelace; - this.run(); - } - }); - }); - } - - protected lovelaceChanged(mutations: MutationRecord[]) { - mutations.forEach(({ addedNodes }): void => { - addedNodes.forEach((node: Element): void => { - if (node.localName === ELEMENT.HUI_ROOT) { - this.run(); - } - }); - }); - } - protected resizeWindow() { window.clearTimeout(this.resizeDelay); this.resizeDelay = window.setTimeout(() => { - this.start(); + this.run(); }, WINDOW_RESIZE_DELAY); } diff --git a/src/utilities/index.ts b/src/utilities/index.ts index ead7d4b..56d3846 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,35 +1,7 @@ import { Position } from '@types'; -import { - NAMESPACE, - MAX_ATTEMPTS, - RETRY_DELAY -} from '@constants'; +import { NAMESPACE } from '@constants'; import { version } from '../../package.json'; -export const getPromisableElement = ( - getElement: () => T, - check: (element: T) => boolean, - elementName: string -): Promise => { - return new Promise((resolve, reject) => { - let attempts = 0; - const select = () => { - const element: T = getElement(); - if (element && check(element)) { - resolve(element); - } else { - attempts++; - if (attempts < MAX_ATTEMPTS) { - setTimeout(select, RETRY_DELAY); - } else { - reject(new Error(`${NAMESPACE}: Cannot select ${elementName} after ${MAX_ATTEMPTS} attempts. Giving up!`)); - } - } - }; - select(); - }); -}; - export const getSpan = (text: string, position: Position): HTMLSpanElement => { const textNode = document.createTextNode(text); const span = document.createElement('span'); @@ -52,11 +24,11 @@ const buildStyles = (): string => { `; }; -const styleExists = (elem: HTMLElement): HTMLStyleElement => { +const styleExists = (elem: Element): HTMLStyleElement => { return elem.querySelector(`#${NAMESPACE}`); }; -export const addStyle = (elem: HTMLElement): void => { +export const addStyle = (elem: Element): void => { let style = styleExists(elem); if (!style) { style = document.createElement('style'); @@ -66,7 +38,7 @@ export const addStyle = (elem: HTMLElement): void => { style.innerHTML = buildStyles(); }; -export const removeStyle = (element: HTMLElement): void => { +export const removeStyle = (element: Element): void => { if (styleExists(element)) { element.querySelector(`#${NAMESPACE}`).remove(); } diff --git a/yarn.lock b/yarn.lock index bfa2f2a..d704fdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -76,6 +76,18 @@ dependencies: "@rollup/pluginutils" "^5.0.1" +"@rollup/plugin-node-resolve@^15.2.3": + version "15.2.3" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" + integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-builtin-module "^3.2.1" + is-module "^1.0.0" + resolve "^1.22.1" + "@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.0.2": version "5.0.4" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.4.tgz#74f808f9053d33bafec0cc98e7b835c9667d32ba" @@ -105,6 +117,11 @@ resolved "https://registry.yarnpkg.com/@types/object-path/-/object-path-0.11.1.tgz#eea5b357518597fc9c0a067ea3147f599fc1514f" integrity sha512-219LSCO9HPcoXcRTC6DbCs0FRhZgBnEMzf16RRqkT40WbkKx3mOeQuz3e2XqbfhOz/AHfbru0kzB1n1RCAsIIg== +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + "@types/semver@^7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" @@ -168,6 +185,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001518: version "1.0.30001523" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001523.tgz#b838f70b1a98c556776b998fafb47d2b64146d4f" @@ -213,6 +235,11 @@ crosspath@^2.0.0: dependencies: "@types/node" "^17.0.36" +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + electron-to-chromium@^1.4.477: version "1.4.503" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.503.tgz#7bd43927ea9b4198697672d28d8fbd0da016a7a1" @@ -238,6 +265,11 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -248,11 +280,44 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + helpertypes@^0.0.19: version "0.0.19" resolved "https://registry.yarnpkg.com/helpertypes/-/helpertypes-0.0.19.tgz#6f8cb18e4e1fad73dc103b98e624ac85cb06a720" integrity sha512-J00e55zffgi3yVnUp0UdbMztNkr2PnizEkOe9URNohnrNhW5X0QpegkuLpOmFQInpi93Nb8MCjQRHAiCDF42NQ== +home-assistant-query-selector@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/home-assistant-query-selector/-/home-assistant-query-selector-1.1.3.tgz#4c32ac377c5de28fa65b238459ccfe9e3fa7f13f" + integrity sha512-igL6IYQCytFGctI9gmXaYiBZn9GCT0jJa2F5qxLSlZjVHTavkohcv5xrxUlsIvOei18lV9HH3D6o1OcxvTV3vQ== + dependencies: + shadow-dom-selector "^1.0.2" + +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + isbot@^3.6.13: version "3.6.13" resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.13.tgz#e060f727cb53475a4dab1569321ced0f84633d63" @@ -301,6 +366,11 @@ object-path@^0.11.8: resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.8.tgz#ed002c02bbdd0070b78a27455e8ae01fc14d4742" integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -318,6 +388,15 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + rollup-plugin-terser@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" @@ -370,6 +449,11 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" +shadow-dom-selector@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shadow-dom-selector/-/shadow-dom-selector-1.0.2.tgz#396700bacdf206eae444d845adf41042b362fe15" + integrity sha512-qezU/X48PFKlVecShKlOl7cZR0DjkXjRTxfFdU//o0VSP94KGwU3QdnBMqJzFbBbfpSf+xTL6llapSzhG1JxHw== + source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -397,6 +481,11 @@ supports-color@^7.0.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + terser@^5.0.0: version "5.19.2" resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e"