├── images ├── TPOH_128.png ├── TPOH_32.png ├── TPOH_64.png └── TPOH_96.png ├── Badges └── Get_Addon_Badge_Firefox.png ├── tpoh_content_script.js ├── manifest.json ├── LICENSE ├── README.md ├── CSS Theme └── userChrome.css ├── .gitignore └── background.js /images/TPOH_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Tab-Preview-On-Hover/HEAD/images/TPOH_128.png -------------------------------------------------------------------------------- /images/TPOH_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Tab-Preview-On-Hover/HEAD/images/TPOH_32.png -------------------------------------------------------------------------------- /images/TPOH_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Tab-Preview-On-Hover/HEAD/images/TPOH_64.png -------------------------------------------------------------------------------- /images/TPOH_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Tab-Preview-On-Hover/HEAD/images/TPOH_96.png -------------------------------------------------------------------------------- /Badges/Get_Addon_Badge_Firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Tab-Preview-On-Hover/HEAD/Badges/Get_Addon_Badge_Firefox.png -------------------------------------------------------------------------------- /tpoh_content_script.js: -------------------------------------------------------------------------------- 1 | document.body.addEventListener("mouseleave", () => { 2 | browser.runtime.sendMessage("TPOH_ON"); 3 | }); 4 | 5 | document.body.addEventListener("mouseenter", () => { 6 | browser.runtime.sendMessage("TPOH_OFF"); 7 | }); 8 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Tab Preview On Hover", 4 | "version": "1.1", 5 | "description": "Adds on-hover tab preview to Firefox (requires CSS theme).", 6 | "author": "Eason Wong", 7 | "browser_specific_settings": { 8 | "gecko": { 9 | "id": "OHTP@EasonWong" 10 | } 11 | }, 12 | "permissions": ["tabs", "theme", ""], 13 | "background": { 14 | "scripts": ["background.js"], 15 | "persistent": true 16 | }, 17 | "content_scripts": [ 18 | { 19 | "matches": [""], 20 | "js": ["tpoh_content_script.js"] 21 | } 22 | ], 23 | "icons": { 24 | "32": "images/TPOH_32.png", 25 | "64": "images/TPOH_64.png", 26 | "96": "images/TPOH_96.png", 27 | "128": "images/TPOH_128.png" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eason Wong 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 | ![Icon](https://github.com/easonwong-de/Tab-Preview-On-Hover/blob/main/images/TPOH_128.png) 2 | ![Mozilla Add-on Users](https://img.shields.io/amo/users/tab-preview-on-hover) 3 | ![Mozilla Add-on Rating](https://img.shields.io/amo/stars/tab-preview-on-hover) 4 | ![Mozilla Add-on](https://img.shields.io/amo/v/OHTP@EasonWong?color=blue&label=version) 5 | 6 | # Tab Preview On Hover (Discontinued) 7 | 8 | This add-on adds on-hover tab preview to Firefox (requires CSS theme). 9 | 10 | **Note:** This add-on is now discontinued as Mozilla has since integrated tab preview functionality directly into Firefox. 11 | 12 | 13 | 14 | 15 | 16 | ## What Does the Add-on & CSS Theme Do? 17 | 18 | With the add-on and the [supporting CSS theme](https://raw.githubusercontent.com/easonwong-de/Tab-Preview-On-Hover/main/CSS%20Theme/userChrome.css), a tab preview will appear when a tab is hovered over. 19 | 20 | 21 | 22 | There will be a short delay before the tab preview appears. If the cursor moves from one tab to another, there will be no delay. However, if the cursor leaves the tab area for a brief moment and then returns, the delay will reappear. This behaviour can be controlled via the CSS theme. Users can modify the following variables to adjust the timing: 23 | 24 | - `--preview-delay` 25 | - `--preview-delay-tolerance` 26 | 27 | ## How to Install the CSS Theme 28 | 29 | 1. Go to `about:support` and locate the `Profile Folder`. 30 | 2. Download the [CSS theme](https://raw.githubusercontent.com/easonwong-de/Tab-Preview-On-Hover/main/CSS%20Theme/userChrome.css) and move it into `Profile Folder -> chrome`. 31 | 3. Go to `about:config` and set `toolkit.legacyUserProfileCustomizations.stylesheets` to `true`. 32 | 4. Restart Firefox. 33 | 34 | ## Compromises 35 | 36 | 1. The background image for the browser navbar will be disabled. 37 | 2. Incompatible with the built-in browser themes “System theme – auto” and “Firefox Alpenglow”. 38 | 3. Only the first ten tabs on the left will have tab previews. 39 | 4. [Adaptive Tab Bar Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) may cause the tab preview to disappear at times, as it resets the browser theme whenever it changes the tab bar colour. 40 | -------------------------------------------------------------------------------- /CSS Theme/userChrome.css: -------------------------------------------------------------------------------- 1 | /* Tab Preview On Hover supporting CSS theme */ 2 | 3 | /* Move this file to profile folder > chrome, or append to existing userChrome.css */ 4 | 5 | /* Use with Tab Preview On Hover add-on: https://addons.mozilla.org/firefox/addon/tab-preview-on-hover/ */ 6 | 7 | /* Version 2024-03-01 */ 8 | 9 | #navigator-toolbox:-moz-lwtheme, 10 | .browserContainer > findbar:-moz-lwtheme { 11 | background-image: none !important; 12 | } 13 | 14 | #navigator-toolbox { 15 | z-index: 10 !important; 16 | } 17 | 18 | #tabbrowser-tab-tooltip { 19 | display: none !important; 20 | } 21 | 22 | .tabbrowser-tab::after { 23 | --preview-delay: 1s; /* Delay of tab preview when the cursor move onto a tab from outside of tab bar */ 24 | --preview-delay-tolerance: 0.75s; /* Max time period of cursor not hovering on tab bar before preview delay re-engages */ 25 | --preview-width: 200px; /* Width of the preview panel */ 26 | --preview-height: 150px; /* Height of the preview panel */ 27 | } 28 | 29 | .tabbrowser-tab::after { 30 | box-shadow: 0 0 50px black, 0 0 0 1px var(--arrowpanel-border-color) inset; 31 | outline: 0.5px solid black; 32 | pointer-events: none; 33 | content: ""; 34 | display: none; 35 | position: absolute; 36 | z-index: 10000; 37 | top: 50px /* Edit if the preview panel is misplaced */; 38 | width: var(--preview-width); 39 | height: var(--preview-height); 40 | background-color: var(--lwt-accent-color); 41 | background-image: var(--lwt-additional-images); 42 | background-repeat: no-repeat; 43 | background-clip: padding-box; 44 | border-radius: 8px; 45 | image-rendering: optimizequality; 46 | } 47 | 48 | #tabbrowser-arrowscrollbox:not(:hover) .tabbrowser-tab::after { 49 | transition: opacity 0.5s, filter 0s var(--preview-delay-tolerance); 50 | filter: opacity(0); 51 | } 52 | 53 | #tabbrowser-arrowscrollbox:hover .tabbrowser-tab::after { 54 | transition: opacity 0.5s, filter 0.5s var(--preview-delay); 55 | filter: opacity(1); 56 | } 57 | 58 | .tabbrowser-tab:not(:hover)::after { 59 | opacity: 0; 60 | } 61 | 62 | .tabbrowser-tab:hover::after { 63 | opacity: 1; 64 | } 65 | 66 | .tabbrowser-tab[selected]:hover::after { 67 | opacity: 0; 68 | transition-duration: 0.25s !important; 69 | } 70 | 71 | .tabbrowser-tab:nth-of-type(1)::after { 72 | display: block; 73 | background-size: cover, 0, 0, 0, 0, 0, 0, 0, 0, 0; 74 | } 75 | 76 | .tabbrowser-tab:nth-of-type(2)::after { 77 | display: block; 78 | background-size: 0, cover, 0, 0, 0, 0, 0, 0, 0, 0; 79 | } 80 | 81 | .tabbrowser-tab:nth-of-type(3)::after { 82 | display: block; 83 | background-size: 0, 0, cover, 0, 0, 0, 0, 0, 0, 0; 84 | } 85 | 86 | .tabbrowser-tab:nth-of-type(4)::after { 87 | display: block; 88 | background-size: 0, 0, 0, cover, 0, 0, 0, 0, 0, 0; 89 | } 90 | 91 | .tabbrowser-tab:nth-of-type(5)::after { 92 | display: block; 93 | background-size: 0, 0, 0, 0, cover, 0, 0, 0, 0, 0; 94 | } 95 | 96 | .tabbrowser-tab:nth-of-type(6)::after { 97 | display: block; 98 | background-size: 0, 0, 0, 0, 0, cover, 0, 0, 0, 0; 99 | } 100 | 101 | .tabbrowser-tab:nth-of-type(7)::after { 102 | display: block; 103 | background-size: 0, 0, 0, 0, 0, 0, cover, 0, 0, 0; 104 | } 105 | 106 | .tabbrowser-tab:nth-of-type(8)::after { 107 | display: block; 108 | background-size: 0, 0, 0, 0, 0, 0, 0, cover, 0, 0; 109 | } 110 | 111 | .tabbrowser-tab:nth-of-type(9)::after { 112 | display: block; 113 | background-size: 0, 0, 0, 0, 0, 0, 0, 0, cover, 0; 114 | } 115 | 116 | .tabbrowser-tab:nth-of-type(10)::after { 117 | display: block; 118 | background-size: 0, 0, 0, 0, 0, 0, 0, 0, 0, cover; 119 | } 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,node,macos,linux 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,node,macos,linux 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Node ### 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* 57 | .pnpm-debug.log* 58 | 59 | # Diagnostic reports (https://nodejs.org/api/report.html) 60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | *.lcov 74 | 75 | # nyc test coverage 76 | .nyc_output 77 | 78 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # Bower dependency directory (https://bower.io/) 82 | bower_components 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (https://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules/ 92 | jspm_packages/ 93 | 94 | # Snowpack dependency directory (https://snowpack.dev/) 95 | web_modules/ 96 | 97 | # TypeScript cache 98 | *.tsbuildinfo 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Optional stylelint cache 107 | .stylelintcache 108 | 109 | # Microbundle cache 110 | .rpt2_cache/ 111 | .rts2_cache_cjs/ 112 | .rts2_cache_es/ 113 | .rts2_cache_umd/ 114 | 115 | # Optional REPL history 116 | .node_repl_history 117 | 118 | # Output of 'npm pack' 119 | *.tgz 120 | 121 | # Yarn Integrity file 122 | .yarn-integrity 123 | 124 | # dotenv environment variable files 125 | .env 126 | .env.development.local 127 | .env.test.local 128 | .env.production.local 129 | .env.local 130 | 131 | # parcel-bundler cache (https://parceljs.org/) 132 | .cache 133 | .parcel-cache 134 | 135 | # Next.js build output 136 | .next 137 | out 138 | 139 | # Nuxt.js build / generate output 140 | .nuxt 141 | dist 142 | 143 | # Gatsby files 144 | .cache/ 145 | # Comment in the public line in if your project uses Gatsby and not Next.js 146 | # https://nextjs.org/blog/next-9-1#public-directory-support 147 | # public 148 | 149 | # vuepress build output 150 | .vuepress/dist 151 | 152 | # vuepress v2.x temp and cache directory 153 | .temp 154 | 155 | # Docusaurus cache and generated files 156 | .docusaurus 157 | 158 | # Serverless directories 159 | .serverless/ 160 | 161 | # FuseBox cache 162 | .fusebox/ 163 | 164 | # DynamoDB Local files 165 | .dynamodb/ 166 | 167 | # TernJS port file 168 | .tern-port 169 | 170 | # Stores VSCode versions used for testing VSCode extensions 171 | .vscode-test 172 | 173 | # yarn v2 174 | .yarn/cache 175 | .yarn/unplugged 176 | .yarn/build-state.yml 177 | .yarn/install-state.gz 178 | .pnp.* 179 | 180 | ### Node Patch ### 181 | # Serverless Webpack directories 182 | .webpack/ 183 | 184 | # Optional stylelint cache 185 | 186 | # SvelteKit build / generate output 187 | .svelte-kit 188 | 189 | ### VisualStudioCode ### 190 | .vscode/* 191 | !.vscode/settings.json 192 | !.vscode/tasks.json 193 | !.vscode/launch.json 194 | !.vscode/extensions.json 195 | !.vscode/*.code-snippets 196 | 197 | # Local History for Visual Studio Code 198 | .history/ 199 | 200 | # Built Visual Studio Code Extensions 201 | *.vsix 202 | 203 | ### VisualStudioCode Patch ### 204 | # Ignore all local history of files 205 | .history 206 | .ionide 207 | 208 | # Support for Project snippet scope 209 | 210 | ### Windows ### 211 | # Windows thumbnail cache files 212 | Thumbs.db 213 | Thumbs.db:encryptable 214 | ehthumbs.db 215 | ehthumbs_vista.db 216 | 217 | # Dump file 218 | *.stackdump 219 | 220 | # Folder config file 221 | [Dd]esktop.ini 222 | 223 | # Recycle Bin used on file shares 224 | $RECYCLE.BIN/ 225 | 226 | # Windows Installer files 227 | *.cab 228 | *.msi 229 | *.msix 230 | *.msm 231 | *.msp 232 | 233 | # Windows shortcuts 234 | *.lnk 235 | 236 | # End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,node,macos,linux 237 | 238 | ### Customs ### 239 | # Backup folders 240 | Backup 241 | Pictures -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const screenshotSettings = { format: "jpeg", quality: 75, scale: 0.5 }; 2 | const canvasWidth = 400; 3 | const canvasHeight = 300; 4 | const tabPreviewWidth = canvasWidth; 5 | const tabPreviewHeight = 200; 6 | const textMargin = 20; 7 | const textSize = 25; 8 | const textSizeSmall = 20; 9 | 10 | const update_debounce = addDebounce(update); 11 | 12 | var updateToggle = true; 13 | var updateInterval = setInterval(update, 2500); 14 | browser.runtime.onMessage.addListener(onMessage); 15 | browser.runtime.onMessageExternal.addListener(onMessage); 16 | 17 | function onMessage(message) { 18 | switch (message) { 19 | case "TPOH_ON": 20 | clearInterval(updateInterval); 21 | updateToggle = true; 22 | update_debounce(); 23 | updateInterval = setInterval(update, 2500); 24 | break; 25 | case "TPOH_OFF": 26 | clearInterval(updateInterval); 27 | updateToggle = false; 28 | break; 29 | case "TPOH_UPDATE": 30 | /* if (updateToggle) update_debounce(); */ 31 | break; 32 | default: 33 | break; 34 | } 35 | } 36 | 37 | var debouncePrevRun = 0; 38 | var debounceTimeout = null; 39 | 40 | /** 41 | * Runs the given function with a maximum rate of 250ms. 42 | * @param {function} fn Fuction without debounce. 43 | * @returns Function with debounce. 44 | * @author cloone8 on GitHub. 45 | */ 46 | function addDebounce(fn) { 47 | const timeoutMs = 250; 48 | return () => { 49 | const currentTime = Date.now(); 50 | if (debounceTimeout) { 51 | // Clear pending function 52 | clearTimeout(debounceTimeout); 53 | debounceTimeout = null; 54 | } 55 | if (currentTime - timeoutMs > debouncePrevRun) { 56 | // No timeout => call the function right away 57 | debouncePrevRun = currentTime; 58 | fn(); 59 | } else { 60 | // Blocked by timeout => delay the function call 61 | debounceTimeout = setTimeout(() => { 62 | debouncePrevRun = Date.now(); 63 | debounceTimeout = null; 64 | fn(); 65 | }, timeoutMs - (currentTime - debouncePrevRun)); 66 | } 67 | }; 68 | } 69 | 70 | function update() { 71 | browser.windows.getAll({ populate: true }).then((windows) => { 72 | windows.forEach((window) => { 73 | browser.theme.getCurrent(window.id).then((theme) => updateTheme(theme, window)); 74 | }); 75 | }); 76 | } 77 | 78 | function updateTheme(theme, window) { 79 | let tabs = window.tabs; 80 | let tabPreviews = []; 81 | let backgroundColor = theme.colors.frame; 82 | let textColor = theme.colors.toolbar_text; 83 | for (let i = 0; i < Math.min(10, tabs.length); i++) { 84 | let tab = tabs[i]; 85 | // Canvas configs 86 | let canvas = document.createElement("canvas"); 87 | let canvasContext = canvas.getContext("2d"); 88 | canvas.width = canvasWidth; 89 | canvas.height = canvasHeight; 90 | // Background 91 | canvasContext.fillStyle = backgroundColor; 92 | canvasContext.fillRect(0, 0, canvasWidth, canvasHeight); 93 | canvasContext.fillStyle = "#000000"; 94 | canvasContext.fillRect(0, 0, tabPreviewWidth, tabPreviewHeight); 95 | // Info box 96 | canvasContext.textAlign = "left"; 97 | canvasContext.textBaseline = "middle"; 98 | let textColorFade = canvasContext.createLinearGradient(textMargin, 0, canvasWidth - textMargin, 0); 99 | textColorFade.addColorStop(0.9, textColor); 100 | textColorFade.addColorStop(1, "transparent"); 101 | canvasContext.fillStyle = textColorFade; 102 | canvasContext.font = `normal normal 900 ${textSize}px apple-system, sans-serif, SF Pro, Arial`; 103 | canvasContext.fillText(tab.title, textMargin, tabPreviewHeight + (canvasHeight - tabPreviewHeight) / 2 - textSizeSmall / 2 - 5); 104 | canvasContext.font = `normal normal 100 ${textSizeSmall}px apple-system, sans-serif, SF Pro, Arial`; 105 | let hostname = new URL(tab.url).hostname; 106 | canvasContext.fillText(hostname || tab.url, textMargin, tabPreviewHeight + (canvasHeight - tabPreviewHeight) / 2 + textSize / 2 + 5); 107 | // Preview box 108 | let promise = new Promise((resolve) => { 109 | if (tab.status == "complete" && !tab.discarded) { 110 | let screenshot = new Image(); 111 | screenshot.onload = () => { 112 | let screenshotAspectRatio = screenshot.width / screenshot.height; 113 | let tabPreviewAspectRatio = tabPreviewWidth / tabPreviewHeight; 114 | if (screenshotAspectRatio > tabPreviewAspectRatio) 115 | canvasContext.drawImage( 116 | screenshot, 117 | (screenshot.width - screenshot.height * tabPreviewAspectRatio) / 2, 118 | 0, 119 | screenshot.height * tabPreviewAspectRatio, 120 | screenshot.height, 121 | 0, 122 | 0, 123 | tabPreviewWidth, 124 | tabPreviewHeight 125 | ); 126 | else 127 | canvasContext.drawImage( 128 | screenshot, 129 | 0, 130 | 0, 131 | screenshot.width, 132 | screenshot.width / tabPreviewAspectRatio, 133 | 0, 134 | 0, 135 | tabPreviewWidth, 136 | tabPreviewHeight 137 | ); 138 | resolve(canvas.toDataURL("image/jpeg")); 139 | canvas.remove(); 140 | }; 141 | browser.tabs.captureTab(tab.id, screenshotSettings).then((screenshotSource) => (screenshot.src = screenshotSource)); 142 | } else { 143 | // Placeholder text 144 | canvasContext.textAlign = "center"; 145 | canvasContext.textBaseline = "middle"; 146 | canvasContext.font = `50px apple-system, sans-serif, SF Pro, Arial`; 147 | canvasContext.fillStyle = textColor; 148 | canvasContext.fillText("Tab inactive", tabPreviewWidth / 2, tabPreviewHeight / 2); 149 | resolve(canvas.toDataURL("image/jpeg")); 150 | canvas.remove(); 151 | } 152 | }); 153 | tabPreviews.push(promise); 154 | } 155 | Promise.all(tabPreviews).then((tabPreviews) => { 156 | theme.images = { additional_backgrounds: tabPreviews }; 157 | browser.theme.update(window.id, theme); 158 | console.log(theme.colors.frame, Date.now()); 159 | }); 160 | } 161 | 162 | browser.runtime.onInstalled.addListener(update); 163 | --------------------------------------------------------------------------------