├── GeneratedSD.png ├── LICENSE ├── README.md ├── VivaldiSDgen.js ├── icon128.png ├── icon16.png ├── icon32.png ├── icon48.png ├── icon64.png ├── logo.svg └── manifest.json /GeneratedSD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/VSDGenerator/1df47800d7f80579fd0eb04bb8bc6cac14dfb6f5/GeneratedSD.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 2-Clause License 2 | 3 | Copyright (c) 2018 André Zanghelini 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vivaldi Speed Dial Generator 2 | 3 | This extension makes the [Vivaldi browser](https://vivaldi.com/) automatically generate a cool Speed Dial image based on possible graphics defined in the meta or link tags, instead of generating a picture of the page. 4 | 5 | ![Preview of some generated speed dials](https://github.com/An-dz/VSDGenerator/raw/master/GeneratedSD.png) 6 | 7 | # Installing 8 | 9 | 1) Head to the [releases page](https://github.com/An-dz/VSDGenerator/releases) 10 | 2) Download the latest version CRX file anywhere in your computer 11 | 3) Open Vivaldi 12 | 4) Open the extensions page 13 | 4) Enable _"Developer mode"_ at the top right 14 | 5) Drag the CRX file inside the Extensions page 15 | 16 | # How to use it 17 | 18 | Just install the extension and reload your Speed Dial images, nothing more. 19 | 20 | Just notice that since there are certain websites that link to non-existent resources the extension checks the resources before using them, this slows down the script execution and the generation may not fire. To be sure you don't fall for this problem make sure to cache the page first by visiting it before creating an image. 21 | 22 | **Will this work on another browser or Speed Dial extension?** 23 | 24 | Probably not. This extension uses a singularity of Vivaldi Speed Dial generation to inject itself and this is probably not the same on other browsers or extensions. 25 | -------------------------------------------------------------------------------- /VivaldiSDgen.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const customChecker = []; 4 | 5 | /** 6 | * Enumeration for different types 7 | */ 8 | const powers = { 9 | colour: { 10 | custom: 4, 11 | theme_color: 3, 12 | mask_icon: 2, 13 | ms_tile_color: 1, 14 | }, 15 | image: { 16 | // for custom getters so it's not overwritten by others 17 | custom: 7, 18 | // social media image, probably related with content 19 | // meta[property="og:image"] 20 | og_image: 6, 21 | // social media image, probably related with content 22 | // meta[name="twitter:image"] 23 | twitter: 5, 24 | // SVG icon, excellent quality 25 | // link[sizes="any"] 26 | svg: 4, 27 | // high probability of an SVG icon 28 | // link[rel="mask-icon"] 29 | mask_icon: 3, 30 | // icons with defined sizes, some apple icons might fall here 31 | // link[sizes] 32 | icon_sizes: 2, 33 | // MS tile, good guaranteed quality of 144px 34 | // meta[name="msapplication-TileImage"] 35 | ms_tile_image: 2.0144, 36 | // could be anything from 1px to infinity 37 | // we assume the old iPhone 1 57px 38 | // link[rel="apple-touch-icon"] 39 | apple: 2.0057, 40 | // favicon for the rescue 41 | favicon: 1, 42 | favicon_ico: 0.5, 43 | }, 44 | }; 45 | 46 | const SD_data = { 47 | colour: { 48 | power: 0, 49 | }, 50 | image: { 51 | power: 0, 52 | list: [], 53 | }, 54 | }; 55 | 56 | async function testImage(prev_power, src) { 57 | // console.log("Testing Image...", src); 58 | let response; 59 | try { 60 | response = await fetch(src, {method: "HEAD"}); 61 | console.log(response); 62 | 63 | if (!response.ok) { 64 | console.log("failed 1"); 65 | SD_data.image.power = prev_power; 66 | const [list_power, list_src] = SD_data.image.list.pop(); 67 | setImage(list_power, list_src); 68 | } 69 | } catch(e) { 70 | console.log("failed 2"); 71 | console.log(response); 72 | SD_data.image.power = prev_power; 73 | const [list_power, list_src] = SD_data.image.list.pop(); 74 | setImage(list_power, list_src); 75 | } 76 | // console.log("Finished fetch."); 77 | } 78 | 79 | function setBackground(power, colour) { 80 | if (SD_data.colour.power < power) { 81 | SD_data.colour.power = power; 82 | 83 | document.getElementById("Vivaldi_SDG_colour").textContent = ` 84 | :root html, :root body { 85 | background-color: ${colour} !important; 86 | } 87 | `; 88 | } 89 | } 90 | 91 | async function setImage(power, src) { 92 | // const prev_power = SD_data.image.power; 93 | 94 | if (SD_data.image.power < power) { 95 | // we must check the existence of the image as there are sites 96 | // dumb enough to put the tag but linked to a dead image 97 | // await testImage(prev_power, src); 98 | 99 | SD_data.image.power = power; 100 | 101 | const centralise = ( 102 | power < powers.image.custom && 103 | power !== powers.image.twitter && 104 | power !== powers.image.og_image 105 | ) ? "auto 50%" : "cover"; 106 | 107 | const position = power >= powers.image.custom ? "top center" : "center"; 108 | 109 | document.getElementById("Vivaldi_SDG_logo").textContent = ` 110 | :root body { 111 | background-image: url('${src}') !important; 112 | background-position: ${position} !important; 113 | background-size: ${centralise} !important; 114 | background-repeat: no-repeat !important; 115 | } 116 | `; 117 | 118 | return; 119 | } 120 | 121 | SD_data.image.list.push([power, src]); 122 | SD_data.image.list.sort((a, b) => (a[0] > b[0]) - (b[0] > a[0])); 123 | } 124 | 125 | async function setImageTagAsSD(selector, extraPower) { 126 | const power = powers.image.custom + (extraPower ? extraPower : 0); 127 | 128 | const imageElement = document.querySelector(selector); 129 | 130 | if (imageElement) { 131 | await setImage(power, imageElement.src); 132 | return; 133 | } 134 | 135 | customChecker.push(async element => { 136 | if (element.tagName !== "IMG") { 137 | return; 138 | } 139 | 140 | const image = document.querySelector(selector); 141 | 142 | if (image) { 143 | await setImage(power, image.src); 144 | } 145 | }); 146 | } 147 | 148 | function createStyles() { 149 | const hidder = document.createElement("style"); 150 | hidder.type = "text/css"; 151 | hidder.id = "Vivaldi_SDG_hidder"; 152 | hidder.textContent = ` 153 | :root html, :root body { 154 | height: 100vh !important; 155 | margin: 0 !important; 156 | padding: 0 !important; 157 | visibility: visible !important; 158 | opacity: 1 !important; 159 | } 160 | html > :not(body), body * { 161 | display: none !important; 162 | visibility: hidden !important; 163 | } 164 | `; 165 | 166 | const colour = document.createElement("style"); 167 | colour.type = "text/css"; 168 | colour.id = "Vivaldi_SDG_colour"; 169 | 170 | const logo = document.createElement("style"); 171 | logo.type = "text/css"; 172 | logo.id = "Vivaldi_SDG_logo"; 173 | 174 | document.head.appendChild(hidder); 175 | document.head.appendChild(colour); 176 | document.head.appendChild(logo); 177 | } 178 | 179 | // check each element loaded in the head 180 | async function metaAnalyser(element) { 181 | const tagName = element.tagName; 182 | 183 | if (tagName === "LINK") { 184 | const rel = element.rel; 185 | const sizes = element.sizes; 186 | 187 | if (sizes.length > 0) { 188 | let size = 0.0001; 189 | 190 | if (rel === "apple-touch-icon") { 191 | size = 0; 192 | } 193 | 194 | if (sizes.value === "any") { 195 | await setImage(powers.image.svg, element.href); 196 | return; 197 | } 198 | 199 | if (sizes.length === 1) { 200 | size += Number.parseInt(sizes.value, 10); 201 | await setImage(powers.image.icon_sizes + size / 10000, element.href); 202 | return; 203 | } 204 | 205 | let max = 0; 206 | 207 | for (var idx = sizes.length - 1; idx >= 0; idx--) { 208 | max = Number.max(max, Number.parseInt(sizes[idx], 10)); 209 | } 210 | 211 | await setImage(powers.image.icon_sizes + (max + size) / 10000, element.href); 212 | 213 | return; 214 | } 215 | 216 | if (rel === "mask-icon") { 217 | setBackground(powers.colour.mask_icon, element.getAttribute("color")); 218 | await setImage(powers.image.mask_icon, element.href); 219 | return; 220 | } 221 | 222 | if (rel === "apple-touch-icon") { 223 | await setImage(powers.image.apple, element.href); 224 | return; 225 | } 226 | 227 | if (rel.search(/\bicon\b/) > -1) { 228 | await setImage(powers.image.favicon, element.href); 229 | } 230 | 231 | return; 232 | } 233 | 234 | if (tagName === "META") { 235 | const name = element.name; 236 | 237 | if (element.property === "og:image") { 238 | await setImage(powers.image.og_image, element.content); 239 | return; 240 | } 241 | 242 | switch (name) { 243 | case "theme-color": 244 | setBackground(powers.colour.theme_color, element.content); 245 | break; 246 | case "msapplication-TileColor": 247 | setBackground(powers.colour.ms_tile_color, element.content); 248 | break; 249 | case "msapplication-TileImage": 250 | await setImage(powers.image.ms_tile_image, element.content); 251 | break; 252 | case "twitter:image": 253 | await setImage(powers.image.twitter, element.content); 254 | break; 255 | default: 256 | } 257 | } 258 | } 259 | 260 | /* Add custom functions below this line to avoid merge conflicts */ 261 | 262 | 263 | /* Add custom functions above this line to avoid merge conflicts */ 264 | 265 | /** 266 | * Custom functions for specific sites 267 | */ 268 | const custom = { 269 | /* 270 | * # Example 1 271 | * Getting an image from the page 272 | * 273 | * ```javascript 274 | * "www.example.com": async () => await setImageTagAsSD(".title img"), 275 | * ``` 276 | * 277 | * # Example 2 278 | * Getting multiple possibilities from the page 279 | * 280 | * The number as second argument defines which 281 | * should be selected if multiple are found, 282 | * higher number means higher preference. 283 | * 284 | * Don't use negative numbers 285 | * 286 | * ```javascript 287 | * "www.example.com": async () => await Promise.all([ 288 | * setImageTagAsSD("img.a", 3), 289 | * setImageTagAsSD("img.b", 2), 290 | * setImageTagAsSD("img.c", 1), 291 | * setImageTagAsSD("img.d", 0), 292 | * ]), 293 | * ``` 294 | * 295 | * # Example 3 296 | * You can also call a more complex function you add above 297 | * 298 | * For example, you may need to read some custom parameter 299 | * because there's a script to lazyload images and the URL 300 | * is inside a data-url parameter, or you may need to inject 301 | * a script on the page as the image URL itself is lazy loaded, 302 | * and you'll need to intercept when this call is done. 303 | * 304 | * ```javascript 305 | * "www.example.com": callSomeThing, 306 | * ``` 307 | */ 308 | }; 309 | 310 | // apply only when generating a SD 311 | if ( 312 | window.innerHeight === 838 && 313 | window.innerWidth === 1024 && 314 | window.innerHeight === window.outerHeight && 315 | window.innerWidth === window.outerWidth 316 | ) { 317 | // check the loading until the head is loaded 318 | const dom_observer = new MutationObserver(async mutations => { 319 | mutations.forEach(async change => { 320 | change.addedNodes.forEach(async element => { 321 | if (element.tagName === "HEAD") { 322 | createStyles(); 323 | await setImage(powers.image.favicon_ico, "/favicon.ico"); 324 | custom[window.location.host] && await custom[window.location.host](); 325 | return; 326 | } 327 | 328 | await metaAnalyser(element); 329 | customChecker.forEach(f => f(element)); 330 | }); 331 | }); 332 | }); 333 | 334 | dom_observer.observe(document, {childList: true, subtree: true}); 335 | } 336 | -------------------------------------------------------------------------------- /icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/VSDGenerator/1df47800d7f80579fd0eb04bb8bc6cac14dfb6f5/icon128.png -------------------------------------------------------------------------------- /icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/VSDGenerator/1df47800d7f80579fd0eb04bb8bc6cac14dfb6f5/icon16.png -------------------------------------------------------------------------------- /icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/VSDGenerator/1df47800d7f80579fd0eb04bb8bc6cac14dfb6f5/icon32.png -------------------------------------------------------------------------------- /icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/VSDGenerator/1df47800d7f80579fd0eb04bb8bc6cac14dfb6f5/icon48.png -------------------------------------------------------------------------------- /icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/An-dz/VSDGenerator/1df47800d7f80579fd0eb04bb8bc6cac14dfb6f5/icon64.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Speed Dial Generator", 3 | "version": "2.0", 4 | "description": "Make Vivaldi automatically generate a cool SD.", 5 | "author": "André Zanghelini (An_dz)", 6 | "homepage_url": "https://github.com/An-dz/VSDGenerator", 7 | "icons": { 8 | "16": "icon16.png", 9 | "32": "icon32.png", 10 | "48": "icon48.png", 11 | "64": "icon64.png", 12 | "128": "icon128.png" 13 | }, 14 | "content_scripts": [ { 15 | "js": [ "VivaldiSDgen.js" ], 16 | "matches": [ "" ], 17 | "run_at": "document_start" 18 | } ], 19 | "manifest_version": 2 20 | } 21 | --------------------------------------------------------------------------------