YouTube: Audio Only

No Video Streaming

目前为 2024-01-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube: Audio Only
  3. // @description No Video Streaming
  4. // @namespace UserScript
  5. // @version 0.4.1
  6. // @author CY Fung
  7. // @match https://www.youtube.com/*
  8. // @match https://www.youtube.com/embed/*
  9. // @match https://www.youtube-nocookie.com/embed/*
  10. // @match https://m.youtube.com/*
  11. // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
  12. // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/YouTube-Audio-Only.png
  13. // @grant GM_registerMenuCommand
  14. // @grant GM.setValue
  15. // @grant GM.getValue
  16. // @run-at document-start
  17. // @license MIT
  18. // @compatible chrome
  19. // @compatible firefox
  20. // @compatible opera
  21. // @compatible edge
  22. // @compatible safari
  23. // @allFrames true
  24. //
  25. // ==/UserScript==
  26.  
  27. (async function () {
  28. 'use strict';
  29.  
  30. let setTimeout_ = setTimeout;
  31.  
  32. /** @type {globalThis.PromiseConstructor} */
  33. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  34.  
  35. async function confirm(message) {
  36. // Create the HTML for the dialog
  37.  
  38. if (!document.body) return;
  39.  
  40. let dialog = document.getElementById('confirmDialog794');
  41. if (!dialog) {
  42.  
  43. const dialogHTML = `
  44. <div id="confirmDialog794" class="dialog-style" style="display: block;">
  45. <div class="confirm-box">
  46. <p>${message}</p>
  47. <div class="confirm-buttons">
  48. <button id="confirmBtn">Confirm</button>
  49. <button id="cancelBtn">Cancel</button>
  50. </div>
  51. </div>
  52. </div>
  53. `;
  54.  
  55. // Append the dialog to the document body
  56. document.body.insertAdjacentHTML('beforeend', dialogHTML);
  57. dialog = document.getElementById('confirmDialog794');
  58.  
  59. }
  60.  
  61. // Return a promise that resolves or rejects based on the user's choice
  62. return new Promise((resolve) => {
  63. document.getElementById('confirmBtn').onclick = () => {
  64. resolve(true);
  65. cleanup();
  66. };
  67.  
  68. document.getElementById('cancelBtn').onclick = () => {
  69. resolve(false);
  70. cleanup();
  71. };
  72.  
  73. function cleanup() {
  74. dialog && dialog.remove();
  75. dialog = null;
  76. }
  77. });
  78. }
  79.  
  80.  
  81.  
  82. if (location.pathname === '/live_chat' || location.pathname === 'live_chat_replay') return;
  83.  
  84.  
  85. const pageInjectionCode = function () {
  86.  
  87.  
  88. /** @type {globalThis.PromiseConstructor} */
  89. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  90.  
  91. const PromiseExternal = ((resolve_, reject_) => {
  92. const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
  93. return class PromiseExternal extends Promise {
  94. constructor(cb = h) {
  95. super(cb);
  96. if (cb === h) {
  97. /** @type {(value: any) => void} */
  98. this.resolve = resolve_;
  99. /** @type {(reason?: any) => void} */
  100. this.reject = reject_;
  101. }
  102. }
  103. };
  104. })();
  105.  
  106.  
  107.  
  108. const observablePromise = (proc, timeoutPromise) => {
  109. let promise = null;
  110. return {
  111. obtain() {
  112. if (!promise) {
  113. promise = new Promise(resolve => {
  114. let mo = null;
  115. const f = () => {
  116. let t = proc();
  117. if (t) {
  118. mo.disconnect();
  119. mo.takeRecords();
  120. mo = null;
  121. resolve(t);
  122. }
  123. }
  124. mo = new MutationObserver(f);
  125. mo.observe(document, { subtree: true, childList: true })
  126. f();
  127. timeoutPromise && timeoutPromise.then(() => {
  128. resolve(null)
  129. });
  130. });
  131. }
  132. return promise
  133. }
  134. }
  135. }
  136.  
  137.  
  138. let vcc = 0;
  139. let vdd = -1;
  140.  
  141. let u33 = null;
  142.  
  143. let cv = null;
  144. document.addEventListener('durationchange', (evt) => {
  145. const target = (evt || 0).target;
  146. if (!(target instanceof HTMLMediaElement)) return;
  147.  
  148. if (target.classList.contains('video-stream') && target.classList.contains('html5-main-video')) {
  149.  
  150. if (target.readyState === 1) {
  151.  
  152. vcc++;
  153.  
  154. }
  155. if (target.readyState === 1 && target.networkState === 2) {
  156. target.__spfgs__ = true;
  157. if (u33) {
  158. u33.resolve();
  159. u33 = null;
  160. }
  161. if (cv) {
  162. cv.resolve();
  163. cv = null;
  164. }
  165. } else {
  166. target.__spfgs__ = false;
  167.  
  168. }
  169.  
  170. }
  171. }, true);
  172.  
  173.  
  174.  
  175. // XMLHttpRequest.prototype.open299 = XMLHttpRequest.prototype.open;
  176. /*
  177.  
  178. XMLHttpRequest.prototype.open2 = function(method, url, ...args){
  179.  
  180. if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) {
  181. if (vcc !== vdd) {
  182. vdd = vcc;
  183. window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*");
  184. }
  185. }
  186.  
  187. return this.open299(method, url, ...args)
  188. }*/
  189.  
  190.  
  191.  
  192. // desktop only
  193. // document.addEventListener('yt-page-data-fetched', async (evt) => {
  194.  
  195. // const pageFetchedDataLocal = evt.detail;
  196. // let isLiveNow;
  197. // try {
  198. // isLiveNow = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.isLiveNow;
  199. // } catch (e) { }
  200. // window.postMessage({ ZECxh: isLiveNow === true }, "*");
  201.  
  202. // }, false);
  203.  
  204. // return;
  205.  
  206. // let clickLockFn = null;
  207.  
  208. let clickLockFn = null;
  209. let clickTarget = null;
  210. if (location.origin === 'https://m.youtube.com') {
  211.  
  212.  
  213.  
  214. EventTarget.prototype.addEventListener322 = EventTarget.prototype.addEventListener;
  215.  
  216. EventTarget.prototype.addEventListener = function (evt, fn, opts) {
  217.  
  218. if (evt === 'visibilitychange') {
  219. evt += 'y'
  220. }
  221. let hn = fn;
  222.  
  223. if (evt === 'click' && this.id === 'movie_player') {
  224.  
  225.  
  226. clickLockFn = fn; clickTarget = this;
  227. // hn = function (e) {
  228.  
  229. // // console.log(22 , e)
  230. // // console.log(433, e.type, e.detail, fn);
  231. // // window.em33 = true;
  232. // // if(e && e.type !=='updateui' && e.type!=='success' && e.type!==''){
  233. // // console.log(433, e.type, e.detail);
  234.  
  235. // // }
  236. // return fn.apply(this, arguments)
  237. // }
  238.  
  239. }
  240.  
  241. /*
  242.  
  243. if(evt ==='player-state-change' || evt == "player-autonav-pause" || evt === "video-data-change" || evt === "state-navigatestart"){
  244.  
  245. hn = function(){
  246.  
  247. let e = arguments[0];
  248. if(e){
  249. console.log(213, e.type, e.detail);
  250.  
  251. }
  252. return fn.apply(this, arguments)
  253. }
  254. }
  255. */
  256.  
  257. return this.addEventListener322(evt, hn, opts)
  258.  
  259. }
  260.  
  261. /*
  262. const XMLHttpRequest_ = XMLHttpRequest;
  263.  
  264. (() => {
  265. XMLHttpRequest = class XMLHttpRequest extends XMLHttpRequest_ {
  266. constructor(...args) {
  267. super(...args);
  268. }
  269. open(method, url, ...args) {
  270.  
  271. if (typeof url === 'string' && url.length > 24 && url.includes('/videoplayback?') && url.replace('?', '&').includes('&source=')) {
  272. if (vcc !== vdd) {
  273. vdd = vcc;
  274. window.postMessage({ ZECxh: url.includes('source=yt_live_broadcast') }, "*");
  275. }
  276. }
  277. return super.open(method, url, ...args)
  278. }
  279. }
  280. })();
  281. */
  282. }
  283.  
  284.  
  285. let setTimeout_ = setTimeout;
  286.  
  287.  
  288. if (location.origin === 'https://www.youtube.com') {
  289.  
  290.  
  291. document.addEventListener('yt-navigate-finish', async () => {
  292.  
  293. const fn = () => {
  294.  
  295. const elm = document.querySelector('ytd-player#ytd-player');
  296. if (!elm) return;
  297. const cnt = elm.polymerController || elm.inst || elm;
  298. if (!cnt) return;
  299.  
  300. if (!cnt.player_) return;
  301. if (!cnt.player_.playVideo) return;
  302.  
  303. return { elm, cnt };
  304. }
  305. let o = fn();
  306. if (!o) {
  307. o = await observablePromise(fn).obtain()
  308. }
  309. const { cnt, elm } = o;
  310. if (!cnt || !cnt.player_ || !cnt.player_.playVideo) return;
  311. if (cnt.player_.getPlayerState() === 3) {
  312. const audio = HTMLElement.prototype.querySelector.call(elm, '.video-stream.html5-main-video');
  313. if (audio.__spfgs__ !== true) { // undefined or false
  314. u33 = new PromiseExternal();
  315. await u33.then();
  316. }
  317.  
  318. if (cnt.player_.getPlayerState() !== 3 || !audio.isConnected) return;
  319. if (audio && audio.__spfgs__ === true) {
  320. await cnt.player_.cancelPlayback();
  321.  
  322. await new Promise(resolve => window.setTimeout(resolve, 1));
  323. await cnt.player_.playVideo();
  324.  
  325. }
  326. }
  327.  
  328. });
  329.  
  330. } else if (location.origin === 'https://m.youtube.com') {
  331.  
  332.  
  333. let qm = new PromiseExternal();
  334.  
  335.  
  336. // document.addEventListener('DOMContentLoaded', (evt) => {
  337.  
  338. // const mo = new MutationObserver((mutations)=>{
  339.  
  340. // console.log(5899, mutations)
  341.  
  342. // });
  343. // mo.observe(document, {subtree: true, childList: true})
  344.  
  345.  
  346. // })
  347.  
  348.  
  349.  
  350. // window.addEventListener('onReady', (evt) => {
  351.  
  352. // console.log(6811)
  353. // }, true);
  354.  
  355. // window.addEventListener('localmediachange', (evt) => {
  356.  
  357. // console.log(6812)
  358. // }, true);
  359.  
  360. // window.addEventListener('onVideoDataChange', (evt) => {
  361.  
  362. // console.log(6813)
  363. // }, true);
  364.  
  365. window.addEventListener('state-navigateend', async (evt) => {
  366.  
  367. // console.log(5910)
  368. if (clickLockFn && clickTarget) {
  369.  
  370.  
  371. let a = HTMLElement.prototype.querySelector.call(clickTarget, '.video-stream.html5-main-video');
  372.  
  373. if (a) {
  374.  
  375. if (a.muted === true && a.paused === true) clickLockFn.call(clickTarget, { type: 'click', target: clickTarget })
  376. // console.log(588, clickLockFn, a.muted)
  377.  
  378. if (a.__spfgs__ !== true && a.paused === true) {
  379. clickLockFn.call(clickTarget, { type: 'click', target: clickTarget })
  380. }
  381.  
  382.  
  383.  
  384. }
  385.  
  386. }
  387. qm.resolve();
  388.  
  389.  
  390. }, true);
  391.  
  392. // window.addEventListener('onStateChange', (evt) => {
  393.  
  394. // console.log(6815)
  395. // }, true);
  396.  
  397. let px = 0;
  398. let fa = 0;
  399. let ez = 0;
  400.  
  401. async function delayRun() {
  402.  
  403.  
  404.  
  405. await qm.then();
  406. if (ez) return;
  407. ez = 1;
  408.  
  409.  
  410. let qq = 0;
  411.  
  412. const { q, a } = await observablePromise(() => {
  413.  
  414. let q = document.querySelector('#movie_player');
  415. if (!q) return;
  416. let a = document.querySelector('.video-stream.html5-main-video');
  417. if (!a) return;
  418. return { q, a };
  419.  
  420. }).obtain();
  421.  
  422. if (a.muted) return;
  423.  
  424. if (a.muted === false && a.readyState === 0 && a.networkState === 2) {
  425.  
  426. } else {
  427. return;
  428. }
  429.  
  430. let cid = setInterval(() => {
  431. if (a.muted === false && a.readyState === 0 && a.networkState === 2) {
  432.  
  433. } else {
  434. clearInterval(cid);
  435. }
  436. if (a.paused !== true) return;
  437. clearInterval(cid);
  438.  
  439. if (qq) return;
  440. qq = 1;
  441.  
  442. if (document.querySelector('.player-controls-content')) return;
  443.  
  444. if (fa !== 1) return;
  445.  
  446. if (a.paused === true && a.muted === false && a.readyState === 0 && a.networkState === 2) document.querySelector('#movie_player').click();
  447. // console.log(a.paused)
  448. // console.log(7710)
  449.  
  450.  
  451.  
  452. }, 10)
  453.  
  454.  
  455.  
  456. }
  457.  
  458. document.addEventListener('durationchange', (evt) => {
  459.  
  460. // console.log(5911)
  461.  
  462.  
  463. if (evt.target.readyState !== 1) {
  464. fa = 1;
  465. if (px) clearTimeout(px);
  466. px = setTimeout_(delayRun, 100);
  467. } else {
  468. fa = 2;
  469. }
  470. // console.log(123123, evt.target, evt.target.duration)
  471.  
  472.  
  473. }, true)
  474.  
  475.  
  476.  
  477. }
  478.  
  479.  
  480.  
  481. let prepared = false;
  482. function prepare() {
  483. if (prepared) return;
  484. prepared = true;
  485.  
  486. if (typeof _yt_player !== 'undefined' && _yt_player && typeof _yt_player === 'object') {
  487.  
  488. for (const [k, v] of Object.entries(_yt_player)) {
  489.  
  490. if (typeof v === 'function' && typeof v.prototype.clone === 'function'
  491. && typeof v.prototype.get === 'function' && typeof v.prototype.set === 'function'
  492.  
  493. && typeof v.prototype.isEmpty === 'undefined' && typeof v.prototype.forEach === 'undefined'
  494. && typeof v.prototype.clear === 'undefined'
  495.  
  496. ) {
  497.  
  498. key = k;
  499.  
  500. }
  501.  
  502. }
  503.  
  504. }
  505.  
  506. if (key) {
  507.  
  508. const ClassX = _yt_player[key];
  509. _yt_player[key] = class extends ClassX {
  510. constructor(...args) {
  511.  
  512. if (typeof args[0] === 'string' && args[0].startsWith('http://')) args[0] = '';
  513. super(...args);
  514.  
  515. }
  516. }
  517. _yt_player[key].luX1Y = 1;
  518. }
  519.  
  520. }
  521. let s3 = Symbol();
  522. Object.defineProperty(Object.prototype, 'deviceIsAudioOnly', {
  523. get() {
  524. return this[s3];
  525. },
  526. set(nv) {
  527. if ('ATTRIBUTE_NODE' in this) {
  528.  
  529. } else {
  530. if (typeof nv === 'boolean') this[s3] = true;
  531. else this[s3] = undefined;
  532. prepare();
  533. }
  534. return true;
  535. },
  536. enumerable: false,
  537. configurable: true
  538. });
  539.  
  540.  
  541. let s1 = Symbol();
  542. let s2 = Symbol();
  543. Object.defineProperty(Object.prototype, 'defraggedFromSubfragments', {
  544. get() {
  545. return undefined;
  546. },
  547. set(nv) {
  548. return true;
  549. },
  550. enumerable: false,
  551. configurable: true
  552. });
  553.  
  554. Object.defineProperty(Object.prototype, 'hasSubfragmentedFmp4', {
  555. get() {
  556. return this[s1];
  557. },
  558. set(nv) {
  559. if (typeof nv === 'boolean') this[s1] = false;
  560. else this[s1] = undefined;
  561. return true;
  562. },
  563. enumerable: false,
  564. configurable: true
  565. });
  566.  
  567. Object.defineProperty(Object.prototype, 'hasSubfragmentedWebm', {
  568. get() {
  569. return this[s2];
  570. },
  571. set(nv) {
  572. if (typeof nv === 'boolean') this[s2] = false;
  573. else this[s2] = undefined;
  574. return true;
  575. },
  576. enumerable: false,
  577. configurable: true
  578. });
  579.  
  580.  
  581. const supportedFormatsConfig = () => {
  582.  
  583. function typeTest(type) {
  584. if (typeof type === 'string' && type.startsWith('video/')) {
  585. return false;
  586. }
  587. }
  588.  
  589. // return a custom MIME type checker that can defer to the original function
  590. function makeModifiedTypeChecker(origChecker) {
  591. // Check if a video type is allowed
  592. return function (type) {
  593. let res = undefined;
  594. if (type === undefined) res = false;
  595. else {
  596. res = typeTest.call(this, type);
  597. }
  598. if (res === undefined) res = origChecker.apply(this, arguments);
  599. return res;
  600. };
  601. }
  602.  
  603. // Override video element canPlayType() function
  604. const proto = (HTMLVideoElement || 0).prototype;
  605. if (proto && typeof proto.canPlayType == 'function') {
  606. proto.canPlayType = makeModifiedTypeChecker(proto.canPlayType);
  607. }
  608.  
  609. // Override media source extension isTypeSupported() function
  610. const mse = window.MediaSource;
  611. // Check for MSE support before use
  612. if (mse && typeof mse.isTypeSupported == 'function') {
  613. mse.isTypeSupported = makeModifiedTypeChecker(mse.isTypeSupported);
  614. }
  615.  
  616. };
  617.  
  618. supportedFormatsConfig();
  619. }
  620.  
  621. const isEnable = (typeof GM !== 'undefined' && typeof GM.getValue === 'function') ? (await GM.getValue("isEnable_aWsjF", true)) : null;
  622. if (typeof isEnable !== 'boolean') throw new DOMException("Please Update your browser", "NotSupportedError");
  623. if (isEnable) {
  624. const element = document.createElement('button');
  625. element.setAttribute('onclick', `(${pageInjectionCode})()`);
  626. element.click();
  627. }
  628.  
  629. GM_registerMenuCommand(`Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`, async function () {
  630. await GM.setValue("isEnable_aWsjF", !isEnable);
  631. location.reload();
  632. });
  633.  
  634. let messageCount = 0;
  635. let busy = false;
  636. window.addEventListener('message', (evt) => {
  637.  
  638. const v = ((evt || 0).data || 0).ZECxh;
  639. if (typeof v === 'boolean') {
  640. if (messageCount > 1e9) messageCount = 9;
  641. const t = ++messageCount;
  642. if (v && isEnable) {
  643. requestAnimationFrame(async () => {
  644. if (t !== messageCount) return;
  645. if (busy) return;
  646. busy = true;
  647. if (await confirm("Livestream is detected. Press OK to disable YouTube Audio Mode.")) {
  648. await GM.setValue("isEnable_aWsjF", !isEnable);
  649. location.reload();
  650. }
  651. busy = false;
  652. });
  653. }
  654. }
  655.  
  656. });
  657.  
  658.  
  659. const pLoad = new Promise(resolve => {
  660. if (document.readyState !== 'loading') {
  661. resolve();
  662. } else {
  663. window.addEventListener("DOMContentLoaded", resolve, false);
  664. }
  665. });
  666.  
  667.  
  668. function contextmenuInfoItemAppearedFn(target) {
  669.  
  670. const btn = target.closest('.ytp-menuitem[role="menuitem"]');
  671. if (!btn) return;
  672. if (btn.parentNode.querySelector('.ytp-menuitem[role="menuitem"].audio-only-toggle-btn')) return;
  673. document.documentElement.classList.add('with-audio-only-toggle-btn');
  674. const newBtn = btn.cloneNode(true)
  675. newBtn.querySelector('.ytp-menuitem-label').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`;
  676. newBtn.classList.add('audio-only-toggle-btn');
  677. btn.parentNode.insertBefore(newBtn, btn.nextSibling);
  678. newBtn.addEventListener('click', async () => {
  679. await GM.setValue("isEnable_aWsjF", !isEnable);
  680. location.reload();
  681. });
  682. }
  683.  
  684.  
  685. function mobileMenuItemAppearedFn(target) {
  686.  
  687. const btn = target.closest('ytm-menu-item');
  688. if (!btn) return;
  689. if (btn.parentNode.querySelector('ytm-menu-item.audio-only-toggle-btn')) return;
  690. document.documentElement.classList.add('with-audio-only-toggle-btn');
  691. const newBtn = btn.cloneNode(true);
  692. newBtn.querySelector('.menu-item-button').textContent = `Turn ${isEnable ? 'OFF' : 'ON'} YouTube Audio Mode`;
  693. newBtn.classList.add('audio-only-toggle-btn');
  694. btn.parentNode.insertBefore(newBtn, btn.nextSibling);
  695. newBtn.addEventListener('click', async () => {
  696. await GM.setValue("isEnable_aWsjF", !isEnable);
  697. location.reload();
  698. });
  699. }
  700.  
  701.  
  702.  
  703.  
  704. pLoad.then(() => {
  705.  
  706. document.addEventListener('animationstart', (evt) => {
  707. const animationName = evt.animationName;
  708. if (!animationName) return;
  709.  
  710. if (animationName === 'contextmenuInfoItemAppeared') contextmenuInfoItemAppearedFn(evt.target);
  711. if (animationName === 'mobileMenuItemAppeared') mobileMenuItemAppearedFn(evt.target);
  712.  
  713. }, true);
  714.  
  715.  
  716. const style = document.createElement('style');
  717. style.textContent = `
  718. @keyframes mobileMenuItemAppeared {
  719. 0% {
  720. background-position-x: 3px;
  721. }
  722. 100% {
  723. background-position-x: 4px;
  724. }
  725. }
  726. ytm-select.player-speed-settings ~ ytm-menu-item:last-of-type {
  727. animation: mobileMenuItemAppeared 1ms linear 0s 1 normal forwards;
  728. }
  729. @keyframes contextmenuInfoItemAppeared {
  730. 0% {
  731. background-position-x: 3px;
  732. }
  733. 100% {
  734. background-position-x: 4px;
  735. }
  736. }
  737. .ytp-contextmenu .ytp-menuitem[role="menuitem"] path[d^="M22 34h4V22h-4v12zm2-30C12.95"]{
  738. animation: contextmenuInfoItemAppeared 1ms linear 0s 1 normal forwards;
  739. }
  740. .with-audio-only-toggle-btn .ytp-contextmenu, .ytp-panel-menu, .ytp-panel {
  741. height: 40vh !important;
  742. }
  743. #confirmDialog794 {
  744. z-index:999999 !important;
  745. display: none;
  746. /* Hidden by default */
  747. position: fixed;
  748. /* Stay in place */
  749. z-index: 1;
  750. /* Sit on top */
  751. left: 0;
  752. top: 0;
  753. width: 100%;
  754. /* Full width */
  755. height: 100%;
  756. /* Full height */
  757. overflow: auto;
  758. /* Enable scroll if needed */
  759. background-color: rgba(0,0,0,0.4);
  760. /* Black w/ opacity */
  761. }
  762. #confirmDialog794 .confirm-box {
  763. position:relative;
  764. color: black;
  765.  
  766. z-index:999999 !important;
  767. background-color: #fefefe;
  768. margin: 15% auto;
  769. /* 15% from the top and centered */
  770. padding: 20px;
  771. border: 1px solid #888;
  772. width: 30%;
  773. /* Could be more or less, depending on screen size */
  774. box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  775. }
  776. #confirmDialog794 .confirm-buttons {
  777. text-align: right;
  778. }
  779. #confirmDialog794 button {
  780. margin-left: 10px;
  781. }
  782.  
  783.  
  784.  
  785. `
  786. document.head.appendChild(style);
  787. })
  788.  
  789.  
  790. })();
  791.