├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── accentmod.css ├── accentmod.js ├── activate-tab-on-hover.js ├── addressfield-theming.css ├── apod-widget ├── apod.css ├── apod.html ├── apod.js └── icons │ ├── favicon.png │ └── noimage.jpg ├── attack-on-the-statusbar.js ├── audio-tab-animation.css ├── backup-keyboard-cheat-sheet.js ├── backup-search-engines.js ├── binary-clock-widget ├── binary.css ├── binary.html ├── binary.js └── icons │ └── favicon.png ├── changing-buttons.css ├── collapse-keyboard-settings.js ├── fortune-widget ├── data.js ├── fortune.css ├── fortune.html ├── fortune.js └── icons │ └── favicon.png ├── history-clock.js ├── import-export-themes.js ├── improved-extension-toggle.js ├── internal-page-theme.js ├── legacy-panel.css ├── load ├── backup-search-engines.js ├── collapse-keyboard-settings.js ├── main.css ├── monochrome-icons.js └── tam710562-import-export-command-chains.js ├── m3.css ├── monochrome-icons.js ├── moon-phase-widget ├── icons │ ├── favicon.png │ └── moon.webp ├── moon.css ├── moon.html └── moon.js ├── moon-phase.js ├── moon-phase.svg ├── move_extensions_to_panel-container.js ├── move_extensions_to_status-bar.js ├── page-actions └── Tab_Lock.js ├── panel-doubleclick-automation.js ├── panel-header-mod.css ├── panel-overlay-toggle.js ├── panel-toggle.js ├── profile-icon.js ├── random-theme-button.js ├── safari-style.js ├── scrollable-startpage-navigation.js ├── second-toolbar.js ├── statusbar.js ├── tab-scroll.js ├── tab-stacks-theming.css ├── tabs_below_address-bar.css ├── theme-interface-plus.js ├── toggle-keys-and-gestures.js ├── toggle_site-info.css ├── toolbar-icons-mod.js ├── update-feeds.js ├── urlbar-spacing.js ├── vivaldi-icon.js └── window-buttons-macos.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 luetage 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 | # Vivaldi Modding 2 | 3 | This repository is a collection of mods I have written for the [**Vivaldi web 4 | browser**][1]. Each file is a standalone modification which works on its own. 5 | Every mod contains a link to the Vivaldi forum topic it was first featured in, 6 | however the most up to date versions can be found in this github repo. I cannot 7 | guarantee that all of these mods function as advertised at this point in time, 8 | since the Vivaldi user interface is an ever changing target, but below is a list 9 | of actively maintained mods. I do use these myself at the moment, so should you 10 | run into problems with any of them, please let me know by creating an issue, or 11 | by posting in the according topic on Vivaldi Forum. 12 | 13 | ## Actively maintained mods 14 | 15 | - [activate-tab-on-hover.js](activate-tab-on-hover.js) 16 | - [addressfield-theming.css](addressfield-theming.css) 17 | - [backup-keyboard-cheatsheet.js](backup-keyboard-cheatsheet.js) 18 | - [backup-search-engines.js](backup-search-engines.js) 19 | - [collapse-keyboard-settings.js](collapse-keyboard-settings.js) 20 | - [moon-phase.js](moon-phase.js) 21 | - [m3.css](m3.css) 22 | - [monochrome-icons.js](monochrome-icons.js) 23 | - [random-theme-button.js](random-theme-button.js) 24 | - [Tab_Lock.js](page-actions/Tab_Lock.js) 25 | - [tab-scroll.js](tab-scroll.js) 26 | 27 | ## How to 28 | 29 | Vivaldi’s user interface is based on web technologies, therefore it’s relatively 30 | easy to customize its appearance and functionality with CSS and Javascript. 31 | Detailed explanations on how to implement these modifications can be found in 32 | the [**Official Guide**][2]. The [**Modding Forum Board**][3] is a great 33 | resource, featuring guides on how to automate the process of patching your 34 | installation after updating the browser and providing you with a plethora of 35 | mods written by the community. 36 | 37 | [1]: https://vivaldi.com/ 38 | [2]: https://forum.vivaldi.net/topic/10549/modding-vivaldi/ 39 | [3]: https://forum.vivaldi.net/category/52/modifications/ 40 | -------------------------------------------------------------------------------- /accentmod.css: -------------------------------------------------------------------------------- 1 | /* 2 | Accent Mod 3 | version 2021.9.0 4 | https://forum.vivaldi.net/topic/61827/accent-mod 5 | Use theme foreground or background color instead of fixed accent foreground 6 | color. Depends on the installation of additional JS code (accentmod.js). 7 | */ 8 | 9 | .accentmod .theme-dark.acc-dark.color-behind-tabs-off .toolbar-mainbar, 10 | .accentmod .theme-light.acc-light.color-behind-tabs-off .toolbar-mainbar { 11 | color: var(--colorFg); 12 | } 13 | .accentmod 14 | #browser.theme-dark.acc-dark.color-behind-tabs-off 15 | .tab-position 16 | .tab.active, 17 | .accentmod 18 | #browser.theme-light.acc-light.color-behind-tabs-off 19 | .tab-position 20 | .tab.active { 21 | color: var(--colorFgIntense); 22 | } 23 | .accentmod .theme-dark.acc-light.color-behind-tabs-off .toolbar-mainbar, 24 | .accentmod .theme-light.acc-dark.color-behind-tabs-off .toolbar-mainbar { 25 | color: var(--colorBg); 26 | } 27 | .accentmod 28 | #browser.theme-dark.acc-light.color-behind-tabs-off 29 | .tab-position 30 | .tab.active, 31 | .accentmod 32 | #browser.theme-light.acc-dark.color-behind-tabs-off 33 | .tab-position 34 | .tab.active { 35 | color: var(--colorBgIntenser); 36 | } 37 | .accentmod #browser.tabs-top.theme-dark.acc-dark.color-behind-tabs-on #header, 38 | .accentmod #browser.tabs-top.theme-light.acc-light.color-behind-tabs-on #header, 39 | .accentmod 40 | #browser.tabs-top.theme-dark.acc-dark.color-behind-tabs-on 41 | .tab-position 42 | .tab:hover:not(.active), 43 | .accentmod 44 | #browser.tabs-top.theme-light.acc-light.color-behind-tabs-on 45 | .tab-position 46 | .tab:hover:not(.active), 47 | .accentmod #browser.theme-dark.acc-dark.color-behind-tabs-on, 48 | .accentmod #browser.theme-light.acc-light.color-behind-tabs-on, 49 | .accentmod 50 | #browser.theme-dark.acc-dark.color-behind-tabs-on 51 | .tab-position 52 | .tab:hover:not(.active), 53 | .accentmod 54 | #browser.theme-light.acc-light.color-behind-tabs-on 55 | .tab-position 56 | .tab:hover:not(.active), 57 | .accentmod 58 | .theme-dark.acc-dark.color-behind-tabs-on 59 | .toolbar-tabbar 60 | > .button-toolbar 61 | > button, 62 | .accentmod 63 | .theme-light.acc-light.color-behind-tabs-on 64 | .toolbar-tabbar 65 | > .button-toolbar 66 | > button, 67 | .accentmod 68 | .theme-dark.acc-dark.color-behind-tabs-on 69 | .toolbar-tabbar 70 | > .toolbar-group 71 | > .button-toolbar 72 | > button, 73 | .accentmod 74 | .theme-light.acc-light.color-behind-tabs-on 75 | .toolbar-tabbar 76 | > .toolbar-group 77 | > .button-toolbar 78 | > button { 79 | color: var(--colorFg); 80 | } 81 | .accentmod 82 | #browser.theme-dark.acc-dark.color-behind-tabs-on 83 | .tab-position 84 | .tab.active.active, 85 | .accentmod 86 | #browser.theme-light.acc-light.color-behind-tabs-on 87 | .tab-position 88 | .tab.active.active, 89 | .accentmod 90 | #browser.theme-dark.acc-light.color-behind-tabs-on 91 | .tab-position 92 | .tab.active.active, 93 | .accentmod 94 | #browser.theme-light.acc-dark.color-behind-tabs-on 95 | .tab-position 96 | .tab.active.active { 97 | color: var(--colorFgIntense); 98 | } 99 | .accentmod #browser.tabs-top.theme-dark.acc-light.color-behind-tabs-on #header, 100 | .accentmod #browser.tabs-top.theme-light.acc-dark.color-behind-tabs-on #header, 101 | .accentmod 102 | #browser.tabs-top.theme-dark.acc-light.color-behind-tabs-on 103 | .tab-position 104 | .tab:hover:not(.active), 105 | .accentmod 106 | #browser.tabs-top.theme-light.acc-dark.color-behind-tabs-on 107 | .tab-position 108 | .tab:hover:not(.active), 109 | .accentmod #browser.theme-dark.acc-light.color-behind-tabs-on, 110 | .accentmod #browser.theme-light.acc-dark.color-behind-tabs-on, 111 | .accentmod 112 | #browser.theme-dark.acc-light.color-behind-tabs-on 113 | .tab-position 114 | .tab:hover:not(.active), 115 | .accentmod 116 | #browser.theme-light.acc-dark.color-behind-tabs-on 117 | .tab-position 118 | .tab:hover:not(.active), 119 | .accentmod 120 | .theme-dark.acc-light.color-behind-tabs-on 121 | .toolbar-tabbar 122 | > .button-toolbar 123 | > button, 124 | .accentmod 125 | .theme-light.acc-dark.color-behind-tabs-on 126 | .toolbar-tabbar 127 | > .button-toolbar 128 | > button, 129 | .accentmod 130 | .theme-dark.acc-light.color-behind-tabs-on 131 | .toolbar-tabbar 132 | > .toolbar-group 133 | > .button-toolbar 134 | > button, 135 | .accentmod 136 | .theme-light.acc-dark.color-behind-tabs-on 137 | .toolbar-tabbar 138 | > .toolbar-group 139 | > .button-toolbar 140 | > button { 141 | color: var(--colorBg); 142 | } 143 | .accentmod.accentswitch 144 | .theme-dark.acc-dark.color-behind-tabs-off 145 | .toolbar-mainbar, 146 | .accentmod.accentswitch 147 | .theme-light.acc-light.color-behind-tabs-off 148 | .toolbar-mainbar { 149 | color: var(--colorBg); 150 | } 151 | .accentmod.accentswitch 152 | #browser.theme-dark.acc-dark.color-behind-tabs-off 153 | .tab-position 154 | .tab.active, 155 | .accentmod.accentswitch 156 | #browser.theme-light.acc-light.color-behind-tabs-off 157 | .tab-position 158 | .tab.active { 159 | color: var(--colorBgIntenser); 160 | } 161 | .accentmod.accentswitch 162 | .theme-dark.acc-light.color-behind-tabs-off 163 | .toolbar-mainbar, 164 | .accentmod.accentswitch 165 | .theme-light.acc-dark.color-behind-tabs-off 166 | .toolbar-mainbar { 167 | color: var(--colorFg); 168 | } 169 | .accentmod.accentswitch 170 | #browser.theme-dark.acc-light.color-behind-tabs-off 171 | .tab-position 172 | .tab.active, 173 | .accentmod.accentswitch 174 | #browser.theme-light.acc-dark.color-behind-tabs-off 175 | .tab-position 176 | .tab.active { 177 | color: var(--colorFgIntense); 178 | } 179 | .accentmod.accentswitch 180 | #browser.tabs-top.theme-dark.acc-dark.color-behind-tabs-on 181 | #header, 182 | .accentmod.accentswitch 183 | #browser.tabs-top.theme-light.acc-light.color-behind-tabs-on 184 | #header, 185 | .accentmod.accentswitch 186 | #browser.tabs-top.theme-dark.acc-dark.color-behind-tabs-on 187 | .tab-position 188 | .tab:hover:not(.active), 189 | .accentmod.accentswitch 190 | #browser.tabs-top.theme-light.acc-light.color-behind-tabs-on 191 | .tab-position 192 | .tab:hover:not(.active), 193 | .accentmod.accentswitch #browser.theme-dark.acc-dark.color-behind-tabs-on, 194 | .accentmod.accentswitch #browser.theme-light.acc-light.color-behind-tabs-on, 195 | .accentmod.accentswitch 196 | #browser.theme-dark.acc-dark.color-behind-tabs-on 197 | .tab-position 198 | .tab:hover:not(.active), 199 | .accentmod.accentswitch 200 | #browser.theme-light.acc-light.color-behind-tabs-on 201 | .tab-position 202 | .tab:hover:not(.active), 203 | .accentmod.accentswitch 204 | .theme-dark.acc-dark.color-behind-tabs-on 205 | .toolbar-tabbar 206 | > .button-toolbar 207 | > button, 208 | .accentmod.accentswitch 209 | .theme-light.acc-light.color-behind-tabs-on 210 | .toolbar-tabbar 211 | > .button-toolbar 212 | > button, 213 | .accentmod.accentswitch 214 | .theme-dark.acc-dark.color-behind-tabs-on 215 | .toolbar-tabbar 216 | > .toolbar-group 217 | > .button-toolbar 218 | > button, 219 | .accentmod.accentswitch 220 | .theme-light.acc-light.color-behind-tabs-on 221 | .toolbar-tabbar 222 | > .toolbar-group 223 | > .button-toolbar 224 | > button { 225 | color: var(--colorBg); 226 | } 227 | .accentmod.accentswitch 228 | #browser.theme-dark.acc-dark.color-behind-tabs-on 229 | .tab-position 230 | .tab.active.active, 231 | .accentmod.accentswitch 232 | #browser.theme-light.acc-light.color-behind-tabs-on 233 | .tab-position 234 | .tab.active.active, 235 | .accentmod.accentswitch 236 | #browser.theme-dark.acc-light.color-behind-tabs-on 237 | .tab-position 238 | .tab.active.active, 239 | .accentmod.accentswitch 240 | #browser.theme-light.acc-dark.color-behind-tabs-on 241 | .tab-position 242 | .tab.active.active { 243 | color: var(--colorFgIntense); 244 | } 245 | .accentmod.accentswitch 246 | #browser.tabs-top.theme-dark.acc-light.color-behind-tabs-on 247 | #header, 248 | .accentmod.accentswitch 249 | #browser.tabs-top.theme-light.acc-dark.color-behind-tabs-on 250 | #header, 251 | .accentmod.accentswitch 252 | #browser.tabs-top.theme-dark.acc-light.color-behind-tabs-on 253 | .tab-position 254 | .tab:hover:not(.active), 255 | .accentmod.accentswitch 256 | #browser.tabs-top.theme-light.acc-dark.color-behind-tabs-on 257 | .tab-position 258 | .tab:hover:not(.active), 259 | .accentmod.accentswitch #browser.theme-dark.acc-light.color-behind-tabs-on, 260 | .accentmod.accentswitch #browser.theme-light.acc-dark.color-behind-tabs-on, 261 | .accentmod.accentswitch 262 | #browser.theme-dark.acc-light.color-behind-tabs-on 263 | .tab-position 264 | .tab:hover:not(.active), 265 | .accentmod.accentswitch 266 | #browser.theme-light.acc-dark.color-behind-tabs-on 267 | .tab-position 268 | .tab:hover:not(.active), 269 | .accentmod.accentswitch 270 | .theme-dark.acc-light.color-behind-tabs-on 271 | .toolbar-tabbar 272 | > .button-toolbar 273 | > button, 274 | .accentmod.accentswitch 275 | .theme-light.acc-dark.color-behind-tabs-on 276 | .toolbar-tabbar 277 | > .button-toolbar 278 | > button, 279 | .accentmod.accentswitch 280 | .theme-dark.acc-light.color-behind-tabs-on 281 | .toolbar-tabbar 282 | > .toolbar-group 283 | > .button-toolbar 284 | > button, 285 | .accentmod.accentswitch 286 | .theme-light.acc-dark.color-behind-tabs-on 287 | .toolbar-tabbar 288 | > .toolbar-group 289 | > .button-toolbar 290 | > button { 291 | color: var(--colorFg); 292 | } 293 | -------------------------------------------------------------------------------- /accentmod.js: -------------------------------------------------------------------------------- 1 | // Accent Mod 2 | // version 2021.11.0 3 | // https://forum.vivaldi.net/topic/61827/accent-mod 4 | // Use theme foreground or background color instead of fixed accent foreground 5 | // color. Depends on the installation of additional CSS code (accentmod.css). 6 | 7 | (function () { 8 | let RGB = (hex) => 9 | hex 10 | .replace( 11 | /^#?([a-f\d])([a-f\d])([a-f\d])$/i, 12 | (r, g, b) => "#" + r + r + g + g + b + b 13 | ) 14 | .substring(1) 15 | .match(/.{2}/g) 16 | .map((x) => parseInt(x, 16)); 17 | 18 | let lum = (r, g, b) => { 19 | let a = [r, g, b].map(function (v) { 20 | v /= 255; 21 | return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); 22 | }); 23 | return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; 24 | }; 25 | 26 | let contrast = (lum1, lum2) => { 27 | const bright = Math.max(lum1, lum2); 28 | const dark = Math.min(lum1, lum2); 29 | return (bright + 0.05) / (dark + 0.05); 30 | }; 31 | 32 | let accentmod = () => { 33 | vivaldi.prefs.get("vivaldi.themes.current", (current) => { 34 | let themes = "user"; 35 | if (current.startsWith("Vivaldi")) { 36 | themes = "system"; 37 | } 38 | vivaldi.prefs.get(`vivaldi.themes.${themes}`, (collection) => { 39 | let index = collection.findIndex((x) => x.id === current); 40 | const theme = collection[index]; 41 | if (theme.accentFromPage === false) { 42 | const app = document.getElementById("app"); 43 | app.classList.add("accentmod"); 44 | const bg = theme.colorBg; 45 | const fg = theme.colorFg; 46 | const ac = theme.colorAccentBg; 47 | const rgbBg = RGB(bg); 48 | const lumBg = lum(rgbBg[0], rgbBg[1], rgbBg[2]); 49 | const rgbFg = RGB(fg); 50 | const lumFg = lum(rgbFg[0], rgbFg[1], rgbFg[2]); 51 | const rgbAc = RGB(ac); 52 | const lumAc = lum(rgbAc[0], rgbAc[1], rgbAc[2]); 53 | const conAc1 = contrast(lumAc, lumBg); 54 | const conAc2 = contrast(lumAc, lumFg); 55 | if ( 56 | (browser.matches(".theme-dark.acc-dark") && conAc1 > conAc2) || 57 | (browser.matches(".theme-light.acc-light") && conAc1 > conAc2) || 58 | (browser.matches(".theme-dark.acc-light") && conAc2 > conAc1) || 59 | (browser.matches(".theme-light.acc-dark") && conAc2 > conAc1) 60 | ) { 61 | if (!app.classList.contains("accentswitch")) 62 | app.classList.add("accentswitch"); 63 | } else if (app.classList.contains("accentswitch")) 64 | app.classList.remove("accentswitch"); 65 | } else app.classList = ""; 66 | }); 67 | }); 68 | }; 69 | 70 | setTimeout(function wait() { 71 | const browser = document.getElementById("browser"); 72 | if (browser) { 73 | accentmod(); 74 | vivaldi.prefs.onChanged.addListener((ch) => { 75 | if ( 76 | ch.path === "vivaldi.themes.current" || 77 | ch.path === "vivaldi.themes.system" || 78 | ch.path === "vivaldi.themes.user" 79 | ) { 80 | accentmod(); 81 | } 82 | }); 83 | } else setTimeout(wait, 300); 84 | }, 300); 85 | })(); 86 | -------------------------------------------------------------------------------- /activate-tab-on-hover.js: -------------------------------------------------------------------------------- 1 | // Activate Tab On Hover 2 | // version 2024.9.0 3 | // https://forum.vivaldi.net/post/395460 4 | // Activates tab on hover. 5 | 6 | (function activateTab() { 7 | "use strict" 8 | 9 | function hover(e, tab) { 10 | if ( 11 | !tab.parentNode.classList.contains("active") && 12 | !e.shiftKey && 13 | !e.ctrlKey 14 | ) { 15 | tab.addEventListener("mouseleave", function () { 16 | clearTimeout(wait); 17 | tab.removeEventListener("mouseleave", tab); 18 | }); 19 | wait = setTimeout(function () { 20 | const id = Number(tab.parentNode.parentNode.id.replace(/^\D+/g, "")); 21 | chrome.tabs.update(id, { active: true, highlighted: true }); 22 | }, delay); 23 | } 24 | } 25 | 26 | let wait; 27 | const delay = 300; //pick a time in milliseconds 28 | let appendChild = Element.prototype.appendChild; 29 | Element.prototype.appendChild = function () { 30 | if ( 31 | arguments[0].tagName === "DIV" && 32 | arguments[0].classList.contains("tab-header") 33 | ) { 34 | setTimeout( 35 | function () { 36 | const trigger = (event) => hover(event, arguments[0]); 37 | arguments[0].addEventListener("mouseenter", trigger); 38 | }.bind(this, arguments[0]) 39 | ); 40 | } 41 | return appendChild.apply(this, arguments); 42 | }; 43 | })(); 44 | -------------------------------------------------------------------------------- /addressfield-theming.css: -------------------------------------------------------------------------------- 1 | /* 2 | Addressfield Theming 3 | version 2023.3.1 4 | https://forum.vivaldi.net/post/356714 5 | Themes hostfragments and changes icon color for blocked content and the bookmark 6 | button. 7 | */ 8 | 9 | .UrlFragment-Wrapper:not(.UrlFragment-Wrapper--ShouldHighlight) { 10 | --HighlightColor: var(--colorFg); 11 | --LowlightColor: var(--colorFg); 12 | } 13 | .UrlFragment-Wrapper--ShouldHighlight { 14 | --HighlightColor: var(--colorFg); 15 | --LowlightColor: var(--colorFgFadedMost); 16 | } 17 | .UrlFragment-Wrapper--ShouldHighlight .UrlFragment-HostFragment-Subdomain, 18 | .UrlFragment-Wrapper--ShouldHighlight .UrlFragment-HostFragment-Basedomain, 19 | .UrlFragment-Wrapper--ShouldHighlight .UrlFragment-HostFragment-TLD { 20 | font-weight: bold; 21 | } 22 | .BookmarkButton .bookmark-animated-fill { 23 | fill: var(--colorFg) !important; 24 | } 25 | .permission-denied circle { 26 | fill: var(--colorHighlightBg); 27 | } 28 | .permission-denied path { 29 | fill: var(--colorHighlightFg); 30 | } 31 | -------------------------------------------------------------------------------- /apod-widget/apod.css: -------------------------------------------------------------------------------- 1 | /* 2 | Astronomy Picture of the Day Dashboard Widget 3 | version 2024.12.0 4 | Guide and updates ☛ https://forum.vivaldi.net/post/783627 5 | ———————— ⁂ ———————— 6 | */ 7 | :focus-visible { 8 | outline: 2px solid var(--colorHighlightBg); 9 | border-radius: var(--radius); 10 | } 11 | a:focus-visible { 12 | border-radius: unset; 13 | } 14 | html { 15 | container-type: normal; 16 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "IBM Plex Sans", 17 | Inter, Ubuntu, sans-serif; 18 | font-size: 100%; 19 | scrollbar-width: thin; 20 | scrollbar-color: color-mix(in oklab, var(--colorFg) 40%, transparent) 21 | color-mix(in oklab, var(--colorBg) 95%, black); 22 | } 23 | @container style(--isDarkTheme: 0) { 24 | input { 25 | color-scheme: light; 26 | } 27 | } 28 | @container style(--isDarkTheme: 1) { 29 | input { 30 | color-scheme: dark; 31 | } 32 | } 33 | body { 34 | color: var(--colorFg); 35 | background: var(--colorBg); 36 | cursor: default; 37 | user-select: none; 38 | margin: 0.4rem; 39 | } 40 | 41 | .hidden { 42 | display: none; 43 | margin: 0; 44 | } 45 | #error { 46 | font-size: 0.8rem; 47 | margin-block-start: 0.6rem; 48 | margin-block-end: 1rem; 49 | margin-left: 0.6rem; 50 | margin-right: 0.6rem 51 | } 52 | #container { 53 | width: fit-content; 54 | margin: 0 auto; 55 | } 56 | h1 { 57 | text-align: center; 58 | font-size: 1rem; 59 | width: 0; 60 | min-width: 100%; 61 | margin: 0 auto 0.4rem; 62 | user-select: text; 63 | } 64 | a { 65 | cursor: default !important; 66 | display: inline-block; 67 | } 68 | #media { 69 | max-width: 100%; 70 | display: block; 71 | } 72 | #text { 73 | margin-top: 0.4rem; 74 | font-size: 0.8rem; 75 | line-height: 1.4; 76 | width: 0; 77 | min-width: 100%; 78 | } 79 | summary { 80 | list-style: none; 81 | } 82 | summary svg { 83 | width: 0.8em; 84 | height: 0.8em; 85 | stroke-width: 3.5; 86 | transform: rotate(90deg); 87 | vertical-align: middle; 88 | } 89 | details[open] summary svg { 90 | transform: rotate(180deg); 91 | } 92 | #explanation { 93 | margin: 0.4rem auto 1.6rem; 94 | overflow-wrap: break-word; 95 | hyphens: auto; 96 | user-select: text; 97 | } 98 | input { 99 | background: var(--colorBg); 100 | color: var(--colorFg) !important; 101 | border: 0.15rem solid color-mix(in oklab, var(--colorFg) 60%, transparent); 102 | border-radius: var(--radius); 103 | outline: none; 104 | margin: 0.28rem; 105 | padding: 0.12rem; 106 | } 107 | input:focus-visible { 108 | border-color: var(--colorHighlightBg); 109 | outline: unset; 110 | } 111 | input::-webkit-calendar-picker-indicator { 112 | outline-color: var(--colorHighlightBg); 113 | } 114 | -------------------------------------------------------------------------------- /apod-widget/apod.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | Astronomy Picture of the Day 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /apod-widget/apod.js: -------------------------------------------------------------------------------- 1 | // Astronomy Picture of the Day Dashboard Widget 2 | // version 2024.12.0 3 | // Guide and updates ☛ https://forum.vivaldi.net/post/783627 4 | // ———————— ⁂ ———————— 5 | 6 | "use strict"; 7 | 8 | // EDIT START 9 | // Generate your own API key @ https://api.nasa.gov and input it below. By 10 | // default the demo key is being used, which limits the number and rate of 11 | // requests and might deny service. 12 | 13 | const api_key = "DEMO_KEY"; 14 | 15 | // EDIT END 16 | 17 | const video_thumb = (url) => { 18 | const regex = 19 | /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/embed\/|youtu\.be\/)([^?&]+)/; 20 | const id = url.match(regex); 21 | return `https://img.youtube.com/vi/${id[1]}/hqdefault.jpg`; 22 | }; 23 | 24 | async function load_image(data) { 25 | let image_url; 26 | switch (data.media_type) { 27 | case "image": 28 | image_url = data.url; 29 | break; 30 | case "video": 31 | image_url = video_thumb(data.url); 32 | break; 33 | default: 34 | image_url = "icons/noimage.jpg"; 35 | } 36 | return new Promise((resolve) => { 37 | em.media.onload = () => resolve(`image loaded ${image_url}`); 38 | em.media.src = image_url; 39 | }); 40 | } 41 | 42 | async function get_data(url) { 43 | return new Promise((resolve, reject) => { 44 | fetch(url) 45 | .then((response) => { 46 | if (!response.ok) { 47 | throw new Error(`HTTP error! status: ${response.status}`); 48 | } 49 | return response.json(); 50 | }) 51 | .then((data) => { 52 | resolve({ 53 | success: true, 54 | data: data, 55 | message: "data loaded", 56 | }); 57 | }) 58 | .catch((error) => { 59 | reject({ 60 | success: false, 61 | error: error.message, 62 | message: "Connection error", 63 | }); 64 | }); 65 | }); 66 | } 67 | 68 | function set(data) { 69 | em.title.innerHTML = data.title; 70 | em.date.innerHTML = data.date; 71 | em.set_date.value = data.date; 72 | if ("copyright" in data) { 73 | em.copyright.innerHTML = data.copyright.trim(); 74 | em.copyright.parentElement.classList.remove("hidden"); 75 | } else em.copyright.parentElement.classList.add("hidden"); 76 | em.explanation.innerHTML = data.explanation; 77 | if (em.first_run === true) { 78 | em.set_date.setAttribute("max", data.date); 79 | em.container.classList.remove("hidden"); 80 | em.first_run = false; 81 | } else { 82 | const mu = data.date.slice(2).replace(/-/g, ""); 83 | em.media.parentElement.setAttribute( 84 | "href", 85 | `https://apod.nasa.gov/apod/ap${mu}.html` 86 | ); 87 | } 88 | } 89 | 90 | const parse = async (data) => { 91 | await load_image(data).then( 92 | (resolve) => { 93 | console.info(resolve); 94 | set(data); 95 | }, 96 | () => { 97 | console.error(`Could not load image ${data.url}`); 98 | em.media.src = "icons/noimage.jpg"; 99 | set(data); 100 | } 101 | ); 102 | }; 103 | 104 | const setup = async (data_url) => { 105 | em.data_error.classList.add("hidden"); 106 | if (em.first_run === true) { 107 | const apod = JSON.parse(localStorage.getItem("apod")); 108 | if (apod !== null && Date.now() - apod.timestamp < 7200000) { 109 | console.info(apod); 110 | parse(apod); 111 | return; 112 | } 113 | } 114 | await get_data(data_url).then( 115 | (resolve) => { 116 | console.info(resolve); 117 | if (em.first_run === true) { 118 | const storage = resolve.data; 119 | storage.timestamp = Date.now(); 120 | localStorage.setItem("apod", JSON.stringify(storage)); 121 | } 122 | parse(resolve.data); 123 | }, 124 | (reject) => { 125 | em.data_error.innerHTML = reject.message; 126 | em.data_error.classList.remove("hidden"); 127 | } 128 | ); 129 | }; 130 | 131 | const new_query = (event) => { 132 | em.set_date.blur(); 133 | const pick = event.target.value; 134 | const archive = `https://api.nasa.gov/planetary/apod?date=${pick}&api_key=${api_key}`; 135 | setup(archive); 136 | }; 137 | 138 | function init() { 139 | const elements = { 140 | container: document.getElementById("container"), 141 | title: document.getElementById("title"), 142 | media: document.getElementById("media"), 143 | text: document.getElementById("text"), 144 | copyright: document.getElementById("copyright"), 145 | explanation: document.getElementById("explanation"), 146 | date: document.getElementById("date"), 147 | set_date: document.getElementById("set_date"), 148 | data_error: document.getElementById("error"), 149 | first_run: true, 150 | }; 151 | elements.set_date.addEventListener("keydown", (e) => { 152 | if (e.key === "Enter" || e.key === " ") e.target.showPicker(); 153 | else if (/^\d$/.test(e.key)) e.preventDefault(); 154 | }); 155 | elements.set_date.addEventListener("change", new_query); 156 | return elements; 157 | } 158 | 159 | const api_request = `https://api.nasa.gov/planetary/apod?api_key=${api_key}`; 160 | const em = init(); 161 | setup(api_request); 162 | -------------------------------------------------------------------------------- /apod-widget/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luetage/vivaldi_modding/f2b41ce58e5a1f753e4076c4bc0ddb92b8d85c24/apod-widget/icons/favicon.png -------------------------------------------------------------------------------- /apod-widget/icons/noimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luetage/vivaldi_modding/f2b41ce58e5a1f753e4076c4bc0ddb92b8d85c24/apod-widget/icons/noimage.jpg -------------------------------------------------------------------------------- /attack-on-the-statusbar.js: -------------------------------------------------------------------------------- 1 | // Attack on the statusbar 2 | // https://forum.vivaldi.net/topic/22766/attack-on-the-status-bar 3 | // Moves the statusbar to the top and makes it compact. Furthermore introduces a button in the statusbar 4 | // to toggle the status-info (link address, status information) and a biscuit button for showing and 5 | // copying the Vivaldi version (biscuit mode needs to be enabled for this). 6 | 7 | { 8 | function statusInfoLogic() { 9 | const statusInfoToggle = document.getElementById('statusInfoToggle'); 10 | const statusInfo = document.querySelector('.StatusInfo'); 11 | if (statusInfoToggle.classList.contains('zeig')) { 12 | statusInfoToggle.classList.remove('zeig'); 13 | statusInfo.removeAttribute('id'); 14 | var info = 'off'; 15 | } 16 | else { 17 | statusInfoToggle.classList.add('zeig'); 18 | statusInfo.id = 'zeig'; 19 | var info = 'on'; 20 | } 21 | chrome.storage.local.set({'statusInfo': info}); 22 | } 23 | 24 | function statusMod() { 25 | const cont = document.createElement('div'); 26 | const statusBar = document.querySelector('.toolbar-statusbar'); 27 | const statusInfo = document.querySelector('.StatusInfo'); 28 | cont.id = 'statusContainer'; 29 | document.querySelector('.inner').appendChild(cont); 30 | cont.appendChild(statusBar); 31 | if (document.querySelector('.biscuit-string')) { 32 | const version = document.querySelector('.biscuit-string').innerText; 33 | const divB = document.createElement('div'); 34 | divB.classList.add('button-toolbar'); 35 | divB.id = 'biscuitButton'; 36 | divB.setAttribute('title', version + '\nClick to copy version string'); 37 | divB.innerHTML = ''; 38 | statusBar.insertBefore(divB, statusInfo); 39 | document.getElementById('biscuitButton').addEventListener('click', function() { 40 | navigator.clipboard.writeText(version); 41 | }) 42 | } 43 | const divL = document.createElement('div'); 44 | divL.classList.add('button-toolbar'); 45 | divL.id = 'statusInfoToggle'; 46 | divL.setAttribute('title', 'Toggle status info'); 47 | divL.innerHTML = ''; 48 | statusBar.insertBefore(divL, statusInfo); 49 | document.getElementById('statusInfoToggle').addEventListener('click', statusInfoLogic); 50 | chrome.storage.local.get({'statusInfo': 'on'}, function(check) { 51 | const info = check.statusInfo; 52 | if (info === 'on') { 53 | document.querySelector('.StatusInfo').id = 'zeig'; 54 | document.getElementById('statusInfoToggle').classList.add('zeig'); 55 | } 56 | }) 57 | } 58 | 59 | setTimeout(function wait() { 60 | const browser = document.getElementById('browser'); 61 | if (browser) { 62 | const style = document.createElement('style'); 63 | style.type = 'text/css'; 64 | style.id = 'statusMod'; 65 | style.innerHTML = `#statusContainer {position: absolute;z-index: 1;max-width: 100vw;right: 0;top: 0;box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);}.toolbar-statusbar {display: none;border-top: none;border-bottom: 1px solid var(--colorBorder);}#statusContainer .toolbar-statusbar {display: flex}.toolbar-statusbar .button-popup.button-popup-above {bottom: unset;top: 22px;}.toolbar-statusbar .button-popup.button-popup-above:before, .toolbar-statusbar .button-popup.button-popup-above:after {opacity: 0;}.biscuit-setting-version {display: none !important;}#biscuitButton button svg, #statusInfoToggle button svg {width: 14px;height: 14px;}#statusInfoToggle.zeig button svg {fill: var(--colorHighlightBg);}.StatusInfo {display: none;}#zeig.StatusInfo.StatusInfo--Visible {display: inline-block;}`; 66 | document.getElementsByTagName('head')[0].appendChild(style); 67 | } 68 | else { 69 | setTimeout(wait, 300); 70 | } 71 | }, 300) 72 | 73 | var appendChild = Element.prototype.appendChild; 74 | Element.prototype.appendChild = function () { 75 | if (arguments[0].tagName === 'DIV') { 76 | setTimeout(function() { 77 | if (this.classList.contains('toolbar-statusbar')) { 78 | const statusContainer = document.getElementById('statusContainer'); 79 | if (!statusContainer) { 80 | statusMod(); 81 | } 82 | } 83 | }.bind(this, arguments[0])) 84 | } 85 | return appendChild.apply(this, arguments); 86 | } 87 | 88 | var removeChild = Element.prototype.removeChild; 89 | Element.prototype.removeChild = function () { 90 | if (arguments[0].tagName === 'DIV' && arguments[0].classList.contains('toolbar-statusbar')) { 91 | document.getElementById('statusContainer').remove(); 92 | } 93 | else { 94 | return removeChild.apply(this, arguments); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /audio-tab-animation.css: -------------------------------------------------------------------------------- 1 | /* 2 | Audio Tab Animation 3 | version 2021.9.0 4 | https://forum.vivaldi.net/topic/18439/audio-tab-animation 5 | Animates tabs playing sound. 6 | */ 7 | 8 | @keyframes greenwave { 9 | from { 10 | background: var(--colorAccentBgFaded); 11 | } 12 | to { 13 | background: #bbffff; 14 | } 15 | } 16 | @keyframes greenactive { 17 | from { 18 | background: var(--colorBg); 19 | } 20 | to { 21 | background: #bbffff; 22 | } 23 | } 24 | .tab.audio-on { 25 | animation-name: greenwave; 26 | } 27 | .tab.active.audio-on { 28 | animation-name: greenactive; 29 | } 30 | .tab.audio-on, 31 | .tab.active.audio-on { 32 | animation-duration: 2s; 33 | animation-iteration-count: infinite; 34 | animation-direction: alternate; 35 | animation-timing-function: ease-in; 36 | } 37 | -------------------------------------------------------------------------------- /backup-keyboard-cheat-sheet.js: -------------------------------------------------------------------------------- 1 | // Backup Keyboard Cheat Sheet 2 | // version 2024.4.5 3 | // https://forum.vivaldi.net/post/745999 4 | // Writes the contents of the keyboard cheat sheet to your clipboard in markdown 5 | // format. Open user interface console, open keyboard cheat sheet popup, paste 6 | // code and hit Enter to execute. 7 | 8 | const sheet = document.querySelector(".keyboardShortcutsWrapper"); 9 | const heading = sheet.querySelector("h1").innerText; 10 | const pb1 = "\n\n"; 11 | const pb2 = `
`; 12 | let output = `${pb1}# ${heading}\n\n\n`; 13 | sheet.querySelectorAll(".category").forEach((category, key, arr) => { 14 | const caps = category.firstChild.innerText.toUpperCase(); 15 | output += ` \n`; 16 | category.querySelectorAll(".keycombo").forEach((command) => { 17 | output += ` \n"; 21 | else output += "
"; 22 | }); 23 | }); 24 | if (Object.is(arr.length - 1, key)) output += "
${caps}${pb2}
${command.innerText}`; 18 | command.querySelectorAll("input").forEach((combo, key, arr) => { 19 | output += combo.value; 20 | if (Object.is(arr.length - 1, key)) output += "
"; 25 | else output += "   \n"; 26 | }); 27 | copy(output); 28 | console.info(output); 29 | -------------------------------------------------------------------------------- /backup-search-engines.js: -------------------------------------------------------------------------------- 1 | // Backup Search Engines 2 | // version 2025.1.0 3 | // https://forum.vivaldi.net/post/277594 4 | // Adds functionality to backup and restore search engines in 5 | // vivaldi://settings/search. 6 | 7 | (function backupSearchEngines() { 8 | function msg(print) { 9 | clearTimeout(msgTimeout); 10 | if (print === "backup") { 11 | info.innerText = "Backup copied to clipboard"; 12 | } else if (print === "restore") { 13 | info.innerText = "Search engines restored"; 14 | } else { 15 | info.innerText = "Code error, aborted"; 16 | } 17 | msgTimeout = setTimeout(() => (info.innerText = ""), 5000); 18 | } 19 | 20 | function bringingItAllBackHome(remains, defaultsArray) { 21 | vivaldi.searchEngines.getTemplateUrls((engines) => { 22 | const getKeys = engines.templateUrls.map((e) => e.keyword); 23 | for (let i = 0; i < defaultsArray.length; i++) { 24 | const index = getKeys.lastIndexOf(defaultsArray[i][0]); 25 | const id = engines.templateUrls[index].id.toString(); 26 | const ds = defaultsArray[i][1]; 27 | vivaldi.searchEngines.setDefault(ds, id); 28 | } 29 | remains.forEach((remove) => { 30 | vivaldi.searchEngines.removeTemplateUrl(remove); 31 | }); 32 | msg("restore"); 33 | }); 34 | } 35 | 36 | function exec(collection) { 37 | vivaldi.searchEngines.getTemplateUrls((engines) => { 38 | const oldDefaults = [ 39 | engines.defaultImage, 40 | engines.defaultPrivate, 41 | engines.defaultSearch, 42 | ]; 43 | const newDefaults = [ 44 | collection.defaultImage, 45 | collection.defaultPrivate, 46 | collection.defaultSearch, 47 | collection.defaultSearchField, 48 | collection.defaultSearchFieldPrivate, 49 | collection.defaultSpeeddials, 50 | collection.defaultSpeeddialsPrivate, 51 | ]; 52 | engines.templateUrls.forEach((engine) => { 53 | if (oldDefaults.indexOf(engine.guid) === -1) { 54 | vivaldi.searchEngines.removeTemplateUrl(engine.guid); 55 | } 56 | }); 57 | console.info("restoring search engines..."); 58 | const defaultsArray = []; 59 | collection.templateUrls.forEach((collect) => { 60 | vivaldi.searchEngines.addTemplateUrl(collect, () => { 61 | console.info(` \u2022 ${collect.name}`); 62 | if (newDefaults.indexOf(collect.guid) > -1) { 63 | const indeces = newDefaults 64 | .map((e, i) => (e === collect.guid ? i : "")) 65 | .filter(String); 66 | indeces.forEach((index) => { 67 | let ds; 68 | if (index === 0) { 69 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_IMAGE; 70 | } else if (index === 1) { 71 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_PRIVATE; 72 | } else if (index === 2) { 73 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SEARCH; 74 | } else if (index === 3) { 75 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SEARCH_FIELD; 76 | } else if (index === 4) { 77 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SEARCH_FIELD_PRIVATE; 78 | } else if (index === 5) { 79 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SPEEDDIALS; 80 | } else { 81 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SPEEDDIALS_PRIVATE; 82 | } 83 | const tunnel = [collect.keyword, ds]; 84 | defaultsArray.push(tunnel); 85 | }); 86 | } 87 | }); 88 | }); 89 | const remains = [...new Set(oldDefaults)]; 90 | bringingItAllBackHome(remains, defaultsArray); 91 | }); 92 | } 93 | 94 | function restore(e) { 95 | e.preventDefault(); 96 | e.stopPropagation(); 97 | let backupCode; 98 | let collection; 99 | if (e.type === "paste") { 100 | const clipboardData = e.clipboardData; 101 | backupCode = clipboardData.getData("text"); 102 | } else { 103 | backupCode = e.dataTransfer.getData("text"); 104 | } 105 | try { 106 | collection = JSON.parse(backupCode); 107 | } catch (err) { 108 | msg("error"); 109 | return; 110 | } 111 | if ( 112 | "defaultImage" in collection && 113 | "defaultPrivate" in collection && 114 | "defaultSearch" in collection 115 | ) { 116 | exec(collection); 117 | } else { 118 | msg("error"); 119 | } 120 | } 121 | 122 | function backup() { 123 | vivaldi.searchEngines.getTemplateUrls((engines) => { 124 | const backupCode = JSON.stringify(engines, null, 2); 125 | navigator.clipboard.writeText(backupCode); 126 | msg("backup"); 127 | }); 128 | } 129 | 130 | function ui() { 131 | const check = document.getElementById("vm-bse-backup"); 132 | if (!check) { 133 | const place = document.querySelector( 134 | ".setting-section > div > .setting-group.unlimited > .setting-single" 135 | ); 136 | const btn = document.createElement("input"); 137 | btn.setAttribute("type", "button"); 138 | btn.setAttribute("value", "Backup"); 139 | btn.classList.add("vm-bse-backup"); 140 | place.insertBefore(btn, place.lastChild); 141 | btn.addEventListener("click", backup); 142 | const input = document.createElement("input"); 143 | input.setAttribute("type", "text"); 144 | input.setAttribute("placeholder", "Restore Backup"); 145 | input.classList.add("vm-bse-restore"); 146 | place.insertBefore(input, place.lastChild); 147 | input.addEventListener("paste", restore); 148 | input.addEventListener("drop", restore); 149 | info = document.createElement("span"); 150 | info.classList.add("vm-bse-msg"); 151 | place.insertBefore(info, place.lastChild); 152 | } 153 | } 154 | 155 | const css = ` 156 | .vm-bse-restore { 157 | width: 130px; 158 | margin-left: 6px; 159 | margin-top: 6px; 160 | } 161 | .vm-bse-restore::-webkit-input-placeholder { 162 | opacity: 1; 163 | color: var(--colorHighlightBg); 164 | text-align: center; 165 | } 166 | .vm-bse-msg { 167 | margin-left: 12px; 168 | } 169 | `; 170 | 171 | let msgTimeout; 172 | const settingsUrl = 173 | "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path="; 174 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 175 | if (changeInfo.url === `${settingsUrl}search`) { 176 | setTimeout(ui, 100); 177 | const check = document.getElementById("vm-bse-css"); 178 | if (!check) { 179 | const style = document.createElement("style"); 180 | style.id = "vm-bse-css"; 181 | style.innerHTML = css; 182 | document.getElementsByTagName("head")[0].appendChild(style); 183 | } 184 | } 185 | }); 186 | })(); 187 | -------------------------------------------------------------------------------- /binary-clock-widget/binary.css: -------------------------------------------------------------------------------- 1 | /* 2 | Binary Clock Widget 3 | version 2024.12.0 4 | Guide and updates ☛ https://forum.vivaldi.net/post/786622 5 | ———————— ⁂ ———————— 6 | */ 7 | html { 8 | container-type: normal; 9 | } 10 | svg circle { 11 | fill: black; 12 | } 13 | @container style(--isDarkTheme: 0) { 14 | body { 15 | --premix: color-mix(in oklab, var(--colorFg) 40%, white); 16 | } 17 | svg circle { 18 | fill: color-mix(in oklab, var(--premix) 70%, var(--colorHighlightBg)); 19 | } 20 | } 21 | @container style(--isDarkTheme: 1) { 22 | body { 23 | --premix: color-mix(in oklab, var(--colorFg) 70%, black); 24 | } 25 | svg circle { 26 | fill: color-mix(in hsl, var(--premix) 70%, var(--colorHighlightBg)); 27 | } 28 | } 29 | body { 30 | height: 100vh; 31 | width: 100vw; 32 | margin: 0; 33 | background: var(--colorBg); 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | } 38 | #container { 39 | height: 90vh; 40 | width: 90vw; 41 | } 42 | svg circle { 43 | opacity: 0.3; 44 | } 45 | svg circle.one { 46 | opacity: 0.8; 47 | } 48 | -------------------------------------------------------------------------------- /binary-clock-widget/binary.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | Binary Clock 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /binary-clock-widget/binary.js: -------------------------------------------------------------------------------- 1 | // Binary Clock Widget 2 | // version 2024.12.0 3 | // Guide and updates ☛ https://forum.vivaldi.net/post/786622 4 | // ———————— ⁂ ———————— 5 | 6 | "use strict"; 7 | 8 | function timer(time) { 9 | const jump = (60 - time.getSeconds()) * 1000 - time.getMilliseconds(); 10 | setTimeout(() => update(), jump); 11 | } 12 | 13 | function update() { 14 | const now = new Date(); 15 | timer(now); 16 | const binary_hours = now.getHours().toString(2).padStart(5, "0"); 17 | const binary_minutes = now.getMinutes().toString(2).padStart(6, "0"); 18 | const binary = binary_hours.concat(binary_minutes).split(""); 19 | circles.forEach((circle, i) => { 20 | if (binary[i] === "1") circle.classList.add("one"); 21 | else circle.classList.remove("one"); 22 | }); 23 | } 24 | 25 | const circles = document.querySelectorAll("svg circle"); 26 | update(); 27 | -------------------------------------------------------------------------------- /binary-clock-widget/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luetage/vivaldi_modding/f2b41ce58e5a1f753e4076c4bc0ddb92b8d85c24/binary-clock-widget/icons/favicon.png -------------------------------------------------------------------------------- /changing-buttons.css: -------------------------------------------------------------------------------- 1 | /* 2 | Changing Buttons 3 | version 2021.10.0 4 | https://forum.vivaldi.net/post/155310 5 | Changes home and trash button svgs. Many more examples in linked topic. 6 | */ 7 | 8 | .UrlBar button[title="Go to homepage"] svg path { 9 | d: path( 10 | "M4 6 h18 v2.5 h-3.5 v11.5 h-2.5 v-9.5 h-6 v9.5 h-2.5 v-11.5 h-3.5 Z" 11 | ); 12 | } 13 | .toggle-trash.button-toolbar button svg path { 14 | d: path("M6.5 8 h13 v2 h-13 Z M6.5 12 h13 v2 h-13 Z M9 16 h8 v2 h-8 Z"); 15 | } 16 | -------------------------------------------------------------------------------- /collapse-keyboard-settings.js: -------------------------------------------------------------------------------- 1 | // Collapse Keyboard Settings 2 | // version 2024.2.0 3 | // https://forum.vivaldi.net/post/501591 4 | // Automatically collapses the keyboard settings items in 5 | // vivaldi://settings/keyboard. 6 | 7 | (function collapseKeyboardSettings() { 8 | const settingsUrl = 9 | "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path="; 10 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 11 | if (changeInfo.url === `${settingsUrl}keyboard`) { 12 | setTimeout(() => { 13 | document.querySelector(".keyboard-shortcut-list .category.show button").click(); 14 | }, 100); 15 | } 16 | }); 17 | })(); 18 | -------------------------------------------------------------------------------- /fortune-widget/fortune.css: -------------------------------------------------------------------------------- 1 | /* 2 | Fortune Widget 3 | version 2024.12.0 4 | Guide and updates ☛ https://forum.vivaldi.net/post/787840 5 | ———————— ⁂ ———————— 6 | */ 7 | html { 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "IBM Plex Sans", 9 | Inter, Ubuntu, sans-serif; 10 | font-size: 100%; 11 | font-weight: 400; 12 | scrollbar-width: thin; 13 | scrollbar-color: color-mix(in oklab, var(--colorFg) 40%, transparent) 14 | color-mix(in oklab, var(--colorBg) 95%, black); 15 | } 16 | body { 17 | height: calc(100vh - 2rem); 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-between; 21 | color: var(--colorFg); 22 | background: var(--colorBg); 23 | cursor: default; 24 | margin: 1rem; 25 | } 26 | #text { 27 | margin: 0; 28 | line-height: 1.3; 29 | overflow-wrap: break-word; 30 | hyphens: auto; 31 | hyphenate-character: "\2010"; 32 | } 33 | footer { 34 | text-align: center; 35 | opacity: 0.6; 36 | padding: 0.8rem 0 1rem; 37 | user-select: none; 38 | } 39 | svg { 40 | fill: var(--colorFg); 41 | height: 1rem; 42 | } 43 | -------------------------------------------------------------------------------- /fortune-widget/fortune.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | Fortune 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /fortune-widget/fortune.js: -------------------------------------------------------------------------------- 1 | // Fortune Widget 2 | // version 2024.12.0 3 | // Guide and updates ☛ https://forum.vivaldi.net/post/787840 4 | // ———————— ⁂ ———————— 5 | 6 | "use strict"; 7 | 8 | function typ(x) { 9 | const bold = /(?$1$2"); 12 | x = x.replace(italic, "$1$2"); 13 | x = x.replace(/---/g, "\u2014"); 14 | x = x.replace(/--/g, "\u2013"); 15 | x = x.replace(/(\d)-(\d)/g, "$1\u2013$2"); 16 | x = x.replace(/(\w)-(\w)/g, "$1\u2010$2"); 17 | x = x.replace(/ - /g, " \u2013 "); 18 | x = x.replace(/\n\n/g, "
"); 19 | return x; 20 | } 21 | 22 | function write_fortune(el) { 23 | const random = Math.floor(Math.random() * data.length); 24 | const rnd = data[random]; 25 | const fortune = rnd.author 26 | ? `${typ(rnd.quote)}
\u2015\u202f${typ(rnd.author)}` 27 | : typ(rnd.quote); 28 | console.info(`${random}: ${fortune}`); 29 | el.innerHTML = fortune; 30 | } 31 | 32 | write_fortune(document.getElementById("text")); 33 | -------------------------------------------------------------------------------- /fortune-widget/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luetage/vivaldi_modding/f2b41ce58e5a1f753e4076c4bc0ddb92b8d85c24/fortune-widget/icons/favicon.png -------------------------------------------------------------------------------- /history-clock.js: -------------------------------------------------------------------------------- 1 | /* 2 | History Clock 3 | https://forum.vivaldi.net/topic/36080/history-clock-keeping-easter-egg-alive 4 | Sets the time on the history clock panel icon, an easter egg featured by Vivaldi on 1st April 2019. 5 | */ 6 | 7 | function historyClock() { 8 | const clock = document.querySelector('#switch button.history'); 9 | var time = new Date(); 10 | var hours = time.getHours(); 11 | var minutes = time.getMinutes(); 12 | if (clockSetInt === true) { 13 | if (clockRelax !== -1 && clockRelax !== minutes) { 14 | clearInterval(clockTimer) 15 | setInterval(historyClock, 60000); 16 | clockSetInt = false; 17 | } 18 | clockRelax = minutes; 19 | } 20 | if (clock) { 21 | clock.style = '--timeHourRotation: rotate(' + Math.floor(hours*30+minutes/2) + 'deg); --timeMinuteRotation: rotate(' + minutes*6 + 'deg)'; 22 | } 23 | }; 24 | 25 | var clockSetInt = true; 26 | var clockRelax = -1; 27 | var clockTimer = setInterval(historyClock, 1000); 28 | -------------------------------------------------------------------------------- /import-export-themes.js: -------------------------------------------------------------------------------- 1 | // Theme Import and Export 2 | // version 2021.10.0 3 | // https://forum.vivaldi.net/topic/33154/import-and-export-themes 4 | // Adds functionality to import, export, backup, sort and move themes to 5 | // Vivaldi's settings page. 6 | 7 | (function () { 8 | function _checkImport() { 9 | // written by tam710562 10 | if ( 11 | typeof _test.colors !== "object" || 12 | typeof _test.colors.accentBg !== "string" || 13 | !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.accentBg) || 14 | typeof _test.colors.baseBg !== "string" || 15 | !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.baseBg) || 16 | typeof _test.colors.baseFg !== "string" || 17 | !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.baseFg) || 18 | typeof _test.colors.highlightBg !== "string" || 19 | !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.highlightBg) || 20 | typeof _test.name !== "string" || 21 | typeof _test.settings !== "object" || 22 | typeof _test.settings.accentFromPage !== "boolean" || 23 | typeof _test.settings.accentOnWindow !== "boolean" || 24 | (typeof _test.settings.borderRadius !== "number" && 25 | typeof _test.settings.borderRadius !== "string") || 26 | typeof _test.settings.tabsTransparent !== "boolean" || 27 | typeof _test.version !== "number" 28 | ) { 29 | return false; 30 | } else { 31 | return true; 32 | } 33 | } 34 | 35 | function _message(pnt) { 36 | clearTimeout(_timeout); 37 | if (pnt === "export") { 38 | _msg.innerText = "Theme code copied to clipboard"; 39 | } else if (pnt === "backup") { 40 | _msg.innerText = "Backup copied to clipboard"; 41 | } else if (pnt === "import") { 42 | _msg.innerText = "Theme imported"; 43 | } else if (pnt === "restore") { 44 | _msg.innerText = "Backup imported"; 45 | } else if (pnt === "notice") { 46 | _msg.innerText = "Nothing to import. Check console.log"; 47 | } else if (pnt === "sort") { 48 | _msg.innerText = "User themes sorted alphabetically"; 49 | } else { 50 | _msg.innerText = "Theme code error"; 51 | } 52 | _timeout = setTimeout(function () { 53 | _msg.innerText = ""; 54 | }, 5000); 55 | } 56 | 57 | function _importBackup() { 58 | chrome.storage.local.get({ THEMES_USER: "" }, function (res) { 59 | var userThemes = res.THEMES_USER; 60 | console.log("Importing themes..."); 61 | for (i = 0; i < _set.length; i++) { 62 | _test = _set[i]; 63 | var test = _checkImport; 64 | if (test()) { 65 | var compare = userThemes.findIndex((x) => x.name == _set[i].name); 66 | if (compare === -1) { 67 | var ok = true; 68 | userThemes.push(_set[i]); 69 | console.log(_set[i].name + " imported"); 70 | } else { 71 | console.log(_set[i].name + " is a duplicate"); 72 | } 73 | } else { 74 | console.log(_set[i].name + " failed"); 75 | } 76 | } 77 | if (ok === true) { 78 | chrome.storage.local.set({ THEMES_USER: userThemes }, function () { 79 | _message("restore"); 80 | }); 81 | } else { 82 | _message("notice"); 83 | } 84 | }); 85 | } 86 | 87 | function _importTheme(e) { 88 | e.stopPropagation(); 89 | e.preventDefault(); 90 | if (e.type === "paste") { 91 | var clipboardData = e.clipboardData; 92 | var themeCode = clipboardData.getData("text"); 93 | } else { 94 | var themeCode = e.dataTransfer.getData("text"); 95 | } 96 | try { 97 | _set = JSON.parse(themeCode); 98 | } catch (err) { 99 | _message("error"); 100 | return; 101 | } 102 | if (Object.keys(_set)[0] === "colors") { 103 | _test = _set; 104 | var test = _checkImport; 105 | if (test()) { 106 | const nameField = document.querySelector(".theme-name"); 107 | nameField.select(); 108 | document.execCommand("insertText", false, _set.name); 109 | chrome.storage.local.get({ THEMES_USER: "" }, function (imp) { 110 | var userThemes = imp.THEMES_USER; 111 | for (i = 0; i < userThemes.length; i++) { 112 | if (userThemes[i].name === nameField.value) { 113 | _set.name = nameField.value; 114 | userThemes[i] = _set; 115 | chrome.storage.local.set({ 116 | THEMES_USER: userThemes, 117 | BROWSER_COLOR_ACCENT_BG: _set.colors.accentBg, 118 | BROWSER_COLOR_BG: _set.colors.baseBg, 119 | BROWSER_COLOR_FG: _set.colors.baseFg, 120 | BROWSER_COLOR_HIGHLIGHT_BG: _set.colors.highlightBg, 121 | TABCOLOR_BEHIND_TABS: _set.settings.accentOnWindow, 122 | USE_TABCOLOR: _set.settings.accentFromPage, 123 | BORDER_RADIUS: _set.settings.borderRadius, 124 | USE_TAB_TRANSPARENT_TABS: _set.settings.tabsTransparent, 125 | THEME_CURRENT: _set.name, 126 | }); 127 | _message("import"); 128 | break; 129 | } 130 | } 131 | }); 132 | } else { 133 | _message("error"); 134 | } 135 | } else if (Object.keys(_set)[0] === "0") { 136 | _importBackup(); 137 | } else { 138 | _message("error"); 139 | } 140 | } 141 | 142 | function _exportTheme(event) { 143 | if (event.altKey) { 144 | var backup = true; 145 | } 146 | if (event.shiftKey) { 147 | var order = true; 148 | } 149 | chrome.storage.local.get( 150 | { 151 | THEME_CURRENT: "", 152 | THEMES_USER: "", 153 | }, 154 | function (exp) { 155 | const themeName = exp.THEME_CURRENT; 156 | const userThemes = exp.THEMES_USER; 157 | if (backup === true) { 158 | const themeCode = JSON.stringify(userThemes); 159 | navigator.clipboard.writeText(themeCode); 160 | _message("backup"); 161 | } else if (order === true) { 162 | userThemes.sort(function (a, b) { 163 | return a.name.localeCompare(b.name); 164 | }); 165 | chrome.storage.local.set({ THEMES_USER: userThemes }, function () { 166 | _message("sort"); 167 | }); 168 | } else { 169 | for (i = 0; i < userThemes.length; i++) { 170 | if (userThemes[i].name === themeName) { 171 | const themeCode = JSON.stringify(userThemes[i]); 172 | navigator.clipboard.writeText(themeCode); 173 | _message("export"); 174 | break; 175 | } 176 | } 177 | } 178 | } 179 | ); 180 | } 181 | 182 | function _moveTheme() { 183 | chrome.storage.local.get( 184 | { 185 | THEME_CURRENT: "", 186 | THEMES_USER: "", 187 | }, 188 | function (mv) { 189 | const themeName = mv.THEME_CURRENT; 190 | const userThemes = mv.THEMES_USER; 191 | var index = userThemes.findIndex((x) => x.name == themeName); 192 | if (index !== -1) { 193 | if (_toMove === "left") { 194 | if (index !== 0) { 195 | var fromI = userThemes[index]; 196 | var toI = userThemes[index - 1]; 197 | userThemes[index - 1] = fromI; 198 | userThemes[index] = toI; 199 | } else { 200 | return; 201 | } 202 | } else { 203 | var last = userThemes.length - 1; 204 | if (index < last) { 205 | var fromI = userThemes[index]; 206 | var toI = userThemes[index + 1]; 207 | userThemes[index + 1] = fromI; 208 | userThemes[index] = toI; 209 | } else { 210 | return; 211 | } 212 | } 213 | chrome.storage.local.set({ THEMES_USER: userThemes }); 214 | } 215 | } 216 | ); 217 | } 218 | 219 | function createPort() { 220 | if ( 221 | document.querySelector(_themeBtn).classList.contains("button-pressed") 222 | ) { 223 | const cont = document.querySelector(".theme-metadata"); 224 | const importBtn = document.createElement("input"); 225 | importBtn.setAttribute("type", "text"); 226 | importBtn.setAttribute("placeholder", "Import"); 227 | importBtn.id = "importTheme"; 228 | cont.appendChild(importBtn); 229 | const exportBtn = document.createElement("input"); 230 | exportBtn.setAttribute("type", "submit"); 231 | exportBtn.classList.add("primary"); 232 | exportBtn.setAttribute("value", "Export"); 233 | exportBtn.setAttribute( 234 | "title", 235 | "Click to export theme\nAlt-click to backup all themes\nShift-click to sort themes" 236 | ); 237 | exportBtn.id = "exportTheme"; 238 | cont.appendChild(exportBtn); 239 | _msg = document.createElement("span"); 240 | _msg.id = "modInfo"; 241 | cont.appendChild(_msg); 242 | document 243 | .getElementById("exportTheme") 244 | .addEventListener("click", _exportTheme); 245 | const importInput = document.getElementById("importTheme"); 246 | importInput.addEventListener("paste", _importTheme); 247 | importInput.addEventListener("drop", _importTheme); 248 | _timeout = {}; 249 | } 250 | } 251 | 252 | function portThemes() { 253 | const styleCheck = document.getElementById("portThemes"); 254 | if (!styleCheck) { 255 | const style = document.createElement("style"); 256 | style.id = "portThemes"; 257 | style.innerHTML = 258 | ".move-left button:focus, .move-right button:focus {border-color: var(--colorBorder) !important;box-shadow: none !important;}#importTheme, #exportTheme {width: 80px;margin-left: 6px;}#importTheme::-webkit-input-placeholder {opacity: 1;color: var(--colorHighlightBg);text-align: center;}#modInfo {margin-top: 6px;margin-left: 12px;}"; 259 | document.getElementsByTagName("head")[0].appendChild(style); 260 | } 261 | const modCheck = document.querySelector(".move-left"); 262 | if (!modCheck) { 263 | const group = document.createElement("div"); 264 | group.classList.add("toolbar", "toolbar-group"); 265 | group.innerHTML = 266 | '

'; 267 | document 268 | .querySelector(_themeBtn) 269 | .parentNode.parentNode.appendChild(group); 270 | document 271 | .querySelector(".move-left") 272 | .addEventListener("click", function () { 273 | _toMove = "left"; 274 | _moveTheme(); 275 | }); 276 | document 277 | .querySelector(".move-right") 278 | .addEventListener("click", function () { 279 | _toMove = "right"; 280 | _moveTheme(); 281 | }); 282 | document.querySelector(_themeBtn).addEventListener("click", function () { 283 | setTimeout(createPort, 50); 284 | }); 285 | } 286 | } 287 | 288 | const settingsUrl = 289 | "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path="; 290 | const _themeBtn = 291 | ".setting-group.unlimited > .toolbar.toolbar-default > .button-toolbar > button"; 292 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 293 | if (changeInfo.url === `${settingsUrl}themes`) { 294 | setTimeout(portThemes, 100); 295 | } 296 | }); 297 | })(); 298 | -------------------------------------------------------------------------------- /improved-extension-toggle.js: -------------------------------------------------------------------------------- 1 | /* 2 | Improved Extension Toggle 3 | https://forum.vivaldi.net/topic/20373/improved-extension-toggle 4 | Keep selected extension buttons visible, show/hide others in extension container. Compatibel with Vivaldi's extension dropdown. 5 | */ 6 | 7 | function extensionToggle() { 8 | 9 | // Add extension IDs of buttons you want to keep permanently visible to the array. Remove example ID. 10 | var selectIDs = 11 | [ 12 | 'extensionPopupIcons', 13 | 'toggleMod', 14 | 'himccccaelhgphommckogopgpddngimf' 15 | ]; 16 | 17 | // create the button 18 | const ext = document.querySelector('.toolbar-extensions'); 19 | var div = document.createElement('div'); 20 | div.classList.add('button-toolbar', 'toggleMod'); 21 | div.innerHTML = ''; 22 | ext.appendChild(div); 23 | const togstat = document.querySelector('.toggleMod'); 24 | togstat.style.order = '1'; 25 | const togstatSVG = document.querySelector('.toggleMod button svg'); 26 | togstatSVG.style.height = '14px'; 27 | togstatSVG.style.width = '14px'; 28 | toggleLogic(); 29 | togstat.addEventListener('click', function() { 30 | if (togstat.classList.contains('expanded')) { 31 | togstat.classList.remove('expanded'); 32 | } 33 | else { 34 | togstat.classList.add('expanded'); 35 | } 36 | toggleLogic(); 37 | }); 38 | 39 | // toggle it! 40 | function toggleLogic() { 41 | const buttons = document.querySelectorAll('.toolbar-extensions .button-toolbar'); 42 | for (var i=0; i < buttons.length; i++) { 43 | if (selectIDs.indexOf(buttons[i].classList.item(1)) != -1 || togstat.classList.contains('expanded')) { 44 | buttons[i].style.display = 'flex'; 45 | } 46 | else { 47 | buttons[i].style.display = 'none'; 48 | } 49 | } 50 | }; 51 | }; 52 | 53 | var appendChild = Element.prototype.appendChild; 54 | Element.prototype.appendChild = function () { 55 | if (arguments[0].tagName === 'DIV') { 56 | setTimeout(function() { 57 | if (this.classList.contains('toolbar-extensions')) { 58 | const extToggle = document.querySelector('.toggleMod'); 59 | if (!extToggle) { 60 | extensionToggle(); 61 | } 62 | } 63 | }.bind(this, arguments[0])); 64 | } 65 | return appendChild.apply(this, arguments); 66 | }; 67 | -------------------------------------------------------------------------------- /internal-page-theme.js: -------------------------------------------------------------------------------- 1 | // Internal Page Theme 2 | // version 2021.9.0 3 | // https://forum.vivaldi.net/topic/57420/theme-internal-pages 4 | // Injects CSS into the internal pages vivaldi://about and about:blank, uses 5 | // native theme colors. Relies on chrome.tabs restore method 6 | // ☛ https://forum.vivaldi.net/topic/57191/restore-methods-for-chrome-tabs 7 | 8 | (function () { 9 | function intpages(id, page) { 10 | const bg = document.documentElement.style.getPropertyValue("--colorBg"); 11 | const bgdark = 12 | document.documentElement.style.getPropertyValue("--colorBgDark"); 13 | const fg = document.documentElement.style.getPropertyValue("--colorFg"); 14 | const fgintense = 15 | document.documentElement.style.getPropertyValue("--colorFgIntense"); 16 | const hi = 17 | document.documentElement.style.getPropertyValue("--colorHighlightBg"); 18 | if (page === "chrome://version/") { 19 | var sendit = ` 20 | html { 21 | background-image: linear-gradient(to bottom, transparent 50%, ${bg} 50%), linear-gradient(to right, ${bgdark} 50%, ${bg} 50%) !important; 22 | background-size: 10px 10px, 10px 10px !important; 23 | } 24 | .label, #company { 25 | color: ${fgintense}; 26 | font-size: 0.9em !important; 27 | } 28 | .version, #slogan { 29 | color: ${fg} !important; 30 | font-size: 0.85em !important; 31 | } 32 | .version, #useragent { 33 | font-family: unset !important; 34 | } 35 | #copyright { 36 | font-size: 0.8em !important; 37 | } 38 | a { 39 | color: ${hi}; 40 | } 41 | `; 42 | } else if (page === "about:blank") { 43 | var sendit = ` 44 | body { 45 | background-image: linear-gradient(to bottom, transparent 50%, ${bg} 50%), linear-gradient(to right, ${bgdark} 50%, ${bg} 50%); 46 | background-size: 10px 10px, 10px 10pt; 47 | } 48 | `; 49 | } 50 | chrome.tabs.insertCSS(id, { code: sendit }); 51 | } 52 | 53 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 54 | if ( 55 | changeInfo.url === "chrome://version/" || 56 | changeInfo.title === "About Version" || 57 | changeInfo.url === "about:blank" 58 | ) { 59 | intpages(tabId, changeInfo.url); 60 | } 61 | }); 62 | })(); 63 | -------------------------------------------------------------------------------- /legacy-panel.css: -------------------------------------------------------------------------------- 1 | /* 2 | Legacy Panel 3 | https://forum.vivaldi.net/topic/14723/legacy-panel 4 | Panel container themed like in the prerelease versions of Vivaldi (dark panel on light theme). 5 | */ 6 | 7 | .theme-light #switch, .theme-light #panels-container::after { 8 | background: #2d2d2d; 9 | box-shadow: none !important; 10 | } 11 | .theme-light #switch button:hover, .theme-light #switch button:focus { 12 | box-shadow: none !important; 13 | } 14 | .theme-light #switch button svg { 15 | fill: #9c9c9c; 16 | } 17 | .theme-light #switch button.active svg { 18 | fill: #484848; 19 | } 20 | .theme-light #switch button.active, .theme-light #switch button.active:hover { 21 | background: #fff !important; 22 | } 23 | .theme-light #switch button:hover { 24 | background: #474747 !important; 25 | } 26 | .theme-light #switch button, .theme-dark #switch .webviewbtn img { 27 | background: transparent !important; 28 | } 29 | -------------------------------------------------------------------------------- /load/backup-search-engines.js: -------------------------------------------------------------------------------- 1 | // Backup Search Engines 2 | // version 2025.1.0 3 | // https://forum.vivaldi.net/post/277594 4 | // Adds functionality to backup and restore search engines in 5 | // vivaldi://settings/search. 6 | 7 | (function backupSearchEngines() { 8 | function msg(print) { 9 | clearTimeout(msgTimeout); 10 | if (print === "backup") { 11 | info.innerText = "Backup copied to clipboard"; 12 | } else if (print === "restore") { 13 | info.innerText = "Search engines restored"; 14 | } else { 15 | info.innerText = "Code error, aborted"; 16 | } 17 | msgTimeout = setTimeout(() => (info.innerText = ""), 5000); 18 | } 19 | 20 | function bringingItAllBackHome(remains, defaultsArray) { 21 | vivaldi.searchEngines.getTemplateUrls((engines) => { 22 | const getKeys = engines.templateUrls.map((e) => e.keyword); 23 | for (let i = 0; i < defaultsArray.length; i++) { 24 | const index = getKeys.lastIndexOf(defaultsArray[i][0]); 25 | const id = engines.templateUrls[index].id.toString(); 26 | const ds = defaultsArray[i][1]; 27 | vivaldi.searchEngines.setDefault(ds, id); 28 | } 29 | remains.forEach((remove) => { 30 | vivaldi.searchEngines.removeTemplateUrl(remove); 31 | }); 32 | msg("restore"); 33 | }); 34 | } 35 | 36 | function exec(collection) { 37 | vivaldi.searchEngines.getTemplateUrls((engines) => { 38 | const oldDefaults = [ 39 | engines.defaultImage, 40 | engines.defaultPrivate, 41 | engines.defaultSearch, 42 | ]; 43 | const newDefaults = [ 44 | collection.defaultImage, 45 | collection.defaultPrivate, 46 | collection.defaultSearch, 47 | collection.defaultSearchField, 48 | collection.defaultSearchFieldPrivate, 49 | collection.defaultSpeeddials, 50 | collection.defaultSpeeddialsPrivate, 51 | ]; 52 | engines.templateUrls.forEach((engine) => { 53 | if (oldDefaults.indexOf(engine.guid) === -1) { 54 | vivaldi.searchEngines.removeTemplateUrl(engine.guid); 55 | } 56 | }); 57 | console.info("restoring search engines..."); 58 | const defaultsArray = []; 59 | collection.templateUrls.forEach((collect) => { 60 | vivaldi.searchEngines.addTemplateUrl(collect, () => { 61 | console.info(` \u2022 ${collect.name}`); 62 | if (newDefaults.indexOf(collect.guid) > -1) { 63 | const indeces = newDefaults 64 | .map((e, i) => (e === collect.guid ? i : "")) 65 | .filter(String); 66 | indeces.forEach((index) => { 67 | let ds; 68 | if (index === 0) { 69 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_IMAGE; 70 | } else if (index === 1) { 71 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_PRIVATE; 72 | } else if (index === 2) { 73 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SEARCH; 74 | } else if (index === 3) { 75 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SEARCH_FIELD; 76 | } else if (index === 4) { 77 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SEARCH_FIELD_PRIVATE; 78 | } else if (index === 5) { 79 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SPEEDDIALS; 80 | } else { 81 | ds = vivaldi.searchEngines.DefaultType.DEFAULT_SPEEDDIALS_PRIVATE; 82 | } 83 | const tunnel = [collect.keyword, ds]; 84 | defaultsArray.push(tunnel); 85 | }); 86 | } 87 | }); 88 | }); 89 | const remains = [...new Set(oldDefaults)]; 90 | bringingItAllBackHome(remains, defaultsArray); 91 | }); 92 | } 93 | 94 | function restore(e) { 95 | e.preventDefault(); 96 | e.stopPropagation(); 97 | let backupCode; 98 | let collection; 99 | if (e.type === "paste") { 100 | const clipboardData = e.clipboardData; 101 | backupCode = clipboardData.getData("text"); 102 | } else { 103 | backupCode = e.dataTransfer.getData("text"); 104 | } 105 | try { 106 | collection = JSON.parse(backupCode); 107 | } catch (err) { 108 | msg("error"); 109 | return; 110 | } 111 | if ( 112 | "defaultImage" in collection && 113 | "defaultPrivate" in collection && 114 | "defaultSearch" in collection 115 | ) { 116 | exec(collection); 117 | } else { 118 | msg("error"); 119 | } 120 | } 121 | 122 | function backup() { 123 | vivaldi.searchEngines.getTemplateUrls((engines) => { 124 | const backupCode = JSON.stringify(engines, null, 2); 125 | navigator.clipboard.writeText(backupCode); 126 | msg("backup"); 127 | }); 128 | } 129 | 130 | function ui() { 131 | const check = document.getElementById("vm-bse-backup"); 132 | if (!check) { 133 | const place = document.querySelector( 134 | ".setting-section > div > .setting-group.unlimited > .setting-single" 135 | ); 136 | const btn = document.createElement("input"); 137 | btn.setAttribute("type", "button"); 138 | btn.setAttribute("value", "Backup"); 139 | btn.classList.add("vm-bse-backup"); 140 | place.insertBefore(btn, place.lastChild); 141 | btn.addEventListener("click", backup); 142 | const input = document.createElement("input"); 143 | input.setAttribute("type", "text"); 144 | input.setAttribute("placeholder", "Restore Backup"); 145 | input.classList.add("vm-bse-restore"); 146 | place.insertBefore(input, place.lastChild); 147 | input.addEventListener("paste", restore); 148 | input.addEventListener("drop", restore); 149 | info = document.createElement("span"); 150 | info.classList.add("vm-bse-msg"); 151 | place.insertBefore(info, place.lastChild); 152 | } 153 | } 154 | 155 | const css = ` 156 | .vm-bse-restore { 157 | width: 130px; 158 | margin-left: 6px; 159 | margin-top: 6px; 160 | } 161 | .vm-bse-restore::-webkit-input-placeholder { 162 | opacity: 1; 163 | color: var(--colorHighlightBg); 164 | text-align: center; 165 | } 166 | .vm-bse-msg { 167 | margin-left: 12px; 168 | } 169 | `; 170 | 171 | let msgTimeout; 172 | const settingsUrl = 173 | "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path="; 174 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 175 | if (changeInfo.url === `${settingsUrl}search`) { 176 | setTimeout(ui, 100); 177 | const check = document.getElementById("vm-bse-css"); 178 | if (!check) { 179 | const style = document.createElement("style"); 180 | style.id = "vm-bse-css"; 181 | style.innerHTML = css; 182 | document.getElementsByTagName("head")[0].appendChild(style); 183 | } 184 | } 185 | }); 186 | })(); 187 | -------------------------------------------------------------------------------- /load/collapse-keyboard-settings.js: -------------------------------------------------------------------------------- 1 | // Collapse Keyboard Settings 2 | // version 2022.4.0 3 | // https://forum.vivaldi.net/post/501591 4 | // Automatically collapses the keyboard settings items in 5 | // vivaldi://settings/keyboard. 6 | 7 | (function collapseKeyboardSettings() { 8 | const settingsUrl = 9 | "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path="; 10 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 11 | if (changeInfo.url === `${settingsUrl}keyboard`) { 12 | setTimeout(() => { 13 | document.querySelector(".category.show button").click(); 14 | }, 100); 15 | } 16 | }); 17 | })(); 18 | -------------------------------------------------------------------------------- /load/main.css: -------------------------------------------------------------------------------- 1 | #main { 2 | --colormix: color-mix( 3 | in hsl, 4 | var(--colorFgFadedMost) 70%, 5 | var(--colorHighlightBg) 6 | ); 7 | } 8 | 9 | /* font */ 10 | #browser.linux, 11 | #browser.linux ~ div, 12 | #browser.linux button, 13 | #browser.linux input, 14 | #browser.linux select, 15 | #browser.linux textarea { 16 | font-family: "Adwaita Sans", system-ui, sans-serif; 17 | /* round quotes and commas, lower-case L with tail, tabular numbers */ 18 | /* font-feature-settings: "ss03", "cv05", "tnum"; */ 19 | } 20 | 21 | /* header */ 22 | .window-buttongroup { 23 | display: none !important; 24 | } 25 | .linux #tabs-container:not(.none).top, 26 | #browser.linux:not(.native).address-top 27 | .toolbar:has(.window-buttongroup.on-mainbar) { 28 | padding-right: unset; 29 | } 30 | .alt#browser.linux:not(.native).address-top 31 | .toolbar:has(.window-buttongroup.on-mainbar) { 32 | padding-left: unset; 33 | } 34 | .alt.disable-titlebar.address-top#browser:not(.tabs-top) .vivaldi { 35 | top: unset; 36 | } 37 | #browser.linux.address-top .toolbar:has(.window-buttongroup.on-mainbar) { 38 | min-height: calc(35px / var(--uiZoomLevel)); 39 | } 40 | #browser.linux.address-top 41 | .toolbar:has(.window-buttongroup.on-mainbar) 42 | .vivaldi { 43 | height: 35px; 44 | width: 34px; 45 | margin: 0; 46 | padding: 0; 47 | } 48 | #browser.linux.address-top 49 | .toolbar:has(.window-buttongroup.on-mainbar) 50 | .vivaldi 51 | svg { 52 | fill-opacity: 0.33; 53 | } 54 | #browser.linux.address-top 55 | .toolbar:has(.window-buttongroup.on-mainbar) 56 | .vivaldi 57 | .expand-arrow { 58 | display: none; 59 | } 60 | 61 | /* addressfield */ 62 | .UrlFragment-Wrapper:not(.UrlFragment-Wrapper--ShouldHighlight) { 63 | --HighlightColor: var(--colorFg); 64 | --LowlightColor: var(--colorFg); 65 | } 66 | .UrlFragment-Wrapper--ShouldHighlight { 67 | --HighlightColor: var(--colorFg); 68 | --LowlightColor: var(--colorFgFadedMost); 69 | } 70 | .UrlFragment-Wrapper--ShouldHighlight .UrlFragment-HostFragment-Subdomain, 71 | .UrlFragment-Wrapper--ShouldHighlight .UrlFragment-HostFragment-Basedomain, 72 | .UrlFragment-Wrapper--ShouldHighlight .UrlFragment-HostFragment-TLD { 73 | font-weight: bold; 74 | } 75 | #browser .BookmarkButton .bookmark-animated-fill { 76 | fill: var(--colormix); 77 | } 78 | .permission-denied circle { 79 | fill: var(--colorHighlightBg); 80 | } 81 | .permission-denied path { 82 | fill: var(--colorHighlightFg); 83 | } 84 | 85 | /* status info */ 86 | .StatusInfo .UrlFragments, 87 | .StatusInfo-Content { 88 | color: var(--colorFgFaded); 89 | } 90 | 91 | /* panel container */ 92 | #window-panel .vivaldi-tree .title.active::before { 93 | content: "⌑"; 94 | } 95 | 96 | /* M3 */ 97 | #mail_panel h1 { 98 | font-size: 0; 99 | } 100 | #mail_panel h1::before { 101 | font-size: 16px; 102 | content: "M3"; 103 | } 104 | 105 | /* clock feature */ 106 | .ClockGraphic-Second-Hand { 107 | fill: var(--colorHighlightBg); 108 | } 109 | .ClockPopup-Header, 110 | .ClockPopup-Passed { 111 | display: none !important; 112 | } 113 | 114 | /* startpage */ 115 | .dashboard-widget .url-wrapper { 116 | display: none; 117 | } 118 | 119 | /* break mode */ 120 | button.HUD { 121 | display: none; 122 | } 123 | 124 | /* extensions drop-down menu */ 125 | .extensionIconPopupMenu { 126 | --popupWidth: fit-content !important; 127 | } 128 | .extensionIconPopupMenu > .toolbar { 129 | overflow: hidden auto; 130 | flex-direction: column; 131 | flex-wrap: nowrap; 132 | align-items: stretch; 133 | } 134 | .extensionIconPopupMenu .ExtensionDropdownIcon > button { 135 | justify-content: flex-start; 136 | padding: 0.5rem; 137 | } 138 | .extensionIconPopupMenu .ExtensionDropdownIcon > button::after { 139 | content: attr(title); 140 | overflow: hidden; 141 | text-overflow: ellipsis; 142 | } 143 | -------------------------------------------------------------------------------- /load/monochrome-icons.js: -------------------------------------------------------------------------------- 1 | // Monochrome icons 2 | // version 2024.11.1 3 | // https://forum.vivaldi.net/post/791344 4 | // Makes web panel thumbnails monochrome depending on theme colors. 5 | 6 | (async function monochrome_icons() { 7 | "use strict"; 8 | 9 | function convert(srgb) { 10 | const val = srgb.slice(11, -1).trim().split(/\s+/); 11 | const r = Math.round(parseFloat(val[0]) * 255); 12 | const g = Math.round(parseFloat(val[1]) * 255); 13 | const b = Math.round(parseFloat(val[2]) * 255); 14 | const calc = 15 | (Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b) * 180) / Math.PI; 16 | return (calc - 38.71).toFixed(2); 17 | } 18 | 19 | function theme(css) { 20 | const color = document.getElementById("main"); 21 | color.style = 22 | "color: color-mix(in hsl, var(--colorFgFadedMost) 70%, var(--colorHighlightBg));"; 23 | const srgb = getComputedStyle(color).getPropertyValue("color"); 24 | color.removeAttribute("style"); 25 | const hue = convert(srgb); 26 | console.info(`hue-change: ${hue}°`); 27 | css.innerHTML = ` 28 | .button-toolbar-webpanel img { 29 | filter: brightness(0.77) sepia(1) hue-rotate(${hue}deg); 30 | } 31 | #browser.isblurred.dim-blurred .button-toolbar-webpanel img { 32 | filter: brightness(0.77) sepia(1) hue-rotate(${hue}deg) opacity(0.65) !important; 33 | } 34 | `; 35 | } 36 | 37 | const add_style = (id) => { 38 | const style = document.createElement("style"); 39 | style.setAttribute("type", "text/css"); 40 | style.id = id; 41 | document.head.appendChild(style); 42 | return document.getElementById(id); 43 | }; 44 | 45 | const wait = () => { 46 | return new Promise((resolve) => { 47 | const check = document.getElementById("browser"); 48 | if (check) return resolve(check); 49 | else { 50 | const startup = new MutationObserver(() => { 51 | const el = document.getElementById("browser"); 52 | if (el) { 53 | startup.disconnect(); 54 | resolve(el); 55 | } 56 | }); 57 | startup.observe(document.body, { childList: true, subtree: true }); 58 | } 59 | }); 60 | }; 61 | 62 | const lazy = (el, observer) => { 63 | observer.observe(el, { attributes: true, attributeFilter: ["style"] }); 64 | }; 65 | 66 | await wait().then((browser) => { 67 | const css = add_style("vm-mi-style"); 68 | theme(css); 69 | const lazy_obs = new MutationObserver(() => { 70 | lazy_obs.disconnect(); 71 | setTimeout(() => { 72 | theme(css); 73 | lazy(browser, lazy_obs); 74 | }, 300); 75 | }); 76 | lazy(browser, lazy_obs); 77 | }); 78 | })(); 79 | -------------------------------------------------------------------------------- /load/tam710562-import-export-command-chains.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Import Export Command Chains 3 | * Written by Tam710562 4 | */ 5 | 6 | (async () => { 7 | 'use strict'; 8 | 9 | const gnoh = { 10 | i18n: { 11 | getMessageName(message, type) { 12 | message = (type ? type + '\x04' : '') + message; 13 | return message.replace(/[^a-z0-9]/g, (i) => '_' + i.codePointAt(0) + '_') + '0'; 14 | }, 15 | getMessage(message, type) { 16 | return chrome.i18n.getMessage(this.getMessageName(message, type)) || message; 17 | }, 18 | }, 19 | uuid: { 20 | check(id) { 21 | return /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(id); 22 | }, 23 | generate(ids) { 24 | let d = Date.now() + performance.now(); 25 | let r; 26 | const id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 27 | r = (d + Math.random() * 16) % 16 | 0; 28 | d = Math.floor(d / 16); 29 | return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); 30 | }); 31 | 32 | if (Array.isArray(ids) && ids.includes(id)) { 33 | return this.generate(ids); 34 | } 35 | return id; 36 | }, 37 | }, 38 | object: { 39 | isObject(item) { 40 | return (item && typeof item === 'object' && !Array.isArray(item)); 41 | }, 42 | merge(target, source) { 43 | let output = Object.assign({}, target); 44 | if (this.isObject(target) && this.isObject(source)) { 45 | for (const key in source) { 46 | if (this.isObject(source[key])) { 47 | if (!(key in target)) 48 | Object.assign(output, { [key]: source[key] }); 49 | else 50 | output[key] = this.merge(target[key], source[key]); 51 | } else { 52 | Object.assign(output, { [key]: source[key] }); 53 | } 54 | } 55 | } 56 | return output; 57 | }, 58 | }, 59 | addStyle(css, id, isNotMin) { 60 | this.styles = this.styles || {}; 61 | if (Array.isArray(css)) { 62 | css = css.join(isNotMin === true ? '\n' : ''); 63 | } 64 | id = id || this.uuid.generate(Object.keys(this.styles)); 65 | this.styles[id] = this.createElement('style', { 66 | html: css || '', 67 | 'data-id': id, 68 | }, document.head); 69 | return this.styles[id]; 70 | }, 71 | createElement(tagName, attribute, parent, inner, options) { 72 | if (typeof tagName === 'undefined') { 73 | return; 74 | } 75 | if (typeof options === 'undefined') { 76 | options = {}; 77 | } 78 | if (typeof options.isPrepend === 'undefined') { 79 | options.isPrepend = false; 80 | } 81 | const el = document.createElement(tagName); 82 | if (!!attribute && typeof attribute === 'object') { 83 | for (const key in attribute) { 84 | if (key === 'text') { 85 | el.textContent = attribute[key]; 86 | } else if (key === 'html') { 87 | el.innerHTML = attribute[key]; 88 | } else if (key === 'style' && typeof attribute[key] === 'object') { 89 | for (const css in attribute.style) { 90 | el.style.setProperty(css, attribute.style[css]); 91 | } 92 | } else if (key === 'events' && typeof attribute[key] === 'object') { 93 | for (const event in attribute.events) { 94 | if (typeof attribute.events[event] === 'function') { 95 | el.addEventListener(event, attribute.events[event]); 96 | } 97 | } 98 | } else if (typeof el[key] !== 'undefined') { 99 | el[key] = attribute[key]; 100 | } else { 101 | if (typeof attribute[key] === 'object') { 102 | attribute[key] = JSON.stringify(attribute[key]); 103 | } 104 | el.setAttribute(key, attribute[key]); 105 | } 106 | } 107 | } 108 | if (inner) { 109 | if (!Array.isArray(inner)) { 110 | inner = [inner]; 111 | } 112 | for (const element of inner) { 113 | if (element.nodeName) { 114 | el.append(element); 115 | } else { 116 | el.append(this.createElementFromHTML(element)); 117 | } 118 | } 119 | } 120 | if (typeof parent === 'string') { 121 | parent = document.querySelector(parent); 122 | } 123 | if (parent) { 124 | if (options.isPrepend) { 125 | parent.prepend(el); 126 | } else { 127 | parent.append(el); 128 | } 129 | } 130 | return el; 131 | }, 132 | createElementFromHTML(html) { 133 | return this.createElement('template', { 134 | html: (html || '').trim(), 135 | }).content; 136 | }, 137 | get constant() { 138 | return { 139 | dialogButtons: { 140 | submit: { 141 | label: this.i18n.getMessage('OK'), 142 | type: 'submit' 143 | }, 144 | cancel: { 145 | label: this.i18n.getMessage('Cancel'), 146 | cancel: true 147 | }, 148 | }, 149 | }; 150 | }, 151 | encode: { 152 | regex(str) { 153 | return !str ? str : str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); 154 | }, 155 | }, 156 | dialog(title, content, buttons = [], config) { 157 | let modalBg; 158 | let dialog; 159 | let cancelEvent; 160 | const id = this.uuid.generate(); 161 | const inner = document.querySelector('#main > .inner, #main > .webpageview'); 162 | 163 | if (!config) { 164 | config = {}; 165 | } 166 | if (typeof config.autoClose === 'undefined') { 167 | config.autoClose = true; 168 | } 169 | 170 | function onKeyCloseDialog(windowId, key) { 171 | if ( 172 | windowId === vivaldiWindowId 173 | && key === 'Esc' 174 | ) { 175 | closeDialog(true); 176 | } 177 | } 178 | 179 | function onClickCloseDialog(windowId, mousedown, button, clientX, clientY) { 180 | if ( 181 | config.autoClose 182 | && windowId === vivaldiWindowId 183 | && mousedown 184 | && !document.elementFromPoint(clientX, clientY).closest('.dialog-custom[data-dialog-id="' + id + '"]') 185 | ) { 186 | closeDialog(true); 187 | } 188 | } 189 | 190 | function closeDialog(isCancel) { 191 | if (isCancel === true && cancelEvent) { 192 | cancelEvent.bind(this)(); 193 | } 194 | if (modalBg) { 195 | modalBg.remove(); 196 | } 197 | vivaldi.tabsPrivate.onKeyboardShortcut.removeListener(onKeyCloseDialog); 198 | vivaldi.tabsPrivate.onWebviewClickCheck.addListener(onClickCloseDialog); 199 | } 200 | 201 | vivaldi.tabsPrivate.onKeyboardShortcut.addListener(onKeyCloseDialog); 202 | vivaldi.tabsPrivate.onWebviewClickCheck.addListener(onClickCloseDialog); 203 | 204 | const buttonElements = []; 205 | for (let button of buttons) { 206 | button.type = button.type || 'button'; 207 | const clickEvent = button.click; 208 | if (button.cancel === true && typeof clickEvent === 'function') { 209 | cancelEvent = clickEvent; 210 | } 211 | button.events = { 212 | click(event) { 213 | event.preventDefault(); 214 | if (typeof clickEvent === 'function') { 215 | clickEvent.bind(this)(); 216 | } 217 | if (button.closeDialog !== false) { 218 | closeDialog(); 219 | } 220 | } 221 | }; 222 | delete button.click; 223 | if (button.label) { 224 | button.value = button.label; 225 | delete button.label; 226 | } 227 | button.element = this.createElement('input', button); 228 | buttonElements.push(button.element); 229 | } 230 | 231 | const focusModal = this.createElement('span', { 232 | class: 'focus_modal', 233 | tabindex: '0', 234 | }); 235 | const div = this.createElement('div', { 236 | style: { 237 | width: config.width ? config.width + 'px' : '', 238 | margin: '0 auto', 239 | } 240 | }); 241 | dialog = this.createElement('form', { 242 | 'data-dialog-id': id, 243 | class: 'dialog-custom modal-wrapper', 244 | }, div); 245 | if (config.class) { 246 | dialog.classList.add(config.class); 247 | } 248 | const dialogHeader = this.createElement('header', { 249 | class: 'dialog-header', 250 | }, dialog, '

