Add vivaldi browser modifications
This commit is contained in:
parent
09d936d7ea
commit
48f9a474d1
18 changed files with 3865 additions and 0 deletions
6
.vivaldi-mods/.luarc.json
Executable file
6
.vivaldi-mods/.luarc.json
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
|
||||||
|
"Lua.diagnostics.globals": [
|
||||||
|
"hilbish"
|
||||||
|
]
|
||||||
|
}
|
6
.vivaldi-mods/after-update.lua
Executable file
6
.vivaldi-mods/after-update.lua
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env hilbish
|
||||||
|
|
||||||
|
-- Run this script after each Vivaldi update.
|
||||||
|
|
||||||
|
hilbish.run("./restore-backup.lua")
|
||||||
|
hilbish.run("./install-mods.lua")
|
112
.vivaldi-mods/css/dialog.css
Executable file
112
.vivaldi-mods/css/dialog.css
Executable file
|
@ -0,0 +1,112 @@
|
||||||
|
/**
|
||||||
|
* Opens links in a dialog, either by key combinations, holding the middle mouse button or context menu
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/92501/open-in-dialog-mod?_=1717490394230
|
||||||
|
*/
|
||||||
|
|
||||||
|
@keyframes dialog-tab-animate-progress-bar {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-container {
|
||||||
|
z-index: 999;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(0, 0, 0, .4);
|
||||||
|
transition-property: background-color;
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
transition-timing-function: ease;
|
||||||
|
transition-delay: 0s;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.dialog-tab {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.options-container {
|
||||||
|
display: flex;
|
||||||
|
height: 35px;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
z-index: 1160;
|
||||||
|
|
||||||
|
.options-button {
|
||||||
|
display: flex;
|
||||||
|
background: transparent;
|
||||||
|
border: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--colorBgAlphaHeavy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-input {
|
||||||
|
background: var(--colorAccentBgAlpha);
|
||||||
|
color: white;
|
||||||
|
border: unset;
|
||||||
|
width: 20%;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 5px;
|
||||||
|
max-width: calc(100% - var(--radius) * 1.5);
|
||||||
|
left: calc(var(--radius)* 0.75);
|
||||||
|
background-color: #0080ff;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: width 0.2s linear;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&[style*="width: 100%"]:after {
|
||||||
|
content: "";
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 255, 255, 0.5),
|
||||||
|
rgba(255, 255, 255, 0.8),
|
||||||
|
rgba(255, 255, 255, 0.5)
|
||||||
|
);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
animation: dialog-tab-animate-progress-bar 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webview {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(webview[src^="https://clearthis.page/"]) .reader-view-toggle {
|
||||||
|
background-color: var(--colorBgAlphaHeavy);
|
||||||
|
border-color: var(--colorBgDarker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
.vivaldi-mods/css/extensions-dropdown.css
Executable file
33
.vivaldi-mods/css/extensions-dropdown.css
Executable file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Vertical extensions drop-down menu
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/96266/vertical-extensions-drop-down-menu
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Vertical Extensions Drop-Down Menu */
|
||||||
|
.extensionIconPopupMenu {
|
||||||
|
--popupWidth: 280px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extensionIconPopupMenu > .toolbar {
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow: hidden auto;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extensionIconPopupMenu .ExtensionDropdownIcon > button {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extensionIconPopupMenu .ExtensionDropdownIcon > button::after {
|
||||||
|
content: attr(title);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extensionIconPopupMenu .ExtensionDropdownIcon .button-badge {
|
||||||
|
top: auto;
|
||||||
|
right: 10px;
|
||||||
|
}
|
202
.vivaldi-mods/css/find-bar.css
Executable file
202
.vivaldi-mods/css/find-bar.css
Executable file
|
@ -0,0 +1,202 @@
|
||||||
|
/*
|
||||||
|
* Minimalist find bar
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/54938/minimalist-find-bar-css-mod
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--caribbean-green: #00cc99
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minimalist find bar */
|
||||||
|
.find-in-page {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3;
|
||||||
|
bottom: 0;
|
||||||
|
left: 118px;
|
||||||
|
right: 0;
|
||||||
|
justify-content: left;
|
||||||
|
max-width: 382px;
|
||||||
|
margin: auto;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page .fip-input-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* findbox */
|
||||||
|
.find-in-page:not(.fip-nomatches) .fip-input-container > * {
|
||||||
|
color: TURQUOISE;
|
||||||
|
font-weight: 500;
|
||||||
|
text-shadow: 0px 0px 4px black, 1px 1px 1px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page:not(.fip-nomatches) .fip-input-container:is(:hover, :focus-within) > * {
|
||||||
|
color: var(--caribbean-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page:not(.fip-nomatches) input[type="search"] {
|
||||||
|
box-shadow: 0 1px #ffd70099;
|
||||||
|
background: linear-gradient(55deg, rgba(0,0,0,.2), rgba(0,0,0,.05));
|
||||||
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page:not(.fip-nomatches) input[type="search"]:focus-within {
|
||||||
|
box-shadow: 0 -1px var(--caribbean-green) inset, 0 1px #ffbe00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page:not(.fip-nomatches) .fip-input-container > input[type=search]::-webkit-search-cancel-button {
|
||||||
|
filter: sepia(100%) saturate(550%) brightness(104%) opacity(77%) drop-shadow(.7px .7px 1.4px #0008);
|
||||||
|
} /* yellow */
|
||||||
|
/.find-in-page:not(.fip-nomatches) .fip-input-container > input[type=search]::-webkit-search-cancel-button {
|
||||||
|
filter: sepia(98%) saturate(270%) hue-rotate(102deg) brightness(103%) contrast(106%) opacity(70%) drop-shadow(1px 0 1.5px #0009);
|
||||||
|
} /* optional: turquoise clear button */
|
||||||
|
/* nomatch alert */
|
||||||
|
.find-in-page.fip-nomatches .fip-input-container > input {
|
||||||
|
font-weight: 500;
|
||||||
|
color: maroon;
|
||||||
|
background-color: #ffbbffD9;
|
||||||
|
box-shadow: 0 0 4px pink inset, 0 0 0 .6px magenta inset;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
transition-delay: .1s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page.fip-nomatches:not(:hover):not(:focus-within) .fip-input-container > input {
|
||||||
|
color: mediumviolet;
|
||||||
|
background: linear-gradient(55deg, #ffbbff80, #ffbbff33);
|
||||||
|
box-shadow: none;
|
||||||
|
transition-delay: .1s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page.fip-nomatches .fip-input-container > input[type=search]::-webkit-search-cancel-button {
|
||||||
|
filter: invert(45%) sepia(25%) saturate(5900%) hue-rotate(311deg) brightness(100%) contrast(65%) drop-shadow(.5px .5px .6px pink);
|
||||||
|
transition-delay: .1s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page.fip-nomatches:focus-within .fip-input-container > input {
|
||||||
|
animation: shake 1s steps(2) .15s 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
9% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
1%, 5% {
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
3%, 7% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* autohide buttons */
|
||||||
|
.find-in-page > :nth-last-child(-n+3) > * {
|
||||||
|
opacity: 0;
|
||||||
|
outline: none;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
transition: opacity .13s ease-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page:is(:hover, :focus-within) > :nth-last-child(-n+3) > * {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
/* arrow buttons */
|
||||||
|
.find-in-page > .toolbar-group button {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
filter: drop-shadow(.5px .5px maroon) drop-shadow(1px 1px 1px #000D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page > .toolbar-group button > span {
|
||||||
|
fill: TURQUOISE;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page > .toolbar-group button:is(:hover, :focus-within) svg {
|
||||||
|
fill: var(--caribbean-green);
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
/* close button */
|
||||||
|
.find-in-page button[title='Cancel search'] {
|
||||||
|
fill: TURQUOISE;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page button[title='Cancel search']:not(:hover):not(:focus-within) > span {
|
||||||
|
filter: drop-shadow(.6px .6px maroon) drop-shadow(1px 1px 1px #000D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page button[title='Cancel search']:is(:hover, :focus-within) {
|
||||||
|
fill: red;
|
||||||
|
box-shadow: 0 0 0 1.7px red inset;
|
||||||
|
filter: drop-shadow(.5px .5px maroon) drop-shadow(.8px .8px .9px #000D);
|
||||||
|
transition: box-shadow .12s !important;
|
||||||
|
}
|
||||||
|
/.find-in-page > .toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
order: 1;
|
||||||
|
} /* optional: reorder close button closer to findbox */
|
||||||
|
/* check-box */
|
||||||
|
.find-in-page label > input[type=checkbox] {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 0 0 1.7px TURQUOISE inset !important;
|
||||||
|
filter: drop-shadow(.5px .5px maroon) drop-shadow(.8px .8px 1px #000D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page label:is(:hover, :focus-within) > input[type=checkbox] {
|
||||||
|
box-shadow: 0 0 0 1.7px var(--caribbean-green) inset !important;
|
||||||
|
}
|
||||||
|
/* match text */
|
||||||
|
.find-in-page .fip-checkbox-label {
|
||||||
|
color: var(--caribbean-green);
|
||||||
|
font-size: 85%;
|
||||||
|
padding-right: 4px;
|
||||||
|
margin-left: -48px;
|
||||||
|
transform: translateX(50px);
|
||||||
|
background-color: transparent;
|
||||||
|
text-shadow: 0 0, 1px 1px 1px maroon, 1px 1px 2px #000D, 1px 1px 3px #000D, 1px 1px 4px #000D;
|
||||||
|
transition: .13s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page label:not(:hover) > input[type=checkbox]:not(:focus-visible) ~ .fip-checkbox-label {
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page:not(:hover):not(:focus-within) label > input[type=checkbox]:checked ~ .fip-checkbox-label {
|
||||||
|
opacity: 1;
|
||||||
|
font-size: 85% !important;
|
||||||
|
margin-left: -140px;
|
||||||
|
}
|
||||||
|
/* automate find box during small window */
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.find-in-page .fip-checkbox-label {
|
||||||
|
margin-left: 0;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page:not(:hover):not(:focus-within) label > input[type=checkbox]:checked ~ .fip-checkbox-label {
|
||||||
|
margin-left: -95px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* enter & exit animation */
|
||||||
|
.find-in-page.find-in-page-enter {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transition: transform 150ms ease-out, opacity 100ms ease-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page.find-in-page-enter-active {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-in-page.find-in-page-exit-active {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transition: transform 150ms ease-in, opacity 150ms ease-in !important;
|
||||||
|
}
|
48
.vivaldi-mods/css/hover-bookmark-bar.css
Executable file
48
.vivaldi-mods/css/hover-bookmark-bar.css
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Autohide bookmark bar
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/95511/overlay-bookmark-bar
|
||||||
|
*/
|
||||||
|
|
||||||
|
#browser {
|
||||||
|
--bookmark-bar-hide-delay: 400ms;
|
||||||
|
--bookmark-bar-show-ctrlkey: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-bar-top:is(.address-top, .tabs-top) {
|
||||||
|
#main:has(.bookmark-bar:hover) #webview-container {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
&.tabs-top #tabs-subcontainer {
|
||||||
|
-webkit-app-region: no-drag !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-bar {
|
||||||
|
height: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.bookmark-bar .observer {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 4 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
border-bottom: solid 1px var(--colorBorder) !important;
|
||||||
|
clip-path: inset(0 0 100% 0) !important;
|
||||||
|
transition-delay: var(--bookmark-bar-hide-delay) !important;
|
||||||
|
}
|
||||||
|
&:has(.button-disabled-preview):has(.tabbar-wrapper:hover).tabs-top .bookmark-bar .observer,
|
||||||
|
&:has(.button-disabled-preview).address-top .bookmark-bar:is(.mainbar:not(:has(.OmniDropdown, nav.menu)):hover ~ *) .observer,
|
||||||
|
.bookmark-bar:is(:focus-within, :hover) .observer {
|
||||||
|
clip-path: inset(0 0 -1px 0) !important;
|
||||||
|
transition: clip-path 150ms cubic-bezier(0, 1, 0, 0.9) 50ms !important;
|
||||||
|
}
|
||||||
|
@container not style(--bookmark-bar-show-ctrlkey: true) {
|
||||||
|
&:has(.tabbar-wrapper:hover).tabs-top .bookmark-bar .observer,
|
||||||
|
&.address-top .bookmark-bar:is(.mainbar:not(:has(.OmniDropdown, nav.menu)):hover ~ *) .observer,
|
||||||
|
.bookmark-bar:is(:focus-within, :hover) .observer {
|
||||||
|
clip-path: inset(0 0 -1px 0) !important;
|
||||||
|
transition: clip-path 150ms cubic-bezier(0, 1, 0, 0.9) 50ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.animation-off .bookmark-bar .observer {
|
||||||
|
transition-duration: 0ms !important;
|
||||||
|
}
|
||||||
|
}
|
29
.vivaldi-mods/css/hover-panel.css
Executable file
29
.vivaldi-mods/css/hover-panel.css
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Autohide panels (while making it possible to use scrollbar still)
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--panel-transition: max-width .2s ease-out, opacity .2s ease-out;
|
||||||
|
--panel-peek-width: 12px;
|
||||||
|
--panel-peek-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base hidden state for panels */
|
||||||
|
#panels-container {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0;
|
||||||
|
max-height: var(--panel-peek-height);
|
||||||
|
max-width: var(--panel-peek-width);
|
||||||
|
transition: var(--panel-transition) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panels-container.right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show panels on hover */
|
||||||
|
#panels-container:hover {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 41px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
124
.vivaldi-mods/css/hover-tabbar.css
Executable file
124
.vivaldi-mods/css/hover-tabbar.css
Executable file
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Autohide tab bar.
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/107399/auto-hide-vertical-tabs-arc-browser-style
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--tabbar-transition: transform .2s ease-out, opacity .2s ease-out;
|
||||||
|
--scrollbar-width: 10px;
|
||||||
|
--scrollbar-border-color: #9fb0cb;
|
||||||
|
--tabbar-peek-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*----- Scrollbar Styling -----*/
|
||||||
|
|
||||||
|
#tabs-tabbar-container:is(.left, .right) .tab-strip::-webkit-scrollbar {
|
||||||
|
padding: 0 2px !important;
|
||||||
|
width: var(--scrollbar-width) !important;
|
||||||
|
border: 1px solid var(--scrollbar-border-color) !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs-tabbar-container:is(.left, .right) .tab-strip::-webkit-scrollbar-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs-tabbar-container:is(.left, .right) .tab-strip::-webkit-scrollbar-thumb {
|
||||||
|
border: 2px solid transparent !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*----- Vertical Tabbar Auto-Hide Behavior -----*/
|
||||||
|
|
||||||
|
/* Base hidden state for vertical tabbar */
|
||||||
|
#browser:is(.tabs-left, .tabs-right) .tabbar-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(calc(-100% + var(--tabbar-peek-width)));
|
||||||
|
transition: var(--tabbar-transition) !important;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right side positioning */
|
||||||
|
#browser.tabs-right .tabbar-wrapper {
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(calc(100% - var(--tabbar-peek-width)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab move functionality support */
|
||||||
|
#browser.tabs-left.isblurred:where(:has(div.tab-yield-space, .tab-acceptsdrop)) .tabbar-wrapper,
|
||||||
|
#browser.tabs-left.isblurred:is(:active, :focus) .tabbar-wrapper:is(:active, :focus) {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show tabbar on hover */
|
||||||
|
#browser:is(.tabs-left, .tabs-right) .tabbar-wrapper:hover {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: var(--tabbar-transition) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show when mouse approaches edge of screen */
|
||||||
|
#browser:is(.tabs-left) .mainbar:hover + .tabbar-wrapper,
|
||||||
|
#browser:is(.tabs-right) .webpage:hover + .tabbar-wrapper {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: var(--tabbar-transition) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show when workspace popups are active */
|
||||||
|
#browser:is(.tabs-left, .tabs-right):has(.WorkspacePopup:visible,
|
||||||
|
.workspace-popup:visible) .tabbar-wrapper {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: var(--tabbar-transition) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep tab bar visible during workspace naming */
|
||||||
|
#browser:is(.tabs-left, .tabs-right):has(.quick-command-container.workspace-naming) .tabbar-wrapper,
|
||||||
|
#browser:is(.tabs-left, .tabs-right):has(.workspace-naming) .tabbar-wrapper,
|
||||||
|
#browser:is(.tabs-left, .tabs-right):has(.WorkspacePopup) .tabbar-wrapper,
|
||||||
|
#browser:is(.tabs-left, .tabs-right):has(input[placeholder*="workspace"]) .tabbar-wrapper {
|
||||||
|
transform: translateX(0) !important;
|
||||||
|
transition: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide on click outside */
|
||||||
|
html:active:not(:has(.tabbar-wrapper:hover,
|
||||||
|
.quick-command-container.workspace-naming,
|
||||||
|
.workspace-naming,
|
||||||
|
input[placeholder="workspace"]:focus,
|
||||||
|
.WorkspacePopup:visible,
|
||||||
|
.workspace-popup:visible)) .tabbar-wrapper {
|
||||||
|
transform: translateX(calc(-100% + var(--tabbar-peek-width))) !important;
|
||||||
|
transition: var(--tabbar-transition) !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix right side hiding behavior on click outside */
|
||||||
|
html:active:not(:has(.tabbar-wrapper:hover,
|
||||||
|
.quick-command-container.workspace-naming,
|
||||||
|
.workspace-naming,
|
||||||
|
input[placeholder="workspace"]:focus,
|
||||||
|
.WorkspacePopup:visible,
|
||||||
|
.workspace-popup:visible)) #browser.tabs-right .tabbar-wrapper {
|
||||||
|
transform: translateX(calc(100% - var(--tabbar-peek-width))) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional reset mechanism for when tabs get stuck */
|
||||||
|
html:active:active .tabbar-wrapper:not(:hover):not(:has(.quick-command-container.workspace-naming,
|
||||||
|
.workspace-naming,
|
||||||
|
input[placeholder="workspace"]:focus)) {
|
||||||
|
transform: translateX(calc(-100% + var(--tabbar-peek-width))) !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional reset for right side */
|
||||||
|
html:active:active #browser.tabs-right .tabbar-wrapper:not(:hover):not(:has(.quick-command-container.workspace-naming,
|
||||||
|
.workspace-naming,
|
||||||
|
input[placeholder="workspace"]:focus)) {
|
||||||
|
transform: translateX(calc(100% - var(--tabbar-peek-width))) !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
7
.vivaldi-mods/css/no-newtab-button.css
Executable file
7
.vivaldi-mods/css/no-newtab-button.css
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* Remove new tab button on tab bar
|
||||||
|
*/
|
||||||
|
|
||||||
|
.newtab {
|
||||||
|
display: none;
|
||||||
|
}
|
9
.vivaldi-mods/css/progress-bar.css
Executable file
9
.vivaldi-mods/css/progress-bar.css
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* Load indicator as underline
|
||||||
|
* Forum link: https://forum.vivaldi.net/post/452647
|
||||||
|
*/
|
||||||
|
|
||||||
|
.UrlBar-AddressField .pageload:not(.unstarted).progressing .pageload-indicator {
|
||||||
|
opacity: 1 !important;
|
||||||
|
background: linear-gradient(0deg, #00cc99 4px, transparent 0);
|
||||||
|
}
|
43
.vivaldi-mods/css/quick-commands.css
Executable file
43
.vivaldi-mods/css/quick-commands.css
Executable file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Make quick commands transparent during mouseout
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/50773/quick-commands-peekaboo-v2
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Compact QC */
|
||||||
|
#modal-bg.qc-modal.float.top:not(:hover) > div {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-bg.qc-modal.float.top:not(:hover) .quick-command-container .quick-commands {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-bg.qc-modal.float.top:not(:hover) .qc-entry input {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #00cc99;
|
||||||
|
text-shadow: 2px 2px 6px black;
|
||||||
|
background: linear-gradient(45deg, #00000099, transparent 80%);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-bg.qc-modal.float.top > div {
|
||||||
|
transition: background .4s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-command-container .quick-commands {
|
||||||
|
transition: opacity .4s !important;
|
||||||
|
}
|
||||||
|
/* expand width */
|
||||||
|
.quick-command-container, .quick-command-container .quick-commands > div {
|
||||||
|
min-width: 86vw;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-command-container .quick-commands > div > div {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-command-container .quick-command {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
46
.vivaldi-mods/css/tab-stack.css
Executable file
46
.vivaldi-mods/css/tab-stack.css
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Automate 2 level tab stacks
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/83079/automate-2-level-tab-stack-columns-v3
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Automate 2 level vertical tabbar columns */
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right) > div {
|
||||||
|
z-index: 1;
|
||||||
|
transition: background-color .15s, flex-basis .12s ease-out .05s !important;
|
||||||
|
}
|
||||||
|
/* active main column */
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right) > #tabs-container:is(:focus-within, :hover):not(:has(.SlideBar--FullHeight:active)) + #tabs-subcontainer {
|
||||||
|
flex-basis: 30px !important;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
/* active sub column */
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right) > #tabs-subcontainer:is(:focus-within, :hover) {
|
||||||
|
flex-basis: calc(100% - 64px) !important;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
/* active sub column with small width tabbar */
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right):is(:focus-within, :hover):is([style^='width: 6'], [style^='width: 7'], [style^='width: 8'], [style^='width: 9']) > #tabs-subcontainer:is(:focus-within, :hover) {
|
||||||
|
flex-basis: calc(100% - 30px) !important;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* inactive tabbar columns' ratio during small width (minimized sub column to favicon) */
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right):not(:focus-within, :hover):is([style^='width: 6'], [style^='width: 7'], [style^='width: 8'], [style^='width: 9']) > #tabs-subcontainer:not(:focus-within, :hover) {flex-basis: 30px !important;}
|
||||||
|
|
||||||
|
/* fix small tab hover effect for title, stack counter, & close button */
|
||||||
|
#tabs-container .tab.tab-small.tab-mini .tab-header {justify-content: unset; padding-left: 5px;}
|
||||||
|
#tabs-container .tab.tab-small.tab-mini .tab-header .title {display: flex;}
|
||||||
|
#browser:not(.alt-tabs).substrip-tabs-on #tabs-tabbar-container:is(.left, .right) .tab-position .tab:hover.tab:not(.button-off, .force-hover, .tab-small) .stack-counter,
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right) .tab-position .tab:not(.tab-mini) .stack-counter {display: block; margin-left: -6px;}
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right) .tab-position :is(.tab:hover.tab-small, .tab.force-hover.tab-small) .close {display: flex; margin-left: -3px;}
|
||||||
|
|
||||||
|
/* autohide inactive columns' scrollbar */
|
||||||
|
#tabs-tabbar-container > div.overflow:not(:focus-within):not(:hover) .tab-strip {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* optional: hide tabbar columns' resizer */
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right) > #tabs-container .SlideBar--FullHeight {visibility: hidden;}
|
||||||
|
|
||||||
|
/* option 2: Minimized main column to favicon + stack counter */
|
||||||
|
.substrip-tabs-on #tabs-tabbar-container:is(.left, .right):not(:focus-within, :hover):not([style^='width: 6']):not([style^='width: 7']):not([style^='width: 8']):not([style^='width: 9']) > #tabs-subcontainer.visible {flex-basis: calc(100% - 64px) !important;}
|
45
.vivaldi-mods/install-mods.lua
Executable file
45
.vivaldi-mods/install-mods.lua
Executable file
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env hilbish
|
||||||
|
|
||||||
|
-- Note: this script only supports 1 Vivaldi install.
|
||||||
|
|
||||||
|
local posix = require("posix")
|
||||||
|
local fs = require("fs")
|
||||||
|
|
||||||
|
local _, mod_dir = posix.pwd()
|
||||||
|
mod_dir = mod_dir:gsub("\n", "") .. "/js"
|
||||||
|
local custom_js = mod_dir .. "/custom.js"
|
||||||
|
|
||||||
|
local _, vivaldi_install = posix.find("/opt", { name = "vivaldi-bin" })
|
||||||
|
local _, dir = posix.dirname(vivaldi_install)
|
||||||
|
dir = dir:gsub("\n", "")
|
||||||
|
local resources_dir = dir .. "/resources/vivaldi"
|
||||||
|
|
||||||
|
print("Patch originating from " .. mod_dir .. " targeting " .. dir)
|
||||||
|
|
||||||
|
local backup_files = fs.glob(resources_dir .. "/window.html.bak")
|
||||||
|
|
||||||
|
if #backup_files == 0 then
|
||||||
|
print("Not patched; backing up window.html to window.html.bak...")
|
||||||
|
posix.cp(resources_dir .. "/window.html", resources_dir .. "/window.html.bak", { sudo = true })
|
||||||
|
|
||||||
|
print("Patching window.html...")
|
||||||
|
posix.sed('s/<\\/body>/<script src="custom.js"><\\/script> <\\/body>/', resources_dir .. "/window.html",
|
||||||
|
{ flags = "-i", sudo = true })
|
||||||
|
else
|
||||||
|
print("window.html has already been patched")
|
||||||
|
end
|
||||||
|
|
||||||
|
print("Combining js files into custom.js...")
|
||||||
|
local _, js_files = posix.find(mod_dir, { maxdepth = 1, type = "f", name = "*.js" })
|
||||||
|
posix.touch(custom_js)
|
||||||
|
for line in string.gmatch(js_files, "[^\r\n]*") do
|
||||||
|
if line == '' then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
posix.cat(line, { append_file = custom_js })
|
||||||
|
end
|
||||||
|
|
||||||
|
print("Copying custom.js...")
|
||||||
|
posix.cp(custom_js, resources_dir .. "/custom.js", { sudo = true, force = true })
|
||||||
|
|
||||||
|
posix.rm(custom_js)
|
863
.vivaldi-mods/js/dialog.js
Executable file
863
.vivaldi-mods/js/dialog.js
Executable file
|
@ -0,0 +1,863 @@
|
||||||
|
/**
|
||||||
|
* Opens links in a dialog, either by key combinations, holding the middle mouse button or context menu
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/92501/open-in-dialog-mod?_=1717490394230
|
||||||
|
*/
|
||||||
|
(() => {
|
||||||
|
const ICON_CONFIG = {
|
||||||
|
linkIcon: '', // if set, an icon shows up after links - example values 'fa-solid fa-up-right-from-square', 'fa-solid fa-circle-info', 'fa-regular fa-square' search for other icons: https://fontawesome.com/search?o=r&ic=free&s=solid&ip=classic
|
||||||
|
linkIconInteractionOnHover: true, // if false, you have to click the icon to show the dialog - if true, the dialog shows on mouseenter
|
||||||
|
showIconDelay: 250, // set to 0 to disable - delays showing the icon on hovering a link
|
||||||
|
showDialogOnHoverDelay: 250 // set to 0 to disable - delays showing the dialog on hovering the linkIcon
|
||||||
|
},
|
||||||
|
CONTEXT_MENU_CONFIG = {
|
||||||
|
menuPrefix: '[Dialog Tab]',
|
||||||
|
linkMenuTitle: 'Open link',
|
||||||
|
searchMenuTitle: 'Search by "%s"',
|
||||||
|
selectSearchMenuTitle: 'Search with'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for the browser to come to a ready state
|
||||||
|
setTimeout(function waitDialog() {
|
||||||
|
const browser = document.getElementById('browser');
|
||||||
|
if (!browser) {
|
||||||
|
return setTimeout(waitDialog, 300);
|
||||||
|
}
|
||||||
|
new DialogMod();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
class DialogMod {
|
||||||
|
webviews = new Map();
|
||||||
|
iconUtils = new IconUtils();
|
||||||
|
searchEngineUtils = new SearchEngineUtils(
|
||||||
|
(url) => this.dialogTab(url),
|
||||||
|
(engineId, searchText) => this.dialogTabSearch(engineId, searchText),
|
||||||
|
CONTEXT_MENU_CONFIG
|
||||||
|
);
|
||||||
|
KEYBOARD_SHORTCUTS = {
|
||||||
|
'Ctrl+Alt+Period': this.searchForSelectedText.bind(this),
|
||||||
|
'Ctrl+Shift+F': this.searchForSelectedText.bind(this),
|
||||||
|
Esc: () => {
|
||||||
|
if (!this.webviews.size) return;
|
||||||
|
|
||||||
|
const webviewValues = Array.from(this.webviews.values());
|
||||||
|
let webviewData = webviewValues.at(-1);
|
||||||
|
if (!webviewData.fromPanel) {
|
||||||
|
const tabId = Number(document.querySelector('.active.visible.webpageview webview').tab_id);
|
||||||
|
webviewData = webviewValues.findLast(_data => _data.tabId === tabId);
|
||||||
|
}
|
||||||
|
webviewData && this.removeDialog(webviewData.webview.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Setup keyboard shortcuts
|
||||||
|
vivaldi.tabsPrivate.onKeyboardShortcut.addListener(this.keyCombo.bind(this));
|
||||||
|
|
||||||
|
new WebsiteInjectionUtils(
|
||||||
|
(navigationDetails) => this.getWebviewConfig(navigationDetails),
|
||||||
|
(url, fromPanel) => this.dialogTab(url, fromPanel),
|
||||||
|
ICON_CONFIG
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the correct configuration for showing the dialog
|
||||||
|
*/
|
||||||
|
getWebviewConfig(navigationDetails) {
|
||||||
|
if (navigationDetails.frameType !== "outermost_frame") return {webview: null, fromPanel: false};
|
||||||
|
|
||||||
|
// first dialog from the tab
|
||||||
|
let webview = document.querySelector(`webview[tab_id="${navigationDetails.tabId}"]`);
|
||||||
|
if (webview) return {webview, fromPanel: false};
|
||||||
|
|
||||||
|
// first dialog from the webpanel
|
||||||
|
webview = document.querySelector(`.webpanel-content webview[src*="${navigationDetails.url}"]`);
|
||||||
|
if (webview) return {webview, fromPanel: true};
|
||||||
|
|
||||||
|
// follow-up dialog from the webpanel
|
||||||
|
webview = Array.from(this.webviews.values()).find(view => view.fromPanel)?.webview;
|
||||||
|
if (webview) return {webview, fromPanel: true};
|
||||||
|
|
||||||
|
// follow-up dialog from tab
|
||||||
|
const lastWebviewId = document.querySelector('.active.visible.webpageview .dialog-container:last-of-type webview')?.id;
|
||||||
|
return {webview: this.webviews.get(lastWebviewId)?.webview, fromPanel: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open Default Search Engine in Dialog and search for the selected text
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async searchForSelectedText() {
|
||||||
|
const tabs = await chrome.tabs.query({active: true});
|
||||||
|
vivaldi.utilities.getSelectedText(tabs[0].id, (text) => this.dialogTabSearch(this.searchEngineUtils.defaultSearchId, text));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares url for search, calls dailogTab function
|
||||||
|
* @param {String} engineId engine id of the engine to be used
|
||||||
|
* @param {int} selectionText the text to search
|
||||||
|
*/
|
||||||
|
async dialogTabSearch(engineId, selectionText) {
|
||||||
|
let searchRequest = await vivaldi.searchEngines.getSearchRequest(engineId, selectionText);
|
||||||
|
this.dialogTab(searchRequest.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a potential keyboard shortcut (copy from KeyboardMachine)
|
||||||
|
* @param {number} id I don't know what this does, but it's an extra argument
|
||||||
|
* @param {String} combination written in the form (CTRL+SHIFT+ALT+KEY)
|
||||||
|
*/
|
||||||
|
keyCombo(id, combination) {
|
||||||
|
const customShortcut = this.KEYBOARD_SHORTCUTS[combination];
|
||||||
|
if (customShortcut) {
|
||||||
|
customShortcut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the dialog for a giveb webview
|
||||||
|
* @param webviewId The id of the webview
|
||||||
|
*/
|
||||||
|
removeDialog(webviewId) {
|
||||||
|
const data = this.webviews.get(webviewId);
|
||||||
|
if (data) {
|
||||||
|
chrome.tabs.query({}, (tabs) => {
|
||||||
|
const tab = tabs.find(tab => tab.vivExtData && tab.vivExtData.includes(`${webviewId}tabId`));
|
||||||
|
if (tab) chrome.tabs.remove(tab.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.divContainer.remove();
|
||||||
|
chrome.tabs.onRemoved.removeListener(data.tabCloseListener);
|
||||||
|
this.webviews.delete(webviewId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current window is the correct window to show the dialog and then opens the dialog
|
||||||
|
* @param {string} linkUrl the url to load
|
||||||
|
* @param {boolean} fromPanel indicates whether the dialog is opened from a panel
|
||||||
|
*/
|
||||||
|
dialogTab(linkUrl, fromPanel = undefined) {
|
||||||
|
chrome.windows.getLastFocused((window) => {
|
||||||
|
if (window.id === vivaldiWindowId && window.state !== chrome.windows.WindowState.MINIMIZED) {
|
||||||
|
this.showDialog(linkUrl, fromPanel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a link in a dialog like display in the current visible tab
|
||||||
|
* @param {string} linkUrl the url to load
|
||||||
|
* @param {boolean} fromPanel indicates whether the dialog is opened from a panel
|
||||||
|
*/
|
||||||
|
showDialog(linkUrl, fromPanel) {
|
||||||
|
const dialogContainer = document.createElement('div'),
|
||||||
|
dialogTab = document.createElement('div'),
|
||||||
|
webview = document.createElement('webview'),
|
||||||
|
webviewId = `dialog-${this.getWebviewId()}`,
|
||||||
|
progressBar = new ProgressBar(webviewId),
|
||||||
|
optionsContainer = document.createElement('div');
|
||||||
|
|
||||||
|
if (fromPanel === undefined && this.webviews.size !== 0) {
|
||||||
|
fromPanel = Array.from(this.webviews.values()).at(-1).fromPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabId = !fromPanel ? Number(document.querySelector('.active.visible.webpageview webview').tab_id) : null;
|
||||||
|
|
||||||
|
this.webviews.set(webviewId, {
|
||||||
|
divContainer: dialogContainer,
|
||||||
|
webview: webview,
|
||||||
|
fromPanel: fromPanel,
|
||||||
|
tabId: tabId
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove dialogs when tab is closed without closing dialogs
|
||||||
|
if (!fromPanel) {
|
||||||
|
const clearWebviews = (closedTabId) => {
|
||||||
|
if (tabId === closedTabId) {
|
||||||
|
this.webviews.forEach((view, key) => view.tabCloseListener === clearWebviews && this.removeDialog(key));
|
||||||
|
chrome.tabs.onRemoved.removeListener(clearWebviews);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.webviews.get(webviewId).tabCloseListener = clearWebviews;
|
||||||
|
chrome.tabs.onRemoved.addListener(clearWebviews);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region dialogTab properties
|
||||||
|
dialogTab.setAttribute('class', 'dialog-tab');
|
||||||
|
dialogTab.style.width = 85 - 5 * this.webviews.size + '%';
|
||||||
|
dialogTab.style.height = 95 - 5 * this.webviews.size + '%';
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region optionsContainer properties
|
||||||
|
optionsContainer.setAttribute('class', 'options-container');
|
||||||
|
optionsContainer.innerHTML = this.iconUtils.ellipsis;
|
||||||
|
|
||||||
|
let timeout;
|
||||||
|
optionsContainer.addEventListener('mouseover', () => {
|
||||||
|
if (optionsContainer.children.length === 1) {
|
||||||
|
optionsContainer.innerHTML = '';
|
||||||
|
this.showWebviewOptions(webviewId, optionsContainer);
|
||||||
|
}
|
||||||
|
clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
optionsContainer.addEventListener('mouseleave', () => {
|
||||||
|
timeout = setTimeout(() => optionsContainer.innerHTML = this.iconUtils.ellipsis, 1500);
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region webview properties
|
||||||
|
webview.id = webviewId;
|
||||||
|
webview.tab_id = `${webviewId}tabId`;
|
||||||
|
webview.setAttribute('src', linkUrl);
|
||||||
|
|
||||||
|
webview.addEventListener('loadstart', () => {
|
||||||
|
webview.style.backgroundColor = 'var(--colorBorder)';
|
||||||
|
progressBar.start();
|
||||||
|
|
||||||
|
const input = document.getElementById(`input-${webview.id}`);
|
||||||
|
if (input !== null) {
|
||||||
|
input.value = webview.src;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
webview.addEventListener('loadstop', () => progressBar.clear(true));
|
||||||
|
fromPanel && webview.addEventListener('mousedown', (event) => event.stopPropagation());
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region dialogContainer properties
|
||||||
|
dialogContainer.setAttribute('class', 'dialog-container');
|
||||||
|
|
||||||
|
let stopEvent = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (event.target.id === `input-${webviewId}`) {
|
||||||
|
const inputElement = event.target;
|
||||||
|
|
||||||
|
// Calculate the cursor position based on the click location
|
||||||
|
const offsetX = event.clientX - inputElement.getBoundingClientRect().left;
|
||||||
|
|
||||||
|
// Create a canvas to measure text width
|
||||||
|
const context = document.createElement('canvas').getContext('2d');
|
||||||
|
context.font = window.getComputedStyle(inputElement).font;
|
||||||
|
|
||||||
|
// Measure the width of the text up to each character
|
||||||
|
let cursorPosition = 0,
|
||||||
|
textWidth = 0;
|
||||||
|
for (let i = 0; i < inputElement.value.length; i++) {
|
||||||
|
const charWidth = context.measureText(inputElement.value[i]).width;
|
||||||
|
if (textWidth + charWidth > offsetX) {
|
||||||
|
cursorPosition = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
textWidth += charWidth;
|
||||||
|
cursorPosition = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually focus the input element and set the cursor position
|
||||||
|
inputElement.focus({preventScroll: true});
|
||||||
|
inputElement.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fromPanel && document.body.addEventListener('pointerdown', stopEvent);
|
||||||
|
|
||||||
|
dialogContainer.addEventListener('click', (event) => {
|
||||||
|
if (event.target === dialogContainer) {
|
||||||
|
fromPanel && document.body.removeEventListener('pointerdown', stopEvent);
|
||||||
|
this.removeDialog(webviewId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
dialogTab.appendChild(optionsContainer);
|
||||||
|
dialogTab.appendChild(progressBar.element);
|
||||||
|
dialogTab.appendChild(webview);
|
||||||
|
|
||||||
|
dialogContainer.appendChild(dialogTab);
|
||||||
|
|
||||||
|
// Get for current tab and append divContainer
|
||||||
|
fromPanel
|
||||||
|
? document.querySelector('#browser').appendChild(dialogContainer)
|
||||||
|
: document.querySelector('.active.visible.webpageview').appendChild(dialogContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays open in tab buttons and current url in input element
|
||||||
|
* @param {string} webviewId is the id of the webview
|
||||||
|
* @param {Object} thisElement the current instance divOptionContainer (div) element
|
||||||
|
*/
|
||||||
|
showWebviewOptions(webviewId, thisElement) {
|
||||||
|
let inputId = `input-${webviewId}`,
|
||||||
|
data = this.webviews.get(webviewId),
|
||||||
|
webview = data ? data.webview : undefined;
|
||||||
|
if (webview && document.getElementById(inputId) === null) {
|
||||||
|
const input = document.createElement('input', 'text'),
|
||||||
|
VALID_URL_PREFIXES = [
|
||||||
|
'http://',
|
||||||
|
'https://',
|
||||||
|
'file://',
|
||||||
|
'vivaldi://'
|
||||||
|
],
|
||||||
|
isValidUrl = (url) => VALID_URL_PREFIXES.some(prefix => url.startsWith(prefix) || url === 'about:blank');
|
||||||
|
|
||||||
|
input.value = webview.src;
|
||||||
|
input.id = inputId;
|
||||||
|
input.setAttribute('class', 'dialog-input');
|
||||||
|
|
||||||
|
input.addEventListener('keydown', async (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
let value = input.value;
|
||||||
|
if (isValidUrl(value)) {
|
||||||
|
webview.src = value;
|
||||||
|
} else {
|
||||||
|
const searchRequest = await vivaldi.searchEngines.getSearchRequest(this.searchEngineUtils.defaultSearchId, value);
|
||||||
|
webview.src = searchRequest.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment(),
|
||||||
|
buttons = [
|
||||||
|
{content: this.iconUtils.back, action: () => webview.back()},
|
||||||
|
{content: this.iconUtils.forward, action: () => webview.forward()},
|
||||||
|
{content: this.iconUtils.reload, action: () => webview.reload()},
|
||||||
|
{
|
||||||
|
content: this.iconUtils.readerView,
|
||||||
|
action: this.showReaderView.bind(this, webview),
|
||||||
|
cls: 'reader-view-toggle'
|
||||||
|
},
|
||||||
|
{content: this.iconUtils.newTab, action: this.openNewTab.bind(this, inputId, true)},
|
||||||
|
{content: this.iconUtils.backgroundTab, action: this.openNewTab.bind(this, inputId, false)},
|
||||||
|
];
|
||||||
|
|
||||||
|
buttons.forEach(button => fragment.appendChild(this.createOptionsButton(button.content, button.action, button.cls || '')));
|
||||||
|
fragment.appendChild(input);
|
||||||
|
|
||||||
|
thisElement.append(fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a button with default style for the web view options.
|
||||||
|
* @param {Node | string} content the content of the button to display
|
||||||
|
* @param {Function} clickListenerCallback the click listeners callback function
|
||||||
|
* @param {string} cls optional additional class for the button
|
||||||
|
*/
|
||||||
|
createOptionsButton(content, clickListenerCallback, cls = '') {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.setAttribute('class', `options-button ${cls}`.trim());
|
||||||
|
button.addEventListener('click', clickListenerCallback);
|
||||||
|
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
button.innerHTML = content;
|
||||||
|
} else {
|
||||||
|
button.appendChild(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random, verified id.
|
||||||
|
*/
|
||||||
|
getWebviewId() {
|
||||||
|
return Math.floor(Math.random() * 10000) + new Date().getTime() % 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the webviews content to a reader version
|
||||||
|
*
|
||||||
|
* @param {webview} webview the webview to update
|
||||||
|
*/
|
||||||
|
showReaderView(webview) {
|
||||||
|
if (webview.src.includes('https://clearthis.page/?u=')) {
|
||||||
|
webview.src = webview.src.replace('https://clearthis.page/?u=', '');
|
||||||
|
} else {
|
||||||
|
webview.src = `https://clearthis.page/?u=${webview.src}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a new Chrome tab with specified active boolean value
|
||||||
|
* @param {string} inputId is the id of the input containing current url
|
||||||
|
* @param {boolean} active indicates whether the tab is active or not (background tab)
|
||||||
|
*/
|
||||||
|
openNewTab(inputId, active) {
|
||||||
|
const url = document.getElementById(inputId).value;
|
||||||
|
chrome.tabs.create({url: url, active: active});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebsiteInjectionUtils {
|
||||||
|
|
||||||
|
constructor(getWebviewConfig, openDialog, iconConfig) {
|
||||||
|
this.iconConfig = JSON.stringify(iconConfig);
|
||||||
|
|
||||||
|
// inject detection of click observers
|
||||||
|
chrome.webNavigation.onCompleted.addListener((navigationDetails) => {
|
||||||
|
const {webview, fromPanel} = getWebviewConfig(navigationDetails);
|
||||||
|
webview && this.injectCode(webview, fromPanel);
|
||||||
|
});
|
||||||
|
|
||||||
|
// react on demand to open a dialog
|
||||||
|
chrome.runtime.onMessage.addListener((message) => {
|
||||||
|
if (message.url) {
|
||||||
|
openDialog(message.url, message.fromPanel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
injectCode(webview, fromPanel) {
|
||||||
|
const handler = WebsiteLinkInteractionHandler.toString(),
|
||||||
|
instantiationCode = `
|
||||||
|
if (!this.dialogEventListenerSet){
|
||||||
|
new (${handler})(${fromPanel}, ${this.iconConfig});
|
||||||
|
this.dialogEventListenerSet = true;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
webview.executeScript({code: instantiationCode});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebsiteLinkInteractionHandler {
|
||||||
|
constructor(fromPanel, config) {
|
||||||
|
this.fromPanel = fromPanel;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
|
||||||
|
this.icon = null;
|
||||||
|
|
||||||
|
this.timers = {
|
||||||
|
showIcon: null,
|
||||||
|
showDialog: null,
|
||||||
|
hideIcon: null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a link is clicked by the middle mouse while pressing Ctrl + Alt, then fires an event with the Url
|
||||||
|
*/
|
||||||
|
#initialize() {
|
||||||
|
this.#setupMouseHandling();
|
||||||
|
|
||||||
|
if (this.config.linkIcon) {
|
||||||
|
this.#setupIconHandling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Richtet die Maus-Event-Listener ein
|
||||||
|
*/
|
||||||
|
#setupMouseHandling() {
|
||||||
|
let holdTimerForMiddleClick;
|
||||||
|
|
||||||
|
document.addEventListener('pointerdown', (event) => {
|
||||||
|
// Check if the Ctrl key, Alt key, and mouse button were pressed
|
||||||
|
if (event.ctrlKey && event.altKey && [0, 1].includes(event.button)) {
|
||||||
|
this.#callDialog(event);
|
||||||
|
} else if (event.button === 1) {
|
||||||
|
holdTimerForMiddleClick = setTimeout(() => this.#callDialog(event), 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('pointerup', (event) => {
|
||||||
|
if (event.button === 1) clearTimeout(holdTimerForMiddleClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupIconHandling() {
|
||||||
|
this.#createIcon();
|
||||||
|
this.#createIconStyle()
|
||||||
|
|
||||||
|
document.addEventListener('mouseover', this.debounce((event) => {
|
||||||
|
const link = this.#getLinkElement(event);
|
||||||
|
if (!link) return;
|
||||||
|
|
||||||
|
clearTimeout(this.timers.hideIcon);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const rect = link.getBoundingClientRect();
|
||||||
|
Object.assign(this.icon.style, {
|
||||||
|
display: 'block',
|
||||||
|
left: `${rect.right + 5}px`,
|
||||||
|
top: `${rect.top + window.scrollY}px`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.icon.dataset.targetUrl = link.href;
|
||||||
|
|
||||||
|
link.addEventListener('mouseleave', this.#hideLinkIcon.bind(this));
|
||||||
|
}, this.config.showIconDelay));
|
||||||
|
}
|
||||||
|
|
||||||
|
#createIcon() {
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = `link-icon ${this.config.linkIcon}`;
|
||||||
|
icon.style.display = 'none';
|
||||||
|
|
||||||
|
if (this.config.linkIconInteractionOnHover) {
|
||||||
|
icon.addEventListener('mouseenter', () => {
|
||||||
|
this.timers.showDialog = setTimeout(() => this.#sendDialogMessage(this.icon.dataset.targetUrl), this.config.showDialogOnHoverDelay);
|
||||||
|
});
|
||||||
|
icon.addEventListener('mouseleave', () => clearTimeout(this.timers.showDialog));
|
||||||
|
} else {
|
||||||
|
icon.addEventListener('click', () => this.#sendDialogMessage(this.icon.dataset.targetUrl));
|
||||||
|
icon.addEventListener('mouseenter', () => clearTimeout(this.timers.hideIcon));
|
||||||
|
icon.addEventListener('mouseleave', this.#hideLinkIcon.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.icon = icon;
|
||||||
|
document.body.appendChild(this.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
#hideLinkIcon() {
|
||||||
|
this.timers.hideIcon = setTimeout(() => {
|
||||||
|
this.icon.style.display = 'none';
|
||||||
|
clearTimeout(this.timers.showIcon);
|
||||||
|
}, this.config.linkIconInteractionOnHover ? 300 : 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getLinkElement(event) {
|
||||||
|
return event.target.closest('a[href]:not([href="#"])');
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendDialogMessage(url) {
|
||||||
|
chrome.runtime.sendMessage({url, fromPanel: this.fromPanel});
|
||||||
|
}
|
||||||
|
|
||||||
|
#callDialog(event) {
|
||||||
|
let link = this.#getLinkElement(event);
|
||||||
|
if (link) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#sendDialogMessage(link.href);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#createIconStyle() {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.link-icon {
|
||||||
|
position: absolute;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 9999;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-icon:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
debounce(fn, delay) {
|
||||||
|
let timer = null;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(fn.bind(this, ...args), delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for adding and updating context menu items
|
||||||
|
*/
|
||||||
|
class SearchEngineUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for SearchEngineUtils
|
||||||
|
* @param {Function} openLinkCallback - Callback for opening links
|
||||||
|
* @param {Function} searchCallback - Callback for searching
|
||||||
|
* @param {Object} [config={}] - Configuration options
|
||||||
|
* @param {string} [config.menuPrefix] - Prefix for the context menu item
|
||||||
|
* @param {string} [config.linkMenuTitle] - Titel for the link menu
|
||||||
|
* @param {string} [config.searchMenuTitle] - title for the search menu
|
||||||
|
* @param {string} [config.selectSearchMenuTitle] - title for the select search menu
|
||||||
|
*/
|
||||||
|
constructor(openLinkCallback, searchCallback, config = {}) {
|
||||||
|
this.openLinkCallback = openLinkCallback;
|
||||||
|
this.searchCallback = searchCallback;
|
||||||
|
|
||||||
|
this.menuPrefix = config.menuPrefix;
|
||||||
|
this.linkMenuTitle = config.linkMenuTitle;
|
||||||
|
this.searchMenuTitle = config.searchMenuTitle;
|
||||||
|
this.selectSearchMenuTitle = config.selectSearchMenuTitle;
|
||||||
|
|
||||||
|
this.createdContextMenuMap = new Map();
|
||||||
|
this.searchEngineCollection = [];
|
||||||
|
this.defaultSearchId = null;
|
||||||
|
this.privateSearchId = null;
|
||||||
|
|
||||||
|
// Cache static IDs for frequent access
|
||||||
|
this.LINK_ID = 'dialog-tab-link';
|
||||||
|
this.SEARCH_ID = 'search-dialog-tab';
|
||||||
|
this.SELECT_SEARCH_ID = 'select-search-dialog-tab';
|
||||||
|
|
||||||
|
this.#initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the context menu and listeners
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async #initialize() {
|
||||||
|
// Create context menu items
|
||||||
|
this.#createContextMenuOption();
|
||||||
|
|
||||||
|
// Initialize search engines and context menus
|
||||||
|
this.#updateSearchEnginesAndContextMenu();
|
||||||
|
|
||||||
|
// Update context menus when search engines change
|
||||||
|
vivaldi.searchEngines.onTemplateUrlsChanged.addListener(() => {
|
||||||
|
this.#removeContextMenuSelectSearch();
|
||||||
|
this.#updateSearchEnginesAndContextMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates context menu items to open a dialog tab
|
||||||
|
*/
|
||||||
|
#createContextMenuOption() {
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: this.LINK_ID,
|
||||||
|
title: `${this.menuPrefix} ${this.linkMenuTitle}`,
|
||||||
|
contexts: ['link']
|
||||||
|
});
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: this.SEARCH_ID,
|
||||||
|
title: `${this.menuPrefix} ${this.searchMenuTitle}`,
|
||||||
|
contexts: ['selection']
|
||||||
|
});
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: this.SELECT_SEARCH_ID,
|
||||||
|
title: `${this.menuPrefix} ${this.selectSearchMenuTitle}`,
|
||||||
|
contexts: ['selection']
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.contextMenus.onClicked.addListener((itemInfo) => {
|
||||||
|
const {menuItemId, parentMenuItemId, linkUrl, selectionText} = itemInfo;
|
||||||
|
|
||||||
|
if (menuItemId === this.LINK_ID) {
|
||||||
|
this.openLinkCallback(linkUrl);
|
||||||
|
} else if (menuItemId === this.SEARCH_ID) {
|
||||||
|
const engineId = window.incognito ? this.privateSearchId : this.defaultSearchId;
|
||||||
|
this.searchCallback(engineId, selectionText);
|
||||||
|
} else if (parentMenuItemId === this.SELECT_SEARCH_ID) {
|
||||||
|
const engineId = menuItemId.substr(parentMenuItemId.length);
|
||||||
|
this.searchCallback(engineId, selectionText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the search engines and context menu
|
||||||
|
*/
|
||||||
|
async #updateSearchEnginesAndContextMenu() {
|
||||||
|
const searchEngines = await vivaldi.searchEngines.getTemplateUrls();
|
||||||
|
this.searchEngineCollection = searchEngines.templateUrls;
|
||||||
|
this.defaultSearchId = searchEngines.defaultSearch;
|
||||||
|
this.privateSearchId = searchEngines.defaultPrivate;
|
||||||
|
|
||||||
|
this.#createContextMenuSelectSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes sub-context menu items for select search engine menu item
|
||||||
|
*/
|
||||||
|
#removeContextMenuSelectSearch() {
|
||||||
|
this.createdContextMenuMap.forEach((_, engineId) => {
|
||||||
|
const menuId = this.SELECT_SEARCH_ID + engineId;
|
||||||
|
chrome.contextMenus.remove(menuId);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.createdContextMenuMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates sub-context menu items for select search engine menu item
|
||||||
|
*/
|
||||||
|
#createContextMenuSelectSearch() {
|
||||||
|
this.searchEngineCollection.forEach((engine) => {
|
||||||
|
if (!this.createdContextMenuMap.has(engine.guid)) {
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: this.SELECT_SEARCH_ID + engine.guid,
|
||||||
|
parentId: this.SELECT_SEARCH_ID,
|
||||||
|
title: engine.name,
|
||||||
|
contexts: ['selection']
|
||||||
|
});
|
||||||
|
this.createdContextMenuMap.set(engine.guid, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProgressBar {
|
||||||
|
constructor(webviewId) {
|
||||||
|
this.webviewId = webviewId;
|
||||||
|
this.progress = 0;
|
||||||
|
this.interval = null;
|
||||||
|
this.element = this.#createProgressBar(webviewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#createProgressBar(webviewId) {
|
||||||
|
const progressBar = document.createElement('div');
|
||||||
|
progressBar.setAttribute('class', 'progress-bar');
|
||||||
|
progressBar.id = `progressBar-${webviewId}`;
|
||||||
|
return progressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.element.style.visibility = 'visible';
|
||||||
|
this.progress = 0;
|
||||||
|
|
||||||
|
if (!this.interval) {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
if (this.progress >= 100) {
|
||||||
|
this.clear();
|
||||||
|
} else {
|
||||||
|
this.progress++;
|
||||||
|
this.element.style.width = this.progress + '%';
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(loadStop = false) {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.interval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadStop) {
|
||||||
|
this.element.style.width = '100%';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.progress = 0;
|
||||||
|
this.element.style.visibility = 'hidden';
|
||||||
|
this.element.style.width = this.progress + '%';
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to manage SVG icons
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
class IconUtils {
|
||||||
|
|
||||||
|
// Static icons
|
||||||
|
static SVG = {
|
||||||
|
ellipsis: '<svg xmlns="http://www.w3.org/2000/svg" height="2em" viewBox="0 0 448 512"><path d="M8 256a56 56 0 1 1 112 0A56 56 0 1 1 8 256zm160 0a56 56 0 1 1 112 0 56 56 0 1 1 -112 0zm216-56a56 56 0 1 1 0 112 56 56 0 1 1 0-112z"/></svg>',
|
||||||
|
readerView: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 4h10v1H3zM3 6h10v1H3zM3 8h10v1H3zM3 10h6v1H3z"></path></svg>',
|
||||||
|
newTab: '<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z"/></svg>',
|
||||||
|
backgroundTab: '<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><path d="M384 32c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96C0 60.7 28.7 32 64 32H384zM160 144c-13.3 0-24 10.7-24 24s10.7 24 24 24h94.1L119 327c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l135-135V328c0 13.3 10.7 24 24 24s24-10.7 24-24V168c0-13.3-10.7-24-24-24H160z"/></svg>'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vivaldi icons
|
||||||
|
static VIVALDI_BUTTONS = [{
|
||||||
|
name: 'back',
|
||||||
|
buttonName: 'Back',
|
||||||
|
fallback: '<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>'
|
||||||
|
}, {
|
||||||
|
name: 'forward',
|
||||||
|
buttonName: 'Forward',
|
||||||
|
fallback: '<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>'
|
||||||
|
}, {
|
||||||
|
name: 'reload',
|
||||||
|
buttonName: 'Reload',
|
||||||
|
fallback: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M125.7 160H176c17.7 0 32 14.3 32 32s-14.3 32-32 32H48c-17.7 0-32-14.3-32-32V64c0-17.7 14.3-32 32-32s32 14.3 32 32v51.2L97.6 97.6c87.5-87.5 229.3-87.5 316.8 0s87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3s-163.8-62.5-226.3 0L125.7 160z"/></svg>'
|
||||||
|
}];
|
||||||
|
|
||||||
|
#initialized = false;
|
||||||
|
#iconMap = new Map();
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#initializeStaticIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes static icons
|
||||||
|
*/
|
||||||
|
#initializeStaticIcons() {
|
||||||
|
Object.entries(IconUtils.SVG).forEach(([key, value]) => {
|
||||||
|
this.#iconMap.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Vivaldi icons from the DOM or use fallback
|
||||||
|
*/
|
||||||
|
#initializeVivaldiIcons() {
|
||||||
|
if (this.#initialized) return;
|
||||||
|
|
||||||
|
IconUtils.VIVALDI_BUTTONS.forEach(button => {
|
||||||
|
this.#iconMap.set(button.name, this.#getVivaldiButton(button.buttonName, button.fallback));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the SVG of a Vivaldi button or returns the fallback
|
||||||
|
* @param {string} buttonName - name of the button in Vivali ui
|
||||||
|
* @param {string} fallbackSVG - fallback svg if no icon is found
|
||||||
|
* @returns {string} - the SVG as a string
|
||||||
|
*/
|
||||||
|
#getVivaldiButton(buttonName, fallbackSVG) {
|
||||||
|
const svg = document.querySelector(`.button-toolbar [name="${buttonName}"] svg`);
|
||||||
|
return svg ? svg.cloneNode(true).outerHTML : fallbackSVG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get icon by name
|
||||||
|
* @param {string} name - Name of the icon
|
||||||
|
* @returns {string} - Icon as SVG string
|
||||||
|
*/
|
||||||
|
getIcon(name) {
|
||||||
|
if (!this.#initialized && IconUtils.VIVALDI_BUTTONS.some(btn => btn.name === name)) {
|
||||||
|
this.#initializeVivaldiIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#iconMap.get(name) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get ellipsis() {
|
||||||
|
return this.getIcon('ellipsis');
|
||||||
|
}
|
||||||
|
|
||||||
|
get back() {
|
||||||
|
return this.getIcon('back');
|
||||||
|
}
|
||||||
|
|
||||||
|
get forward() {
|
||||||
|
return this.getIcon('forward');
|
||||||
|
}
|
||||||
|
|
||||||
|
get reload() {
|
||||||
|
return this.getIcon('reload');
|
||||||
|
}
|
||||||
|
|
||||||
|
get readerView() {
|
||||||
|
return this.getIcon('readerView');
|
||||||
|
}
|
||||||
|
|
||||||
|
get newTab() {
|
||||||
|
return this.getIcon('newTab');
|
||||||
|
}
|
||||||
|
|
||||||
|
get backgroundTab() {
|
||||||
|
return this.getIcon('backgroundTab');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
();
|
||||||
|
|
1955
.vivaldi-mods/js/global-media-controls.js
Executable file
1955
.vivaldi-mods/js/global-media-controls.js
Executable file
File diff suppressed because it is too large
Load diff
172
.vivaldi-mods/js/group-tabs-by-domain.js
Executable file
172
.vivaldi-mods/js/group-tabs-by-domain.js
Executable file
|
@ -0,0 +1,172 @@
|
||||||
|
/**
|
||||||
|
* Groups tabs by domain.
|
||||||
|
* Forum link: https://forum.vivaldi.net/topic/98184/group-tabs-by-domain?lang=en-US&page=1
|
||||||
|
*/
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
// Use the base domain for tab stacks (true: enabled, false: disabled)
|
||||||
|
base_domain: false,
|
||||||
|
|
||||||
|
// Automatically change the name of the tab stack (0: disabled, 1: use hostname, 2: generate from base domain)
|
||||||
|
rename_stack: 0,
|
||||||
|
|
||||||
|
// Workspaces that allow automatic tab stacking (exact match or <default_workspace>)
|
||||||
|
// * If not set, automatic tab stacking is allowed in all workspaces
|
||||||
|
allow_workspaces: [
|
||||||
|
// "Shopping",
|
||||||
|
],
|
||||||
|
|
||||||
|
// Domains that allow automatic tab stacking (exact match or regular expression)
|
||||||
|
// * If not set, automatic tab stacking is allowed for all domains
|
||||||
|
allow_domains: [
|
||||||
|
// "www.example.com",
|
||||||
|
// /^(.+\.)?example\.net$/,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Domains to exclude from automatic tab stacking (exact match or regular expression)
|
||||||
|
block_domains: [
|
||||||
|
// "www.example.com",
|
||||||
|
// /^(.+\.)?example\.net$/,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeArrays = (...arrays) => [...new Set(arrays.flat())];
|
||||||
|
|
||||||
|
const getUrlFragments = (url) => vivaldi.utilities.getUrlFragments(url);
|
||||||
|
|
||||||
|
const getBaseDomain = (url) => {
|
||||||
|
const {hostForSecurityDisplay, tld} = getUrlFragments(url);
|
||||||
|
return hostForSecurityDisplay.match(`([^.]+\\.${ tld })$`)?.[1] || hostForSecurityDisplay;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHostname = (url) => {
|
||||||
|
const {hostForSecurityDisplay} = getUrlFragments(url);
|
||||||
|
return config.base_domain ? getBaseDomain(url) : hostForSecurityDisplay;
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchHostRule = (url, rule) => {
|
||||||
|
const {hostForSecurityDisplay} = getUrlFragments(url);
|
||||||
|
return rule instanceof RegExp ? rule.test(hostForSecurityDisplay) : hostForSecurityDisplay === rule;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTab = async (tabId) => {
|
||||||
|
const tab = await chrome.tabs.get(tabId);
|
||||||
|
|
||||||
|
if (tab.vivExtData) {
|
||||||
|
tab.vivExtData = JSON.parse(tab.vivExtData);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabIndex = async (tabId) => (await getTab(tabId)).index;
|
||||||
|
|
||||||
|
const getWorkspaceName = async (workspaceId) => {
|
||||||
|
if (!workspaceId) {
|
||||||
|
return '<default_workspace>';
|
||||||
|
}
|
||||||
|
const workspaceList = await vivaldi.prefs.get('vivaldi.workspaces.list');
|
||||||
|
return workspaceList.find(item => item.id === workspaceId).name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabsByWorkspace = async () => {
|
||||||
|
const tabs = (await chrome.tabs.query({ currentWindow: true }))
|
||||||
|
.filter(tab => tab.id !== -1 && tab.vivExtData)
|
||||||
|
.map(tab => Object.assign(tab, { vivExtData: JSON.parse(tab.vivExtData) }))
|
||||||
|
.filter(tab => !tab.pinned && !tab.vivExtData.panelId)
|
||||||
|
.filter(tab => !config.allow_domains.length || config.allow_domains.find(rule => matchHostRule(tab.url, rule)))
|
||||||
|
.filter(tab => !config.block_domains.length || !config.block_domains.find(rule => matchHostRule(tab.url, rule)));
|
||||||
|
|
||||||
|
return Object.groupBy(tabs, tab => tab.vivExtData.workspaceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabsByStack = (tabs) => Object.groupBy(tabs, tab => tab.vivExtData.group);
|
||||||
|
|
||||||
|
const getTabsByHost = (tabs) => Object.groupBy(tabs, tab => getHostname(tab.url));
|
||||||
|
|
||||||
|
const getMaxTabsStackId = (tabsByStack, targetHost) => {
|
||||||
|
const counts = {};
|
||||||
|
|
||||||
|
for (const [stackId, tabs] of Object.entries(tabsByStack)) {
|
||||||
|
if (stackId !== 'undefined') {
|
||||||
|
const tabsByHost = getTabsByHost(tabs);
|
||||||
|
const count = tabsByHost[targetHost]?.length || 0;
|
||||||
|
|
||||||
|
delete tabsByHost[targetHost];
|
||||||
|
counts[stackId] = Object.values(tabsByHost)
|
||||||
|
.reduce((acc, tabs) => {
|
||||||
|
return acc > tabs.length ? acc : 0;
|
||||||
|
}, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(counts)
|
||||||
|
.reduce((acc, [stackId, count]) => {
|
||||||
|
return acc[1] < count ? [stackId, count] : acc;
|
||||||
|
}, [, 0])[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabStackName = (url) => {
|
||||||
|
let stackName;
|
||||||
|
|
||||||
|
switch (config.rename_stack) {
|
||||||
|
case 1:
|
||||||
|
stackName = getHostname(url);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
stackName = getBaseDomain(url).split('.')[0];
|
||||||
|
stackName = stackName.charAt(0).toUpperCase() + stackName.slice(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return stackName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTabStack = async (tabId, stackId, stackName) => {
|
||||||
|
const {vivExtData} = await getTab(tabId);
|
||||||
|
|
||||||
|
if (stackName) {
|
||||||
|
vivExtData.fixedGroupTitle = stackName;
|
||||||
|
}
|
||||||
|
vivExtData.group = stackId;
|
||||||
|
chrome.tabs.update(tabId, { vivExtData: JSON.stringify(vivExtData) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const stackingTabs = async (workspaceId) => {
|
||||||
|
const workspaceName = await getWorkspaceName(workspaceId);
|
||||||
|
|
||||||
|
if (!config.allow_workspaces.length || config.allow_workspaces.includes(workspaceName)) {
|
||||||
|
const tabsByWorkspace = await getTabsByWorkspace();
|
||||||
|
const tabsByStack = getTabsByStack(tabsByWorkspace[workspaceId]);
|
||||||
|
const tabsByHost = getTabsByHost(tabsByWorkspace[workspaceId]);
|
||||||
|
|
||||||
|
for (const [host, tabs] of Object.entries(tabsByHost)) {
|
||||||
|
const targetStackId = getMaxTabsStackId(tabsByStack, host) || crypto.randomUUID();
|
||||||
|
const targetStackTabs = tabsByStack[targetStackId] ? getTabsByHost(tabsByStack[targetStackId])[host] : [];
|
||||||
|
const targetTabs = mergeArrays(targetStackTabs, tabs);
|
||||||
|
const targetStackName = getTabStackName(tabs[0].pendingUrl || tabs[0].url);
|
||||||
|
|
||||||
|
let tabIndex = await getTabIndex(targetTabs[0].id);
|
||||||
|
|
||||||
|
for (const tab of targetTabs) {
|
||||||
|
addTabStack(tab.id, targetStackId, targetStackName);
|
||||||
|
chrome.tabs.move(tab.id, { index: tabIndex });
|
||||||
|
tabIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.webNavigation.onCommitted.addListener(async details => {
|
||||||
|
if (details.tabId !== -1) {
|
||||||
|
const tab = await getTab(details.tabId);
|
||||||
|
|
||||||
|
if (tab && !tab.pinned && !tab.vivExtData.panelId && details.frameType === 'outermost_frame') {
|
||||||
|
const workspaceId = tab.vivExtData.workspaceId;
|
||||||
|
stackingTabs(workspaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
147
.vivaldi-mods/posix.lua
Executable file
147
.vivaldi-mods/posix.lua
Executable file
|
@ -0,0 +1,147 @@
|
||||||
|
-- This file contains wrappers for hilbish.run for common POSIX utilities.
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
M.pwd = function()
|
||||||
|
return hilbish.run("pwd", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.touch = function(file, opts)
|
||||||
|
local cmd = "touch "
|
||||||
|
if opts then
|
||||||
|
if opts.sudo ~=nil and opts.sudo == true then
|
||||||
|
cmd = "sudo " .. cmd
|
||||||
|
end
|
||||||
|
if opts.change_mod_time ~= nil and opts.change_mod_time == true then
|
||||||
|
cmd = cmd .. "-m "
|
||||||
|
end
|
||||||
|
if opts.change_access_time ~= nil and opts.change_access_time == true then
|
||||||
|
cmd = cmd .. "-a "
|
||||||
|
end
|
||||||
|
if opts.flags ~= nil then
|
||||||
|
cmd = cmd .. opts.flags .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cmd = cmd .. file
|
||||||
|
return hilbish.run(cmd, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.cp = function(file1, file2, opts)
|
||||||
|
local cmd = "cp "
|
||||||
|
if opts then
|
||||||
|
if opts.sudo ~= nil and opts.sudo == true then
|
||||||
|
cmd = "sudo " .. cmd
|
||||||
|
end
|
||||||
|
if opts.force ~= nil and opts.force == true then
|
||||||
|
cmd = cmd .. "-f "
|
||||||
|
end
|
||||||
|
if opts.recursive ~= nil and opts.recursive == true then
|
||||||
|
cmd = cmd .. "-r "
|
||||||
|
end
|
||||||
|
if opts.flags ~= nil then
|
||||||
|
cmd = cmd .. opts.flags .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cmd = cmd .. file1 .. " " .. file2
|
||||||
|
return hilbish.run(cmd, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.rm = function(file, opts)
|
||||||
|
local cmd = "rm "
|
||||||
|
if opts then
|
||||||
|
if opts.sudo ~= nil and opts.sudo == true then
|
||||||
|
cmd = "sudo " .. cmd
|
||||||
|
end
|
||||||
|
if opts.force ~= nil and opts.force == true then
|
||||||
|
cmd = cmd .. "-f "
|
||||||
|
end
|
||||||
|
if opts.recursive ~= nil and opts.recursive == true then
|
||||||
|
cmd = cmd .. "-r "
|
||||||
|
end
|
||||||
|
if opts.flags ~= nil then
|
||||||
|
cmd = cmd .. opts.flags .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cmd = cmd .. file
|
||||||
|
return hilbish.run(cmd, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.mv = function(file1, file2, opts)
|
||||||
|
local cmd = "mv "
|
||||||
|
if opts then
|
||||||
|
if opts.sudo ~= nil and opts.sudo == true then
|
||||||
|
cmd = "sudo " .. cmd
|
||||||
|
end
|
||||||
|
if opts.flags ~= nil then
|
||||||
|
cmd = cmd .. opts.flags .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cmd = cmd .. file1 .. " " .. file2
|
||||||
|
return hilbish.run(cmd, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.find = function(dir, opts)
|
||||||
|
local cmd = "find " .. dir .. " "
|
||||||
|
if opts then
|
||||||
|
if opts.depth ~= nil then
|
||||||
|
cmd = cmd .. "-depth " .. opts.depth .. " "
|
||||||
|
end
|
||||||
|
if opts.maxdepth ~= nil then
|
||||||
|
cmd = cmd .. "-maxdepth " .. opts.maxdepth .. " "
|
||||||
|
end
|
||||||
|
if opts.type ~= nil then
|
||||||
|
cmd = cmd .. "-type " .. opts.type .. " "
|
||||||
|
end
|
||||||
|
if opts.name ~= nil then
|
||||||
|
cmd = cmd .. "-name " .. opts.name .. " "
|
||||||
|
end
|
||||||
|
if opts.flags ~= nil then
|
||||||
|
cmd = cmd .. opts.flags .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return hilbish.run(cmd, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.dirname = function(dir)
|
||||||
|
return hilbish.run("dirname " .. dir, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.sed = function(script, file, opts)
|
||||||
|
local cmd = "sed -e '" .. script .. "' "
|
||||||
|
if opts then
|
||||||
|
if opts.sudo ~= nil and opts.sudo == true then
|
||||||
|
cmd = "sudo " .. cmd
|
||||||
|
end
|
||||||
|
if opts.script_file ~= nil then
|
||||||
|
cmd = cmd .. "-f " .. opts.script_file .. " "
|
||||||
|
end
|
||||||
|
if opts.suppress_default ~= nil and opts.suppress_default == true then
|
||||||
|
cmd = cmd .. "-n "
|
||||||
|
end
|
||||||
|
if opts.flags ~= nil then
|
||||||
|
cmd = cmd .. opts.flags .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cmd = cmd .. file
|
||||||
|
return hilbish.run(cmd, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
M.cat = function(files, opts)
|
||||||
|
local cmd = "cat " .. files .. " "
|
||||||
|
if opts then
|
||||||
|
if opts.sudo ~= nil and opts.sudo == true then
|
||||||
|
cmd = "sudo " .. cmd
|
||||||
|
end
|
||||||
|
-- The following two options are mutually exclusive
|
||||||
|
if opts.output_file ~= nil then
|
||||||
|
cmd = cmd .. "> " .. opts.output_file
|
||||||
|
elseif opts.append_file ~= nil then
|
||||||
|
cmd = cmd .. ">> " .. opts.append_file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return hilbish.run(cmd, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
18
.vivaldi-mods/restore-backup.lua
Executable file
18
.vivaldi-mods/restore-backup.lua
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env hilbish
|
||||||
|
|
||||||
|
local fs = require("fs")
|
||||||
|
local posix = require("posix")
|
||||||
|
|
||||||
|
|
||||||
|
local _, vivaldi_install = posix.find("/opt", { name = "vivaldi-bin" })
|
||||||
|
local _, dir = posix.dirname(vivaldi_install)
|
||||||
|
dir = dir:gsub("\n", "")
|
||||||
|
|
||||||
|
local matches = fs.glob(dir .. "/resources/vivaldi/window.html.bak")
|
||||||
|
|
||||||
|
if #matches ~= 0 then
|
||||||
|
print("Restoring window.html backup in " .. dir)
|
||||||
|
posix.mv(matches[1], dir .. "/resources/vivaldi/window.html", {sudo = true})
|
||||||
|
else
|
||||||
|
print("No backup")
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue