Guest User

custom.js

a guest
Oct 8th, 2021
311
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 79.43 KB | None | 0 0
  1. /* ================================================================================================================================================================================================================================= */
  2.  
  3. /*
  4. * Autosave Sessions (a mod for Vivaldi)
  5. * Written by LonM
  6. * v4 : Localise to current timezone, l10n
  7. * v3 : Has own settings section & support private windows again
  8. * v2 : Better handling of multiple windows
  9. */
  10.  
  11. (function autoSaveSessionsMod(){
  12. "use strict";
  13.  
  14. const LANGUAGE = 'en_gb';
  15.  
  16. const l10n = {
  17. en_gb: {
  18. delay: 'Period (minutes)',
  19. restart: 'This setting requires a restart to take full effect.',
  20. maxoldsessions: 'Old Sessions Count',
  21. prefix: 'Prefix',
  22. prefixdesc: 'A unique prefix made up of the following characters: A-Z 0-9 _',
  23. saveprivate: 'Save Private Windows'
  24. },
  25. }[LANGUAGE];
  26.  
  27. let CURRENT_SETTINGS = {};
  28.  
  29. /**
  30. * Copied from bundle.js © Vivaldi - Check if a filename is valid
  31. * @param {string} s
  32. */
  33. function isValidName(e){
  34. return /^[^\\/:\*\?"<>\|]+$/.test(e) && !/^\./.test(e) && !/^(nul|prn|con|lpt[0-9]|com[0-9])(\.|$)/i.test(e);
  35. }
  36.  
  37. /**
  38. * Turns a date into a string that can be used in a file name
  39. * Locale string seems to be the best at getting the correct time for any given timezone
  40. * @param {Date} date object
  41. */
  42. function dateToFileSafeString(date){
  43. const badChars = /[\\/:\*\?"<>\|]/gi;
  44. return date.toLocaleString().replace(badChars, '.');
  45. }
  46.  
  47. /**
  48. * Enable Autosaving sessions
  49. */
  50. function autoSaveSession(isPrivate){
  51. vivaldi.sessionsPrivate.getAll(allSessions => {
  52. const priv = isPrivate ? "PRIV" : "";
  53. const prefix = CURRENT_SETTINGS["LONM_SESSION_AUTOSAVE_PREFIX"] + priv;
  54. const maxOld = CURRENT_SETTINGS["LONM_SESSION_AUTOSAVE_MAX_OLD_SESSIONS"];
  55. const now = new Date();
  56. const autosavesOnly = allSessions.filter(x => x.name.indexOf(prefix)===0);
  57. const oldestFirst = autosavesOnly.sort((a,b) => {return a.createDateJS - b.createDateJS;});
  58.  
  59. /* create the new session */
  60. const name = prefix + dateToFileSafeString(now);
  61. /* final sanity check */
  62. if (!isValidName(name)){
  63. throw new Error("[Autosave Sessions] Cannot name a session as " + name);
  64. }
  65. const options = {
  66. saveOnlyWindowId: 0
  67. };
  68. vivaldi.sessionsPrivate.saveOpenTabs(name, options, () => {}); /* there is no way to tell if it failed */
  69.  
  70. /* delete older sessions */
  71. let numberOfSessions = oldestFirst.length + 1; /* length + 1 as we have just added a new one */
  72. let oldestIndex = 0;
  73. while(numberOfSessions > maxOld){
  74. vivaldi.sessionsPrivate.delete(oldestFirst[oldestIndex].name,() => {});
  75. oldestIndex++;
  76. numberOfSessions--;
  77. }
  78. });
  79. }
  80.  
  81. /**
  82. * Check if this is the most recent window, and if the most recent window is still open
  83. * if not, then stop saving the sessions
  84. */
  85. function triggerAutosave(){
  86. chrome.storage.local.get("LONM_SESSION_AUTOSAVE_LAST_WINDOW", data => {
  87. const lastOpenedWindow = data["LONM_SESSION_AUTOSAVE_LAST_WINDOW"];
  88. if(window.vivaldiWindowId===lastOpenedWindow){
  89. /* We know this window is correct, skip the checks */
  90. autoSaveSession();
  91. return;
  92. }
  93. chrome.windows.getAll(openWindows => {
  94. const foundLastOpen = openWindows.find(window => window.id===lastOpenedWindow);
  95. if(foundLastOpen){
  96. /*Most recent window still active, use that one instead*/
  97. } else {
  98. /*Most recent window was closed, revert to this one*/
  99. chrome.storage.local.set({
  100. "LONM_SESSION_AUTOSAVE_LAST_WINDOW": window.vivaldiWindowId
  101. }, () => {
  102. autoSaveSession();
  103. });
  104. }
  105. });
  106. });
  107. }
  108. function triggerAutosavePrivate(){
  109. chrome.storage.local.get("LONM_SESSION_AUTOSAVE_LAST_PRIV_WINDOW", data => {
  110. const lastOpenedWindow = data["LONM_SESSION_AUTOSAVE_LAST_PRIV_WINDOW"];
  111. if(window.vivaldiWindowId===lastOpenedWindow){
  112. /* We know this window is correct, skip the checks */
  113. autoSaveSession(true);
  114. return;
  115. }
  116. chrome.windows.getAll(openWindows => {
  117. const foundLastOpen = openWindows.find(window => window.id===lastOpenedWindow);
  118. if(foundLastOpen){
  119. /*Most recent window still active, use that one instead*/
  120. } else {
  121. /*Most recent window was closed, revert to this one*/
  122. chrome.storage.local.set({
  123. "LONM_SESSION_AUTOSAVE_LAST_PRIV_WINDOW": window.vivaldiWindowId
  124. }, () => {
  125. autoSaveSession(true);
  126. });
  127. }
  128. });
  129. });
  130. }
  131.  
  132. /**
  133. * Mod the settings page to show settings there
  134. * Wait a little bit after a settings page has been opened and add settings in
  135. */
  136. const SETTINGSPAGE = "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path=general";
  137. function modSettingsPageListener(newTab){
  138. if(newTab.url === SETTINGSPAGE || newTab.pendingUrl === SETTINGSPAGE){
  139. setTimeout(modSettingsPage, 1000);
  140. }
  141. }
  142. function modSettingsPage(){
  143. const settingsHTML = document.createElement("section");
  144. settingsHTML.className = "setting-section";
  145. settingsHTML.id = "lonmAutosaveSessionsSettings";
  146. const settingsDiv = document.createElement("div");
  147. settingsDiv.insertAdjacentHTML("beforeend", "<h2>Autosave Sessions Mod</h2>");
  148. MOD_SETTINGS.forEach(setting => {
  149. settingsDiv.appendChild(makeSettingElement(setting));
  150. });
  151. settingsHTML.appendChild(settingsDiv);
  152. document.querySelector(".vivaldi-settings .settings-content section").insertAdjacentElement("afterbegin", settingsHTML);
  153. }
  154.  
  155. /**
  156. * For a mod setting you need:
  157. *
  158. * A) Load it when the mod starts
  159. * B) Make an option for it when settings is opened
  160. * C) Change the saved and current state with new value when setting is changed
  161. *
  162. * Mod setting has:
  163. * Key: string
  164. * Default Value: string|int
  165. * Description: string
  166. */
  167. const MOD_SETTINGS = [
  168. {
  169. id: "LONM_SESSION_AUTOSAVE_DELAY_MINUTES",
  170. type: Number,
  171. min: 1,
  172. max: undefined,
  173. default: 5,
  174. title: l10n.delay,
  175. description: l10n.restart
  176. },
  177. {
  178. id: "LONM_SESSION_AUTOSAVE_MAX_OLD_SESSIONS",
  179. type: Number,
  180. min: 1,
  181. max: undefined,
  182. default: 5,
  183. title: l10n.maxoldsessions
  184. },
  185. {
  186. id: "LONM_SESSION_AUTOSAVE_PREFIX",
  187. type: String,
  188. pattern: "[\\w_]{0,20}",
  189. default: "VSESAUTOSAVE_",
  190. title: l10n.prefix,
  191. description: l10n.prefixdesc
  192. },
  193. {
  194. id: "LONM_SESSION_SAVE_PRIVATE_WINDOWS",
  195. type: Boolean,
  196. default: false,
  197. title: l10n.saveprivate,
  198. description: l10n.restart
  199. }
  200. ];
  201.  
  202. /**
  203. * Handle a change to a setting input
  204. * Should be bound in a listener to the setting object
  205. * @param {InputEvent} input
  206. */
  207. function settingUpdated(input){
  208. if(input.target.type === "checkbox"){
  209. CURRENT_SETTINGS[this.id] = input.target.checked;
  210. } else {
  211. input.target.checkValidity();
  212. if(input.target.reportValidity() && input.target.value !== ""){
  213. CURRENT_SETTINGS[this.id] = input.target.value;
  214. }
  215. }
  216. chrome.storage.local.set(CURRENT_SETTINGS);
  217. }
  218.  
  219. /**
  220. * Create an element for the current setting
  221. * @param modSetting
  222. */
  223. function makeSettingElement(modSetting) {
  224. const currentSettingValue = CURRENT_SETTINGS[modSetting.id];
  225. const div = document.createElement("div");
  226. div.className = "setting-single";
  227. const title = document.createElement("h3");
  228. title.innerText = modSetting.title;
  229. div.appendChild(title);
  230. if(modSetting.description){
  231. const info = document.createElement("p");
  232. info.className = "info";
  233. info.innerText = modSetting.description;
  234. div.appendChild(info);
  235. }
  236. const input = document.createElement("input");
  237. input.id = modSetting.id;
  238. input.value = currentSettingValue;
  239. input.autocomplete = "off";
  240. input.autocapitalize = "off";
  241. input.autocorrect = "off";
  242. input.spellcheck = "off";
  243. switch (modSetting.type) {
  244. case Number:
  245. input.type = "number";
  246. break;
  247. case String:
  248. input.type = "text";
  249. break;
  250. case Boolean:
  251. input.type = "checkbox";
  252. if(currentSettingValue){input.checked = "checked";}
  253. break;
  254. default:
  255. throw Error("Unknown setting type!");
  256. }
  257. if(modSetting.max){input.max = modSetting.max;}
  258. if(modSetting.min){input.min = modSetting.min;}
  259. if(modSetting.pattern){input.pattern = modSetting.pattern;}
  260. input.addEventListener("input", settingUpdated.bind(modSetting));
  261. div.appendChild(input);
  262. return div;
  263. }
  264.  
  265. /**
  266. * Init the mod, but only if we are not incognito, to maintain privacy.
  267. * Save the window id in storage, and only use the most recent window to save sessions
  268. */
  269. function init(){
  270. if(window.vivaldiWindowId){
  271. chrome.windows.getCurrent(window => {
  272. if(!window.incognito){
  273. chrome.storage.local.set({
  274. "LONM_SESSION_AUTOSAVE_LAST_WINDOW": window.vivaldiWindowId
  275. }, () => {
  276. setInterval(triggerAutosave, CURRENT_SETTINGS["LONM_SESSION_AUTOSAVE_DELAY_MINUTES"]*60*1000);
  277. });
  278. }
  279. if(CURRENT_SETTINGS["LONM_SESSION_SAVE_PRIVATE_WINDOWS"] && window.incognito){
  280. chrome.storage.local.set({
  281. "LONM_SESSION_AUTOSAVE_LAST_PRIV_WINDOW": window.vivaldiWindowId
  282. }, () => {
  283. setInterval(triggerAutosavePrivate, CURRENT_SETTINGS["LONM_SESSION_AUTOSAVE_DELAY_MINUTES"]*60*1000);
  284. });
  285. }
  286. chrome.tabs.onCreated.addListener(modSettingsPageListener);
  287. });
  288. } else {
  289. setTimeout(init, 500);
  290. }
  291. }
  292.  
  293. /**
  294. * Load the settings and call the initialiser function
  295. */
  296. function loadSettingsAndInit(){
  297. const keys = MOD_SETTINGS.reduce((prev, current) => {
  298. prev[current.id] = current.default;
  299. return prev;
  300. }, {});
  301. chrome.storage.local.get(keys, value => {
  302. CURRENT_SETTINGS = value;
  303. setTimeout(init, 500);
  304. });
  305. }
  306.  
  307. loadSettingsAndInit();
  308. })();
  309.  
  310. /* ================================================================================================================================================================================================================================= */
  311.  
  312. // Backup Search Engines
  313. // version 2021.9.0
  314. // https://forum.vivaldi.net/topic/35443/backup-search-engines
  315. // Adds functionality to backup and restore search engines in
  316. // vivaldi://settings/search.
  317.  
  318. (function () {
  319. function _msgSearch(pnt) {
  320. clearTimeout(_msgTimeout);
  321. if (pnt === "backup") {
  322. _infoSearch.innerText = "Search engines backup copied to clipboard.";
  323. } else if (pnt === "restore") {
  324. _infoSearch.innerText = "Search engines restored.";
  325. } else {
  326. _infoSearch.innerText = "Search engines code error.";
  327. }
  328. _msgTimeout = setTimeout(function () {
  329. _infoSearch.innerText = "";
  330. }, 5000);
  331. }
  332.  
  333. function _restoreSearch() {
  334. event.preventDefault();
  335. event.stopPropagation();
  336. if (_eventSearch === "paste") {
  337. var clipboardData = event.clipboardData || window.clipboardData;
  338. var engineCode = clipboardData.getData("text");
  339. } else {
  340. var engineCode = event.dataTransfer.getData("text");
  341. }
  342. try {
  343. var engines = JSON.parse(engineCode);
  344. } catch (err) {
  345. _msgSearch("error");
  346. return;
  347. }
  348. if (
  349. "engines" in engines &&
  350. "default" in engines &&
  351. "defaultPrivate" in engines
  352. ) {
  353. chrome.storage.local.set(
  354. { SEARCH_ENGINE_COLLECTION: engines },
  355. function () {
  356. _msgSearch("restore");
  357. }
  358. );
  359. } else {
  360. _msgSearch("error");
  361. }
  362. }
  363.  
  364. function _backupSearch() {
  365. chrome.storage.local.get({ SEARCH_ENGINE_COLLECTION: "" }, function (back) {
  366. const collection = back.SEARCH_ENGINE_COLLECTION;
  367. collection.engines = collection.engines.filter(
  368. (engine) => engine.removed === false
  369. );
  370. for (let i = 0; i < collection.engines.length; i++) {
  371. collection.engines[i].historyId = i + 1;
  372. }
  373. collection.nextHistoryId = collection.engines.length + 1;
  374. console.log(collection);
  375. const engineCode = JSON.stringify(collection);
  376. navigator.clipboard.writeText(engineCode);
  377. _msgSearch("backup");
  378. });
  379. }
  380.  
  381. function searchEngines() {
  382. const styleCheck = document.getElementById("searchEngines");
  383. if (!styleCheck) {
  384. const style = document.createElement("style");
  385. style.type = "text/css";
  386. style.id = "searchEngines";
  387. style.innerHTML =
  388. "#backupSearch, #restoreSearch {margin-left: 6px;}#restoreSearch{width: 130px;margin-top: 6px;}#restoreSearch::-webkit-input-placeholder {opacity: 1;color: var(--colorHighlightBg);text-align: center;}#msgConfirm{margin-left: 12px}";
  389. document.getElementsByTagName("head")[0].appendChild(style);
  390. }
  391. const modCheck = document.getElementById("backupSearch");
  392. if (!modCheck) {
  393. const place = document.querySelector(
  394. ".setting-section > div > .setting-group.unlimited > .setting-single"
  395. );
  396. const backupBtn = document.createElement("input");
  397. backupBtn.setAttribute("type", "button");
  398. backupBtn.setAttribute("value", "Backup");
  399. backupBtn.id = "backupSearch";
  400. place.insertBefore(backupBtn, place.lastChild);
  401. const restoreInput = document.createElement("input");
  402. restoreInput.setAttribute("type", "text");
  403. restoreInput.setAttribute("placeholder", "Restore Backup");
  404. restoreInput.id = "restoreSearch";
  405. place.insertBefore(restoreInput, place.lastChild);
  406. _infoSearch = document.createElement("span");
  407. _infoSearch.id = "msgConfirm";
  408. place.insertBefore(_infoSearch, place.lastChild);
  409. document
  410. .getElementById("backupSearch")
  411. .addEventListener("click", _backupSearch);
  412. const restoreSearch = document.getElementById("restoreSearch");
  413. restoreSearch.addEventListener("paste", function () {
  414. _eventSearch = "paste";
  415. _restoreSearch(event);
  416. });
  417. restoreSearch.addEventListener("drop", function () {
  418. _eventSearch = "drop";
  419. _restoreSearch(event);
  420. });
  421. _msgTimeout = {};
  422. }
  423. }
  424.  
  425. const settingsUrl =
  426. "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path=";
  427. chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  428. if (changeInfo.url === `${settingsUrl}search`) {
  429. setTimeout(searchEngines, 100);
  430. }
  431. });
  432. })();
  433.  
  434. /* ================================================================================================================================================================================================================================= */
  435.  
  436.  
  437. //Bookmarks button before AddressBar
  438. //Кнопка с выпадающими закладками перед строкой адреса
  439.  
  440. {
  441. const position = { separate: "separate", addressfield: "addressfield" }
  442.  
  443. vivaldi.jdhooks.hookModuleExport("vivaldiSettings", "default", exports => {
  444. let oldGetDefault = exports.getDefault
  445. exports.getDefault = name => {
  446. switch (name) {
  447. case "BOOKMARK_BUTTON_POSITION": return position.separate
  448. default: return oldGetDefault(name)
  449. }
  450. }
  451. return exports
  452. })
  453.  
  454. function bookmarksOnClick(event) {
  455. const CommandManager = vivaldi.jdhooks.require("_CommandManager")
  456. const ShowMenu = vivaldi.jdhooks.require("_ShowMenu")
  457.  
  458. const menu = CommandManager.getNamedMenu("vivaldi", true)
  459. .concat(CommandManager.getNamedMenu("menubar", true))
  460.  
  461. const bookmarks = menu.find(i => i.commandName == "MENU_BOOKMARKS")
  462. if (bookmarks) {
  463. const rect = event.target.getBoundingClientRect()
  464. const props = {
  465. id: 0,
  466. rect: {
  467. x: parseInt(rect.left),
  468. y: parseInt(rect.top),
  469. width: parseInt(rect.width),
  470. height: parseInt(rect.height)
  471. },
  472. menu: {
  473. items: bookmarks.items,
  474. expandId: "",
  475. preferred: []
  476. }
  477. }
  478. ShowMenu.show(props.id, [props], "bottom")
  479. }
  480. }
  481.  
  482. vivaldi.jdhooks.hookClass("createbookmark_createbookmark", origClass => {
  483. const ReactDom = vivaldi.jdhooks.require("ReactDOM")
  484.  
  485. class newCreateBookmarkButton extends origClass {
  486. constructor(...e) {
  487. super(...e)
  488. this.bookmarksButtonJs = {
  489. pointerUp: (e => {
  490. if (e.button == 2 && this.state.jdVivaldiSettings.BOOKMARK_BUTTON_POSITION == position.addressfield) {
  491. bookmarksOnClick(e)
  492. }
  493. }).bind(this)
  494. }
  495. }
  496.  
  497. componentDidMount() {
  498. if (super.componentDidMount) super.componentDidMount()
  499.  
  500. const button = ReactDom.findDOMNode(this)
  501. if (button) button.addEventListener("pointerup", this.bookmarksButtonJs.pointerUp, true)
  502. }
  503.  
  504. componentWillUnmount() {
  505. const button = ReactDom.findDOMNode(this)
  506. if (button) button.removeEventListener("pointerup", this.bookmarksButtonJs.pointerUp, true)
  507.  
  508. if (super.componentWillUnmount) super.componentWillUnmount()
  509. }
  510. }
  511. return vivaldi.jdhooks.insertWatcher(newCreateBookmarkButton, { settings: ["BOOKMARK_BUTTON_POSITION"] })
  512. })
  513.  
  514. vivaldi.jdhooks.hookClass("toolbars_Toolbar", origClass => {
  515. const React = vivaldi.jdhooks.require("React")
  516. const SettingsPaths = vivaldi.jdhooks.require("_PrefKeys")
  517. const ToolbarButton = vivaldi.jdhooks.require("toolbars_ToolbarButton")
  518.  
  519. class newClass extends origClass {
  520. render() {
  521. let ret = super.render()
  522. if (this.props.name == SettingsPaths.kToolbarsNavigation) {
  523. ret.props.children.push(
  524. this.state.jdVivaldiSettings.BOOKMARK_BUTTON_POSITION == position.separate &&
  525. this.state.jdPrefs[SettingsPaths.kMenuDisplay] != "top"
  526.  
  527. ? React.createElement(ToolbarButton, {
  528. tooltip: "Bookmarks",
  529. onClick: bookmarksOnClick,
  530. image: vivaldi.jdhooks.require("_svg_bookmarks_large")
  531. })
  532.  
  533. : null
  534. )
  535. }
  536. return ret
  537. }
  538. }
  539. return vivaldi.jdhooks.insertWatcher(newClass, {
  540. settings: ["BOOKMARK_BUTTON_POSITION"],
  541. prefs: [SettingsPaths.kMenuDisplay]
  542. })
  543. })
  544.  
  545. vivaldi.jdhooks.hookClass("settings_bookmarks_BookmarkSettings", origClass => {
  546. const React = vivaldi.jdhooks.require("React")
  547. const RadioGroup = vivaldi.jdhooks.require("common_RadioGroup")
  548. const VivaldiSettings = vivaldi.jdhooks.require("vivaldiSettings")
  549. const Settings_SettingsSearchCategoryChild = vivaldi.jdhooks.require("settings_SettingsSearchCategoryChild")
  550.  
  551. function toArray(i) {
  552. if (Array.isArray(i)) return i;
  553. return [i];
  554. }
  555.  
  556. const Setting = vivaldi.jdhooks.insertWatcher(class extends React.PureComponent {
  557. render() {
  558. return React.createElement(Settings_SettingsSearchCategoryChild, { filter: this.props.filter },
  559. React.createElement("div", { className: "setting-group" },
  560. React.createElement("h3", {}, "Bookmark Button"),
  561. React.createElement(RadioGroup,
  562. {
  563. name: "bookmark_button_position",
  564. value: this.state.jdVivaldiSettings.BOOKMARK_BUTTON_POSITION,
  565. onChange: (evt) => VivaldiSettings.set({ BOOKMARK_BUTTON_POSITION: evt.target.value })
  566. },
  567. React.createElement("div", { className: "setting-single" },
  568. React.createElement("label", {},
  569. React.createElement("input",
  570. {
  571. type: "radio",
  572. value: position.separate,
  573. }),
  574. React.createElement("span", {}, "Separate button")
  575. )
  576. ),
  577. React.createElement("div", { className: "setting-single" },
  578. React.createElement("label", {},
  579. React.createElement("input",
  580. {
  581. type: "radio",
  582. value: position.addressfield,
  583. }),
  584. React.createElement("span", {},
  585. 'Right click on "Add bookmark" button in the address field'
  586. )
  587. )
  588. )
  589. )
  590. )
  591. )
  592. }
  593. }, { settings: ["BOOKMARK_BUTTON_POSITION"] })
  594.  
  595. return class extends origClass {
  596. render() {
  597. let r = super.render()
  598. r.props.children = toArray(r.props.children)
  599. r.props.children.push(React.createElement(Setting, this.props))
  600. return r
  601. }
  602. }
  603. })
  604. }
  605.  
  606. /* ================================================================================================================================================================================================================================= */
  607.  
  608. /* History Clock */
  609.  
  610. function historyClock() {
  611. const clock = document.querySelector("#switch button.history");
  612. var time = new Date();
  613. var hours = time.getHours();
  614. var minutes = time.getMinutes();
  615. var seconds = time.getSeconds();
  616. if (clock) {
  617. clock.style =
  618. "--timeHourRotation: rotate(" +
  619. Math.floor(hours * 30 + minutes / 2) +
  620. "deg); --timeMinuteRotation: rotate(" +
  621. minutes * 6 +
  622. "deg); --timeSecondRotation: rotate(" +
  623. seconds * 6 +
  624. "deg)";
  625. clock.title =
  626. String(hours).padStart(2, 0) + ":" + String(minutes).padStart(2, 0) + ":" + String(seconds).padStart(2, 0);
  627. if (!document.getElementById("opClock")) {
  628. const cnp = document.querySelector("#switch button.history span svg");
  629. cnp.id = "opClock";
  630. cnp.innerHTML = `
  631. <rect x="12" y="8.5" width="1" height="5" rx="1" ry="1" style="transform-origin: center; transform: var(--timeHourRotation, rotate(130deg))"></rect>
  632. <rect x="12" y="8" width="1" height="6" rx="1" ry="1" style="transform-origin: center; transform: var(--timeMinuteRotation, rotate(0deg))"></rect>
  633. <rect x="12" y="8" width="0.75" height="6.5" rx="1" ry="1" style="transform-origin: center; transform: var(--timeSecondRotation, rotate(280deg))"></rect>
  634. <path fill-rule="evenodd" d="M5 13C5 11.1401 5 10.2101 5.20445 9.44709C5.75925 7.37653 7.37653 5.75925 9.44709 5.20445C10.2101 5 11.1401 5 13 5C14.8599 5 15.7899 5 16.5529 5.20445C18.6235 5.75925 20.2408 7.37653 20.7956 9.44709C21 10.2101 21 11.1401 21 13C21 14.8599 21 15.7899 20.7956 16.5529C20.2408 18.6235 18.6235 20.2408 16.5529 20.7956C15.7899 21 14.8599 21 13 21C11.1401 21 10.2101 21 9.44709 20.7956C7.37653 20.2408 5.75925 18.6235 5.20445 16.5529C5 15.7899 5 14.8599 5 13ZM13 19C11.0113 19 10.4093 18.9828 9.96472 18.8637C8.58436 18.4938 7.50617 17.4156 7.1363 16.0353C7.01718 15.5907 7 14.9887 7 13C7 11.0113 7.01718 10.4093 7.1363 9.96472C7.50617 8.58436 8.58436 7.50617 9.96472 7.1363C10.4093 7.01718 11.0113 7 13 7C14.9887 7 15.5907 7.01718 16.0353 7.1363C17.4156 7.50617 18.4938 8.58436 18.8637 9.96472C18.9828 10.4093 19 11.0113 19 13C19 14.9887 18.9828 15.5907 18.8637 16.0353C18.4938 17.4156 17.4156 18.4938 16.0353 18.8637C15.5907 18.9828 14.9887 19 13 19Z"></path>
  635. `;
  636. }
  637. }
  638. }
  639.  
  640. setInterval(historyClock, 1000);
  641.  
  642. /* ================================================================================================================================================================================================================================= */
  643.  
  644. (function () {
  645. // ============================================================================================================
  646. // Hibernate Background Tabs Status Bar Button Mod and Random Theme Button by luetage
  647. // - made by nomadic on the Vivaldi Forums
  648. // ============================================================================================================
  649. function combinedStatusBarButtons() {
  650. // Config ------------
  651.  
  652. // 0 is farthest left on the status bar's right grouping of buttons
  653. // keep adding 1 to the variable to move it over to the left.
  654. const HIBERNATE_BTN_POSITION = -2;
  655. const RANDOMTHEME_BTN_POSITION = -3;
  656.  
  657. // -------------------
  658. // if they are equal, defaults to random theme button first
  659. const RANDOMTHEME_BTN_IS_ADDED_FIRST = RANDOMTHEME_BTN_POSITION > HIBERNATE_BTN_POSITION ? false : true;
  660.  
  661. // ################################ START Hibernate Button Functions ################################
  662. function hibernateBackgroundTabs() {
  663. vivaldi.windowPrivate.getCurrentId((currentWindowId) => {
  664. chrome.tabs.query({ windowType: "normal" }, (tabs) => {
  665. tabs.forEach((tab) => {
  666. if (tab.windowId === currentWindowId && tab.active) return;
  667. chrome.tabs.discard(tab.id);
  668. });
  669. });
  670. });
  671. }
  672.  
  673. // shamelessly modified from @luetage's "Random Theme Button Mod" because I am lazy
  674. function addHibernateButton(callback) {
  675. let div = document.createElement("div");
  676. div.classList.add("button-toolbar", "hibernate");
  677. div.innerHTML =
  678. '<button draggable="true" tabindex="-1" title="Hibernate Background Tabs" type="button"><span><svg width="12" height="13" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M24.46.65c.1 0 .2.1.18.22l-.04.08-.06.2-.3.74-1.51 3.77-.34.85-.1.28-.04.06v.05h.16l.32-.05 2.17-.36c.15 0 .27-.04.35-.06h.26l.06.14c.11.55.2 1.12.27 1.7l-.02.02a1.36 1.36 0 01-.4.13l-3.48.6-3.6.59-.23.04h-.08a.24.24 0 01-.14-.06.16.16 0 01-.02-.08v-.16l.28-.71L19.4 5.5l.6-1.46.19-.45.04-.12v-.06h-.02V3.4h-.12c-.79.12-1.58.24-2.39.4l-.33.06h-.26l-.08-.12c-.1-.46-.2-.9-.25-1.36l-.04-.2v-.08l.02-.06a.4.4 0 01.06-.06l.15-.06.54-.11 3.11-.54 2.69-.43.83-.14.23-.04zm-7.61 4.06c.12-.01.21.06.25.2V5l-.04.22c0 .2-.06.5-.11.9l-.64 4.5-.15.99-.04.32v.11h.02v.04h.06l.07-.06.36-.13 1.1-.48 1.15-.5.37-.15.1-.04h.16c.04 0 .06.04.12.1.27.6.51 1.16.75 1.76 0 0 0 .04-.04.06a1.52 1.52 0 01-.4.23l-6.43 2.8-.88.38c-.1.04-.2.06-.24.1l-.08.02h-.02c-.06.02-.14 0-.16-.04a.16.16 0 01-.06-.08l.61-4.74c.1-.67.2-1.28.24-1.73l.08-.52.02-.13v-.1l-.02.04v-.04h-.02l-.02.02c-.02 0-.06 0-.1.04l-.31.14-2.17.9-.34.16-.12.04h-.16c-.03 0-.07-.04-.11-.1l-.4-.79-.33-.8-.02-.07v-.1l.06-.07.13-.1.56-.28a211.5 211.5 0 016.86-2.96l.26-.08.06-.04h.02zm-.71 7.32l-.02.02h.02zm-5.35 0c.14 0 .26.14.26.32l-.02.12-.08.25-2.57 8.15v.1l-.02.02v.04h.08l.14-.02.45-.12 3.63-.87h.2c.04.04.08.08.12.18l.6 2.25-.03.08-.06.06a1.93 1.93 0 01-.55.23c-.65.2-2.01.54-4.8 1.23l-4.92 1.2-.34.08H2.8l-.04.02a.34.34 0 01-.2-.08.2.2 0 01-.05-.12v-.24l.31-1.02 2.1-6.6.19-.66.06-.18v-.1H5.1l-.12.04-.43.1-1.42.36-1.44.35-.48.1-.13.02H.85l-.12-.16c-.2-.63-.35-1.24-.51-1.87 0-.12-.04-.2-.06-.28v-.2a.55.55 0 01.1-.1l.2-.1.74-.19a295.7 295.7 0 018-2.01l1.14-.28.31-.06.1-.02h.02z"/></svg><span></button>';
  679. // positon the button based on the config
  680. let elBefore = document.querySelector(".StatusInfo").nextSibling;
  681. for (let i = 0; i < HIBERNATE_BTN_POSITION; i++) {
  682. elBefore = elBefore.nextSibling;
  683. }
  684. document.querySelector(".toolbar-statusbar").insertBefore(div, elBefore);
  685. document.querySelector(".hibernate button svg").style = "width: 14px; height: 14px;";
  686. document.querySelector(".hibernate").addEventListener("click", hibernateBackgroundTabs);
  687. callback();
  688. }
  689. // ################################ END Hibernate Button Functions ################################
  690.  
  691. // ################################ START Random Theme Button Functions ################################
  692. function randomizeTheme() {
  693. chrome.storage.local.get(
  694. {
  695. THEMES_USER: "",
  696. THEME_CURRENT: "",
  697. },
  698. function (rd) {
  699. var userThemes = rd.THEMES_USER;
  700. var currentTheme = rd.THEME_CURRENT;
  701. if (userThemes.length > 1) {
  702. while (random === undefined || random.name === currentTheme) {
  703. var random = userThemes[Math.floor(Math.random() * userThemes.length)];
  704. }
  705. chrome.storage.local.set({
  706. THEME_CURRENT: random.name,
  707. BROWSER_COLOR_ACCENT_BG: random.colors.accentBg,
  708. BROWSER_COLOR_BG: random.colors.baseBg,
  709. BROWSER_COLOR_FG: random.colors.baseFg,
  710. BROWSER_COLOR_HIGHLIGHT_BG: random.colors.highlightBg,
  711. TABCOLOR_BEHIND_TABS: random.settings.accentOnWindow,
  712. USE_TABCOLOR: random.settings.accentFromPage,
  713. BORDER_RADIUS: random.settings.borderRadius,
  714. USE_TAB_TRANSPARENT_TABS: random.settings.tabsTransparent,
  715. });
  716. } else {
  717. console.log("Please create additional themes in vivaldi://settings/themes.");
  718. }
  719. }
  720. );
  721. }
  722.  
  723. function addRandomThemeButton(callback) {
  724. let div = document.createElement("div");
  725. div.classList.add("button-toolbar", "random");
  726. div.innerHTML =
  727. '<button draggable="true" tabindex="-1" title="Random theme" type="button"><span><svg width="12" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M666 481q-60 92-137 273-22-45-37-72.5t-40.5-63.5-51-56.5-63-35-81.5-14.5h-224q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h224q250 0 410 225zm1126 799q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192q-32 0-85 .5t-81 1-73-1-71-5-64-10.5-63-18.5-58-28.5-59-40-55-53.5-56-69.5q59-93 136-273 22 45 37 72.5t40.5 63.5 51 56.5 63 35 81.5 14.5h256v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm0-896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-256q-48 0-87 15t-69 45-51 61.5-45 77.5q-32 62-78 171-29 66-49.5 111t-54 105-64 100-74 83-90 68.5-106.5 42-128 16.5h-224q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h224q48 0 87-15t69-45 51-61.5 45-77.5q32-62 78-171 29-66 49.5-111t54-105 64-100 74-83 90-68.5 106.5-42 128-16.5h256v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23z"/></svg><span></button>';
  728. // positon the button based on the config
  729. let elBefore = document.querySelector(".StatusInfo").nextSibling;
  730. for (let i = 0; i < RANDOMTHEME_BTN_POSITION; i++) {
  731. elBefore = elBefore.nextSibling;
  732. }
  733. document.querySelector(".toolbar-statusbar").insertBefore(div, elBefore);
  734. document.querySelector(".random button svg").style = "width: 14px; height: 14px;";
  735. document.querySelector(".random").addEventListener("click", randomizeTheme);
  736. callback();
  737. }
  738. // ################################ END Random Theme Button Functions ################################
  739.  
  740. // Re-add the buttons after exiting fullscreen
  741. let browser = document.getElementById("browser");
  742. let oldState = browser.classList.contains("fullscreen") || browser.classList.contains("minimal-ui");
  743. let browserObserver = new MutationObserver(function (mutations) {
  744. mutations.forEach(function (mutation) {
  745. if (mutation.attributeName == "class") {
  746. let isFullscreen =
  747. mutation.target.classList.contains("fullscreen") || mutation.target.classList.contains("minimal-ui");
  748. if (oldState !== isFullscreen) {
  749. oldState = isFullscreen;
  750. if (!isFullscreen) {
  751. if (RANDOMTHEME_BTN_IS_ADDED_FIRST) addRandomThemeButton(addHibernateButton);
  752. else addHibernateButton(addRandomThemeButton);
  753. }
  754. }
  755. }
  756. });
  757. });
  758. browserObserver.observe(browser, { attributes: true });
  759.  
  760. // initially add the buttons depending on orders given
  761. if (RANDOMTHEME_BTN_IS_ADDED_FIRST) addRandomThemeButton(addHibernateButton);
  762. else addHibernateButton(addRandomThemeButton);
  763. }
  764.  
  765. let intervalID = setInterval(() => {
  766. const browser = document.getElementById("browser");
  767. if (browser) {
  768. clearInterval(intervalID);
  769. combinedStatusBarButtons();
  770. }
  771. }, 300);
  772. })();
  773.  
  774. /* ================================================================================================================================================================================================================================= */
  775.  
  776. /* Small pinned tabs in vertical tab bar */
  777.  
  778. vivaldi.jdhooks.hookModuleExport("yoga-layout", "", exprt => {
  779. // If it didn't return the yogised layout, normally you'd hook
  780. // tabs_TabStrip.createFlexBoxLayout
  781. return function(layout, ...e) {
  782. // See https://yogalayout.com/playground/ (or just the root URL)
  783. // to get a better understanding of some of the properties
  784.  
  785. // Yoga is used only for the tab bar, but anyway check it
  786. if (layout.children && layout.children.some(t => t.type === "tab")
  787. && layout.style.flexDirection === "column") { // It's vertical
  788. let lastPinned = -1, // They should come first but just in case
  789. thisRowWidth = 0,
  790. fromTop = 0
  791. layout.children = layout.children.reduce((ary, elm) => {
  792. let keep = false;
  793. if (elm.type !== "tab")
  794. keep = true;
  795. // Isn't pinned (nor is any other in its stack)
  796. if (!keep && !(elm.tab.page && elm.tab.page.pinned)
  797. && (elm.tab.get("type") !== "group" || elm.tab.pages.every(p => !p.pinned)))
  798. keep = true;
  799. // Might be pinned, but this is the "normal" display in 2nd tab bar
  800. if (!keep && elm.tab.page.getIn(["extData", "group"]) && elm.tab.get("type") === "tab")
  801. keep = true;
  802. if (keep) {
  803. ary.push(elm)
  804. return ary
  805. }
  806. // It's a pinned tab
  807. if ((thisRowWidth + 30) > layout.style.width) {
  808. fromTop += 30
  809. thisRowWidth = 0
  810. }
  811. elm.style = {
  812. ...elm.style,
  813. ...{
  814. position: "absolute",
  815. left: thisRowWidth,
  816. top: fromTop,
  817. flex: false,
  818. width: 30,
  819. minWidth: 30,
  820. maxWidth: 30,
  821. height: 30,
  822. minHeight: 30,
  823. maxHeight: 30
  824. }
  825. }
  826. thisRowWidth += 30
  827. // Make sure that it's before non-pinned tabs
  828. ary.splice(lastPinned + 1, 0, elm)
  829. lastPinned++
  830. return ary
  831. }, [])
  832.  
  833. if (lastPinned > -1) {
  834. // Add a spacer so that normal tabs don't cover pinned
  835. layout.children.splice(lastPinned + 1, 0, {
  836. type: "blank",
  837. style: {
  838. flex: false,
  839. height: fromTop + 30,
  840. minHeight: fromTop + 30,
  841. maxHeight: fromTop + 30
  842. },
  843. tab: {} // Don't crash/error pls
  844. })
  845. const ret = exprt(layout, ...e)
  846. // Avoid actually rendering it
  847. layout.children[lastPinned + 1].layout = undefined
  848. return ret
  849. }
  850. }
  851. return exprt(layout, ...e)
  852. }
  853. })
  854.  
  855. /* ================================================================================================================================================================================================================================= */
  856.  
  857. /* Change the width of a tab */
  858.  
  859. (function(){
  860. const defaultSettings = {
  861. horizontal: {
  862. unpinnedActiveWidthMax: 180,
  863. unpinnedInactiveWidthMax: 180,
  864. pinnedActiveWidthMax: 30,
  865. pinnedInactiveWidthMax: 30,
  866. pinnedAllowShrink: false
  867. },
  868. vertical: {
  869. unpinnedActiveHeightMax: 150,
  870. unpinnedInactiveHeightMax: 150,
  871. pinnedActiveHeightMax: 30,
  872. pinnedInactiveHeightMax: 30,
  873. unpinnedActiveHeightMin: 30,
  874. unpinnedInactiveHeightMin: 30,
  875. pinnedActiveHeightMin: 30,
  876. pinnedInactiveHeightMin: 30,
  877. }
  878. }
  879. const maxWidthLimit = 500
  880. const maxHeightLimit = 200
  881.  
  882. function completeObject(obj, fallback = defaultSettings) {
  883. Object.keys(fallback).forEach(key => {
  884. if (!obj.hasOwnProperty(key))
  885. obj[key] = fallback[key]
  886. else if (typeof obj[key] == "object")
  887. obj[key] = completeObject(obj[key], fallback[key])
  888. })
  889. return obj
  890. }
  891.  
  892. vivaldi.jdhooks.hookModuleExport("vivaldiSettings", "default", exports => {
  893. let oldGetDefault = exports.getDefault
  894. exports.getDefault = name => {
  895. switch (name) {
  896. case "TAB_SIZE": return defaultSettings
  897. default: return oldGetDefault(name)
  898. }
  899. }
  900. return exports
  901. })
  902.  
  903. vivaldi.jdhooks.hookClass('tabs_TabStrip', cls => {
  904. return vivaldi.jdhooks.insertWatcher(class extends cls {
  905. getTabStyle(info, horz, focus, pin, stack, thumb) {
  906. let tabStyle = super.getTabStyle(...arguments)
  907.  
  908. const tabSize = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  909. if (horz) {
  910. const key = (pin ? 'pinned' : 'unpinned')
  911. + (focus ? 'Active' : 'Inactive') + 'WidthMax'
  912. tabStyle.maxWidth = tabSize.horizontal[key]
  913. if (pin) {
  914. if (tabSize.horizontal.pinnedAllowShrink) {
  915. tabStyle.flex = true
  916. delete tabStyle.minWidth
  917. } else tabStyle.minWidth = tabStyle.maxWidth
  918. }
  919. if (tabStyle.maxWidth > maxWidthLimit)
  920. delete tabStyle.maxWidth
  921. } else {
  922. const key = (pin ? 'pinned' : 'unpinned')
  923. + (focus ? 'Active' : 'Inactive')
  924. tabStyle.maxHeight = tabSize.vertical[key + 'HeightMax']
  925. tabStyle.minHeight = tabSize.vertical[key + 'HeightMin']
  926. if (tabStyle.maxHeight < tabStyle.minHeight)
  927. tabStyle.maxHeight = tabStyle.minHeight
  928. if (tabStyle.maxHeight > maxHeightLimit)
  929. delete tabStyle.maxHeight
  930. tabStyle.flex = !tabStyle.maxHeight || (tabStyle.minHeight != tabStyle.maxHeight)
  931. }
  932.  
  933. return tabStyle
  934. }
  935. }, { settings: ['TAB_SIZE'] })
  936. })
  937.  
  938. // Settings
  939. vivaldi.jdhooks.hookClass('settings_tabs_TabOptions', cls => {
  940. const React = vivaldi.jdhooks.require('React')
  941. const settings = vivaldi.jdhooks.require('vivaldiSettings')
  942. const PrefKeys = vivaldi.jdhooks.require("_PrefKeys")
  943.  
  944. const TabWidthSettings = vivaldi.jdhooks.insertWatcher(class extends React.Component {
  945. onSliderChanged(dir, setting, event) {
  946. if (event.target && event.target.value) {
  947. let newState = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  948. let newVal = parseInt([event.target.value])
  949. newState[dir] = {
  950. ...newState[dir],
  951. ...{ [setting]: newVal }
  952. }
  953. settings.set({ ['TAB_SIZE']: newState })
  954. }
  955. }
  956. onAllowShrinkChanged(event) {
  957. if (event.target && event.target.checked !== null) {
  958. let newState = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  959. newState.horizontal = {
  960. ...newState.horizontal,
  961. ...{ ['pinnedAllowShrink']: !!event.target.checked }
  962. }
  963. settings.set({ ['TAB_SIZE']: newState })
  964. }
  965. }
  966. createSlider(dir, setting, label, canAuto = true) {
  967. let val = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)[dir][setting]
  968. let displ = ''
  969. const maxLimit = (dir == 'horizontal') ? maxWidthLimit : maxHeightLimit
  970. if (canAuto) {
  971. if (val > maxLimit) val = maxLimit + 10
  972. displ = val > maxLimit ? 'Auto' : val + 'px'
  973. } else {
  974. val = Math.min(val, maxLimit)
  975. displ = val + 'px'
  976. }
  977. return React.createElement('div', { className: 'setting-single' },
  978. React.createElement('h3', null, label),
  979. React.createElement('input', {
  980. type: 'range',
  981. value: val,
  982. min: 30, // Less than 30 seems to have no effect
  983. max: canAuto ? maxLimit + 10 : maxLimit,
  984. step: 10,
  985. onChange: this.onSliderChanged.bind(this, dir, setting)
  986. }),
  987. React.createElement('span', null, displ))
  988. }
  989. render() {
  990. const tabSize = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  991. const barPosition = this.state.jdPrefs[PrefKeys.kTabsBarPosition]
  992. if (barPosition == 'top' || barPosition == 'bottom') {
  993. return [
  994. this.createSlider('horizontal', 'unpinnedActiveWidthMax', 'Active Tab Max Width'),
  995. this.createSlider('horizontal', 'unpinnedInactiveWidthMax', 'Inactive Tab Max Width'),
  996. this.createSlider('horizontal', 'pinnedActiveWidthMax', 'Active Pinned Tab Max Width'),
  997. this.createSlider('horizontal', 'pinnedInactiveWidthMax', 'Pinned Tab Max Width'),
  998. React.createElement('div', { className: 'setting-single' },
  999. React.createElement('label', null,
  1000. React.createElement('input', {
  1001. type: 'checkbox',
  1002. checked: tabSize.horizontal.pinnedAllowShrink,
  1003. onChange: this.onAllowShrinkChanged.bind(this)
  1004. }),
  1005. React.createElement('span', null, 'Allow Shrinking of Pinned Tabs')))]
  1006. } else {
  1007. const ary = [];
  1008. [true, false].forEach(isMax => {
  1009. const side = isMax ? 'Max' : 'Min'
  1010. ary.push(
  1011. this.createSlider('vertical', `unpinnedActiveHeight${side}`, `Active Tab ${side} Height`, isMax),
  1012. this.createSlider('vertical', `unpinnedInactiveHeight${side}`, `Inactive Tab ${side} Height`, isMax),
  1013. this.createSlider('vertical', `pinnedActiveHeight${side}`, `Active Pinned Tab ${side} Height`, isMax),
  1014. this.createSlider('vertical', `pinnedInactiveHeight${side}`, `Pinned Tab ${side} Height`, isMax))
  1015. })
  1016. return ary
  1017. }
  1018. }
  1019. }, { settings: ['TAB_SIZE'], prefs: [PrefKeys.kTabsBarPosition] })
  1020.  
  1021. return class extends cls {
  1022. render() {
  1023. let sup = super.render()
  1024. if (sup.props && sup.props.children) {
  1025. let section = sup.props.children.find(c =>
  1026. c && c.props && c.props.className == 'setting-group tab-min-width')
  1027. if (section && section.props && section.props.children) {
  1028. let custom = React.createElement(TabWidthSettings, this.props)
  1029. section.props.children.push(custom)
  1030. }
  1031. }
  1032. return sup
  1033. }
  1034. }
  1035. })
  1036. })()
  1037.  
  1038.  
  1039.  
  1040. // Change the width of a tab
  1041.  
  1042. (function(){
  1043. const defaultSettings = {
  1044. horizontal: {
  1045. unpinnedActiveWidthMax: 180,
  1046. unpinnedInactiveWidthMax: 180,
  1047. pinnedActiveWidthMax: 30,
  1048. pinnedInactiveWidthMax: 30,
  1049. pinnedAllowShrink: false
  1050. },
  1051. vertical: {
  1052. unpinnedActiveHeightMax: 150,
  1053. unpinnedInactiveHeightMax: 150,
  1054. pinnedActiveHeightMax: 30,
  1055. pinnedInactiveHeightMax: 30,
  1056. unpinnedActiveHeightMin: 30,
  1057. unpinnedInactiveHeightMin: 30,
  1058. pinnedActiveHeightMin: 30,
  1059. pinnedInactiveHeightMin: 30,
  1060. }
  1061. }
  1062. const maxWidthLimit = 500
  1063. const maxHeightLimit = 200
  1064.  
  1065. function completeObject(obj, fallback = defaultSettings) {
  1066. Object.keys(fallback).forEach(key => {
  1067. if (!obj.hasOwnProperty(key))
  1068. obj[key] = fallback[key]
  1069. else if (typeof obj[key] == "object")
  1070. obj[key] = completeObject(obj[key], fallback[key])
  1071. })
  1072. return obj
  1073. }
  1074.  
  1075. vivaldi.jdhooks.hookModuleExport("vivaldiSettings", "default", exports => {
  1076. let oldGetDefault = exports.getDefault
  1077. exports.getDefault = name => {
  1078. switch (name) {
  1079. case "TAB_SIZE": return defaultSettings
  1080. default: return oldGetDefault(name)
  1081. }
  1082. }
  1083. return exports
  1084. })
  1085.  
  1086. vivaldi.jdhooks.hookClass('tabs_TabStrip', cls => {
  1087. return vivaldi.jdhooks.insertWatcher(class extends cls {
  1088. getTabStyle(info, horz, focus, pin, stack, thumb) {
  1089. let tabStyle = super.getTabStyle(...arguments)
  1090.  
  1091. const tabSize = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  1092. if (horz) {
  1093. const key = (pin ? 'pinned' : 'unpinned')
  1094. + (focus ? 'Active' : 'Inactive') + 'WidthMax'
  1095. tabStyle.maxWidth = tabSize.horizontal[key]
  1096. if (pin) {
  1097. if (tabSize.horizontal.pinnedAllowShrink) {
  1098. tabStyle.flex = true
  1099. delete tabStyle.minWidth
  1100. } else tabStyle.minWidth = tabStyle.maxWidth
  1101. }
  1102. if (tabStyle.maxWidth > maxWidthLimit)
  1103. delete tabStyle.maxWidth
  1104. } else {
  1105. const key = (pin ? 'pinned' : 'unpinned')
  1106. + (focus ? 'Active' : 'Inactive')
  1107. tabStyle.maxHeight = tabSize.vertical[key + 'HeightMax']
  1108. tabStyle.minHeight = tabSize.vertical[key + 'HeightMin']
  1109. if (tabStyle.maxHeight < tabStyle.minHeight)
  1110. tabStyle.maxHeight = tabStyle.minHeight
  1111. if (tabStyle.maxHeight > maxHeightLimit)
  1112. delete tabStyle.maxHeight
  1113. tabStyle.flex = !tabStyle.maxHeight || (tabStyle.minHeight != tabStyle.maxHeight)
  1114. }
  1115.  
  1116. return tabStyle
  1117. }
  1118. }, { settings: ['TAB_SIZE'] })
  1119. })
  1120.  
  1121. // Settings
  1122. vivaldi.jdhooks.hookClass('settings_tabs_TabOptions', cls => {
  1123. const React = vivaldi.jdhooks.require('React')
  1124. const settings = vivaldi.jdhooks.require('vivaldiSettings')
  1125. const PrefKeys = vivaldi.jdhooks.require("_PrefKeys")
  1126.  
  1127. const TabWidthSettings = vivaldi.jdhooks.insertWatcher(class extends React.Component {
  1128. onSliderChanged(dir, setting, event) {
  1129. if (event.target && event.target.value) {
  1130. let newState = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  1131. let newVal = parseInt([event.target.value])
  1132. newState[dir] = {
  1133. ...newState[dir],
  1134. ...{ [setting]: newVal }
  1135. }
  1136. settings.set({ ['TAB_SIZE']: newState })
  1137. }
  1138. }
  1139. onAllowShrinkChanged(event) {
  1140. if (event.target && event.target.checked !== null) {
  1141. let newState = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  1142. newState.horizontal = {
  1143. ...newState.horizontal,
  1144. ...{ ['pinnedAllowShrink']: !!event.target.checked }
  1145. }
  1146. settings.set({ ['TAB_SIZE']: newState })
  1147. }
  1148. }
  1149. createSlider(dir, setting, label, canAuto = true) {
  1150. let val = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)[dir][setting]
  1151. let displ = ''
  1152. const maxLimit = (dir == 'horizontal') ? maxWidthLimit : maxHeightLimit
  1153. if (canAuto) {
  1154. if (val > maxLimit) val = maxLimit + 10
  1155. displ = val > maxLimit ? 'Auto' : val + 'px'
  1156. } else {
  1157. val = Math.min(val, maxLimit)
  1158. displ = val + 'px'
  1159. }
  1160. return React.createElement('div', { className: 'setting-single' },
  1161. React.createElement('h3', null, label),
  1162. React.createElement('input', {
  1163. type: 'range',
  1164. value: val,
  1165. min: 30, // Less than 30 seems to have no effect
  1166. max: canAuto ? maxLimit + 10 : maxLimit,
  1167. step: 10,
  1168. onChange: this.onSliderChanged.bind(this, dir, setting)
  1169. }),
  1170. React.createElement('span', null, displ))
  1171. }
  1172. render() {
  1173. const tabSize = completeObject(this.state.jdVivaldiSettings.TAB_SIZE)
  1174. const barPosition = this.state.jdPrefs[PrefKeys.kTabsBarPosition]
  1175. if (barPosition == 'top' || barPosition == 'bottom') {
  1176. return [
  1177. this.createSlider('horizontal', 'unpinnedActiveWidthMax', 'Active Tab Max Width'),
  1178. this.createSlider('horizontal', 'unpinnedInactiveWidthMax', 'Inactive Tab Max Width'),
  1179. this.createSlider('horizontal', 'pinnedActiveWidthMax', 'Active Pinned Tab Max Width'),
  1180. this.createSlider('horizontal', 'pinnedInactiveWidthMax', 'Pinned Tab Max Width'),
  1181. React.createElement('div', { className: 'setting-single' },
  1182. React.createElement('label', null,
  1183. React.createElement('input', {
  1184. type: 'checkbox',
  1185. checked: tabSize.horizontal.pinnedAllowShrink,
  1186. onChange: this.onAllowShrinkChanged.bind(this)
  1187. }),
  1188. React.createElement('span', null, 'Allow Shrinking of Pinned Tabs')))]
  1189. } else {
  1190. const ary = [];
  1191. [true, false].forEach(isMax => {
  1192. const side = isMax ? 'Max' : 'Min'
  1193. ary.push(
  1194. this.createSlider('vertical', `unpinnedActiveHeight${side}`, `Active Tab ${side} Height`, isMax),
  1195. this.createSlider('vertical', `unpinnedInactiveHeight${side}`, `Inactive Tab ${side} Height`, isMax),
  1196. this.createSlider('vertical', `pinnedActiveHeight${side}`, `Active Pinned Tab ${side} Height`, isMax),
  1197. this.createSlider('vertical', `pinnedInactiveHeight${side}`, `Pinned Tab ${side} Height`, isMax))
  1198. })
  1199. return ary
  1200. }
  1201. }
  1202. }, { settings: ['TAB_SIZE'], prefs: [PrefKeys.kTabsBarPosition] })
  1203.  
  1204. return class extends cls {
  1205. render() {
  1206. let sup = super.render()
  1207. if (sup.props && sup.props.children) {
  1208. let section = sup.props.children.find(c =>
  1209. c && c.props && c.props.className == 'setting-group tab-min-width')
  1210. if (section && section.props && section.props.children) {
  1211. let custom = React.createElement(TabWidthSettings, this.props)
  1212. section.props.children.push(custom)
  1213. }
  1214. }
  1215. return sup
  1216. }
  1217. }
  1218. })
  1219. })()
  1220.  
  1221.  
  1222. /* ================================================================================================================================================================================================================================= */
  1223.  
  1224. (function () {
  1225. // ============================================================================================================
  1226. // Gives Zoom Interface in the Address Bar
  1227. // - made by nomadic on the Vivaldi Forums
  1228. // ============================================================================================================
  1229. function zoomControl() {
  1230. // CONFIGURATION: ---------------------------------------------------------------
  1231. // - in Vivaldi's settings you can set the default page zoom, this
  1232. // will follow that if RESET_ZOOM_LEVEL is set to 100
  1233. const RESET_ZOOM_LEVEL = 100; // 100 % -> the zoom that hitting the reset button will default to
  1234. const ZOOM_INCREMENT_AMOUNT = 10; // 10 % -> the amount the zoom is either raised or lowered
  1235. // MODES----------------
  1236. // Mode 0: only clicking button opens and closes the panel
  1237. // Mode 1: clicking the button opens the panel and the panel auto closes if not hovered over
  1238. // Option for mode 1:
  1239. // FADE_OUT_TIME -> the number of seconds the panel goes without hover before closing
  1240. // Mode 2: just hovering over the button will open the panel and the panel auto closes if not hovered over
  1241. // Options for mode 2:
  1242. // FADE_OUT_TIME -> the number of seconds the panel goes without hover before closing
  1243. // IS_AUTO_OPENED_ON_ADDRESSBAR -> instead of only the button being hovered, the whole address bar is used
  1244. const MODE = 1;
  1245. // ---------------------
  1246. // Option for modes 1 and 2:
  1247. const FADE_OUT_TIME = 3; // 3 seconds -> can be set to any positive half second increment (ex. 0, 0.5, 1, 1.5 ...)
  1248. // Option for mode 2:
  1249. const IS_AUTO_OPENED_ON_ADDRESSBAR = false;
  1250. // ------------------------------------------------------------------------------
  1251.  
  1252. // Creates the zoom button and panel initially, and then updates the icon depending on the zoom level
  1253. function updateZoomIcon(zoomInfo) {
  1254. let newZoom = zoomInfo.newZoomFactor;
  1255. let zoomIconPath;
  1256.  
  1257. // create the button if it isn't already there
  1258. let alreadyExists = document.getElementById("zoomIcon-c");
  1259. if (!alreadyExists) {
  1260. // CHANGE: Added in Update #4
  1261. // a guaranteed div to the left of the button
  1262. let elementToTheLeft = document.createElement("div");
  1263. elementToTheLeft.style.transition = "0.5s";
  1264. elementToTheLeft.id = "el2left";
  1265.  
  1266. let zoomBtn = document.createElement("div");
  1267. zoomBtn.id = "zoom-hover-target";
  1268. zoomBtn.innerHTML = `
  1269. <div class="zoom-parent">
  1270. <div class="zoom-panel">
  1271. <div class="page-zoom-controls-c">
  1272. <div class="button-toolbar button-toolbar-c reset-zoom-c" title="Reset Zoom">
  1273. <button tabindex="-1" class="button-textonly-c" id="zoom-reset-c">
  1274. <span class="button-title">Reset</span>
  1275. </button>
  1276. </div>
  1277. <div class="button-toolbar button-toolbar-c" title="Zoom Out">
  1278. <button tabindex="-1" id="zoom-out-c">
  1279. <span>
  1280. <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
  1281. <path d="M4 8C4 8.55228 4.44772 9 5 9H11C11.5523 9 12 8.55228 12 8C12 7.44772 11.5523 7 11 7H5C4.44772 7 4 7.44772 4 8Z"></path>
  1282. </svg>
  1283. </span>
  1284. </button>
  1285. </div>
  1286. <span id="zoom-percent-c"></span>
  1287. <div class="button-toolbar button-toolbar-c" title="Zoom In">
  1288. <button tabindex="-1" id="zoom-in-c">
  1289. <span>
  1290. <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
  1291. <path d="M7 7V5C7 4.44772 7.44772 4 8 4C8.55228 4 9 4.44772 9 5V7H11C11.5523 7 12 7.44772 12 8C12 8.55228 11.5523 9 11 9H9V11C9 11.5523 8.55228 12 8 12C7.44772 12 7 11.5523 7 11V9H5C4.44772 9 4 8.55228 4 8C4 7.44772 4.44772 7 5 7H7Z"></path>
  1292. </svg>
  1293. </span>
  1294. </button>
  1295. </div>
  1296. </div>
  1297. </div>
  1298. </div>
  1299. <div class="button-toolbar ZoomButton-Button">
  1300. <button tabindex="-1" title="Adjust Zoom" id="zoom-panel-btn" type="button" class="ToolbarButton-Button">
  1301. <span>
  1302. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewbox="0 0 16 16" id="zoomIcon-c">
  1303. </svg>
  1304. </span>
  1305. </button>
  1306. </div>
  1307. `;
  1308.  
  1309. // inserts the button to the left of the bookmark icon
  1310. const addressBar = document.querySelector(".UrlBar-AddressField");
  1311. const bookmarkBtn = addressBar.getElementsByClassName("BookmarkButton")[0];
  1312. let before = bookmarkBtn;
  1313. if (!bookmarkBtn) {
  1314. const lastAddressBarElement = document.querySelector(
  1315. ".toolbar-insideinput.toolbar-insideinput.toolbar-insideinput:last-of-type"
  1316. );
  1317. before = lastAddressBarElement;
  1318. }
  1319. addressBar.insertBefore(zoomBtn, before);
  1320. // CHANGE:Added in Update #4
  1321. // divs next to the button aren't static,so created my own div to push
  1322. addressBar.insertBefore(elementToTheLeft, zoomBtn);
  1323.  
  1324. // listener for the magnifying glass button to expand or collapse the control panel
  1325. document.getElementById("zoom-panel-btn").addEventListener("click", function () {
  1326. let nav = document.getElementsByClassName("zoom-panel")[0];
  1327. navToggle(nav, elementToTheLeft);
  1328. });
  1329.  
  1330. // listener for the zoom in button in the zoom control panel
  1331. document.getElementById("zoom-in-c").addEventListener("click", incrementPercent);
  1332.  
  1333. // listener for the zoom out button in the zoom control panel
  1334. document.getElementById("zoom-out-c").addEventListener("click", decrementPercent);
  1335.  
  1336. // listener for the zoom reset button in the zoom control panel
  1337. document.getElementById("zoom-reset-c").addEventListener("click", resetZoom);
  1338.  
  1339. // starts esentially a hover listener that modes 1 and 2 need
  1340. if (MODE === 1 || MODE === 2) {
  1341. zoomPanelHoverTracker();
  1342. }
  1343. }
  1344.  
  1345. // set the icon based on the new zoom level
  1346. if (newZoom < RESET_ZOOM_LEVEL / 100) {
  1347. // zoomed in
  1348. zoomIconPath = `
  1349. <path d="M5.83 9.65a.5.5 0 00-.29.13L1.32 14c-.46.47.23 1.17.7.7l4.22-4.22a.5.5 0 00-.42-.83zm3.6-8.5a5.41 5.41 0 00-5.4 5.4 5.4 5.4 0 105.4-5.4zm0 .99a4.4 4.4 0 11-4.41 4.41 4.4 4.4 0 014.42-4.42zM7.16 6.06c-.66 0-.66.98 0 .98h4.57c.65 0 .65-.98 0-.98z"/>
  1350. `;
  1351. } else if (newZoom > RESET_ZOOM_LEVEL / 100) {
  1352. // zoomed out
  1353. zoomIconPath = `
  1354. <path d="M5.83 9.65a.5.5 0 00-.3.13L1.31 14c-.46.47.23 1.17.7.7l4.22-4.22a.5.5 0 00-.4-.84zm3.6-8.5a5.41 5.41 0 00-5.4 5.4 5.4 5.4 0 0010.81 0 5.4 5.4 0 00-5.4-5.4zm0 .98a4.4 4.4 0 014.42 4.41 4.41 4.41 0 11-4.41-4.4zm-.06 1.63a.5.5 0 00-.43.5v1.79h-1.8c-.65 0-.65.98 0 .98h1.8v1.81c0 .66.99.66.99 0v-1.8h1.79c.65 0 .65-.99 0-1h-1.8V4.27a.5.5 0 00-.55-.5z"/>
  1355. `;
  1356. } else {
  1357. // default zoom icon
  1358. zoomIconPath = `
  1359. <path d="M5.87 9.71c-.11.01-.2.06-.29.14l-4.37 4.37c-.46.45.23 1.14.7.68l4.36-4.37a.48.48 0 00-.41-.82zm3.55-8.36A5.33 5.33 0 004.1 6.67a5.32 5.32 0 105.32-5.32zm0 .97a4.33 4.33 0 11-4.34 4.34 4.33 4.33 0 014.34-4.35z"/>
  1360. `;
  1361. }
  1362.  
  1363. // insert the new icon
  1364. let zoomSVG = document.getElementById("zoomIcon-c");
  1365. zoomSVG.innerHTML = zoomIconPath;
  1366.  
  1367. // make the percent in the controls match the current zoom level
  1368. updatePercent(newZoom);
  1369. }
  1370.  
  1371. // Makes the zoom controls slide out
  1372. function openNav(nav, elToLeft) {
  1373. nav.classList.add("expanded-nav-c");
  1374. nav.parentElement.parentElement.classList.add("zoom-hover-target--active");
  1375. elToLeft.classList.add("expanded-left-c");
  1376. }
  1377.  
  1378. // Hides the zoom controls
  1379. function closeNav(nav, elToLeft) {
  1380. nav.classList.remove("expanded-nav-c");
  1381. nav.parentElement.parentElement.classList.remove("zoom-hover-target--active");
  1382. elToLeft.classList.remove("expanded-left-c");
  1383. }
  1384.  
  1385. // Toggles the zoom controls open or closed depending on the current state
  1386. function navToggle(nav, elToLeft) {
  1387. if (nav.offsetWidth === 0) {
  1388. return openNav(nav, elToLeft);
  1389. } else {
  1390. return closeNav(nav, elToLeft);
  1391. }
  1392. }
  1393.  
  1394. // Puts the zoom level percentage in the zoom controls panel
  1395. function updatePercent(zoomLevel) {
  1396. let zoomPercent = Math.round(zoomLevel * 100);
  1397. let percentageSpan = document.getElementById("zoom-percent-c");
  1398. percentageSpan.innerHTML = zoomPercent + " %";
  1399. }
  1400.  
  1401. // Zooms in the page by the specified increment
  1402. function incrementPercent() {
  1403. chrome.tabs.getZoom(function (zoomLevel) {
  1404. let newZoomLevel = zoomLevel + ZOOM_INCREMENT_AMOUNT / 100;
  1405.  
  1406. // Max zoom that Vivaldi allows is 500 %
  1407. if (newZoomLevel <= 5) {
  1408. chrome.tabs.setZoom(newZoomLevel, updatePercent(newZoomLevel));
  1409. }
  1410. });
  1411. }
  1412.  
  1413. // Zooms out the page by the specified increment
  1414. function decrementPercent() {
  1415. chrome.tabs.getZoom(function (zoomLevel) {
  1416. let newZoomLevel = zoomLevel - ZOOM_INCREMENT_AMOUNT / 100;
  1417.  
  1418. // Min zoom that Vivaldi allows is 20 %
  1419. if (newZoomLevel >= 0.2) {
  1420. chrome.tabs.setZoom(newZoomLevel, updatePercent(newZoomLevel));
  1421. }
  1422. });
  1423. }
  1424.  
  1425. // Sets the zoom back to the default zoom level
  1426. // - in Vivaldi's settings you can set the default page zoom, this
  1427. // will follow that if RESET_ZOOM_LEVEL is set to "100"
  1428. function resetZoom() {
  1429. let zoomLevel = RESET_ZOOM_LEVEL / 100;
  1430. chrome.tabs.setZoom(zoomLevel, updatePercent(zoomLevel));
  1431. }
  1432.  
  1433. // For modes 1 and 2:
  1434. // Tracks if you are hovering over the zoom controls
  1435. function zoomPanelHoverTracker() {
  1436. let zoomPanel = document.getElementsByClassName("zoom-panel")[0];
  1437. let elementToTheLeft = zoomPanel.parentElement.parentElement.previousElementSibling;
  1438. let isHovered = false;
  1439. let intervalID = null;
  1440. let count = 0;
  1441.  
  1442. // selects which element must be hovered to trigger action
  1443. let hoverElement;
  1444. if (MODE === 2 && IS_AUTO_OPENED_ON_ADDRESSBAR) {
  1445. let addressBar = document.querySelector(".UrlBar-AddressField");
  1446. hoverElement = addressBar;
  1447. } else {
  1448. let zoomBtnAndPanel = document.getElementById("zoom-hover-target");
  1449. hoverElement = zoomBtnAndPanel;
  1450. }
  1451.  
  1452. // when the element is hovered, reset the interval counter and opens the controls if needed
  1453. hoverElement.onmouseover = function () {
  1454. count = 0;
  1455. isHovered = true;
  1456. if (MODE !== 1) {
  1457. openNav(zoomPanel, elementToTheLeft);
  1458. }
  1459. };
  1460.  
  1461. // when the element loses hover, closes the controls if enough time has passed
  1462. hoverElement.onmouseout = function () {
  1463. // removes any previous counters (needed for if hover is lost and regained multiple times)
  1464. if (intervalID) {
  1465. clearInterval(intervalID);
  1466. }
  1467. isHovered = false;
  1468. // start a counter to see how long it has been since the element was last hovered
  1469. intervalID = setInterval(function () {
  1470. // only increment the counter as long as hover isn't regained
  1471. if (isHovered === false) {
  1472. count++;
  1473. }
  1474. // once the correct amount of time has ellapsed, close the controls panel
  1475. if (count >= FADE_OUT_TIME * 2) {
  1476. closeNav(zoomPanel, elementToTheLeft);
  1477. clearInterval(intervalID);
  1478. }
  1479. }, 500);
  1480. };
  1481. }
  1482.  
  1483. // CHANGE: Added in Update #1
  1484. // updates zoom percentage on tab change
  1485. function tabChangeUpdateZoomWrapper() {
  1486. chrome.tabs.getZoom(function (zoomLevel) {
  1487. let zoomInfo = {
  1488. newZoomFactor: zoomLevel,
  1489. };
  1490. updateZoomIcon(zoomInfo);
  1491. });
  1492. }
  1493.  
  1494. // zoom change listener
  1495. chrome.tabs.onZoomChange.addListener(updateZoomIcon);
  1496.  
  1497. // CHANGE: Added in Update #1
  1498. // Listener for active tab change
  1499. chrome.tabs.onActivated.addListener(tabChangeUpdateZoomWrapper);
  1500.  
  1501. // CHANGE: Added in Update #4
  1502. // Initially load icon, stopped getting added on startup in 3.5
  1503. chrome.tabs.getZoom(function (zoomLevel) {
  1504. let zoomInfo = {
  1505. newZoomFactor: zoomLevel,
  1506. };
  1507. updateZoomIcon(zoomInfo);
  1508. });
  1509. }
  1510.  
  1511. // Loop waiting for the browser to load the UI
  1512. let intervalID = setInterval(function () {
  1513. const browser = document.getElementById("browser");
  1514. if (browser) {
  1515. clearInterval(intervalID);
  1516. zoomControl();
  1517. }
  1518. }, 300);
  1519. })();
  1520.  
  1521. /* ================================================================================================================================================================================================================================= */
  1522.  
  1523. /* Toggle tab button before AddressBar */
  1524. {
  1525. function toggleTabsOnClick() {
  1526. const WindowActions = vivaldi.jdhooks.require("WindowActions");
  1527. WindowActions.toggleTabs(window.vivaldiWindowId);
  1528. }
  1529.  
  1530. vivaldi.jdhooks.hookClass("toolbars_Toolbar", (origClass) => {
  1531. const React = vivaldi.jdhooks.require("React");
  1532. const SettingsPaths = vivaldi.jdhooks.require("_PrefKeys");
  1533. const ToolbarButton = vivaldi.jdhooks.require("toolbars_ToolbarButton");
  1534.  
  1535. return class extends origClass {
  1536. render() {
  1537. let ret = super.render();
  1538. if (this.props.name == SettingsPaths.kToolbarsNavigation) {
  1539. ret.props.children.push(
  1540. React.createElement(ToolbarButton, {
  1541. tooltip: "Toggle Tab Bar",
  1542. onClick: toggleTabsOnClick,
  1543. image: vivaldi.jdhooks.require("_svg_tabs_large"),
  1544. })
  1545. );
  1546. }
  1547. return ret;
  1548. }
  1549. };
  1550. });
  1551. }
  1552.  
  1553. /* ================================================================================================================================================================================================================================= */
  1554.  
  1555. /* Theme Import and Export */
  1556. // version 2021.9.0
  1557. // https://forum.vivaldi.net/topic/33154/import-and-export-themes
  1558. // Adds functionality to import, export, backup, sort and move themes to
  1559. // Vivaldi's settings page.
  1560.  
  1561. (function () {
  1562. function _checkImport() {
  1563. // written by tam710562
  1564. if (
  1565. typeof _test.colors !== "object" ||
  1566. typeof _test.colors.accentBg !== "string" ||
  1567. !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.accentBg) ||
  1568. typeof _test.colors.baseBg !== "string" ||
  1569. !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.baseBg) ||
  1570. typeof _test.colors.baseFg !== "string" ||
  1571. !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.baseFg) ||
  1572. typeof _test.colors.highlightBg !== "string" ||
  1573. !/^#(?:[0-9a-f]{3}){1,2}$/i.test(_test.colors.highlightBg) ||
  1574. typeof _test.name !== "string" ||
  1575. typeof _test.settings !== "object" ||
  1576. typeof _test.settings.accentFromPage !== "boolean" ||
  1577. typeof _test.settings.accentOnWindow !== "boolean" ||
  1578. (typeof _test.settings.borderRadius !== "number" &&
  1579. typeof _test.settings.borderRadius !== "string") ||
  1580. typeof _test.settings.tabsTransparent !== "boolean" ||
  1581. typeof _test.version !== "number"
  1582. ) {
  1583. return false;
  1584. } else {
  1585. return true;
  1586. }
  1587. }
  1588.  
  1589. function _message(pnt) {
  1590. clearTimeout(_timeout);
  1591. if (pnt === "export") {
  1592. _msg.innerText = "Theme code copied to clipboard.";
  1593. } else if (pnt === "backup") {
  1594. _msg.innerText = "Backup copied to clipboard.";
  1595. } else if (pnt === "import") {
  1596. _msg.innerText = "Theme imported.";
  1597. } else if (pnt === "restore") {
  1598. _msg.innerText = "Backup imported.";
  1599. } else if (pnt === "notice") {
  1600. _msg.innerText = "Nothing to import. Check console.log";
  1601. } else if (pnt === "sort") {
  1602. _msg.innerText = "User themes sorted alphabetically.";
  1603. } else {
  1604. _msg.innerText = "Theme code error.";
  1605. }
  1606. _timeout = setTimeout(function () {
  1607. _msg.innerText = "";
  1608. }, 5000);
  1609. }
  1610.  
  1611. function _importBackup() {
  1612. chrome.storage.local.get({ THEMES_USER: "" }, function (res) {
  1613. var userThemes = res.THEMES_USER;
  1614. console.log("Importing themes...");
  1615. for (i = 0; i < _set.length; i++) {
  1616. _test = _set[i];
  1617. var test = _checkImport;
  1618. if (test()) {
  1619. var compare = userThemes.findIndex((x) => x.name == _set[i].name);
  1620. if (compare === -1) {
  1621. var ok = true;
  1622. userThemes.push(_set[i]);
  1623. console.log(_set[i].name + " imported");
  1624. } else {
  1625. console.log(_set[i].name + " is a duplicate");
  1626. }
  1627. } else {
  1628. console.log(_set[i].name + " failed");
  1629. }
  1630. }
  1631. if (ok === true) {
  1632. chrome.storage.local.set({ THEMES_USER: userThemes }, function () {
  1633. _message("restore");
  1634. });
  1635. } else {
  1636. _message("notice");
  1637. }
  1638. });
  1639. }
  1640.  
  1641. function _importTheme() {
  1642. event.stopPropagation();
  1643. event.preventDefault();
  1644. if (_eventType === "paste") {
  1645. var clipboardData = event.clipboardData || window.clipboardData;
  1646. var themeCode = clipboardData.getData("text");
  1647. } else {
  1648. var themeCode = event.dataTransfer.getData("text");
  1649. }
  1650. try {
  1651. _set = JSON.parse(themeCode);
  1652. } catch (err) {
  1653. _message("error");
  1654. return;
  1655. }
  1656. if (Object.keys(_set)[0] === "colors") {
  1657. _test = _set;
  1658. var test = _checkImport;
  1659. if (test()) {
  1660. const nameField = document.querySelector(".theme-name");
  1661. nameField.select();
  1662. document.execCommand("insertText", false, _set.name);
  1663. chrome.storage.local.get({ THEMES_USER: "" }, function (imp) {
  1664. var userThemes = imp.THEMES_USER;
  1665. for (i = 0; i < userThemes.length; i++) {
  1666. if (userThemes[i].name === nameField.value) {
  1667. _set.name = nameField.value;
  1668. userThemes[i] = _set;
  1669. chrome.storage.local.set({
  1670. THEMES_USER: userThemes,
  1671. BROWSER_COLOR_ACCENT_BG: _set.colors.accentBg,
  1672. BROWSER_COLOR_BG: _set.colors.baseBg,
  1673. BROWSER_COLOR_FG: _set.colors.baseFg,
  1674. BROWSER_COLOR_HIGHLIGHT_BG: _set.colors.highlightBg,
  1675. TABCOLOR_BEHIND_TABS: _set.settings.accentOnWindow,
  1676. USE_TABCOLOR: _set.settings.accentFromPage,
  1677. BORDER_RADIUS: _set.settings.borderRadius,
  1678. USE_TAB_TRANSPARENT_TABS: _set.settings.tabsTransparent,
  1679. THEME_CURRENT: _set.name,
  1680. });
  1681. _message("import");
  1682. break;
  1683. }
  1684. }
  1685. });
  1686. } else {
  1687. _message("error");
  1688. }
  1689. } else if (Object.keys(_set)[0] === "0") {
  1690. _importBackup();
  1691. } else {
  1692. _message("error");
  1693. }
  1694. }
  1695.  
  1696. function _exportTheme(event) {
  1697. if (event.altKey) {
  1698. var backup = true;
  1699. }
  1700. if (event.shiftKey) {
  1701. var order = true;
  1702. }
  1703. chrome.storage.local.get(
  1704. {
  1705. THEME_CURRENT: "",
  1706. THEMES_USER: "",
  1707. },
  1708. function (exp) {
  1709. const themeName = exp.THEME_CURRENT;
  1710. const userThemes = exp.THEMES_USER;
  1711. if (backup === true) {
  1712. const themeCode = JSON.stringify(userThemes);
  1713. navigator.clipboard.writeText(themeCode);
  1714. _message("backup");
  1715. } else if (order === true) {
  1716. userThemes.sort(function (a, b) {
  1717. return a.name.localeCompare(b.name);
  1718. });
  1719. chrome.storage.local.set({ THEMES_USER: userThemes }, function () {
  1720. _message("sort");
  1721. });
  1722. } else {
  1723. for (i = 0; i < userThemes.length; i++) {
  1724. if (userThemes[i].name === themeName) {
  1725. const themeCode = JSON.stringify(userThemes[i]);
  1726. navigator.clipboard.writeText(themeCode);
  1727. _message("export");
  1728. break;
  1729. }
  1730. }
  1731. }
  1732. }
  1733. );
  1734. }
  1735.  
  1736. function _moveTheme() {
  1737. chrome.storage.local.get(
  1738. {
  1739. THEME_CURRENT: "",
  1740. THEMES_USER: "",
  1741. },
  1742. function (mv) {
  1743. const themeName = mv.THEME_CURRENT;
  1744. const userThemes = mv.THEMES_USER;
  1745. var index = userThemes.findIndex((x) => x.name == themeName);
  1746. if (index !== -1) {
  1747. if (_toMove === "left") {
  1748. if (index !== 0) {
  1749. var fromI = userThemes[index];
  1750. var toI = userThemes[index - 1];
  1751. userThemes[index - 1] = fromI;
  1752. userThemes[index] = toI;
  1753. } else {
  1754. return;
  1755. }
  1756. } else {
  1757. var last = userThemes.length - 1;
  1758. if (index < last) {
  1759. var fromI = userThemes[index];
  1760. var toI = userThemes[index + 1];
  1761. userThemes[index + 1] = fromI;
  1762. userThemes[index] = toI;
  1763. } else {
  1764. return;
  1765. }
  1766. }
  1767. chrome.storage.local.set({ THEMES_USER: userThemes });
  1768. }
  1769. }
  1770. );
  1771. }
  1772.  
  1773. function createPort() {
  1774. if (
  1775. document.querySelector(_themeBtn).classList.contains("button-pressed")
  1776. ) {
  1777. const cont = document.querySelector(".theme-metadata");
  1778. const importBtn = document.createElement("input");
  1779. importBtn.setAttribute("type", "text");
  1780. importBtn.setAttribute("placeholder", "Import");
  1781. importBtn.id = "importTheme";
  1782. cont.appendChild(importBtn);
  1783. const exportBtn = document.createElement("input");
  1784. exportBtn.setAttribute("type", "submit");
  1785. exportBtn.classList.add("primary");
  1786. exportBtn.setAttribute("value", "Export");
  1787. exportBtn.setAttribute(
  1788. "title",
  1789. "Click to export theme\nAlt-click to backup all themes\nShift-click to sort themes"
  1790. );
  1791. exportBtn.id = "exportTheme";
  1792. cont.appendChild(exportBtn);
  1793. _msg = document.createElement("span");
  1794. _msg.id = "modInfo";
  1795. cont.appendChild(_msg);
  1796. document
  1797. .getElementById("exportTheme")
  1798. .addEventListener("click", _exportTheme);
  1799. const importInput = document.getElementById("importTheme");
  1800. importInput.addEventListener("paste", function () {
  1801. _eventType = "paste";
  1802. _importTheme(event);
  1803. });
  1804. importInput.addEventListener("drop", function () {
  1805. _eventType = "drop";
  1806. _importTheme(event);
  1807. });
  1808. _timeout = {};
  1809. }
  1810. }
  1811.  
  1812. function portThemes() {
  1813. const styleCheck = document.getElementById("portThemes");
  1814. if (!styleCheck) {
  1815. const style = document.createElement("style");
  1816. style.type = "text/css";
  1817. style.id = "portThemes";
  1818. style.innerHTML =
  1819. ".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;}";
  1820. document.getElementsByTagName("head")[0].appendChild(style);
  1821. }
  1822. const modCheck = document.querySelector(".move-left");
  1823. if (!modCheck) {
  1824. const group = document.createElement("div");
  1825. group.classList.add("toolbar", "toolbar-group");
  1826. group.innerHTML =
  1827. '<div class="button-toolbar move-left"><button draggable="false" tabindex="auto" title="Move Theme Left" class=""><svg width="16" height="16" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1216 448v896q0 26-19 45t-45 19-45-19l-448-448q-19-19-19-45t19-45l448-448q19-19 45-19t45 19 19 45z"/></svg></button></div><hr><div class="button-toolbar move-right"><button draggable="false" tabindex="auto" title="Move Theme Right" class=""><svg width="16" height="16" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1152 896q0 26-19 45l-448 448q-19 19-45 19t-45-19-19-45v-896q0-26 19-45t45-19 45 19l448 448q19 19 19 45z"/></svg></button></div>';
  1828. document
  1829. .querySelector(_themeBtn)
  1830. .parentNode.parentNode.appendChild(group);
  1831. document
  1832. .querySelector(".move-left")
  1833. .addEventListener("click", function () {
  1834. _toMove = "left";
  1835. _moveTheme();
  1836. });
  1837. document
  1838. .querySelector(".move-right")
  1839. .addEventListener("click", function () {
  1840. _toMove = "right";
  1841. _moveTheme();
  1842. });
  1843. document.querySelector(_themeBtn).addEventListener("click", function () {
  1844. setTimeout(createPort, 50);
  1845. });
  1846. }
  1847. }
  1848.  
  1849. const settingsUrl =
  1850. "chrome-extension://mpognobbkildjkofajifpdfhcoklimli/components/settings/settings.html?path=";
  1851. const _themeBtn =
  1852. ".setting-group.unlimited > .toolbar.toolbar-default > .button-toolbar > button";
  1853. chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
  1854. if (changeInfo.url === `${settingsUrl}themes`) {
  1855. setTimeout(portThemes, 100);
  1856. }
  1857. });
  1858. })();
  1859.  
  1860. /* ================================================================================================================================================================================================================================= */
Advertisement
Add Comment
Please, Sign In to add comment