' + (title || '') + '

'); 251 | const dialogContent = this.createElement('div', { 252 | class: 'dialog-content', 253 | style: { 254 | maxHeight: '65vh', 255 | }, 256 | }, dialog, content); 257 | if (buttons && buttons.length > 0) { 258 | const dialogFooter = this.createElement('footer', { 259 | class: 'dialog-footer', 260 | }, dialog, buttonElements); 261 | } 262 | modalBg = this.createElement('div', { 263 | id: 'modal-bg', 264 | class: 'slide', 265 | }, inner, [focusModal.cloneNode(true), div, focusModal.cloneNode(true)]); 266 | return { 267 | dialog, 268 | dialogHeader, 269 | dialogContent, 270 | modalBg, 271 | buttons: buttonElements, 272 | close: closeDialog, 273 | }; 274 | }, 275 | alert(message, okEvent) { 276 | const buttonOkElement = this.object.merge(this.constant.dialogButtons.submit, { 277 | cancel: true, 278 | }); 279 | if (typeof okEvent === 'function') { 280 | buttonOkElement.click = function (data) { 281 | okEvent.bind(this)(data); 282 | }; 283 | } 284 | 285 | return this.dialog('Alert', message, [buttonOkElement], { 286 | width: 400, 287 | class: 'dialog-javascript', 288 | }); 289 | }, 290 | timeOut(callback, condition, timeOut = 300) { 291 | let timeOutId = setTimeout(function wait() { 292 | let result; 293 | if (!condition) { 294 | result = document.getElementById('browser'); 295 | } else if (typeof condition === 'string') { 296 | result = document.querySelector(condition); 297 | } else if (typeof condition === 'function') { 298 | result = condition(); 299 | } else { 300 | return; 301 | } 302 | if (result) { 303 | callback(result); 304 | } else { 305 | timeOutId = setTimeout(wait, timeOut); 306 | } 307 | }, timeOut); 308 | 309 | function stop() { 310 | if (timeOutId) { 311 | clearTimeout(timeOutId); 312 | } 313 | } 314 | 315 | return { 316 | stop, 317 | }; 318 | }, 319 | element: { 320 | appendAtIndex(element, parentElement, index) { 321 | if (index >= parentElement.children.length) { 322 | parentElement.append(element) 323 | } else { 324 | parentElement.insertBefore(element, parentElement.children[index]) 325 | } 326 | }, 327 | getIndex(element) { 328 | return Array.from(element.parentElement.children).indexOf(element); 329 | }, 330 | }, 331 | }; 332 | 333 | const messageType = 'import-export-command-chains'; 334 | let timeOut; 335 | 336 | const urls = { 337 | quickCommands: 'chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path=qc', 338 | general: 'chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path=general', 339 | }; 340 | 341 | const icons = { 342 | import: '', 343 | export: '', 344 | checkAll: '', 345 | uncheckAll: '', 346 | }; 347 | 348 | const messageKey = { 349 | textSelection: { 350 | message: 'selection', 351 | type: 'textselection', 352 | }, 353 | websiteLink: { 354 | message: 'link url', 355 | type: 'websitelink', 356 | }, 357 | pageUrl: { 358 | message: 'page url', 359 | type: 'pageurl', 360 | }, 361 | webpageTitle: { 362 | message: 'page title', 363 | type: 'webpagetitle', 364 | }, 365 | elementSource: { 366 | message: 'src url', 367 | type: 'elementsource', 368 | }, 369 | }; 370 | 371 | const langs = { 372 | general: gnoh.i18n.getMessage('General'), 373 | quickCommands: gnoh.i18n.getMessage('Quick Commands'), 374 | copy: gnoh.i18n.getMessage('Copy', 'verb'), 375 | export: gnoh.i18n.getMessage('Export', 'verb'), 376 | import: gnoh.i18n.getMessage('Import'), 377 | install: gnoh.i18n.getMessage('Install'), 378 | installed: gnoh.i18n.getMessage('Installed'), 379 | update: gnoh.i18n.getMessage('Update'), 380 | preview: gnoh.i18n.getMessage('Preview'), 381 | commandParameter: gnoh.i18n.getMessage('Command Parameter', 'chainedcommand'), 382 | textSelection: gnoh.i18n.getMessage(messageKey.textSelection.message, messageKey.textSelection.type), 383 | websiteLink: gnoh.i18n.getMessage(messageKey.websiteLink.message, messageKey.websiteLink.type), 384 | pageUrl: gnoh.i18n.getMessage(messageKey.pageUrl.message, messageKey.pageUrl.type), 385 | webpageTitle: gnoh.i18n.getMessage(messageKey.webpageTitle.message, messageKey.webpageTitle.type), 386 | elementSource: gnoh.i18n.getMessage(messageKey.elementSource.message, messageKey.elementSource.type), 387 | }; 388 | 389 | 390 | const placeholdersCurrent = Object.keys(messageKey).map(key => langs[key].replace(/\s/g, '_')); 391 | const placeholdersEn = Object.values(messageKey).map(m => m.message.replace(/\s/g, '_')); 392 | 393 | gnoh.addStyle([ 394 | '.import-export-command-chains { --jsonKey: #0451a5; --jsonNumber: #098658; --jsonBool: #0000ff; --jsonString: #a31515; }', 395 | '.theme-dark .import-export-command-chains { --jsonKey: #9cdcfe; --jsonNumber: #b5cea8; --jsonBool: #569cd6; --jsonString: #ce9178; }', 396 | '.import-export-command-chains input[type="file"]::file-selector-button { border: 0px; border-right: 1px solid var(--colorBorder); height: 28px; padding: 0 18px; color: var(--colorFg); background: linear-gradient(var(--colorBgLightIntense) 0%, var(--colorBg) 100%); margin-right: 18px; }', 397 | '.import-export-command-chains input[type="file"]::file-selector-button:hover { background: linear-gradient(var(--colorBg), var(--colorBg)); }', 398 | '.import-export-command-chains .editor { width: 100%; height: 300px; overflow: auto; white-space: pre-wrap; word-break: break-word; background-color: var(--colorBgIntense); color: var(--colorFg); user-select: text; border-radius: var(--radius); border: 1px solid var(--colorBorder); font-size: 13px; font-family: monospace; line-height: 1.3; tab-size: 2; padding: 6px; }', 399 | '.import-export-command-chains .editor::highlight(json-key) { color: var(--jsonKey); }', 400 | '.import-export-command-chains .editor::highlight(json-number) { color: var(--jsonNumber); }', 401 | '.import-export-command-chains .editor::highlight(json-bool) { color: var(--jsonBool); }', 402 | '.import-export-command-chains .editor::highlight(json-string) { color: var(--jsonString); }', 403 | '.import-export-command-chains .export.master-detail { max-height: 335px; height: auto; }', 404 | '.import-export-command-chains .chained-command-item-value { background-color: var(--colorBgIntense); padding: 6px 12px; white-space: nowrap; overflow: auto; scrollbar-width: none; user-select: text; }', 405 | ], 'import-export-command-chains'); 406 | 407 | const buttons = { 408 | import: { 409 | icon: icons.import, 410 | title: langs.import, 411 | click(key) { 412 | showDialogImport(); 413 | }, 414 | index: 2, 415 | }, 416 | export: { 417 | icon: icons.export, 418 | title: langs.export, 419 | click(key) { 420 | showDialogExport(key); 421 | }, 422 | index: 3, 423 | }, 424 | }; 425 | 426 | const commands = await getCommands(); 427 | 428 | async function getCommands() { 429 | const response = await fetch(chrome.runtime.getURL('bundle.js')); 430 | const bundleScript = await response.text(); 431 | const matches = Array.from(bundleScript.matchAll(/category\s*:\s*"([^"]+)",[\s\S]+?guid\s*:\s*"([^"]+)",[\s\S]+?\("(([^"]+)"\s*,\s*")?([^"]+)"\)/g)); 432 | 433 | const commands = {}; 434 | matches.forEach(match => { 435 | commands[match[2]] = { 436 | category: match[1], 437 | key: match[2], 438 | message: match[5], 439 | messageType: match[4], 440 | label: gnoh.i18n.getMessage(match[5], match[4]), 441 | }; 442 | }); 443 | 444 | return commands; 445 | } 446 | 447 | function highlightJson(element, text) { 448 | CSS.highlights.delete('json-number'); 449 | CSS.highlights.delete('json-bool'); 450 | CSS.highlights.delete('json-key'); 451 | CSS.highlights.delete('json-string'); 452 | 453 | if (!text) { 454 | return; 455 | } 456 | 457 | const jsonNumbers = [...text.matchAll(/-?\d+\.?\d*((e)\+\d+)?/ig)].map((match) => { 458 | const range = new Range(); 459 | range.setStart(element.firstChild, match.index); 460 | range.setEnd(element.firstChild, match.index + match[0].length); 461 | return range; 462 | }); 463 | 464 | const jsonNumbersHighlight = new Highlight(...jsonNumbers); 465 | CSS.highlights.set('json-number', jsonNumbersHighlight); 466 | 467 | const jsonBooleans = [...text.matchAll(/false|true|null/ig)].map((match) => { 468 | const range = new Range(); 469 | range.setStart(element.firstChild, match.index); 470 | range.setEnd(element.firstChild, match.index + match[0].length); 471 | return range; 472 | }); 473 | 474 | const jsonBooleansHighlight = new Highlight(...jsonBooleans); 475 | CSS.highlights.set('json-bool', jsonBooleansHighlight); 476 | 477 | const jsonKeys = []; 478 | const jsonStrings = []; 479 | 480 | [...text.matchAll(/(("([^"]|\\")+?[^\\]")|"")(\s*.)/ig)].forEach((match) => { 481 | const range = new Range(); 482 | range.setStart(element.firstChild, match.index); 483 | range.setEnd(element.firstChild, match.index + match[1].length); 484 | 485 | if (match[4].trim() === ':') { 486 | jsonKeys.push(range); 487 | } else { 488 | jsonStrings.push(range); 489 | } 490 | }); 491 | const jsonKeysHighlight = new Highlight(...jsonKeys); 492 | CSS.highlights.set('json-key', jsonKeysHighlight); 493 | const jsonStringsHighlight = new Highlight(...jsonStrings); 494 | CSS.highlights.set('json-string', jsonStringsHighlight); 495 | } 496 | 497 | function createEditor(attribute = {}, parent, inner, options) { 498 | if (!attribute.events) { 499 | attribute.events = {}; 500 | } 501 | 502 | const inputEventOrigin = attribute.events.input; 503 | attribute.events.input = function () { 504 | inputEventOrigin?.apply(this, arguments); 505 | setValue(this.textContent); 506 | } 507 | 508 | const editor = gnoh.createElement('div', gnoh.object.merge({ 509 | class: 'editor', 510 | contentEditable: 'plaintext-only', 511 | }, attribute), parent, inner, options); 512 | 513 | function setValue(value) { 514 | editor.textContent = value; 515 | highlightJson(editor, value); 516 | } 517 | 518 | setValue(attribute.value || ''); 519 | 520 | return { 521 | editor, 522 | setValue, 523 | }; 524 | } 525 | 526 | async function parseTextFile(file) { 527 | return new Promise((resolve, reject) => { 528 | const fileReader = new FileReader(); 529 | fileReader.onload = event => resolve(event.target.result); 530 | fileReader.onerror = error => reject(error); 531 | fileReader.readAsText(file); 532 | }) 533 | } 534 | 535 | function fixLanguageImport(commandChains) { 536 | commandChains.forEach((commandChain) => { 537 | commandChain.chain.forEach(chain => { 538 | if (chain.param && typeof chain.param === 'string') { 539 | chain.param = chain.param.replace( 540 | new RegExp('{(' + placeholdersEn.map(p => gnoh.encode.regex(p)).join('|') + ')}', 'gi'), 541 | (match, p1) => '{' + placeholdersCurrent[placeholdersEn.findIndex(p => p === p1)] + '}', 542 | ); 543 | } 544 | }); 545 | }); 546 | } 547 | 548 | function fixLanguageExport(commandChain) { 549 | commandChain.chain.forEach(chain => { 550 | if (chain.param && typeof chain.param === 'string') { 551 | chain.param = chain.param.replace( 552 | new RegExp('{(' + placeholdersCurrent.map(p => gnoh.encode.regex(p)).join('|') + ')}', 'gi'), 553 | (match, p1) => '{' + placeholdersEn[placeholdersCurrent.findIndex(p => p === p1)] + '}', 554 | ); 555 | } 556 | }); 557 | } 558 | 559 | async function showDialogImport(commandChainsText) { 560 | const buttonInputElement = gnoh.object.merge(gnoh.constant.dialogButtons.submit, { 561 | label: langs.import, 562 | disabled: true, 563 | async click() { 564 | await importCommandChains(JSON.parse(commandChainsText)); 565 | await reloadSetting(); 566 | }, 567 | }); 568 | 569 | const buttonPreviewElement = gnoh.object.merge(gnoh.constant.dialogButtons.submit, { 570 | label: langs.preview, 571 | disabled: true, 572 | async click() { 573 | await showDialogPreview(commandChainsText); 574 | }, 575 | }); 576 | 577 | const buttonCancelElement = gnoh.object.merge(gnoh.constant.dialogButtons.cancel); 578 | 579 | const p1 = gnoh.createElement('p', { 580 | class: 'info', 581 | text: 'Import from code', 582 | }); 583 | 584 | const editor = createEditor({ 585 | events: { 586 | input() { 587 | commandChainsText = this.textContent.trim(); 588 | inputFile.value = ''; 589 | 590 | if (!commandChainsText || !checkCommandChains(commandChainsText)) { 591 | buttonInputElement.element.disabled = true; 592 | buttonPreviewElement.element.disabled = true; 593 | } else { 594 | buttonInputElement.element.disabled = false; 595 | buttonPreviewElement.element.disabled = false; 596 | } 597 | } 598 | } 599 | }); 600 | 601 | let p2 = null; 602 | let inputFile = null; 603 | const content = [p1, editor.editor]; 604 | 605 | if (commandChainsText) { 606 | editor.editor.contentEditable = false; 607 | editor.setValue(commandChainsText); 608 | buttonInputElement.disabled = false; 609 | buttonPreviewElement.disabled = false; 610 | } else { 611 | p2 = gnoh.createElement('p', { 612 | class: 'info', 613 | text: 'Import from file', 614 | }); 615 | 616 | inputFile = gnoh.createElement('input', { 617 | name: 'file', 618 | type: 'file', 619 | accept: 'application/json', 620 | events: { 621 | async change() { 622 | commandChainsText = await parseTextFile(this.files[0]); 623 | editor.setValue(''); 624 | 625 | if (!commandChainsText || !checkCommandChains(commandChainsText)) { 626 | buttonInputElement.element.disabled = true; 627 | buttonPreviewElement.element.disabled = true; 628 | } else { 629 | buttonInputElement.element.disabled = false; 630 | buttonPreviewElement.element.disabled = false; 631 | } 632 | } 633 | } 634 | }); 635 | 636 | content.push(p2, inputFile); 637 | } 638 | 639 | gnoh.dialog( 640 | 'Import Command Chain', 641 | content, 642 | [buttonInputElement, buttonPreviewElement, buttonCancelElement], 643 | { 644 | width: 500, 645 | class: 'import-export-command-chains', 646 | } 647 | ); 648 | } 649 | 650 | async function showDialogExport(key) { 651 | if (!key) { 652 | return; 653 | } 654 | const commandList = await getCommandChains(); 655 | const commandChain = commandList.find(c => c.key === key); 656 | fixLanguageExport(commandChain); 657 | let commandChainText = JSON.stringify(commandChain); 658 | let commandListChecked = { 659 | [key]: commandChain, 660 | }; 661 | 662 | const buttonCopyElement = gnoh.object.merge(gnoh.constant.dialogButtons.submit, { 663 | label: langs.copy, 664 | click: () => { 665 | navigator.clipboard.writeText(commandChainText); 666 | }, 667 | }); 668 | 669 | const buttonExportElement = gnoh.object.merge(gnoh.constant.dialogButtons.submit, { 670 | label: langs.export, 671 | click: () => { 672 | const values = Object.values(commandListChecked); 673 | let filename = ''; 674 | 675 | if (values.length > 1) { 676 | const d = new Date(); 677 | const year = d.getFullYear(); 678 | const month = (d.getMonth() + 1).toString().padStart(2, '0'); 679 | const date = d.getDate().toString().padStart(2, '0'); 680 | const hour = d.getHours().toString().padStart(2, '0'); 681 | const minute = d.getMinutes().toString().padStart(2, '0'); 682 | const second = d.getSeconds().toString().padStart(2, '0'); 683 | const millisecond = d.getMilliseconds().toString().padStart(3, '0'); 684 | filename = `command-chains_${year}-${month}-${date}_${hour}${minute}${second}${millisecond}`; 685 | } else { 686 | filename = values[0].label.trim() 687 | .replace(/\s+/g, '-').toLowerCase() 688 | .replace(/[^\p{L}0-9-]/gu, '') 689 | .replace(/(?:^-+)|(?:-+$)/g, '') || key; 690 | } 691 | 692 | const commandChainUrl = URL.createObjectURL(new Blob([commandChainText], { type: 'application/json' })); 693 | 694 | chrome.downloads.download({ 695 | url: commandChainUrl, 696 | filename: filename + '.json', 697 | saveAs: true, 698 | }); 699 | 700 | URL.revokeObjectURL(commandChainUrl); 701 | }, 702 | }); 703 | 704 | const buttonCancelElement = gnoh.object.merge(gnoh.constant.dialogButtons.cancel); 705 | 706 | const editor = createEditor({ 707 | contentEditable: false, 708 | value: commandChainText, 709 | }); 710 | 711 | const masterDetailWrapper = gnoh.createElement('div', { 712 | class: 'export master-detail', 713 | }); 714 | 715 | const master = gnoh.createElement('div', { 716 | class: 'master master-layout-single', 717 | }, masterDetailWrapper); 718 | 719 | const masterDetail = gnoh.createElement('div', { 720 | class: 'master-detail sortable-list', 721 | }, master); 722 | 723 | const masterItems = gnoh.createElement('div', { 724 | class: 'master-items', 725 | tabindex: 0, 726 | }, masterDetail); 727 | 728 | const checkboxes = []; 729 | 730 | commandList.forEach(command => { 731 | const item = gnoh.createElement('label', { 732 | class: 'item', 733 | tabindex: -1, 734 | style: { 735 | padding: '1px 6px', 736 | }, 737 | }, masterItems, command.label); 738 | 739 | const checkbox = gnoh.createElement('input', { 740 | type: 'checkbox', 741 | checked: command.key === key, 742 | events: { 743 | change: () => { 744 | if (checkbox.checked) { 745 | commandListChecked[command.key] = command; 746 | } else { 747 | delete commandListChecked[command.key]; 748 | } 749 | const values = Object.values(commandListChecked); 750 | if (values.length === 0) { 751 | buttonCopyElement.element.disabled = true; 752 | buttonExportElement.element.disabled = true; 753 | commandChainText = ''; 754 | } else if (values.length === 1) { 755 | buttonCopyElement.element.disabled = false; 756 | buttonExportElement.element.disabled = false; 757 | commandChainText = JSON.stringify(values[0]); 758 | } else { 759 | buttonCopyElement.element.disabled = false; 760 | buttonExportElement.element.disabled = false; 761 | commandChainText = JSON.stringify(values); 762 | } 763 | editor.setValue(commandChainText); 764 | }, 765 | }, 766 | }, item, undefined, { 767 | isPrepend: true, 768 | }); 769 | 770 | checkboxes.push(checkbox); 771 | }); 772 | 773 | const masterToolbar = gnoh.createElement('div', { 774 | class: 'master-toolbar', 775 | }, masterDetail); 776 | 777 | const buttonToolbarExports = [ 778 | { 779 | title: 'Check all', 780 | html: icons.checkAll, 781 | events: { 782 | click: () => { 783 | checkboxes.forEach(checkbox => checkbox.checked = true); 784 | commandListChecked = commandList.reduce((previousValue, currentValue) => { 785 | previousValue[currentValue.key] = currentValue; 786 | return previousValue; 787 | }, {}); 788 | const values = Object.values(commandListChecked); 789 | if (values.length === 0) { 790 | buttonCopyElement.element.disabled = true; 791 | buttonExportElement.element.disabled = true; 792 | commandChainText = ''; 793 | } else if (values.length === 1) { 794 | buttonCopyElement.element.disabled = false; 795 | buttonExportElement.element.disabled = false; 796 | commandChainText = JSON.stringify(values[0]); 797 | } else { 798 | buttonCopyElement.element.disabled = false; 799 | buttonExportElement.element.disabled = false; 800 | commandChainText = JSON.stringify(values); 801 | } 802 | editor.setValue(commandChainText); 803 | }, 804 | }, 805 | }, 806 | { 807 | title: 'Uncheck all', 808 | html: icons.uncheckAll, 809 | events: { 810 | click: () => { 811 | checkboxes.forEach(checkbox => checkbox.checked = false); 812 | commandListChecked = {}; 813 | buttonCopyElement.element.disabled = true; 814 | buttonExportElement.element.disabled = true; 815 | commandChainText = ''; 816 | editor.setValue(commandChainText); 817 | }, 818 | }, 819 | }, 820 | ]; 821 | 822 | buttonToolbarExports.forEach(button => { 823 | const buttonToolbar = createButtonToolbar(button, masterToolbar); 824 | }); 825 | 826 | const detail = gnoh.createElement('div', { 827 | class: 'detail', 828 | }, masterDetailWrapper); 829 | 830 | const detailContent = gnoh.createElement('div', { 831 | class: 'detail-content', 832 | }, detail, [editor.editor]); 833 | 834 | gnoh.dialog( 835 | 'Export Command Chain', 836 | masterDetailWrapper, 837 | [buttonCopyElement, buttonExportElement, buttonCancelElement], 838 | { 839 | width: 750, 840 | class: 'import-export-command-chains', 841 | } 842 | ); 843 | } 844 | 845 | function createCommandChainItem(chain, index) { 846 | const chainedCommandItem = gnoh.createElement('div', { 847 | class: 'ChainedCommand-Item', 848 | }); 849 | 850 | const chainedCommandItemTitle = gnoh.createElement('div', { 851 | class: 'ChainedCommand-Item--Title floating-label', 852 | }, chainedCommandItem); 853 | 854 | const chainedCommandItemName = gnoh.createElement('span', { 855 | class: 'chained-command-item-name', 856 | text: 'Command ' + index, 857 | }, chainedCommandItemTitle); 858 | 859 | const chainedCommandItemValue = gnoh.createElement('div', { 860 | class: 'chained-command-item-value', 861 | text: commands[chain.key]?.label || chain.label || '', 862 | }, chainedCommandItemTitle); 863 | 864 | if (typeof chain.param !== 'undefined') { 865 | const chainedCommandItemParameter = gnoh.createElement('div', { 866 | class: 'ChainedCommand-Item--Parameter floating-label', 867 | }, chainedCommandItem); 868 | 869 | const chainedCommandItemParameterName = gnoh.createElement('span', { 870 | class: 'chained-command-item-name', 871 | text: langs.commandParameter, 872 | }, chainedCommandItemParameter); 873 | 874 | const chainedCommandItemParameterValue = gnoh.createElement('div', { 875 | class: 'chained-command-item-value', 876 | text: chain.param || chain.defaultValue || '', 877 | }, chainedCommandItemParameter); 878 | } 879 | 880 | return chainedCommandItem; 881 | } 882 | 883 | function createCommandChains(commandChains) { 884 | return commandChains.map((commandChain) => { 885 | const chainName = gnoh.createElement('div', { 886 | class: 'chain-name', 887 | text: commandChain.label, 888 | }); 889 | 890 | const chainedCommand = gnoh.createElement('div', { 891 | class: 'ChainedCommand', 892 | }); 893 | 894 | commandChain.chain.forEach((chain, index) => { 895 | chainedCommand.append(createCommandChainItem(chain, index + 1)); 896 | }); 897 | 898 | return [chainName, chainedCommand]; 899 | }).flat(); 900 | } 901 | 902 | async function showDialogPreview(commandChainsText) { 903 | if (!checkCommandChains(commandChainsText)) { 904 | return; 905 | } 906 | 907 | let commandChains = JSON.parse(commandChainsText); 908 | 909 | if (!Array.isArray(commandChains)) { 910 | commandChains = [commandChains]; 911 | } 912 | 913 | fixLanguageImport(commandChains); 914 | 915 | const chainedCommand = createCommandChains(commandChains); 916 | 917 | const buttonInputElement = gnoh.object.merge(gnoh.constant.dialogButtons.submit, { 918 | label: langs.import, 919 | async click() { 920 | if (!commandChainsText || !checkCommandChains(commandChainsText)) { 921 | gnoh.alert('Import failed'); 922 | } else { 923 | await importCommandChains(JSON.parse(commandChainsText)); 924 | await reloadSetting(); 925 | } 926 | }, 927 | }); 928 | 929 | const buttonCancelElement = gnoh.object.merge(gnoh.constant.dialogButtons.cancel); 930 | 931 | const dialog = gnoh.dialog( 932 | 'Preview Command Chain', 933 | chainedCommand, 934 | [buttonInputElement, buttonCancelElement], 935 | { 936 | width: 750, 937 | class: 'import-export-command-chains', 938 | } 939 | ); 940 | 941 | dialog.modalBg.style.bottom = 0; 942 | dialog.dialog.style.display = 'flex'; 943 | dialog.dialog.style.flexDirection = 'column'; 944 | dialog.dialog.style.maxHeight = '100%'; 945 | dialog.dialogContent.style.flex = '1'; 946 | } 947 | 948 | function checkCommandChains(commandChainsText) { 949 | let commandChains = null; 950 | try { 951 | commandChains = JSON.parse(commandChainsText); 952 | } catch (e) { 953 | return false; 954 | } 955 | 956 | if (!Array.isArray(commandChains)) { 957 | commandChains = [commandChains]; 958 | } 959 | 960 | return commandChains.every( 961 | (commandChain) => commandChain.category === 'CATEGORY_COMMAND_CHAIN' 962 | && Array.isArray(commandChain.chain) 963 | && commandChain.chain.every(c => typeof c.key === 'string' && gnoh.uuid.check(c.key)) 964 | && typeof commandChain.key === 'string' 965 | && typeof commandChain.label === 'string' 966 | && typeof commandChain.name === 'string' 967 | ); 968 | } 969 | 970 | async function getCommandChains() { 971 | return vivaldi.prefs.get('vivaldi.chained_commands.command_list'); 972 | } 973 | 974 | async function getCommandChainByKey(key) { 975 | const commandList = await getCommandChains(); 976 | const commandChain = commandList.find(c => c.key === key); 977 | if (commandChain) { 978 | return commandChain; 979 | } else { 980 | throw new Error('Key not found'); 981 | } 982 | } 983 | 984 | async function importCommandChains(commandChains) { 985 | if (!Array.isArray(commandChains)) { 986 | commandChains = [commandChains]; 987 | } 988 | 989 | fixLanguageImport(commandChains); 990 | 991 | const commandList = await getCommandChains(); 992 | 993 | commandChains.forEach((commandChain) => { 994 | const index = commandList.findIndex(c => c.key === commandChain.key); 995 | 996 | if (index === -1) { 997 | commandList.push(commandChain); 998 | } else { 999 | commandList[index] = commandChain; 1000 | } 1001 | }) 1002 | 1003 | vivaldi.prefs.set({ 1004 | path: 'vivaldi.chained_commands.command_list', 1005 | value: commandList 1006 | }); 1007 | } 1008 | 1009 | function getMenuItem(name) { 1010 | const menuItem = document.evaluate(`//div[contains(concat(" ", normalize-space(@class), " "), " tree-row ") and contains(., "${name}")]`, document, null, XPathResult.ANY_TYPE, null); 1011 | return menuItem.iterateNext(); 1012 | } 1013 | 1014 | async function reloadSetting() { 1015 | try { 1016 | const window = await chrome.windows.getLastFocused({ windowTypes: ['popup'] }); 1017 | if (window) { 1018 | try { 1019 | const vivExtData = JSON.parse(window.vivExtData); 1020 | if (vivExtData.isSettings) { 1021 | chrome.runtime.sendMessage({ 1022 | type: messageType, 1023 | action: 'reload-setting', 1024 | windowId: window.id, 1025 | }); 1026 | } 1027 | } catch (error) { 1028 | console.error(error); 1029 | } 1030 | } 1031 | } catch (error) { 1032 | console.error(error); 1033 | } 1034 | 1035 | 1036 | const tabs = await chrome.tabs.query({ url: urls.quickCommands }); 1037 | tabs.forEach(async tab => { 1038 | chrome.tabs.onUpdated.addListener(async function listener(tabId, changeInfo) { 1039 | if (changeInfo.status === 'complete' && tabId === tab.id) { 1040 | chrome.tabs.onUpdated.removeListener(listener); 1041 | await chrome.tabs.update(tab.id, { url: urls.quickCommands }); 1042 | } 1043 | }); 1044 | await chrome.tabs.update(tab.id, { url: urls.general }); 1045 | }); 1046 | } 1047 | 1048 | chrome.runtime.onMessage.addListener((info, sender, sendResponse) => { 1049 | if (info.type === messageType) { 1050 | (async () => { 1051 | const window = await chrome.windows.getLastFocused({ windowTypes: ['normal'] }); 1052 | 1053 | if (window && window.id === vivaldiWindowId || info.windowId === vivaldiWindowId) { 1054 | switch (info.action) { 1055 | case 'import': 1056 | showDialogImport(info.data); 1057 | break; 1058 | case 'check': 1059 | if (checkCommandChains(info.data)) { 1060 | let commandChains = JSON.parse(info.data); 1061 | if (!Array.isArray(commandChains)) { 1062 | commandChains = [commandChains]; 1063 | } 1064 | 1065 | fixLanguageImport(commandChains); 1066 | 1067 | const status = { 1068 | installed: 0, 1069 | update: 0, 1070 | new: 0, 1071 | }; 1072 | 1073 | for (const commandChain of commandChains) { 1074 | try { 1075 | const commandChainOld = await getCommandChainByKey(commandChain.key); 1076 | if (JSON.stringify(commandChainOld) === JSON.stringify(commandChain)) { 1077 | status.installed++; 1078 | } else { 1079 | status.update++; 1080 | } 1081 | } catch { 1082 | status.new++; 1083 | } 1084 | } 1085 | 1086 | switch (commandChains.length) { 1087 | case status.installed: 1088 | sendResponse('installed'); 1089 | break; 1090 | case status.new: 1091 | sendResponse('new'); 1092 | break; 1093 | default: 1094 | sendResponse('update'); 1095 | break; 1096 | } 1097 | } else { 1098 | sendResponse('fail'); 1099 | } 1100 | break; 1101 | case 'reload-setting': 1102 | const menuItemGeneralElement = getMenuItem(langs.general); 1103 | const menuItemQuickCommandsElement = getMenuItem(langs.quickCommands); 1104 | if (menuItemGeneralElement && menuItemQuickCommandsElement) { 1105 | setTimeout(() => menuItemGeneralElement.click()); 1106 | setTimeout(() => menuItemQuickCommandsElement.click()); 1107 | } 1108 | break; 1109 | } 1110 | } 1111 | })(); 1112 | return true; 1113 | } 1114 | }); 1115 | 1116 | vivaldi.prefs.onChanged.addListener(async newValue => { 1117 | if (newValue.path === 'vivaldi.chained_commands.command_list') { 1118 | const tabs = await chrome.tabs.query({ url: '*://forum.vivaldi.net/topic/*' }); 1119 | tabs.forEach(tab => { 1120 | chrome.tabs.sendMessage(tab.id, { 1121 | type: messageType, 1122 | action: 'change', 1123 | }); 1124 | }); 1125 | } 1126 | }); 1127 | 1128 | function createButtonToolbar(attribute = {}, parent, inner, options) { 1129 | return gnoh.createElement('button', gnoh.object.merge({ 1130 | class: 'button-toolbar', 1131 | }, attribute), parent, inner, options); 1132 | } 1133 | 1134 | function createSettings() { 1135 | if (timeOut) { 1136 | timeOut.stop(); 1137 | } 1138 | timeOut = gnoh.timeOut(chainedCommand => { 1139 | chainedCommand.dataset.importExportCommandChains = true; 1140 | const masterToolbar = chainedCommand.querySelector('.master-toolbar:not([data-import-export-command-chains="true"])'); 1141 | masterToolbar.dataset.importExportCommandChains = true; 1142 | 1143 | const master = chainedCommand.querySelector('.master:not([data-import-export-command-chains="true"])'); 1144 | master.dataset.importExportCommandChains = true; 1145 | 1146 | async function selectedKey() { 1147 | const itemSelected = master.querySelector('.master-items .item-selected'); 1148 | if (!itemSelected) { 1149 | return; 1150 | } 1151 | const commandList = await getCommandChains(); 1152 | const indexSelected = gnoh.element.getIndex(itemSelected); 1153 | return commandList[indexSelected]?.key; 1154 | } 1155 | 1156 | Object.values(buttons).forEach((button) => { 1157 | gnoh.element.appendAtIndex( 1158 | createButtonToolbar({ 1159 | html: button.icon, 1160 | title: button.title, 1161 | events: { 1162 | async click(e) { 1163 | e.preventDefault(); 1164 | const key = await selectedKey(); 1165 | button.click(key); 1166 | }, 1167 | }, 1168 | }), 1169 | masterToolbar, 1170 | button.index, 1171 | ); 1172 | }); 1173 | }, '.Setting--ChainedCommand.master-detail:not([data-import-export-command-chains="true"])'); 1174 | } 1175 | 1176 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 1177 | if (changeInfo.status === 'complete') { 1178 | if (tab.url === urls.quickCommands) { 1179 | createSettings(); 1180 | } else if (/https:\/\/forum\.vivaldi\.net\/topic\/*/.test(tab.url)) { 1181 | chrome.scripting.executeScript({ 1182 | target: { 1183 | tabId: tab.id 1184 | }, 1185 | args: [messageType, langs], 1186 | func: (messageType, langs) => { 1187 | if (window.importExportCommandChains) { 1188 | return; 1189 | } else { 1190 | window.importExportCommandChains = true; 1191 | } 1192 | 1193 | const buttonInstallElements = []; 1194 | 1195 | chrome.runtime.onMessage.addListener((info) => { 1196 | if (info.type === messageType) { 1197 | switch (info.action) { 1198 | case 'change': 1199 | buttonInstallElements.forEach(async (buttonElement) => { 1200 | const status = await chrome.runtime.sendMessage({ 1201 | type: messageType, 1202 | action: 'check', 1203 | data: buttonElement.code, 1204 | }); 1205 | 1206 | updateButton(buttonElement, status); 1207 | }); 1208 | break; 1209 | } 1210 | } 1211 | }); 1212 | 1213 | async function onInstallClick(event) { 1214 | chrome.runtime.sendMessage({ 1215 | type: messageType, 1216 | action: 'import', 1217 | data: event.target.code, 1218 | }); 1219 | } 1220 | 1221 | function updateButton(buttonElement, status) { 1222 | switch (status) { 1223 | case 'new': 1224 | buttonElement.classList.add('btn-primary'); 1225 | buttonElement.classList.remove('btn-secondary'); 1226 | buttonElement.innerText = langs.install; 1227 | buttonElement.disabled = false; 1228 | buttonElement.addEventListener('click', onInstallClick); 1229 | break; 1230 | case 'update': 1231 | buttonElement.classList.add('btn-primary'); 1232 | buttonElement.classList.remove('btn-secondary'); 1233 | buttonElement.innerText = langs.update; 1234 | buttonElement.disabled = false; 1235 | buttonElement.addEventListener('click', onInstallClick); 1236 | break; 1237 | case 'installed': 1238 | buttonElement.classList.remove('btn-primary'); 1239 | buttonElement.classList.add('btn-secondary'); 1240 | buttonElement.innerText = langs.installed; 1241 | buttonElement.disabled = true; 1242 | buttonElement.removeEventListener('click', onInstallClick); 1243 | break; 1244 | } 1245 | } 1246 | 1247 | function createButton(node) { 1248 | const codes = node.querySelectorAll('code:not([data-import-export-command-chains="true"])'); 1249 | 1250 | codes.forEach(async (codeElement) => { 1251 | codeElement.dataset.importExportCommandChains = true; 1252 | 1253 | const code = codeElement.innerText.trim(); 1254 | 1255 | const status = await chrome.runtime.sendMessage({ 1256 | type: messageType, 1257 | action: 'check', 1258 | data: code, 1259 | }); 1260 | 1261 | if (status === 'fail') { 1262 | return; 1263 | } 1264 | 1265 | const commandChainElement = document.createElement('div'); 1266 | commandChainElement.className = 'command-chain'; 1267 | 1268 | const buttonInstallElement = document.createElement('button'); 1269 | buttonInstallElement.code = code; 1270 | buttonInstallElement.className = 'btn mb-3'; 1271 | 1272 | updateButton(buttonInstallElement, status); 1273 | buttonInstallElements.push(buttonInstallElement); 1274 | 1275 | commandChainElement.append(buttonInstallElement); 1276 | 1277 | const preElement = codeElement.closest('pre'); 1278 | if (preElement) { 1279 | preElement.parentNode.insertBefore(commandChainElement, preElement.nextSibling); 1280 | } else { 1281 | codeElement.parentNode.insertBefore(commandChainElement, codeElement.nextSibling); 1282 | } 1283 | }); 1284 | } 1285 | 1286 | createButton(document.body); 1287 | 1288 | const observer = new MutationObserver((mutationList) => { 1289 | mutationList.forEach(mutation => { 1290 | if (mutation.addedNodes.length) { 1291 | mutation.addedNodes.forEach(node => { 1292 | if (node.nodeType === 1) { 1293 | createButton(node); 1294 | } 1295 | }); 1296 | } 1297 | }); 1298 | }); 1299 | 1300 | observer.observe(document.body, { 1301 | childList: true, 1302 | subtree: true, 1303 | }); 1304 | }, 1305 | }); 1306 | } 1307 | } 1308 | }); 1309 | 1310 | gnoh.timeOut(async () => { 1311 | if (document.querySelector('#main > .webpageview')) { 1312 | const menuItemQuickCommandsElement = getMenuItem(langs.quickCommands); 1313 | 1314 | menuItemQuickCommandsElement.addEventListener('click', createSettings); 1315 | } else { 1316 | const tabs = await chrome.tabs.query({ active: true, windowId: vivaldiWindowId }); 1317 | if (tabs.length) { 1318 | const tab = tabs[0]; 1319 | if (tab.url === urls.quickCommands) { 1320 | createSettings(); 1321 | } 1322 | } 1323 | } 1324 | }, '#main'); 1325 | })(); 1326 | -------------------------------------------------------------------------------- /m3.css: -------------------------------------------------------------------------------- 1 | /* 2 | M3 3 | version 2022.6.0 4 | https://forum.vivaldi.net/post/430762 5 | Changes the name of the mail panel ^^ 6 | */ 7 | 8 | #mail_panel h1 { 9 | font-size: 0; 10 | } 11 | #mail_panel h1::before { 12 | font-size: 16px; 13 | content: "M3"; 14 | } 15 | -------------------------------------------------------------------------------- /monochrome-icons.js: -------------------------------------------------------------------------------- 1 | // Monochrome icons 2 | // version 2024.11.1 3 | // https://forum.vivaldi.net/post/791344 4 | // Makes web panel thumbnails monochrome depending on theme colors. 5 | 6 | (async function monochrome_icons() { 7 | "use strict"; 8 | 9 | function convert(srgb) { 10 | const val = srgb.slice(11, -1).trim().split(/\s+/); 11 | const r = Math.round(parseFloat(val[0]) * 255); 12 | const g = Math.round(parseFloat(val[1]) * 255); 13 | const b = Math.round(parseFloat(val[2]) * 255); 14 | const calc = 15 | (Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b) * 180) / Math.PI; 16 | return (calc - 38.71).toFixed(2); 17 | } 18 | 19 | function theme(css) { 20 | const color = document.getElementById("main"); 21 | color.style = 22 | "color: color-mix(in hsl, var(--colorFgFadedMost) 70%, var(--colorHighlightBg));"; 23 | const srgb = getComputedStyle(color).getPropertyValue("color"); 24 | color.removeAttribute("style"); 25 | const hue = convert(srgb); 26 | console.info(`hue-change: ${hue}°`); 27 | css.innerHTML = ` 28 | .button-toolbar-webpanel img { 29 | filter: brightness(0.77) sepia(1) hue-rotate(${hue}deg); 30 | } 31 | #browser.isblurred.dim-blurred .button-toolbar-webpanel img { 32 | filter: brightness(0.77) sepia(1) hue-rotate(${hue}deg) opacity(0.65) !important; 33 | } 34 | `; 35 | } 36 | 37 | const add_style = (id) => { 38 | const style = document.createElement("style"); 39 | style.setAttribute("type", "text/css"); 40 | style.id = id; 41 | document.head.appendChild(style); 42 | return document.getElementById(id); 43 | }; 44 | 45 | const wait = () => { 46 | return new Promise((resolve) => { 47 | const check = document.getElementById("browser"); 48 | if (check) return resolve(check); 49 | else { 50 | const startup = new MutationObserver(() => { 51 | const el = document.getElementById("browser"); 52 | if (el) { 53 | startup.disconnect(); 54 | resolve(el); 55 | } 56 | }); 57 | startup.observe(document.body, { childList: true, subtree: true }); 58 | } 59 | }); 60 | }; 61 | 62 | const lazy = (el, observer) => { 63 | observer.observe(el, { attributes: true, attributeFilter: ["style"] }); 64 | }; 65 | 66 | await wait().then((browser) => { 67 | const css = add_style("vm-mi-style"); 68 | theme(css); 69 | const lazy_obs = new MutationObserver(() => { 70 | lazy_obs.disconnect(); 71 | setTimeout(() => { 72 | theme(css); 73 | lazy(browser, lazy_obs); 74 | }, 300); 75 | }); 76 | lazy(browser, lazy_obs); 77 | }); 78 | })(); 79 | -------------------------------------------------------------------------------- /moon-phase-widget/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luetage/vivaldi_modding/f2b41ce58e5a1f753e4076c4bc0ddb92b8d85c24/moon-phase-widget/icons/favicon.png -------------------------------------------------------------------------------- /moon-phase-widget/icons/moon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luetage/vivaldi_modding/f2b41ce58e5a1f753e4076c4bc0ddb92b8d85c24/moon-phase-widget/icons/moon.webp -------------------------------------------------------------------------------- /moon-phase-widget/moon.css: -------------------------------------------------------------------------------- 1 | /* 2 | Moon Phase Widget 3 | version 2024.12.0 4 | Guide and updates ☛ https://forum.vivaldi.net/post/783627 5 | ———————— ⁂ ———————— 6 | */ 7 | :focus-visible { 8 | outline: 2px solid var(--colorHighlightBg); 9 | outline-offset: -1px; 10 | } 11 | html { 12 | container-type: normal; 13 | scrollbar-width: thin; 14 | scrollbar-color: color-mix(in oklab, var(--colorFg) 40%, transparent) 15 | color-mix(in oklab, var(--colorBg) 95%, black); 16 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "IBM Plex Sans", 17 | Inter, Ubuntu, sans-serif; 18 | font-size: 100%; 19 | font-variant-numeric: "tabular-nums"; 20 | font-feature-settings: "tnum"; 21 | font-weight: 400; 22 | } 23 | @container style(--isDarkTheme: 0) { 24 | body { 25 | --premix: color-mix(in oklab, var(--colorFg) 40%, white); 26 | --fgalt: color-mix(in oklab, var(--colorFg) 70%, white); 27 | --bgalt: color-mix(in oklab, var(--colorBg) 97%, black); 28 | --bgbtn: color-mix(in oklab, var(--colorBg) 97%, black); 29 | } 30 | #outer, 31 | #phase strong { 32 | color: color-mix(in oklab, var(--premix) 70%, var(--colorHighlightBg)); 33 | } 34 | #inner { 35 | color: color-mix(in oklab, var(--colorBg) 70%, var(--colorHighlightBg)); 36 | } 37 | } 38 | @container style(--isDarkTheme: 1) { 39 | body { 40 | --premix: color-mix(in oklab, var(--colorFg) 70%, black); 41 | --fgalt: color-mix(in oklab, var(--colorFg) 80%, black); 42 | --bgalt: color-mix(in oklab, var(--colorBg) 94%, black); 43 | --bgbtn: color-mix(in oklab, var(--colorBg) 94%, white); 44 | } 45 | #outer, 46 | #inner, 47 | #phase strong { 48 | color: color-mix(in hsl, var(--premix) 70%, var(--colorHighlightBg)); 49 | } 50 | } 51 | body { 52 | color: var(--colorFg); 53 | background: var(--colorBg); 54 | cursor: default; 55 | margin: 0; 56 | line-height: 1.2; 57 | user-select: none; 58 | overflow: hidden; 59 | } 60 | #error { 61 | font-size: 0.8rem; 62 | text-align: center; 63 | margin-block-start: 1rem; 64 | margin-block-end: 1rem; 65 | margin-left: 1rem; 66 | margin-right: 1rem; 67 | } 68 | #reload { 69 | display: flex; 70 | position: fixed; 71 | top: 0.5rem; 72 | left: 0.5rem; 73 | padding: 0.5ch; 74 | background-color: transparent; 75 | border-color: transparent; 76 | border-radius: var(--radius); 77 | color: var(--fgalt); 78 | } 79 | #reload:hover { 80 | background-color: var(--bgbtn); 81 | } 82 | @keyframes rotate { 83 | to { 84 | transform: rotate(-360deg); 85 | } 86 | } 87 | #reload.load svg { 88 | animation: rotate 0.8s linear; 89 | transform-origin: center; 90 | } 91 | #container { 92 | width: calc(100vw - 1rem); 93 | height: calc(100vh - 1rem); 94 | margin: 0.5rem; 95 | display: flex; 96 | flex-direction: row; 97 | justify-content: space-evenly; 98 | } 99 | #left, 100 | #right { 101 | display: flex; 102 | flex-direction: column; 103 | justify-content: center; 104 | align-items: center; 105 | } 106 | #phase { 107 | min-width: 15ch; 108 | text-align: center; 109 | margin-block-start: 0.25rem; 110 | margin-block-end: unset; 111 | } 112 | #right { 113 | background: var(--bgalt); 114 | border-radius: var(--radius); 115 | font-size: 0.8rem; 116 | } 117 | #events { 118 | display: grid; 119 | grid-template-columns: 1.8ch auto 1.5ch auto 0.3rem; 120 | white-space: nowrap; 121 | padding: 0.5rem 0; 122 | padding-inline-start: 0.3rem; 123 | padding-inline-end: unset; 124 | margin-block-start: unset; 125 | margin-block-end: unset; 126 | list-style-type: none; 127 | overflow-y: auto; 128 | overflow-x: hidden; 129 | scrollbar-width: thin; 130 | scrollbar-gutter: stable; 131 | } 132 | li { 133 | display: contents; 134 | } 135 | .main { 136 | color: var(--colorHighlightBg); 137 | } 138 | .contain { 139 | position: relative; 140 | } 141 | .before, 142 | .after { 143 | position: absolute; 144 | color: var(--colorHighlightBg); 145 | } 146 | .before { 147 | top: -50%; 148 | } 149 | .after { 150 | bottom: -50%; 151 | } 152 | .hidden { 153 | display: none !important; 154 | } 155 | footer { 156 | display: none; 157 | } 158 | @media (min-height: 180px) { 159 | #error { 160 | text-align: start; 161 | } 162 | #reload, 163 | #right, 164 | #phase { 165 | display: none; 166 | } 167 | #container { 168 | height: calc(100vh - 3.06rem); 169 | } 170 | #angle { 171 | width: calc(100vw - 1rem); 172 | margin: 0.3rem 0; 173 | } 174 | text { 175 | fill: var(--colorFg); 176 | } 177 | footer { 178 | display: flex; 179 | opacity: 0; 180 | justify-content: space-between; 181 | padding: 0.3rem 1rem 1rem; 182 | font-size: 0.8rem; 183 | background: var(--bgalt); 184 | color: var(--fgalt); 185 | } 186 | #map-pin { 187 | height: 0.8rem; 188 | vertical-align: middle; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /moon-phase-widget/moon.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | Moon Phase 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 58 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /moon-phase-widget/moon.js: -------------------------------------------------------------------------------- 1 | // Moon Phase Widget 2 | // version 2024.12.0 3 | // Guide and updates ☛ https://forum.vivaldi.net/post/798319 4 | // ———————— ⁂ ———————— 5 | 6 | "use strict"; 7 | 8 | // EDIT START 9 | // Latitude and longitude are being determined by an API. For more accuracy 10 | // on a stationary install, or when using a VPN service, input your decimal 11 | // coordinates below. E.g. [latitude, longitude] for Reykjavík: [64.14, -21.89] 12 | 13 | const coordinates = []; 14 | 15 | // EDIT END 16 | 17 | const lunation = { 18 | newmoon: [ 19 | 1730465220, 1733034060, 1735597620, 1738154160, 1740703500, 1743245880, 20 | 1745782260, 1748314920, 1750847460, 1753384260, 1755929160, 1758484440, 21 | 1761049500, 1763621220, 1766194980, 1768765920, 1771329660, 1773883380, 22 | 1776426720, 1778961660, 1781492040, 1784022180, 1786556220, 1789097220, 23 | 1791647400, 1794207720, 1796777520, 1799353440, 1801929360, 1804498140, 24 | 1807055460, 1809601080, 1812138000, 1814670120, 1817201100, 1819734060, 25 | 1822271760, 1824816960, 1827372240, 1829938320, 1832512320, 1835087820, 26 | 1837657860, 1840218420, 1842768960, 1845311220, 1847847720, 1850381040, 27 | 1852914240, 1855450620, 1857993480, 1860545160, 1863105840, 1865673060, 28 | 1868242740, 1870810800, 1873374120, 1875930600, 1878479460, 1881021360, 29 | 1883558640, 1886094840, 1888633440, 1891176720, 1893725340, 1896278820, 30 | 1898836440, 1901397720, 1903961520, 1906525260, 1909085640, 1911640260, 31 | 1914188820, 1916733240, 1952456940, 1955005560, 1957550760, 1960093440, 32 | 1962635040, 1965177540, 1967722560, 1970271120, 1972824060, 1975381860, 33 | 1977944160, 1980509160, 1983073500, 1985633580, 1988187420, 1990735200, 34 | 1993278180, 1995817860, 1998355560, 2000892960, 2003432820, 2005978320, 35 | 2008532400, 2011095600, 2013665280, 2016236340, 2018803560, 2021364060, 36 | 2023917000, 2026462440, 2029001160, 2031534720, 2034066360, 2036600100, 37 | 2039140380, 2041690440, 2044251120, 2046820560, 2049394440, 2051967780, 38 | 2054535720, 2057094540, 2059642680, 2062181040, 2064712860, 2067242340, 39 | 2069773920, 2072311140, 2074856820, 2077412280, 2079977820, 2082551460, 40 | 2085128220, 2087701140, 2090264220, 2092815180, 2095355820, 2097889740, 41 | 2100421020, 2102952900, 2105488260, 2108029800, 2110580040, 2113140840, 42 | 2115711240, 2118286440, 2120859360, 2123424420, 2125979640, 2128525800, 43 | 2131065120, 2133600060, 2136133500, 2138668440, 2141208180, 2143755480, 44 | 2146311660, 45 | ], 46 | progress: (em) => { 47 | const ln = lunation.newmoon; 48 | for (let i = 0; i < ln.length; i++) { 49 | if (em.timestamp >= ln[i] && em.timestamp < ln[i + 1]) { 50 | const lunation = ln[i + 1] - ln[i]; 51 | const frac = (em.timestamp - ln[i]) / lunation; 52 | return Math.trunc(frac * 100); 53 | } 54 | } 55 | }, 56 | }; 57 | 58 | function schedule(events, em) { 59 | em.events.innerHTML = ""; 60 | events.forEach((e, i, a) => { 61 | const li = document.createElement("li"); 62 | let ind = ""; 63 | let ch = ""; 64 | if (em.time === e.time) li.classList.add("main"); 65 | else if (i === 0 && em.time < e.time) { 66 | ind = "before"; 67 | ch = arrow; 68 | } else if ( 69 | (i === a.length - 1 && em.time > e.time) || 70 | (em.time > e.time && em.time < a[i + 1].time) 71 | ) { 72 | ind = "after"; 73 | ch = arrow; 74 | } 75 | li.innerHTML = `
${ch}
${e.time}${e.phen}`; 76 | em.events.appendChild(li); 77 | }); 78 | } 79 | 80 | function calc_ry(p, i) { 81 | return p.includes("Crescent") ? 250 - 5 * i : i * 5 - 250; 82 | } 83 | 84 | function parse(data, em) { 85 | const prop = data.properties.data; 86 | const geo = data.geometry.coordinates; 87 | const progress = lunation.progress(em); 88 | const phase = prop.curphase; 89 | const spacing = phase.length / 4 + phase.length; 90 | const num = prop.fracillum.length + String(progress).length + 9; 91 | const spacing2 = num / 4 + num; 92 | em.phase.innerHTML = `${phase}
${progress}%`; 93 | em.ctext.innerHTML = phase.toUpperCase(); 94 | em.ctext.setAttribute("textLength", `${spacing.toFixed(2)}ch`); 95 | em.ctext2.innerHTML = `ILLUM ${parseFloat( 96 | prop.fracillum 97 | )} • LUN ${progress}`; 98 | em.ctext2.setAttribute("textLength", `${spacing2.toFixed(2)}ch`); 99 | const svg = prop.moon.find((entry) => entry.hasOwnProperty(phase)); 100 | const ry = Object.values(svg)[0][2]; 101 | const illum = ry === 1 ? calc_ry(phase, parseFloat(prop.fracillum)) : ry; 102 | em.rec.setAttribute("y", Object.values(svg)[0][0]); 103 | em.rec.setAttribute("fill", Object.values(svg)[0][1]); 104 | em.ell.setAttribute("ry", illum); 105 | em.ell.setAttribute("fill", Object.values(svg)[0][3]); 106 | em.angle.setAttribute("transform", `rotate(${geo[1]})`); 107 | schedule(prop.events, em); 108 | em.coor.innerHTML = `${geo[1]}, ${geo[0]}`.replace(/-/g, "\u2212"); 109 | em.coor.parentElement.parentElement.style.opacity = 1; 110 | em.container.classList.remove("hidden"); 111 | } 112 | 113 | function edit(arr, obj) { 114 | arr.forEach((entry) => { 115 | switch (entry.phen) { 116 | case "Rise": 117 | entry.phen = `${obj}rise`; 118 | break; 119 | case "Upper Transit": 120 | entry.phen = `${obj} Transit`; 121 | break; 122 | case "Set": 123 | entry.phen = `${obj}set`; 124 | break; 125 | case "Begin Civil Twilight": 126 | entry.phen = "Civil Dawn"; 127 | break; 128 | default: 129 | entry.phen = "Civil Dusk"; 130 | } 131 | Object.assign(entry, { obj: obj }); 132 | }); 133 | return arr; 134 | } 135 | 136 | function storage(data, em) { 137 | data.date = em.date; 138 | const prop = data.properties.data; 139 | const cp = prop.closestphase; 140 | const month = String(cp.month).padStart(2, "0"); 141 | const day = String(cp.day).padStart(2, "0"); 142 | const date_string = `${cp.year}-${month}-${day}`; 143 | const events = edit(prop.moondata, "Moon").concat(edit(prop.sundata, "Sun")); 144 | if (date_string === em.date) { 145 | prop.curphase = cp.phase; 146 | const main = { phen: cp.phase, time: cp.time, obj: "Moon" }; 147 | events.push(main); 148 | } 149 | prop.events = events.sort((a, b) => { 150 | if (a.time !== null) return a.time.localeCompare(b.time); 151 | }); 152 | prop.moon = [ 153 | { "New Moon": [0, "black", 0, "black"] }, 154 | { "Waxing Crescent": [-250, "white", 1, "black"] }, 155 | { "First Quarter": [-250, "white", 0, "black"] }, 156 | { "Waxing Gibbous": [-250, "white", 1, "white"] }, 157 | { "Full Moon": [0, "white", 250, "white"] }, 158 | { "Waning Gibbous": [0, "white", 1, "white"] }, 159 | { "Last Quarter": [0, "white", 0, "black"] }, 160 | { "Waning Crescent": [0, "white", 1, "black"] }, 161 | ]; 162 | return data; 163 | } 164 | 165 | async function load_data(url) { 166 | return new Promise((resolve, reject) => { 167 | fetch(url) 168 | .then((response) => { 169 | if (!response.ok) 170 | throw new Error(`HTTP error! status: ${response.status}`); 171 | return response.json(); 172 | }) 173 | .then((data) => { 174 | resolve({ 175 | success: true, 176 | data: data, 177 | message: "Data loaded", 178 | }); 179 | }) 180 | .catch((error) => { 181 | reject({ 182 | success: false, 183 | error: error.message, 184 | message: "Connection error", 185 | }); 186 | }); 187 | }); 188 | } 189 | 190 | async function astro(lat, lon, em) { 191 | await load_data( 192 | `https://aa.usno.navy.mil/api/rstt/oneday?date=${em.date}&coords=${lat},${lon}&tz=${em.tz}` 193 | ).then( 194 | (resolve) => { 195 | console.info(resolve); 196 | const sto = storage(resolve.data, em); 197 | localStorage.setItem("moon-phase", JSON.stringify(sto)); 198 | parse(sto, em); 199 | }, 200 | (reject) => { 201 | em.error.innerHTML = reject.message; 202 | em.error.classList.remove("hidden"); 203 | } 204 | ); 205 | } 206 | 207 | async function loc(em) { 208 | if (Array.isArray(coordinates) && coordinates.length === 2) { 209 | astro(coordinates[0], coordinates[1], em); 210 | } else { 211 | await load_data( 212 | "http://ip-api.com/json?fields=status,message,lat,lon" 213 | ).then( 214 | (resolve) => { 215 | console.info(resolve); 216 | if (!resolve.data.message) 217 | astro(resolve.data.lat, resolve.data.lon, em); 218 | }, 219 | (reject) => { 220 | em.error.innerHTML = reject.message; 221 | em.error.classList.remove("hidden"); 222 | } 223 | ); 224 | } 225 | } 226 | 227 | function check_storage(em) { 228 | em.error.classList.add("hidden"); 229 | const saved = JSON.parse(localStorage.getItem("moon-phase")); 230 | if (saved !== null && saved.date === em.date) { 231 | console.info(saved); 232 | parse(saved, em); 233 | } else loc(em); 234 | } 235 | 236 | function init() { 237 | const now = new Date(); 238 | const hours = String(now.getHours()).padStart(2, "0"); 239 | const minutes = String(now.getMinutes()).padStart(2, "0"); 240 | const elements = { 241 | error: document.getElementById("error"), 242 | container: document.getElementById("container"), 243 | angle: document.getElementById("angle"), 244 | ctext: document.getElementById("ctext"), 245 | ctext2: document.getElementById("ctext2"), 246 | ell: document.getElementById("ell"), 247 | rec: document.getElementById("rec"), 248 | phase: document.getElementById("phase"), 249 | events: document.getElementById("events"), 250 | coor: document.getElementById("coor"), 251 | date: now.toLocaleDateString("en-ca"), 252 | time: `${hours}:${minutes}`, 253 | tz: -now.getTimezoneOffset() / 60, 254 | timestamp: Math.floor(now.getTime() / 1000), 255 | }; 256 | check_storage(elements); 257 | } 258 | 259 | const arrow = ``; 260 | init(); 261 | const reload = document.getElementById("reload"); 262 | reload.addEventListener("click", () => { 263 | reload.classList.add("load"); 264 | init(); 265 | setTimeout(() => reload.classList.remove("load"), 800); 266 | }); 267 | -------------------------------------------------------------------------------- /moon-phase.js: -------------------------------------------------------------------------------- 1 | // Moon Phase 2 | // version 2024.11.0 3 | // https://forum.vivaldi.net/post/461432 4 | // Displays the current moon phase as command chain button. Download the 5 | // moon-phase.svg file and load it in theme settings. Moon phase calculation 6 | // adapted from https://minkukel.com/en/various/calculating-moon-phase/ 7 | 8 | (async function moonPhase() { 9 | "use strict"; 10 | 11 | // EDIT START 12 | // choose a digit from 0 to 4 approximating your latitude 13 | // north[0] northern[1] equator[2] southern[3] south[4] 14 | const approx = 0; 15 | // alternatively input your exact latitude in degrees (between 90 and -90) 16 | const latitude = 48.3; 17 | // command chain identifier (inspect UI and input your own) 18 | const command = "COMMAND_cln9yq818001n2v649xyaiird"; 19 | // EDIT END 20 | 21 | const lunation = 29.53058770576; 22 | const lunartime = lunation * 86400; 23 | const newmoon = 947182440; 24 | const moon = { 25 | phases: [ 26 | ["New Moon", 1], 27 | ["Waxing Crescent", 6.38264692644], 28 | ["First Quarter", 8.38264692644], 29 | ["Waxing Gibbous", 13.76529385288], 30 | ["Full Moon", 15.76529385288], 31 | ["Waning Gibbous", 21.14794077932], 32 | ["Last Quarter", 23.14794077932], 33 | ["Waning Crescent", 28.53058770576], 34 | ["", lunation], 35 | ], 36 | illum: [ 37 | [-5, 0], 38 | [-5, 3], 39 | [-5, 5], 40 | [-5, 7], 41 | [-5, 10], 42 | [-2, 7], 43 | [0, 5], 44 | [2, 3], 45 | ], 46 | lat: [90, 45, 0, -45, -90], 47 | phase: () => { 48 | const unixtime = Math.floor(Date.now() / 1000); 49 | const progress = ((unixtime - newmoon) % lunartime) / lunartime; 50 | const age = progress * lunation; 51 | for (let i = 0; i < moon.phases.length; i++) { 52 | if (age <= moon.phases[i][1]) { 53 | if (i === 8) i = 0; 54 | return { 55 | name: moon.phases[i][0], 56 | age: Math.trunc(age), 57 | progress: Math.trunc(progress * 100), 58 | coordinate: moon.illum[i][0], 59 | range: moon.illum[i][1], 60 | angle: typeof latitude === "number" ? latitude : moon.lat[approx], 61 | }; 62 | } 63 | } 64 | }, 65 | }; 66 | 67 | function moonwatch(btn) { 68 | const get = moon.phase(); 69 | const number = get.age === 1 ? "day" : "days"; 70 | btn.title = `${get.name}\n${get.age} ${number} \u{21ba} ${get.progress}%`; 71 | const mod = btn.querySelector("#vm-mp-mod"); 72 | if (mod) { 73 | mod.setAttribute("y", get.coordinate); 74 | mod.setAttribute("height", get.range); 75 | mod.setAttribute("transform", `rotate(${get.angle})`); 76 | btn.classList.add("vm-mp"); 77 | } 78 | } 79 | 80 | function conflate(el) { 81 | const btn = el.getElementsByTagName("BUTTON"); 82 | for (let i = 0; i < btn.length; i++) { 83 | if (btn[i].name === command && !btn[i].classList.contains("vm-mp")) { 84 | const send = () => moonwatch(btn[i]); 85 | send(); 86 | btn[i].addEventListener("click", send); 87 | } 88 | } 89 | } 90 | 91 | const wait = () => { 92 | return new Promise((resolve) => { 93 | const check = document.getElementById("browser"); 94 | if (check) return resolve(check); 95 | else { 96 | const startup = new MutationObserver(() => { 97 | const el = document.getElementById("browser"); 98 | if (el) { 99 | startup.disconnect(); 100 | resolve(el); 101 | } 102 | }); 103 | startup.observe(document.body, { childList: true, subtree: true }); 104 | } 105 | }); 106 | }; 107 | 108 | const lazy = (el, observer) => { 109 | observer.observe(el, { childList: true, subtree: true }); 110 | }; 111 | 112 | await wait().then((browser) => { 113 | const lazy_obs = new MutationObserver(() => { 114 | lazy_obs.disconnect(); 115 | setTimeout(() => { 116 | conflate(browser); 117 | lazy(browser, lazy_obs); 118 | }, 666); 119 | }); 120 | conflate(browser); 121 | lazy(browser, lazy_obs); 122 | }); 123 | })(); 124 | -------------------------------------------------------------------------------- /moon-phase.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /move_extensions_to_panel-container.js: -------------------------------------------------------------------------------- 1 | /* 2 | Move Extensions to Panel 3 | https://forum.vivaldi.net/topic/17879/moving-extension-icons-next-to-the-panel-toggles/16 4 | Moves the extension action buttons to the panel. 5 | */ 6 | 7 | function csse() { 8 | const style = document.createElement('style'); 9 | style.type = 'text/css'; 10 | style.innerHTML = ` 11 | #switch .extensions-wrapper { 12 | display: flex; 13 | flex-flow: row wrap; 14 | } 15 | #switch .extensions-wrapper { 16 | -webkit-app-region: no-drag; 17 | } 18 | #switch .button-toolbar.browserAction-button img { 19 | height: auto; 20 | width: 19px; 21 | } 22 | #switch .button-toolbar.toggle-extensions-group svg { 23 | height: 16px; 24 | width: 4px; 25 | } 26 | #switch .extensions-wrapper .dragging-cancelled, #switch .toggle-extensions-group { 27 | background-color: transparent !important; 28 | } 29 | #switch .extensions-wrapper span:hover, #switch .toggle-extensions-group:hover { 30 | background-color: var(--colorBgDarker) !important; 31 | } 32 | #switch { 33 | contain: initial; 34 | } 35 | #switch .extension-popup.top::before, #switch .extension-popup.top::after { 36 | display: none !important; 37 | } 38 | #panels-container.left #switch .extension-popup { 39 | position: absolute !important; 40 | top: 1px !important; 41 | left: 35px !important; 42 | } 43 | #panels-container.right #switch .extensionaction { 44 | position: absolute !important; 45 | top: 1px !important; 46 | left: unset !important; 47 | right: 35px; 48 | } 49 | `; 50 | document.getElementsByTagName('head')[0].appendChild(style); 51 | }; 52 | 53 | function extPanel() { 54 | csse(); 55 | const wrapper = document.querySelector('.UrlBar > .extensions-wrapper'); 56 | const pref = document.getElementById('overlay'); 57 | const panel = document.getElementById('switch'); 58 | panel.insertBefore(wrapper, pref); 59 | }; 60 | 61 | // Loop waiting for the browser to load the UI. You can call all functions from just one instance. 62 | 63 | setTimeout(function wait() { 64 | const browser = document.getElementById('browser'); 65 | if (browser) { 66 | extPanel(); 67 | } 68 | else { 69 | setTimeout(wait, 300); 70 | } 71 | }, 300); 72 | -------------------------------------------------------------------------------- /move_extensions_to_status-bar.js: -------------------------------------------------------------------------------- 1 | /* 2 | Move Extensions to Status-Bar 3 | https://forum.vivaldi.net/topic/20643/showing-extension-icons-on-the-bottom-of-the-browser/6 4 | Moves the extension action buttons to the status bar. 5 | */ 6 | 7 | function csse() { 8 | const style = document.createElement('style'); 9 | style.type = 'text/css'; 10 | style.innerHTML = ` 11 | #footer .extensions-wrapper { 12 | -webkit-app-region: no-drag; 13 | } 14 | #footer .extensions-wrapper img { 15 | height: 16px; 16 | width: 16px; 17 | } 18 | #footer .extensions-wrapper .button-badge { 19 | max-height: 10px; 20 | max-width: 10px; 21 | min-width: 5px; 22 | } 23 | #footer .toggle-extensions-group svg { 24 | vertical-align: middle; 25 | } 26 | #footer .extensions-wrapper .dragging-cancelled, #footer .toggle-extensions-group { 27 | border-right: none; 28 | } 29 | #footer .extensions-wrapper .dragging-cancelled, #footer .toggle-extensions-group { 30 | background-color: transparent; 31 | } 32 | #footer .extensions-wrapper span:hover, #footer .toggle-extensions-group:hover { 33 | background-color: var(--colorBgDark); 34 | } 35 | #footer .extensions-wrapper button:focus:not([tabindex='-1']) { 36 | box-shadow: none; 37 | border-color: var(--colorBorder); 38 | } 39 | #footer .extension-popup.top::before, #footer .extension-popup.top::after { 40 | display: none !important; 41 | } 42 | #footer .extension-popup { 43 | position: absolute; 44 | top: unset !important; 45 | bottom: 22px; 46 | } 47 | #footer input.button-toolbar-small:last-of-type { 48 | margin-right: 6px; 49 | } 50 | `; 51 | document.getElementsByTagName('head')[0].appendChild(style); 52 | }; 53 | 54 | function extStatus() { 55 | csse(); 56 | const wrapper = document.querySelector('.UrlBar > .extensions-wrapper'); 57 | const footer = document.getElementById('footer'); 58 | footer.appendChild(wrapper); 59 | }; 60 | 61 | // Loop waiting for the browser to load the UI. You can call all functions from just one instance. 62 | 63 | setTimeout(function wait() { 64 | const browser = document.getElementById('browser'); 65 | if (browser) { 66 | extStatus(); 67 | } 68 | else { 69 | setTimeout(wait, 300); 70 | } 71 | }, 300); 72 | -------------------------------------------------------------------------------- /page-actions/Tab_Lock.js: -------------------------------------------------------------------------------- 1 | // Tab Lock 2 | // version 2021.9.0 3 | // https://forum.vivaldi.net/post/241508 4 | // Custom page action. Throws a warning when you try to navigate. Please read 5 | // installation instructions on the forum. 6 | 7 | (function () { 8 | window.addEventListener("beforeunload", (event) => { 9 | event.preventDefault(); 10 | event.returnValue = ""; 11 | }); 12 | window.addEventListener("popstate", () => console.log("block navigation")); 13 | })(); 14 | -------------------------------------------------------------------------------- /panel-doubleclick-automation.js: -------------------------------------------------------------------------------- 1 | // Panel doubleclick automation 2 | // https://forum.vivaldi.net/post/391224 3 | // Switches the tab or opens a bookmark by simulating a doubleclick on a tab or a bookmark, in the windows or bookmark panel. 4 | 5 | { 6 | function doClick(event) { 7 | event.stopPropagation(); 8 | event.preventDefault(); 9 | var ev = document.createEvent("MouseEvents"); 10 | ev.initEvent("dblclick", true, true); 11 | this.parentNode.dispatchEvent(ev); 12 | } 13 | 14 | function checkParents(el, sel) { 15 | while (el.parentNode) { 16 | el = el.parentNode; 17 | if (el.id === sel[0]) { 18 | return win; 19 | } else if (el.classList !== undefined && el.classList.contains(sel[1])) { 20 | return book; 21 | } 22 | } 23 | return null; 24 | } 25 | 26 | // Set win to 0 or 1 for either triggering the doubleclick on favicon, or window title. 27 | // Set book to 0 or 1 for either triggering the doubleclick on favicon, or bookmark title. 28 | const win = 1; 29 | const book = 1; 30 | 31 | var appendChild = Element.prototype.appendChild; 32 | Element.prototype.appendChild = function () { 33 | if (this.tagName === "LABEL" && arguments[0].tagName === "IMG") { 34 | setTimeout( 35 | function () { 36 | var check = checkParents(this, ["window-panel", "panel-bookmarks"]); 37 | if (check === 0 || check === 1) { 38 | this.childNodes[check].addEventListener("click", doClick); 39 | } 40 | }.bind(this, arguments[0]) 41 | ); 42 | } 43 | return appendChild.apply(this, arguments); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /panel-header-mod.css: -------------------------------------------------------------------------------- 1 | /* 2 | Panel Header mod 3 | https://forum.vivaldi.net/topic/18030/panel-header-modification 4 | Hides search and toolbar icons in panel, shows them when hovering header. – Useful for small displays, but looks neat too. ^^ 5 | */ 6 | 7 | .panel header { 8 | padding-top: 9px; 9 | } 10 | .panel header h1 { 11 | margin-bottom: 5px; 12 | } 13 | .panel.panel-bookmarks header:hover > .toolbar.icons, #notes-panel.panel header:hover > .toolbar.icons { 14 | max-height: 68px; 15 | margin-bottom: 0px; 16 | opacity: 1; 17 | } 18 | .panel.panel-bookmarks header:hover h1, #notes-panel.panel header:hover > h1 { 19 | color: var(--colorHighlightBg); 20 | } 21 | .panel.panel-bookmarks .toolbar.icons, #notes-panel.panel .toolbar.icons { 22 | max-height: 0px; 23 | margin-bottom: -6px; 24 | opacity: 0; 25 | transition: max-height 0.5s ease-out, margin 0.5s ease-out, opacity 0.5s ease-out; 26 | } 27 | -------------------------------------------------------------------------------- /panel-overlay-toggle.js: -------------------------------------------------------------------------------- 1 | /* 2 | Panel Overlay Toggle 3 | https://forum.vivaldi.net/topic/10590/overlay-panels/151 4 | A custom button to toggle the panel overlay mode right from the panels container. The panel toggle must be visible. Move it to the address bar where it will be made invisible by css, then you can reset the status bar to get the panel toggle back and hide or show the status bar as you please. 5 | */ 6 | 7 | function overlayToggle() { 8 | 9 | function icn() { 10 | if (cont.classList.contains('overlay') && exec === 0 || !cont.classList.contains('overlay') && exec === 1) { 11 | pathS.setAttribute('d', circE); 12 | } 13 | else { 14 | pathS.setAttribute('d', circD); 15 | } 16 | exec = 1; 17 | }; 18 | 19 | function simulateClick() { 20 | const toggle = document.querySelector('.panel-clickoutside-ignore button'); 21 | var down = new PointerEvent('pointerdown', { 22 | bubbles: true, 23 | cancelable: true, 24 | pointerType: "mouse", 25 | altKey: true 26 | }); 27 | var up = new PointerEvent('pointerup', { 28 | bubbles: true, 29 | cancelable: true, 30 | pointerType: "mouse", 31 | altKey: true 32 | }); 33 | icn(); 34 | toggle.dispatchEvent(down); 35 | toggle.dispatchEvent(up); 36 | }; 37 | 38 | var exec = 0; 39 | const style = document.createElement('style'); 40 | style.type = 'text/css'; 41 | style.innerHTML = '.UrlBar .panel-clickoutside-ignore.button-toolbar {width: 0px !important;visibility: hidden;}'; 42 | document.getElementsByTagName('head')[0].appendChild(style); 43 | const circE = 'M 13 13m -6, 0a 6,6 0 1,0 12,0a 6,6 0 1,0 -12,0 M 13 13m -4, 0a 4,4 0 1,0 8,0a 4,4 0 1,0 -8,0 M 13 13m -2, 0a 2,2 0 1,0 4,0a 2,2 0 1,0 -4,0'; 44 | const circD = 'M 13 13m -5.5, 0a 5.5,5.5 0 1,0 11,0a 5.5,5.5 0 1,0 -11,0 M 13 13m -2, 0a 2,2 0 1,0 4,0a 2,2 0 1,0 -4,0'; 45 | const cont = document.getElementById('panels-container'); 46 | const switchS = document.getElementById('switch'); 47 | var btnS = document.createElement('button'); 48 | btnS.classList.add('preferences'); 49 | btnS.id = 'overlayToggle'; 50 | btnS.title = 'Toggle Overlay'; 51 | btnS.setAttribute('tabindex', '-1'); 52 | btnS.innerHTML = ''; 53 | switchS.lastChild.style = 'margin-top: 0px'; 54 | switchS.insertBefore(btnS,switchS.lastChild); 55 | const pathS = document.querySelector('#overlayToggle svg path'); 56 | icn(); 57 | document.getElementById('overlayToggle').addEventListener('click', simulateClick); 58 | }; 59 | 60 | // Loop waiting for the browser to load the UI. You can call all functions from just one instance. 61 | 62 | setTimeout(function wait() { 63 | const browser = document.getElementById('browser'); 64 | if (browser) { 65 | overlayToggle(); 66 | } 67 | else { 68 | setTimeout(wait, 300); 69 | } 70 | }, 300); 71 | -------------------------------------------------------------------------------- /panel-toggle.js: -------------------------------------------------------------------------------- 1 | /* 2 | Panel Toggle 3 | https://forum.vivaldi.net/topic/22835/panel-toggle-to-address-bar 4 | Moves the panel toggle from status to address bar and gives it two states (on/off). 5 | */ 6 | 7 | function panelToggle() { 8 | const adr = document.querySelector('.UrlBar'); 9 | var panel = document.getElementById('panels-container'); 10 | var paneltog = document.querySelector('.paneltogglefooter'); 11 | var panelsvg = document.querySelector('.paneltogglefooter svg'); 12 | var panelpath = document.querySelector('.paneltogglefooter svg path'); 13 | var sright = 'd: path("M20 8v10h-14v-10h14zm-2 8v-6h-4v6h4z")'; 14 | var sleft = 'd: path("M20 8v10h-14v-10h14zm-8 8v-6h-4v6h4z")'; 15 | paneltog.classList.add('button-toolbar'); 16 | paneltog.classList.remove('button-toolbar-small'); 17 | paneltog.style.order = 'unset'; 18 | panelsvg.style.transform = 'none'; 19 | panelsvg.setAttributeNS(null, 'viewBox', '0 0 26 26'); 20 | 21 | if (panel.classList.contains('right')) { 22 | adr.appendChild(paneltog); 23 | var pof = sright; 24 | var pon = sleft; 25 | } 26 | else { 27 | adr.insertBefore(paneltog,adr.firstChild); 28 | var pof = sleft; 29 | var pon = sright; 30 | } 31 | if (panel.classList.contains('switcher')) { 32 | panelpath.style = pof; 33 | } 34 | else { 35 | panelpath.style = pon; 36 | } 37 | 38 | paneltog.addEventListener('click', function(event) { 39 | if (!event.altKey) { 40 | if (panel.classList.contains('switcher')) { 41 | panelpath.style = pon; 42 | } 43 | else { 44 | panelpath.style = pof; 45 | } 46 | } 47 | }); 48 | }; 49 | 50 | // Loop waiting for the browser to load the UI. You can call all functions from just one instance. 51 | 52 | setTimeout(function wait() { 53 | const browser = document.getElementById('browser'); 54 | if (browser) { 55 | panelToggle(); 56 | } 57 | else { 58 | setTimeout(wait, 300); 59 | } 60 | }, 300); 61 | -------------------------------------------------------------------------------- /profile-icon.js: -------------------------------------------------------------------------------- 1 | // Profile Icon 2 | // version 2023.2.0 3 | // https://forum.vivaldi.net/post/522106 4 | // Exchanges your account/default profile image (sync), for a proper SVG icon 5 | // using theme colors. Credits to tam710562 for coming up with a solution to 6 | // reinstate the SVG when the address bar is being toggled. Icon: Font Awesome 6 7 | // Free. 8 | 9 | (function profileIcon() { 10 | let appendChild = Element.prototype.appendChild; 11 | Element.prototype.appendChild = function () { 12 | if (arguments[0].tagName === "BUTTON") { 13 | setTimeout( 14 | function () { 15 | if (this.classList.contains("ToolbarButton-Button")) { 16 | if (this.name === "AccountButton") { 17 | this.innerHTML = ``; 18 | } 19 | } 20 | }.bind(this, arguments[0]) 21 | ); 22 | } 23 | return appendChild.apply(this, arguments); 24 | }; 25 | })(); 26 | -------------------------------------------------------------------------------- /random-theme-button.js: -------------------------------------------------------------------------------- 1 | // Random Theme Button 2 | // version 2022.10.2 3 | // https://forum.vivaldi.net/topic/34767/random-theme-button 4 | // Load a random theme when clicking a command chain button. 5 | 6 | (function randomTheme() { 7 | function randomize() { 8 | vivaldi.prefs.get("vivaldi.themes.current", (current) => { 9 | vivaldi.prefs.get("vivaldi.themes.user", (collection) => { 10 | if (collection.length > 1) { 11 | let rd = ""; 12 | while (rd === "" || rd.id === current) { 13 | rd = collection[Math.floor(Math.random() * collection.length)]; 14 | } 15 | vivaldi.prefs.set({ path: "vivaldi.themes.current", value: rd.id }); 16 | } else { 17 | console.log( 18 | "Please create additional themes in vivaldi://settings/themes" 19 | ); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | let appendChild = Element.prototype.appendChild; 26 | Element.prototype.appendChild = function () { 27 | if (this.tagName === "BUTTON") { 28 | setTimeout( 29 | function () { 30 | if ( 31 | this.title === "Random Theme" && 32 | this.classList.contains("ToolbarButton-Button") 33 | ) { 34 | this.innerHTML = ``; 35 | this.addEventListener("click", randomize); 36 | } 37 | }.bind(this, arguments[0]) 38 | ); 39 | } 40 | return appendChild.apply(this, arguments); 41 | }; 42 | })(); 43 | -------------------------------------------------------------------------------- /safari-style.js: -------------------------------------------------------------------------------- 1 | /* 2 | Safari Style 3 | https://forum.vivaldi.net/topic/23138/solved-modding-the-adressbar-top-window-to-get-it-look-like-safari-browser/6 4 | Safari (browser) like address bar. Requested mod. 5 | */ 6 | 7 | function cssm() { 8 | const style = document.createElement('style'); 9 | style.type = 'text/css'; 10 | style.innerHTML = ` 11 | #tabs-container.no-thumbs.bottom { 12 | order: -1; 13 | padding-top: 4px; 14 | padding-bottom: 0px; 15 | } 16 | .toolbar.UrlBar { 17 | display: flex; 18 | order: -2; 19 | } 20 | .stacks-on.tabs-bottom .tab-strip .tab-group-indicator { 21 | bottom: 28px; 22 | } 23 | .stacks-on.tabs-bottom .tab-strip .tab-group-indicator .tab-indicator.active { 24 | padding-top: 3px; 25 | } 26 | #header { 27 | min-height: 0 !important; 28 | z-index: auto; 29 | } 30 | .container { 31 | width: 100%; 32 | display: flex; 33 | justify-content: space-between; 34 | -webkit-app-region: drag; 35 | } 36 | #browser.mac .window-buttongroup { 37 | display: flex; 38 | margin-top: 9px !important; 39 | } 40 | #browser.mac .window-buttongroup button { 41 | margin-right: 8px; 42 | } 43 | #browser.mac .window-buttongroup button.window-minimze { 44 | order: 1; 45 | } 46 | #browser.mac .window-buttongroup button.window-maximize { 47 | order: 2; 48 | } 49 | .UrlBar-AddressField { 50 | max-width: 600px; 51 | } 52 | `; 53 | document.getElementsByTagName('head')[0].appendChild(style); 54 | }; 55 | 56 | function safariStyle() { 57 | cssm(); 58 | const adr = document.querySelector('.UrlBar'); 59 | var windowbuttons = document.querySelector('.window-buttongroup'); 60 | var container = document.createElement('div'); 61 | var extwrapper = document.querySelector('.UrlBar > .extensions-wrapper'); 62 | var tools = document.querySelector('.UrlBar .toolbar'); 63 | var adfield = document.querySelector('.UrlBar-AddressField'); 64 | container.classList.add('container') 65 | adr.insertBefore(windowbuttons,adr.firstChild); 66 | adr.insertBefore(container,adr.lastChild); 67 | container.appendChild(tools); 68 | container.appendChild(adfield); 69 | container.appendChild(extwrapper); 70 | }; 71 | 72 | // Loop waiting for the browser to load the UI. You can call all functions from just one instance. 73 | 74 | setTimeout(function wait() { 75 | const browser = document.getElementById('browser'); 76 | if (browser) { 77 | safariStyle(); 78 | } 79 | else { 80 | setTimeout(wait, 300); 81 | } 82 | }, 300); 83 | -------------------------------------------------------------------------------- /scrollable-startpage-navigation.js: -------------------------------------------------------------------------------- 1 | // Scrollable Startpage Navigation 2 | // version 2022.03.1 3 | // https://forum.vivaldi.net/post/561919 4 | // Navigate startpage categories with mousewheel. 5 | 6 | (function () { 7 | let scroll = (e) => { 8 | const btns = Array.from( 9 | document.querySelectorAll(".startpage-navigation-group button") 10 | ); 11 | const index = btns.findIndex((x) => x.classList.contains("active")); 12 | const dir = e.wheelDelta > 0 ? "up" : "down"; 13 | if (dir === "up") { 14 | if (index > 0) { 15 | btns[index - 1].click(); 16 | } else { 17 | btns[btns.length - 1].click(); 18 | } 19 | } else { 20 | if (index < btns.length - 1) { 21 | btns[index + 1].click(); 22 | } else { 23 | btns[0].click(); 24 | } 25 | } 26 | }; 27 | 28 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 29 | try { 30 | if (changeInfo.url.startsWith("chrome://vivaldi-webui/startpage")) { 31 | const check = document.querySelector(".vm-scroll"); 32 | if (!check) { 33 | const nav = document.querySelector(".startpage-navigation"); 34 | nav.classList.add("vm-scroll"); 35 | nav.addEventListener("wheel", (event) => { 36 | let timeout; 37 | if (timeout) window.cancelAnimationFrame(timeout); 38 | timeout = window.requestAnimationFrame(() => scroll(event)); 39 | }, {passive: true}); 40 | } 41 | } 42 | } catch (error) { 43 | void 0; 44 | } 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /second-toolbar.js: -------------------------------------------------------------------------------- 1 | // Second Toolbar 2 | // version 2022.4.0 3 | // https://forum.vivaldi.net/post/560359 4 | // Adds a second toolbar to the UrlBar and moves »numberOfButtons« from the 5 | // original toolbar to it. 6 | 7 | (function secondToolbar() { 8 | function addToolbar(adr) { 9 | const check = document.querySelector(".vm-st-move"); 10 | if (!check) { 11 | const div = document.createElement("div"); 12 | div.classList.add( 13 | "toolbar", 14 | "toolbar-droptarget", 15 | "toolbar-mainbar", 16 | "toolbar-large", 17 | "vm-st-move" 18 | ); 19 | adr.parentNode.insertBefore(div, adr.nextSibling); 20 | const target = document.querySelector(".vm-st-move"); 21 | const numberOfButtons = 2; // change number of buttons to be moved 22 | for (let i = 0; i < numberOfButtons; i++) { 23 | const btn = document.querySelector( 24 | ".UrlBar .toolbar-mainbar .button-toolbar button" 25 | ); 26 | target.appendChild(btn.parentNode); 27 | } 28 | } 29 | } 30 | 31 | let appendChild = Element.prototype.appendChild; 32 | Element.prototype.appendChild = function () { 33 | if (arguments[0].tagName === "DIV") { 34 | setTimeout( 35 | function () { 36 | if (this.classList.contains("UrlBar-AddressField")) { 37 | addToolbar(this); 38 | } 39 | }.bind(this, arguments[0]) 40 | ); 41 | } 42 | return appendChild.apply(this, arguments); 43 | }; 44 | })(); 45 | -------------------------------------------------------------------------------- /statusbar.js: -------------------------------------------------------------------------------- 1 | // Statusbar above panel 2 | // version 2023.4.0 3 | // https://forum.vivaldi.net/post/652235 4 | // Moves the statusbar above the panel, adds style to fit the theme 5 | // (transparency, blur), and lines up the first and last button. 6 | 7 | (function statusbarAbovePanel() { 8 | const css = ` 9 | .address-top.tabs-bottom .mainbar > .toolbar-mainbar { 10 | border-bottom-color: transparent !important; 11 | } 12 | #tabs-tabbar-container.bottom { 13 | padding: unset; 14 | } 15 | footer { 16 | background-color: var(--colorBgAlphaBlur) !important; 17 | backdrop-filter: var(--backgroundBlur); 18 | } 19 | footer:has(.toolbar-statusbar) { 20 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 21 | } 22 | .toolbar-statusbar > .button-toolbar:first-of-type > button { 23 | padding-left: 3px; 24 | } 25 | .toolbar-statusbar > .button-toolbar:last-of-type > button { 26 | padding-right: 3px; 27 | } 28 | `; 29 | 30 | setTimeout(function wait() { 31 | const footer = document.querySelector("footer"); 32 | if (footer) { 33 | const style = document.createElement("style"); 34 | style.id = "vm-sap-css"; 35 | style.innerHTML = css; 36 | document.getElementsByTagName("head")[0].appendChild(style); 37 | document 38 | .getElementById("main") 39 | .insertBefore(footer, document.querySelector(".inner")); 40 | } else setTimeout(wait, 300); 41 | }, 300); 42 | })(); 43 | -------------------------------------------------------------------------------- /tab-scroll.js: -------------------------------------------------------------------------------- 1 | // Tab Scroll 2 | // version 2024.9.2 3 | // https://forum.vivaldi.net/post/214898 4 | // Clicking on an active tab scrolls page to top, clicking it again returns to 5 | // previous scroll position. Credits to tam710562 from Vivaldi Forum for coming 6 | // up with the sessionStorage solution, which made this possible. 7 | 8 | (function tabScroll() { 9 | "use strict"; 10 | 11 | // EDIT START 12 | // choose scroll behavior, instant or smooth 13 | const scb = "instant"; 14 | // EDIT END 15 | 16 | function exit(tab) { 17 | tab.removeEventListener("mousemove", exit); 18 | tab.removeEventListener("click", trigger); 19 | } 20 | 21 | function trigger(tab) { 22 | chrome.scripting.executeScript({ 23 | target: { tabId: Number(tab.parentNode.id.replace(/\D/g, "")) }, 24 | func: script, 25 | args: [scb], 26 | }); 27 | exit(tab); 28 | } 29 | 30 | function react(e, tab) { 31 | if ( 32 | tab.classList.contains("active") && 33 | e.which === 1 && 34 | !(e.target.nodeName === "path" || e.target.nodeName === "svg") && 35 | !e.shiftKey && 36 | !e.ctrlKey && 37 | !e.altKey && 38 | !e.metaKey 39 | ) { 40 | tab.addEventListener("mousemove", exit(tab)); 41 | tab.addEventListener("click", trigger(tab)); 42 | } 43 | } 44 | 45 | const script = (scb) => { 46 | let offset = window.scrollY; 47 | if (offset > 0) { 48 | window.sessionStorage.setItem("offset", offset); 49 | window.scrollTo({ top: 0, behavior: scb }); 50 | } else { 51 | window.scrollTo({ 52 | top: window.sessionStorage.getItem("offset") || 0, 53 | behavior: scb, 54 | }); 55 | } 56 | }; 57 | 58 | let appendChild = Element.prototype.appendChild; 59 | Element.prototype.appendChild = function () { 60 | if ( 61 | arguments[0].tagName === "DIV" && 62 | arguments[0].classList.contains("tab") 63 | ) { 64 | setTimeout( 65 | function () { 66 | const ts = (event) => react(event, arguments[0]); 67 | arguments[0].addEventListener("mousedown", ts); 68 | }.bind(this, arguments[0]) 69 | ); 70 | } 71 | return appendChild.apply(this, arguments); 72 | }; 73 | })(); 74 | -------------------------------------------------------------------------------- /tab-stacks-theming.css: -------------------------------------------------------------------------------- 1 | /* 2 | Tab Stacks Theming 3 | https://forum.vivaldi.net/topic/16594/tab-stacks-theming 4 | Lets you theme the indicators in tab stacks. Colors are just examples, use whatever works for you. Would probably make sense to implement calculated colors provided by Vivaldi instead, to ensure theme compatibility. 5 | */ 6 | 7 | .tab-group-indicator .tab-indicator.active { 8 | background-color: #fd3563 !important; 9 | } 10 | .tab-group-indicator .tab-indicator { 11 | background-color: #fff !important; 12 | } 13 | .tab-group-indicator .tab-indicator:hover:not(.active) { 14 | background-color: #000 !important; 15 | } 16 | -------------------------------------------------------------------------------- /tabs_below_address-bar.css: -------------------------------------------------------------------------------- 1 | /* 2 | Tabs Below Address-Bar 3 | https://forum.vivaldi.net/topic/15834/tabs-on-bottom-for-1-8/41?page=3 4 | Displays tab bar below the address bar. Simple CSS only solution. Set tab bar to bottom in vivaldi://settings, or this mod won't work!!! 5 | */ 6 | 7 | #tabs-container.bottom { 8 | order: -1; 9 | border-top: none; 10 | padding-top: var(--padding); 11 | padding-bottom: 0px !important; 12 | } 13 | 14 | /* bookmarks bar above tabs */ 15 | /* 16 | .bookmark-bar { 17 | order: -2; 18 | } 19 | */ 20 | 21 | .UrlBar { 22 | order: -3; 23 | } 24 | 25 | /* corner rounding */ 26 | .tabs-bottom .tab-position .tab { 27 | border-top-left-radius: var(--radiusHalf); 28 | border-top-right-radius: var(--radiusHalf); 29 | border-bottom-left-radius: unset; 30 | border-bottom-right-radius: unset; 31 | } 32 | 33 | /* tab group indicators*/ 34 | .tabs-bottom .tab-strip .tab-group-indicator { 35 | bottom: 28px !important; 36 | }; 37 | -------------------------------------------------------------------------------- /theme-interface-plus.js: -------------------------------------------------------------------------------- 1 | // Theme Interface plus 2 | // version 2022.5.0 3 | // https://forum.vivaldi.net/post/531981 4 | // Adds functionality to toggle system themes, sort user themes alphabetically, 5 | // move themes individually and expand the overview, to Vivaldi’s settings page. 6 | 7 | (function themeInterfacePlus() { 8 | function toggle(init) { 9 | const css = document.getElementById("vm-tip-css"); 10 | if ( 11 | (systemDefault === 0 && init === 1) || 12 | (systemDefault === 1 && init !== 1) 13 | ) { 14 | if (!css) { 15 | vivaldi.prefs.get("vivaldi.themes.current", (current) => { 16 | vivaldi.prefs.get("vivaldi.themes.system", (sys) => { 17 | let index = sys.findIndex((x) => x.id === current); 18 | const hide = document.createElement("style"); 19 | hide.id = "vm-tip-css"; 20 | hide.innerHTML = ` 21 | .ThemePreviews > div:nth-child(-n + ${sys.length}):not(:nth-child(${index + 1})) { 22 | display: none; 23 | } 24 | `; 25 | document.getElementsByTagName("head")[0].appendChild(hide); 26 | }); 27 | }); 28 | } 29 | systemDefault = 0; 30 | } else { 31 | if (css) css.parentNode.removeChild(css); 32 | systemDefault = 1; 33 | } 34 | } 35 | 36 | function sort() { 37 | vivaldi.prefs.get("vivaldi.themes.user", (collection) => { 38 | collection.sort((a, b) => { 39 | return a.name.localeCompare(b.name); 40 | }); 41 | vivaldi.prefs.set({ path: "vivaldi.themes.user", value: collection }); 42 | }); 43 | } 44 | 45 | function move(dir) { 46 | vivaldi.prefs.get("vivaldi.themes.current", (current) => { 47 | vivaldi.prefs.get("vivaldi.themes.user", (collection) => { 48 | let index = collection.findIndex((x) => x.id === current); 49 | if (index > -1 && dir === "right") { 50 | if (index === collection.length - 1) { 51 | collection.unshift(collection.splice(index, 1)[0]); 52 | } else { 53 | let fromI = collection[index]; 54 | let toI = collection[index + 1]; 55 | collection[index + 1] = fromI; 56 | collection[index] = toI; 57 | } 58 | } else if (index > -1 && dir !== "right") { 59 | if (index === 0) { 60 | collection.push(collection.splice(index, 1)[0]); 61 | } else { 62 | let fromI = collection[index]; 63 | let toI = collection[index - 1]; 64 | collection[index - 1] = fromI; 65 | collection[index] = toI; 66 | } 67 | } else return; 68 | vivaldi.prefs.set({ path: "vivaldi.themes.user", value: collection }); 69 | }); 70 | }); 71 | } 72 | 73 | function expand(opt) { 74 | const view = document.querySelector(".TabbedView"); 75 | if (opt === 1 || expansion === 0) { 76 | view.style.maxWidth = "unset"; 77 | expansion = 1; 78 | } else if (opt === 0) { 79 | view.style.maxWidth = "660px"; 80 | } else { 81 | view.style.maxWidth = "660px"; 82 | expansion = 0; 83 | } 84 | } 85 | 86 | const goUI = { 87 | buttons: [ 88 | ["Toggle", toggle], 89 | ["Sort", sort], 90 | ["\u{25C2}", move], 91 | ["\u{25B8}", () => move("right")], 92 | ["\u{FF3B}\u{FF3D}", expand], 93 | ], 94 | load: () => { 95 | const footer = document.querySelector(".TabbedView-Footer"); 96 | const link = document.querySelector(".TabbedView-Footer a"); 97 | if (!footer.classList.contains("vm-tip-footer")) { 98 | footer.classList.add("vm-tip-footer"); 99 | goUI.buttons.forEach((button) => { 100 | let b = document.createElement("input"); 101 | b.type = "button"; 102 | b.value = button[0]; 103 | footer.insertBefore(b, link); 104 | b.addEventListener("click", button[1]); 105 | }); 106 | } 107 | if (expansion === 1) expand(1); 108 | }, 109 | }; 110 | 111 | function mi5(mutations) { 112 | mutations.forEach((mutation) => { 113 | mutation.addedNodes.forEach((node) => { 114 | if ( 115 | node.classList.contains("TabbedView-Content") && 116 | document.querySelector(".ThemePreviews") 117 | ) { 118 | goUI.load(); 119 | } else { 120 | if (expansion === 1) expand(0); 121 | } 122 | }); 123 | }); 124 | } 125 | 126 | let systemDefault = 0; // set to »1« to display system themes by default 127 | let expansion = 0; // set to »1« for the maximum number of themes per row by default 128 | const settingsUrl = 129 | "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path="; 130 | toggle(1); 131 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 132 | if (changeInfo.url === `${settingsUrl}themes`) { 133 | goUI.load(); 134 | const view = document.querySelector(".TabbedView"); 135 | new MutationObserver(mi5).observe(view, { childList: true }); 136 | } 137 | }); 138 | })(); 139 | -------------------------------------------------------------------------------- /toggle-keys-and-gestures.js: -------------------------------------------------------------------------------- 1 | // Toggle Keys and Gestures 2 | // version 2023.5.0 3 | // https://forum.vivaldi.net/post/622831 4 | // Button for toggling keyboard shortcuts and mouse gestures. Highlight 5 | // indicates whether something is disabled. Can be toggled together or 6 | // individually (shift‐click/alt‐click). See instructions on the forum for 7 | // creating a custom icon. 8 | 9 | (function toggleKeysAndGestures() { 10 | function run(e, el) { 11 | vivaldi.prefs.get(sk, (k) => { 12 | vivaldi.prefs.get(sm, (m) => { 13 | if (e !== null && !e.ctrlKey) { 14 | if (e.shiftKey) { 15 | if (k === true) k = false; 16 | else k = true; 17 | } else if (e.altKey) { 18 | if (m === true) m = false; 19 | else m = true; 20 | } else { 21 | if (k === true) k = false; 22 | else k = true; 23 | if (m === true) m = false; 24 | else m = true; 25 | } 26 | vivaldi.prefs.set({ path: sk, value: k }); 27 | vivaldi.prefs.set({ path: sm, value: m }); 28 | } 29 | if (k === false || m === false) { 30 | el.style = "color: var(--colorHighlightBg)"; 31 | } else { 32 | el.style = "color: unset"; 33 | } 34 | el.title = "Toggle Keys and Gestures"; 35 | if (k === true) el.title += "\n\u{2022} Keys enabled (shift)"; 36 | else el.title += "\n\u{2022} Keys disabled (shift)"; 37 | if (m === true) el.title += "\n\u{2022} Gestures enabled (alt)"; 38 | else el.title += "\n\u{2022} Gestures disabled (alt)"; 39 | }); 40 | }); 41 | } 42 | 43 | const sk = "vivaldi.keyboard.shortcuts.enable"; 44 | const sm = "vivaldi.mouse_gestures.enabled"; 45 | let check = 0 46 | let appendChild = Element.prototype.appendChild; 47 | Element.prototype.appendChild = function () { 48 | if (this.tagName === "BUTTON") { 49 | setTimeout( 50 | function () { 51 | if ( 52 | this.title === "Toggle Keys and Gestures") { 53 | run(null, this); 54 | const toggle = (event) => run(event, this); 55 | this.addEventListener("click", toggle); 56 | if (check === 0) { 57 | vivaldi.prefs.onChanged.addListener((e) => { 58 | if (e.path === sk || e.path === sm) run(null, this); 59 | }); 60 | check = 1; 61 | } 62 | } 63 | }.bind(this, arguments[0]) 64 | ); 65 | } 66 | return appendChild.apply(this, arguments); 67 | }; 68 | })(); 69 | -------------------------------------------------------------------------------- /toggle_site-info.css: -------------------------------------------------------------------------------- 1 | /* 2 | Toggle Site Info 3 | version 2021.9.0 4 | Hides site info text in address field, displays it on hover. 5 | */ 6 | 7 | .siteinfo-symbol:hover + .siteinfo-text { 8 | display: block; 9 | } 10 | .siteinfo-text { 11 | display: none; 12 | } 13 | -------------------------------------------------------------------------------- /toolbar-icons-mod.js: -------------------------------------------------------------------------------- 1 | // Toolbar icons mod 2 | // version 2023.3.2 3 | // Icons: Font Awesome 6 Free 4 | 5 | (function toolbarIconsMod() { 6 | function toggleKeysAndGestures(e, el) { 7 | vivaldi.prefs.get(sk, (k) => { 8 | vivaldi.prefs.get(sm, (m) => { 9 | if (e !== null && !e.ctrlKey) { 10 | if (e.shiftKey) { 11 | if (k === true) k = false; 12 | else k = true; 13 | } else if (e.altKey) { 14 | if (m === true) m = false; 15 | else m = true; 16 | } else { 17 | if (k === true) k = false; 18 | else k = true; 19 | if (m === true) m = false; 20 | else m = true; 21 | } 22 | vivaldi.prefs.set({ path: sk, value: k }); 23 | vivaldi.prefs.set({ path: sm, value: m }); 24 | } 25 | if (k === false || m === false) { 26 | el.style = "color: var(--colorHighlightBg)"; 27 | } else { 28 | el.style = "color: unset"; 29 | } 30 | el.title = "Toggle Keys and Gestures"; 31 | if (k === true) el.title += "\n\u{2022} Keys enabled (shift)"; 32 | else el.title += "\n\u{2022} Keys disabled (shift)"; 33 | if (m === true) el.title += "\n\u{2022} Gestures enabled (alt)"; 34 | else el.title += "\n\u{2022} Gestures disabled (alt)"; 35 | }); 36 | }); 37 | } 38 | 39 | const sk = "vivaldi.keyboard.shortcuts.enable"; 40 | const sm = "vivaldi.mouse_gestures.enabled"; 41 | let check = 0; 42 | 43 | let appendChild = Element.prototype.appendChild; 44 | Element.prototype.appendChild = function () { 45 | if (this.tagName === "BUTTON") { 46 | setTimeout( 47 | function () { 48 | if (this.classList.contains("ToolbarButton-Button")) { 49 | if (this.name === "AccountButton") { 50 | this.innerHTML = ``; 51 | } 52 | if (this.title === "Toggle Keys and Gestures") { 53 | toggleKeysAndGestures(null, this); 54 | const run = (event) => toggleKeysAndGestures(event, this); 55 | this.addEventListener("click", run); 56 | if (check === 0) { 57 | vivaldi.prefs.onChanged.addListener((e) => { 58 | if (e.path === sk || e.path === sm) 59 | toggleKeysAndGestures(null, this); 60 | }); 61 | check = 1; 62 | } 63 | } 64 | if (this.title === "Toolbar Editor") { 65 | this.addEventListener("click", () => { 66 | if (document.querySelector(".toolbar-editor")) { 67 | document 68 | .querySelector( 69 | ".toolbar-editor .dialog-footer input:last-of-type" 70 | ) 71 | .click(); 72 | } 73 | }); 74 | } 75 | if (this.title === "Update Feeds") { 76 | this.addEventListener("click", () => { 77 | setTimeout(() => { 78 | document 79 | .querySelector("input[value='Update All Feeds']") 80 | .click(); 81 | }, 150); 82 | }); 83 | } 84 | if (this.name === "COMMAND_clg2nkc2700u92v61tsani64d") { 85 | const util = vivaldi.utilities.getVersion(); 86 | const version = util.vivaldiVersion; 87 | this.title = `Vivaldi ${version}\nMail ${util.mailVersion}\nChromium ${util.chromiumVersion}`; 88 | this.addEventListener("click", () => { 89 | this.focus(); 90 | navigator.clipboard.writeText(version); 91 | this.blur(); 92 | }); 93 | } 94 | if (this.title.startsWith("Show Closed Tabs")) { 95 | this.innerHTML = ``; 96 | } 97 | } 98 | }.bind(this, arguments[0]) 99 | ); 100 | } 101 | return appendChild.apply(this, arguments); 102 | }; 103 | })(); 104 | -------------------------------------------------------------------------------- /update-feeds.js: -------------------------------------------------------------------------------- 1 | // Update feeds 2 | // version 2023.1.0 3 | // https://forum.vivaldi.net/post/641164 4 | // Custom button to update feeds from any toolbar. See linked topic for full 5 | // instructions. 6 | 7 | (function updateFeeds() { 8 | let appendChild = Element.prototype.appendChild; 9 | Element.prototype.appendChild = function () { 10 | if (this.tagName === "BUTTON") { 11 | setTimeout( 12 | function () { 13 | if (this.classList.contains("ToolbarButton-Button")) { 14 | // make sure following title exactly matches the name of your 15 | // command chain 16 | if (this.title === "Update Feeds") { 17 | this.addEventListener("click", () => { 18 | setTimeout(() => { 19 | // make sure following input value exactly matches the title 20 | // of the button to update all feeds in vivaldi://settings/rss 21 | // this is dependent on your browser language settings 22 | document 23 | .querySelector("input[value='Update All Feeds']") 24 | .click(); 25 | }, 150); 26 | }); 27 | } 28 | } 29 | }.bind(this, arguments[0]) 30 | ); 31 | } 32 | return appendChild.apply(this, arguments); 33 | }; 34 | })(); 35 | -------------------------------------------------------------------------------- /urlbar-spacing.js: -------------------------------------------------------------------------------- 1 | // UrlBar Spacing 2 | // version 2022.4.0 3 | // https://forum.vivaldi.net/post/400239 4 | // Adds a flexible margin around the Addressfield, depending 5 | // on width of the window. The window can be dragged by clicking 6 | // the margins. 7 | 8 | (function urlBarSpacing() { 9 | const spacing = "92%"; // change percentage to control spacing inside wrapper 10 | const css = ` 11 | .vm-us-wrapper { 12 | flex: 1 0; 13 | -webkit-app-region: drag; 14 | } 15 | .vm-us-spacer { 16 | display: flex; 17 | margin-left: auto; 18 | margin-right: auto; 19 | width: ${spacing}; 20 | } 21 | `; 22 | 23 | function space(url) { 24 | const check = document.getElementById("vm-us-css"); 25 | if (!check) { 26 | const style = document.createElement("style"); 27 | style.id = "vm-us-css"; 28 | style.innerHTML = css; 29 | document.getElementsByTagName("head")[0].appendChild(style); 30 | } 31 | const wrapper = document.createElement("div"); 32 | wrapper.classList.add("vm-us-wrapper"); 33 | const spacer = document.createElement("div"); 34 | spacer.classList.add("vm-us-spacer"); 35 | url.parentNode.replaceChild(wrapper, url); 36 | wrapper.appendChild(spacer); 37 | spacer.appendChild(url); 38 | } 39 | 40 | let appendChild = Element.prototype.appendChild; 41 | Element.prototype.appendChild = function () { 42 | if (arguments[0].tagName === "DIV") { 43 | setTimeout( 44 | function () { 45 | if (arguments[0].classList.contains("UrlBar-AddressField")) { 46 | const check = document.querySelector(".vm-us-spacer"); 47 | if (!check) { 48 | space(arguments[0]); 49 | } 50 | } 51 | }.bind(this, arguments[0]) 52 | ); 53 | } 54 | return appendChild.apply(this, arguments); 55 | }; 56 | })(); 57 | -------------------------------------------------------------------------------- /vivaldi-icon.js: -------------------------------------------------------------------------------- 1 | // Vivaldi Icon 2 | // version 2021.9.0 3 | // https://forum.vivaldi.net/topic/28047/vivaldi-icon 4 | // Places the Vivaldi logo in the UI. Mostly cosmetic. 5 | 6 | (function () { 7 | function vivIcon() { 8 | const panel = document.getElementById("switch"); 9 | const div = document.createElement("div"); 10 | div.id = "vivIcn"; 11 | div.style.height = "34px"; 12 | div.innerHTML = 13 | ''; 14 | panel.insertBefore(div, panel.firstChild); 15 | document.querySelector("#vivIcn svg").style = 16 | "width: 16px; height: 16px; fill: var(--colorFgFaded); display: block; margin: 9px auto;"; 17 | document.getElementById("vivIcn").style = "-webkit-app-region: drag"; 18 | } 19 | 20 | setTimeout(function wait() { 21 | const browser = document.getElementById("browser"); 22 | if (browser) { 23 | vivIcon(); 24 | } else { 25 | setTimeout(wait, 300); 26 | } 27 | }, 300); 28 | })(); 29 | -------------------------------------------------------------------------------- /window-buttons-macos.css: -------------------------------------------------------------------------------- 1 | /* 2 | Window buttons mod for macOS 3 | https://forum.vivaldi.net/topic/33606/window-buttons-mod-for-macos 4 | Changes the shape of unhovered window buttons on macOS. A version for Windows is available in the topic. 5 | */ 6 | 7 | #browser.mac .window-buttongroup button svg { 8 | display: none; 9 | } 10 | #browser.mac #titlebar { 11 | height: 100%; 12 | } 13 | #browser.mac .window-buttongroup { 14 | -webkit-app-region: no-drag; 15 | height: 100%; 16 | margin: 0; 17 | padding: 14px 8px 0; 18 | } 19 | #browser.mac #titlebar.tabless .window-buttongroup { 20 | margin-top: 0px; 21 | padding-top: 5px; 22 | } 23 | .stacks-off.fullscreen#browser.mac .window-buttongroup, .tabs-at-edge#browser.mac .window-buttongroup { 24 | margin-top: 0px; 25 | padding-top: 9px; 26 | } 27 | #browser.mac .window-buttongroup button { 28 | margin-top: 3px; 29 | height: 6px; 30 | width: 12px; 31 | opacity: 0.2; 32 | background-color: var(--colorFg); 33 | border: 1px solid rgba(0, 0, 0, 0.15); 34 | border-radius: 2px; 35 | transition-duration: 150ms; 36 | transition-property: margin, height, border-radius; 37 | } 38 | #browser.mac .window-buttongroup:hover button { 39 | margin-top: 0px; 40 | height: 12px; 41 | border-radius: 50%; 42 | } 43 | :not(.fullscreen).hasfocus#browser.mac .window-buttongroup.graphite button.window-minimize, .hasfocus#browser.mac .window-buttongroup.graphite button.window-maximize, .hasfocus#browser.mac .window-buttongroup.graphite button.window-close { 44 | opacity: 1; 45 | background-color: #8e8e93; 46 | border-color: #727277; 47 | } 48 | :not(.fullscreen).hasfocus#browser.mac .window-buttongroup:not(.graphite) button.window-minimize { 49 | opacity: 1; 50 | background-color: #f7bb3f; 51 | border-color: #dc9e34; 52 | } 53 | .hasfocus#browser.mac .window-buttongroup:not(.graphite) button.window-maximize { 54 | opacity: 1; 55 | background-color: #34c749; 56 | border-color: #1fa42d; 57 | } 58 | .hasfocus#browser.mac .window-buttongroup:not(.graphite) button.window-close { 59 | opacity: 1; 60 | background-color: #f8625e; 61 | border-color: #de4947; 62 | } 63 | --------------------------------------------------------------------------------