TweetDeck Custom Background Plus

Customize background script for Tweetdeck.

  1. // ==UserScript==
  2. // @name TweetDeck Custom Background Plus
  3. // @name:ja TweetDeck 背景透過+
  4. // @description Customize background script for Tweetdeck.
  5. // @description:ja Tweetdeckに背景を付けるスクリプトです。
  6. // @match *://tweetdeck.twitter.com/*
  7. // @match *://twitter.com/i/cards/*
  8. // @match *://x.com/i/tweetdeck
  9. // @version 1.01
  10. // @author ziopuzzle
  11. // @namespace https://twitter.com/puzzle_koa/
  12. // @grant GM_addStyle
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_deleteValue
  16. // @run-at document-start
  17. // ==/UserScript==
  18.  
  19. // matchに「*://twitter.com/i/cards/*」が存在しますが、Twitterのカード機能がiframeで読み込まれる為、スタイルを適用するために必要になります。
  20.  
  21. // htmlタグに #tdbgRoot をIDに持つdivタグを作成し、
  22. // 子要素として #tdbg-variable, #tdbg-bg, #tdbg-style をそれぞれIDとするstyleタグを追加します。
  23. // #tdbg-variable : 色情報やその他細かなオプションなどの値をCSSで変数として定義するstyleタグです。
  24. // #tdbg-bg : 背景画像の情報を変数として定義するstyleタグです。
  25. // #tdbg-style : 背景画像を表示させる為の透過や変数を実際に適用するstyleタグです。
  26.  
  27. (()=>{
  28. 'use strict';
  29.  
  30. const flagSendLog = true;
  31. const flagUIAnimation = true;
  32.  
  33. // iframe等で読み込まれた場合はスクリプトを無効化
  34. // Disable scripts when loaded in an iframe.
  35. if (window.top !== window.self && new URL(document.referrer).hostname !== 'tweetdeck.twitter.com') {
  36. return;
  37. }
  38. // Twitterのカード機能はiframeで読み込まれる為、例外処理でスタイル適用
  39. // for Twitter Cards
  40. if (location.hostname === 'twitter.com') {
  41. const tag = document.createElement('style');
  42. tag.id = 'tdbg-card';
  43. tag.type = 'text/css';
  44. tag.innerText = '.TwitterCard-container { background-color: ' + GM_getValue('tdbg-color-card', 'rgba(0, 0, 0, 0.5)') + ' !important; }';
  45. document.getElementsByTagName('body').item(0).appendChild(tag);
  46. return;
  47. }
  48.  
  49. let tag, tagBG, tagVariable, navIconSpace;
  50. let bgMain, bgDrawer;
  51. let colPanel, colColumnHeader, colColumn, colTextBase, colTextName, colTextId, colTextTweet, colTextHashtag, colTextLink;
  52. // deleteData();
  53. initializeScript();
  54.  
  55. function initializeScript() {
  56. // cssを動的に変更するために用いるstyleタグの作成
  57. makeTag();
  58. //ローディングアイコン書き換え
  59. var svgLoadingSVG = '<?xml version="1.0" encoding="UTF-8"?><svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><defs><filter id="outline"><feDropShadow dx="-2" dy="-2" stdDeviation="0" flood-color="#000"></feDropShadow><feDropShadow dx="2" dy="-2" stdDeviation="0" flood-color="#000"></feDropShadow><feDropShadow dx="-2" dy="2" stdDeviation="0" flood-color="#000"></feDropShadow><feDropShadow dx="2" dy="2" stdDeviation="0" flood-color="#000"></feDropShadow></filter></defs><g filter="url(#outline)"><circle class="loader" cx="100" cy="100" r="85" stroke= "#ffffff" stroke-dasharray="533.8" stroke-width="13" fill="none"><animate attributeName="stroke-dashoffset" values="533.8;661.912;533.8" keyTimes="0.0;0.4;1.0" calcMode="spline" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" dur="1.2s" repeatCount="indefinite" /><animateTransform attributeName="transform" attributeType="XML" type="rotate" values="270 100 100;630 100 100;630 100 100" keyTimes="0.0;0.8;1.0" calcMode="spline" keySplines="0.42 0 0.58 1;0.42 0 0.58 1" dur="1.2s" repeatCount="indefinite"/></circle></g></svg>';
  60. var base64LoadingSVG = 'data:image/svg+xml,' + encodeURIComponent(svgLoadingSVG);
  61. const elemAppLoadingImg = document.querySelector('.login-container .block .block img');
  62. elemAppLoadingImg.setAttribute('width', '74px');
  63. elemAppLoadingImg.setAttribute('height', '74px');
  64. elemAppLoadingImg.src = base64LoadingSVG;
  65. //
  66. navIconSpace = GM_getValue('tdbg-navicon-space', 'smallest');
  67. loadData(0x01); //背景読み込み
  68. styleBGUpdate(); // 背景適用
  69. styleUpdate(); // 透過処理とかもあるので1回実行(色はundefinedなのでここでは適用されない)
  70. // Tweetdeckのデフォルトテーマが読み込まれるまで監視
  71. if (flagSendLog) { console.log('[TDBG] Tweetdeck initialize waiting...'); }
  72. const checkElement = document.querySelector('div.js-app');
  73. (new MutationObserver((records, ob) => {
  74. records.some(record => {
  75. if ('class' == record.attributeName) {
  76. const toClass = record.target.getAttribute(record.attributeName);
  77. // div.js-appを監視して、読み込みが終わって.is-hiddenクラスが無くなったタイミングで初期化
  78. if (toClass.match(/(?:(?<=\s)|^)js-app(\s|$)/) && !toClass.match(/(?:(?<=\s)|^)is-hidden(\s|$)/)) {
  79. // 監視終了
  80. ob.disconnect();
  81. // 変数の初期化
  82. if (flagSendLog) { console.log('[TDBG] Initialize...'); }
  83. initializeColor();
  84. loadData(0x02); // 保存されているデータの読み込み
  85. makeObserver(); // SettingsパネルにBackgroundメニューを追加するオブジェクトの作成
  86. styleVariableUpdate(); // 変数の適用
  87. styleUpdate(); // cssを適用
  88. if (flagSendLog) { console.log('[TDBG] Initialize complete!'); }
  89. return true;
  90. }
  91. }
  92. });
  93. })).observe(document.body, { attributes: true, subtree: true });
  94. }
  95. function initializeColor() {
  96. // デフォルトのTweetdeckで使われる色を用いて、色を管理する変数を初期化
  97. const theme = (()=>{ if (!document.querySelector('html.dark')) { return 'light'; } else { return 'dark'; } })();
  98. switch (theme) {
  99. case 'light':
  100. colPanel = '#FFFFFFFF'; colColumnHeader = '#FFFFFFFF'; colColumn = '#FFFFFFFF'; colTextBase = '#FFFFFFFF'; colTextName = '#38444D'; colTextId = '#8899A6'; colTextTweet = '#FFFFFF'; colTextHashtag = '#1DA1F2'; colTextLink = '#1DA1F2';
  101. break;
  102. case 'dark': default:
  103. colPanel = '#15202BFF'; colColumnHeader = '#15202BFF'; colColumn = '#15202BFF'; colTextBase = '#8899A6'; colTextName = '#FFFFFF'; colTextId = '#8899A6'; colTextTweet = '#FFFFFF'; colTextHashtag = '#1DA1F2'; colTextLink = '#1DA1F2';
  104. break;
  105. /*
  106. case 'blue':
  107. colPanel = '#FFFFFFFF'; colColumnHeader = '#000000FF'; colColumn = '#000000FF'; colTextBase = '#FFFFFF'; colTextName = '#FFFFFF'; colTextId = '#FFFFFF'; colTextTweet = '#FFFFFF'; colTextHashtag = '#FFFFFF'; colTextLink = '#FFFFFF';
  108. break;
  109. case 'green':
  110. colPanel = '#FFFFFFFF'; colColumnHeader = '#000000FF'; colColumn = '#000000FF'; colTextBase = '#FFFFFF'; colTextName = '#FFFFFF'; colTextId = '#FFFFFF'; colTextTweet = '#FFFFFF'; colTextHashtag = '#FFFFFF'; colTextLink = '#FFFFFF';
  111. break;
  112. case 'purple':
  113. colPanel = '#FFFFFFFF'; colColumnHeader = '#000000FF'; colColumn = '#000000FF'; colTextBase = '#FFFFFF'; colTextName = '#FFFFFF'; colTextId = '#FFFFFF'; colTextTweet = '#FFFFFF'; colTextHashtag = '#FFFFFF'; colTextLink = '#FFFFFF';
  114. break;
  115. */
  116. }
  117. }
  118. function deleteData() {
  119. GM_deleteValue('tdbg-bg-main');
  120. GM_deleteValue('tdbg-bg-drawer');
  121. GM_deleteValue('tdbg-navicon-space');
  122. GM_deleteValue('tdbg-color-panel');
  123. GM_deleteValue('tdbg-color-column-header');
  124. GM_deleteValue('tdbg-color-column');
  125. GM_deleteValue('tdbg-color-base');
  126. GM_deleteValue('tdbg-color-name');
  127. GM_deleteValue('tdbg-color-id');
  128. GM_deleteValue('tdbg-color-tweet');
  129. GM_deleteValue('tdbg-color-hashtag');
  130. GM_deleteValue('tdbg-color-link');
  131. }
  132. function saveData(f) {
  133. // bitフラグで種類を指定可能
  134. // 000X : 背景
  135. // 00X0 : 色データ
  136. if (f & 0x01) {
  137. GM_setValue('tdbg-bg-main', bgMain);
  138. GM_setValue('tdbg-bg-drawer', bgDrawer);
  139. }
  140. if (f & 0x02) {
  141. GM_setValue('tdbg-navicon-space', navIconSpace);
  142. GM_setValue('tdbg-color-panel', colPanel);
  143. GM_setValue('tdbg-color-column-header', colColumnHeader);
  144. GM_setValue('tdbg-color-column', colColumn);
  145. GM_setValue('tdbg-color-base', colTextBase);
  146. GM_setValue('tdbg-color-name', colTextName);
  147. GM_setValue('tdbg-color-id', colTextId);
  148. GM_setValue('tdbg-color-tweet', colTextTweet);
  149. GM_setValue('tdbg-color-hashtag', colTextHashtag);
  150. GM_setValue('tdbg-color-link', colTextLink);
  151. }
  152. }
  153. function loadData(f) {
  154. // 保存されているデータの読み込み
  155. // 引数はビットで読み込むデータの指定
  156. // データタイプはsaveDate()を参照
  157. if (f & 0x01) {
  158. bgMain = GM_getValue('tdbg-bg-main', null);
  159. bgDrawer = GM_getValue('tdbg-bg-drawer', null);
  160. }
  161. if (f & 0x02) {
  162. navIconSpace = GM_getValue('tdbg-navicon-space', 'smallest');
  163. colPanel = GM_getValue('tdbg-color-panel', colPanel);
  164. colColumnHeader = GM_getValue('tdbg-color-column-header', colColumnHeader);
  165. colColumn = GM_getValue('tdbg-color-column', colColumn);
  166. colTextBase = GM_getValue('tdbg-color-base', colTextBase);
  167. colTextName = GM_getValue('tdbg-color-name', colTextName);
  168. colTextId = GM_getValue('tdbg-color-id', colTextId);
  169. colTextTweet = GM_getValue('tdbg-color-tweet', colTextTweet);
  170. colTextHashtag = GM_getValue('tdbg-color-hashtag', colTextHashtag);
  171. colTextLink = GM_getValue('tdbg-color-link', colTextLink);
  172. }
  173. }
  174. function makeObserver() {
  175. // Settingsパネルの開閉を監視し、Backgroundsメニューの追加を行うオブジェクトの作成
  176. (new MutationObserver((records, ob) => {
  177. const nav = document.querySelector(".settings-modal");
  178. if (nav) {
  179. ob.disconnect();
  180. (new MutationObserver(records => {
  181. records.forEach(record => {
  182. record.addedNodes.forEach(node => {
  183. const menu = node.querySelector("ul.js-setting-list");
  184. if (menu) appendMenu(menu);
  185. });
  186. });
  187. })).observe(nav, { childList: true });
  188. }
  189. })).observe(document.body, { childList: true, subtree: true });
  190. }
  191.  
  192. // 変更時適用にしてしまうとカラーピッカーで色をスライドするだけで重くなってしまうため、一定時間そのままなら適用。
  193. let colorChanegeWait = null;
  194. function funcInputColorWait(e) {
  195. if (colorChanegeWait) clearTimeout(colorChanegeWait);
  196. colorChanegeWait = setTimeout(funcInputColor, 100, e);
  197. }
  198. function funcInputColor(e) {
  199. colorChanegeWait = null;
  200. if (null == document.getElementById('tdbg-colorpicker-panel')) return true;
  201. if (null == document.getElementById('tdbg-colorpicker-column-header')) return true;
  202. if (null == document.getElementById('tdbg-colorpicker-column')) return true;
  203. colPanel = document.getElementById('tdbg-colorpicker-panel').value + parseInt(document.getElementById('tdbg-colorslider-panel').value).toString(16).padStart(2,'0');
  204. colColumnHeader = document.getElementById('tdbg-colorpicker-column-header').value + parseInt(document.getElementById('tdbg-colorslider-column-header').value).toString(16).padStart(2,'0');
  205. colColumn = document.getElementById('tdbg-colorpicker-column').value + parseInt(document.getElementById('tdbg-colorslider-column').value).toString(16).padStart(2,'0');
  206. colTextBase = document.getElementById('tdbg-colorpicker-base').value;
  207. colTextTweet = document.getElementById('tdbg-colorpicker-tweet').value;
  208. colTextHashtag = document.getElementById('tdbg-colorpicker-hashtag').value;
  209. colTextLink = document.getElementById('tdbg-colorpicker-link').value;
  210. // const selectColorPreset = document.querySelector('#tdbg-select-colorpreset');
  211. // selectColorPreset.value = 'custom';
  212. styleVariableUpdate();
  213. saveData(0x02);
  214. }
  215.  
  216. function funcInputPicture(e) {
  217. const elmID = e.target.id;
  218. const preview = document.getElementById(elmID + '-preview');
  219. const url = document.getElementById(elmID + '-url');
  220. const file = document.getElementById(elmID).files[0];
  221. const reader = new FileReader();
  222. reader.addEventListener('load', function () { // reader.result is base64
  223. //preview.src = dataURItoObjectURL(reader.result);
  224. preview.src = reader.result;
  225. url.value = '[fileinput]';
  226. if (elmID == 'tdbg-main-input') bgMain = reader.result;
  227. if (elmID == 'tdbg-drawer-input') bgDrawer = reader.result;
  228. styleBGUpdate();
  229. saveData(0x01);
  230. }, false);
  231. if (file) {
  232. reader.readAsDataURL(file);
  233. }
  234. // styleBGUpdate();
  235. }
  236. function funcRadioIconSpace(e) {
  237. navIconSpace = e.target.value;
  238. styleVariableUpdate();
  239. saveData(0x02);
  240. }
  241.  
  242. // 設定パネルを開いた際にこのユーザースクリプト用の設定ができる項目を"Backgrounds"として追加
  243. function appendMenu(menu) {
  244. const list = document.querySelectorAll("ul.js-setting-list li:not(.tdbg-setting)");
  245. list.forEach(v => {v.addEventListener("click", event => {document.querySelector(".tdbg-setting").classList.remove("selected");});});
  246. const a = document.createElement("a");
  247. a.href = "#";
  248. a.className = "list-link";
  249. a.dataset.action = "background";
  250. a.innerHTML = "<strong>Backgrounds</strong>";
  251. a.addEventListener("click", event => openSettings());
  252. const li = document.createElement("li");
  253. li.className = "tdbg-setting";
  254. li.appendChild(a);
  255. menu.appendChild(li);
  256. }
  257. // 設定パネルの中の"Backgrounds"セクションをクリックした際にメニューを表示
  258. function openSettings() {
  259. document.querySelector("ul.js-setting-list li.selected:not(.td-userscript-background-setting)").classList.remove("selected");
  260. const menu = document.querySelector(".tdbg-setting:not(.selected)");
  261. // セレクタで発見できなかったら関数を抜ける
  262. if (menu == undefined || menu == null) { return true; }
  263. menu.classList.add("selected");
  264. const form = document.querySelector("#global-settings");
  265. const nonebg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAADklEQVR4nGNgGAWDEwAAAZoAAR2CVqgAAAAASUVORK5CYII=';
  266. form.innerHTML = `
  267. <fieldset id="tdbg-settings">
  268. <legend>Backgrounds Settings</legend>
  269. <!--
  270. <div>
  271. <i class="icon icon-small color-twitter-blue js-toggle-switch is-actionable align-top icon-toggle-off" id="tdbg-hide-modalshadow"></i>
  272. <span class="margin-l--4">Hide shadow(Temporary)</span>
  273. </div>
  274. <div class="divider-bar"></div>
  275. -->
  276. <div class="control-group">
  277. <label class="control-label">Navigate Icon space</label>
  278. <div class="tdbg-radio-group item-3 controls">
  279. <label>
  280. <input type="radio" name="navIconSpace" value="default">
  281. &nbsp;Default
  282. </label>
  283. <label>
  284. <input type="radio" name="navIconSpace" value="small">
  285. &nbsp;Small
  286. </label>
  287. <label>
  288. <input type="radio" name="navIconSpace" value="smallest">
  289. &nbsp;Smallest
  290. </label>
  291. </div>
  292. </div>
  293. <div class="divider-bar"></div>
  294. <!--
  295. <div class="tdbg-controls">
  296. <select id="tdbg-theme-select" class="tdbg-select-container" name="ThemeSelect"></select>
  297. <div class="tdbg-theme-new">
  298. <button>New</button>
  299. </div>
  300. <div class="tdbg-theme-delete">
  301. <button>Delete</button>
  302. </div>
  303. </div>
  304. -->
  305. <div>
  306. <div class="tdbg-controls">
  307. <div>
  308. <div class="tdbg-input-container">
  309. <div class="tdbg-control-group">
  310. <span>Main Background</span>
  311. <div>
  312. <label>
  313. <div>&#x1F4C4;</div>
  314. <input id="tdbg-main-input" type="file" accept="image/*">
  315. </label>
  316. </div>
  317. <div>
  318. <input id="tdbg-main-input-url" type="text" name="inputBGMainImageorURL" placeholder="https://" pattern="^((https://.*)|(\[fileinput\]))$" title="URLはhttpsから始まる必要があります">
  319. </div>
  320. </div>
  321. <div class="tdbg-controls">
  322. <img id="tdbg-main-input-preview" class="tdbg-input-preview" src="' + nonebg + '" width="150" height="100" alt="Image preview...">
  323. </div>
  324. </div>
  325. </div>
  326. <div>
  327. <div class="tdbg-input-container">
  328. <div class="tdbg-control-group">
  329. <span>Drawer Background</span>
  330. <div>
  331. <label>
  332. <div>&#x1F4C4;</div>
  333. <input id="tdbg-drawer-input" type="file" accept="image/*">
  334. </label>
  335. </div>
  336. <div>
  337. <input id="tdbg-drawer-input-url" type="text" name="inputBGDrawerImageorURL" placeholder="https://" pattern="^((https://.*)|(\[fileinput\]))$" title="URLはhttpsから始まる必要があります">
  338. </div>
  339. </div>
  340. <div class="tdbg-controls">
  341. <img id="tdbg-drawer-input-preview" class="tdbg-input-preview" src="' + nonebg + '" width="150" height="100" alt="Image preview...">
  342. </div>
  343. </div>
  344. </div>
  345. </div>
  346. <div style="clear:both;">
  347. <!--
  348. <div>Color Preset</div>
  349. <select id="tdbg-select-colorpreset" class="tdbg-select-container" name="color preset">
  350. <option value="custom">Custom</option>
  351. <option value="light">Light(Default theme)</option>
  352. <option value="dark">Dark(Default Dark theme)</option>
  353. <option value="blue">Blue</option>
  354. <option value="green">Green</option>
  355. <option value="purple">Purple</option>
  356. </select>
  357. -->
  358. <div id="tdbg-setting-color">
  359. <div class="tdbg-flex tdbg-flex-row">
  360. <div class="tdbg-flex tdbg-flex-column tdbg-flex-space-around">
  361. <label class="tdbg-colorpicker"><span>Panel</span><input id="tdbg-colorpicker-panel" type="color"></label>
  362. <label class="tdbg-colorpicker"><span>ColumnHeader</span><input id="tdbg-colorpicker-column-header" type="color"></label>
  363. <label class="tdbg-colorpicker"><span>Column</span><input id="tdbg-colorpicker-column" type="color"></label>
  364. </div>
  365. <div class="tdbg-flex tdbg-flex-column tdbg-flex-space-around tdbg-flex-auto">
  366. <label class="tdbg-colorslider"><input id="tdbg-colorslider-panel" type="range" min="0" max="255"></label>
  367. <label class="tdbg-colorslider"><input id="tdbg-colorslider-column-header" type="range" min="0" max="255"></label>
  368. <label class="tdbg-colorslider"><input id="tdbg-colorslider-column" type="range" min="0" max="255"></label>
  369. </div>
  370. </div>
  371. <!--
  372. <div class="tdbg-colorpicker-alpha">
  373. <label class="tdbg-colorpicker">Panel<input id="tdbg-colorpicker-panel" type="color"></label>
  374. <label class="tdbg-colorslider"><input id="tdbg-colorslider-panel" type="range" min="0" max="255"></label>
  375. </div>
  376. <div class="tdbg-colorpicker-alpha">
  377. <label class="tdbg-colorpicker">Column<input id="tdbg-colorpicker-column" type="color"></label>
  378. <label class="tdbg-colorslider"><input id="tdbg-colorslider-column" type="range" min="0" max="255"></label>
  379. </div>
  380. -->
  381. <label class="tdbg-colorpicker">Base<input id="tdbg-colorpicker-base" type="color"></label>
  382. <label class="tdbg-colorpicker">Name<input id="tdbg-colorpicker-name" type="color"></label>
  383. <label class="tdbg-colorpicker">Tweet<input id="tdbg-colorpicker-tweet" type="color"></label>
  384. <label class="tdbg-colorpicker">Hashtag<input id="tdbg-colorpicker-hashtag" type="color"></label>
  385. <label class="tdbg-colorpicker">Link<input id="tdbg-colorpicker-link" type="color"></label>
  386. </div>
  387. </div>
  388. </div>
  389. </fieldset>
  390. `;
  391. document.querySelector('input[name=navIconSpace][value=' + navIconSpace + ']').checked = true;
  392. document.getElementById('tdbg-colorpicker-panel').value = colPanel.match(/#.{6}/)[0];
  393. document.getElementById('tdbg-colorslider-panel').value = parseInt(colPanel.match(/(?<=#.{6}).{2}/)[0], 16);
  394. document.getElementById('tdbg-colorpicker-column-header').value = colColumnHeader.match(/#.{6}/)[0];
  395. document.getElementById('tdbg-colorslider-column-header').value = parseInt(colColumnHeader.match(/(?<=#.{6}).{2}/)[0], 16);
  396. document.getElementById('tdbg-colorpicker-column').value = colColumn.match(/#.{6}/)[0];
  397. document.getElementById('tdbg-colorslider-column').value = parseInt(colColumn.match(/(?<=#.{6}).{2}/)[0], 16);
  398. document.getElementById('tdbg-colorpicker-base').value = colTextBase;
  399. document.getElementById('tdbg-colorpicker-name').value = colTextName;
  400. document.getElementById('tdbg-colorpicker-tweet').value = colTextTweet;
  401. document.getElementById('tdbg-colorpicker-hashtag').value = colTextHashtag;
  402. document.getElementById('tdbg-colorpicker-link').value = colTextLink;
  403. const radioIconSpace = document.getElementsByName('navIconSpace');
  404. for (let i = 0; i < radioIconSpace.length; i++) { radioIconSpace[i].addEventListener("click", funcRadioIconSpace); }
  405. const inputMain = document.querySelector("#tdbg-main-input");
  406. inputMain.addEventListener('input', funcInputPicture );
  407. if (bgMain !== undefined && bgMain !== null) {
  408. document.querySelector("#tdbg-main-input-preview").src = bgMain;
  409. if (isAcceptURL(bgMain)) document.querySelector("#tdbg-main-input-url").value = bgMain; else document.querySelector("#tdbg-main-input-url").value = '[fileinput]';
  410. }
  411. const inputDrawer = document.querySelector("#tdbg-drawer-input");
  412. inputDrawer.addEventListener('input', funcInputPicture );
  413. if (bgDrawer !== undefined && bgDrawer !== null) {
  414. document.querySelector("#tdbg-drawer-input-preview").src = bgDrawer;
  415. if (isAcceptURL(bgDrawer)) document.querySelector("#tdbg-drawer-input-url").value = bgDrawer; else document.querySelector("#tdbg-drawer-input-url").value = '[fileinput]';
  416. }
  417. const inputColorPickers = document.querySelectorAll('.tdbg-colorpicker input, .tdbg-colorslider input');
  418. inputColorPickers.forEach( function (elm) { elm.addEventListener('input', funcInputColorWait); });
  419. const selectColorPreset = document.querySelector('#tdbg-select-colorpreset');
  420. //const isDark = document.querySelector('html.dark') != null;
  421. //if (isDark) {selectColorPreset.value = 'dark';} else {selectColorPreset.value = 'light';}
  422. selectColorPreset.value = 'custom';
  423. selectColorPreset.addEventListener('change', (event) => {
  424. selectColorPreset.value
  425. });
  426. }
  427.  
  428. // ファイルとURLを同じ変数で扱うため、URLか否かをチェックするための関数
  429. function isAcceptURL(str) {
  430. const regex = /^https:\/\/.*/;
  431. return regex.test(str);
  432. }
  433.  
  434. // URIじゃなくてblobで表示させたかったんだけどあれなのでblobにしたかったけどTweetdeckのContent Security Policyに引っかかってしまう
  435. /*
  436. function dataURItoBlob(data) {
  437. // URIからblobを作成します。
  438. var dataURI = data;
  439. var byteString = atob( dataURI.split(',')[1] ) ;
  440. var mimeType = dataURI.match( /(:)([a-z\/]+)(;)/ )[2];
  441. for ( var i=0, l=byteString.length, content=new Uint8Array(l); l>i; i++ ) {
  442. content[i] = byteString.charCodeAt(i);
  443. }
  444. return new Blob([ content ], { type: mimeType });
  445. }
  446. function dataURItoObjectURL(data) {
  447. // URIから作成したblobのURLを取得します
  448. const blob = dataURItoBlob(data);
  449. return blob? URL.createObjectURL(blob): null;
  450. }
  451. */
  452.  
  453. // 動的にcssを変更するためにstyleタグを作成するための関数
  454. function makeTag() {
  455. if (!document.getElementById('tdbgRoot')) {
  456. // いくつかのStyleタグを作成するのでひとまとめにするためのdivタグを作成
  457. const tagTDBG = document.createElement('div');
  458. tagTDBG.id = 'tdbgRoot';
  459. document.getElementsByTagName('html').item(0).appendChild(tagTDBG);
  460. const tagsID = ['tdbg-variable', 'tdbg-bg', 'tdbg-style'];
  461. let tagsElement = [];
  462. tagsID.forEach(v => {
  463. const styleTag = document.createElement('style');
  464. styleTag.id = v;
  465. styleTag.type = 'text/css';
  466. tagTDBG.appendChild(styleTag);
  467. tagsElement.push(styleTag);
  468. });
  469. tagVariable = tagsElement.shift();
  470. tagBG = tagsElement.shift();
  471. tag = tagsElement.shift();
  472. }
  473. }
  474. function styleVariableUpdate() {
  475. let style = '';
  476. // 変数設定
  477. const tableNavIconSpace = { default: 45, small: 35, smallest: 31 };
  478. let _navIconSpacePixel = tableNavIconSpace.default;
  479. if (navIconSpace in tableNavIconSpace) _navIconSpacePixel = tableNavIconSpace[navIconSpace];
  480. (()=>{
  481. style += ':root {';
  482. if (colPanel !== undefined) style += '--tdbg-color-panel: ' + colPanel + ';';
  483. if (colColumnHeader !== undefined) style += '--tdbg-color-column-header: ' + colColumnHeader + ';';
  484. if (colColumn !== undefined) style += '--tdbg-color-column: ' + colColumn + ';';
  485. if (colTextTweet !== undefined) style += '--tdbg-color-tweet: ' + colTextTweet + ';';
  486. if (colTextHashtag !== undefined) style += '--tdbg-color-hashtag: ' + colTextHashtag + ';';
  487. if (colTextLink !== undefined) style += '--tdbg-color-link: ' + colTextLink + ';';
  488.  
  489. style += '--tdbg-color-accent: rgb(224, 192, 128);';
  490. style += '--tdbg-color-accent-text: #4040FF;';
  491. style += '--tdbg-color-subaccent: rgb(160, 128, 192);';
  492.  
  493. style += '--tdbg-navicon-space: ' + _navIconSpacePixel + 'px;';
  494. style += '}';
  495. })();
  496. tagVariable.innerText = style;
  497. }
  498. function styleBGUpdate() {
  499. let style = '';
  500. if (bgMain !== undefined && bgMain !== null) style += 'body { background-image: url("' + bgMain + '") !important; }'; else style += 'body { background: #4080A0 !important; }';
  501. if (bgDrawer !== undefined && bgDrawer !== null) style += '.drawer { background-image: url("' + bgDrawer + '") !important; }'; else style += '.drawer { background: #206080 !important; }';
  502. tagBG.innerText = style;
  503. }
  504. function styleUpdate() {
  505. // styleタグを書き換えることでcssを適用
  506. // 管理用styleタグが存在しないときはコンソールにエラー
  507. if (tag == null) { console.error('[TDBG] Failed to apply the css because the "#tdbg-style" style tag was not found.'); return true; }
  508. let style = '';
  509. // 色を設定する部分
  510. (()=>{
  511. // タイムラインパネルやポップアップパネルの色
  512. if (colPanel !== undefined) {
  513. style += '#settings-modal .js-modal-panel, .js-modal-inner { background-color: var(--tdbg-color-panel) !important; }';
  514. // カラム追加ポップアップ
  515. style += '.mdl:not(.med-fullpanel) { background-color: var(--tdbg-color-panel) !important; }';
  516. // 検索ポップアップ
  517. style += '.popover { background-color: #000000A0 !important; }';
  518. style += '.popover .caret-inner { border-bottom-color: var(--tdbg-color-panel) !important; }'
  519. style += '.js-popover-content { background-color: var(--tdbg-color-panel) !important; border-radius: 14px !important; }'
  520. }
  521. // カラム(およびカラムヘッダー)の色
  522. if (colColumnHeader !== undefined) {
  523. style += '.column .column-header, .column-options { background-color: var(--tdbg-color-column-header) !important; }';
  524. }
  525. if (colColumn !== undefined) {
  526. style += '.column .column-scroller, .column-message { background-color: var(--tdbg-color-column) !important; }';
  527. } else {
  528. style += '.column .column-scroller, .column-message { background-color: rgba(0, 0, 0, 0.7) !important; }';
  529. }
  530. // ツイートに関する色
  531. if (colTextTweet !== undefined) style += '.tweet-text { color: var(--tdbg-color-tweet) !important }';
  532. if (colTextHashtag !== undefined) style += '[rel=hashtag] { color: var(--tdbg-color-hashtag) !important }';
  533. if (colTextLink !== undefined) style += '[data-full-url] { color: var(--tdbg-color-link) !important; }';
  534.  
  535.  
  536. // カラムヘッダー部の更新通知の色
  537. style += '.is-new .column-type-icon { color: var(--tdbg-color-accent) !important; }';
  538. style += '.more-tweets-glow { background: radial-gradient(ellipse farthest-corner at 50% 100%, var(--tdbg-color-accent) 0, var(--tdbg-color-accent) 25%,hsla(0,0%,100%,0) 75%) !important; }';
  539. // ナビゲーションバーの更新通知ドット
  540. style += '.column-nav-updates { color: var(--tdbg-color-accent) !important; }';
  541. // ナビゲーションバーでカラムを選択した際に選択されたカラムの周りに表示されるボーダーの色
  542. style += '.column.is-focused { box-shadow: 0 0 0 2px var(--tdbg-color-accent) !important; }';
  543. // スクロールバーの色
  544. style += '.antiscroll-scrollbar, ::-webkit-scrollbar-thumb { background-color: var(--tdbg-color-accent) !important; }'; //"::-webkit-scrollbar-thumb" for Blink and WebKit base browser
  545. style += '* { scrollbar-color: var(--tdbg-color-accent) !important; }'; //"scrollbar-color" property for Firefox
  546. // 各種アイコン
  547. style += '.column-header-link, .app-nav-link, .app-nav-tab, .app-search-fake { color: var(--tdbg-color-accent) !important; }';
  548. style += '.lst-launcher .icon, html .lst-launcher .is-disabled .icon, html.dark .lst-launcher .is-disabled .icon, html .lst-launcher .is-disabled a:hover .icon, html.dark .lst-launcher .is-disabled a:hover .icon,';
  549. style += 'html .lst-launcher .is-disabled a:focus .icon, html.dark .lst-launcher .is-disabled a:focus .icon { color: var(--tdbg-color-accent) !important; }';
  550. style += '.app-search-fake { color: var(--tdbg-color-accent) !important; border-color: var(--tdbg-color-accent) !important; background-color: transparent !important; }';
  551. // 各種ボタン
  552. style += '.Button, input[type="button"], button { color: var(--tdbg-color-accent) !important; border-color: var(--tdbg-color-accent) !important; }';
  553. // 各種アクション時
  554. style += '.list-item.is-selected, .list-item:hover, html .lst-group .selected, html.dark .lst-group .selected,';
  555. style += 'html .dropdown-menu .is-selectable.is-selected a , html.dark .dropdown-menu .is-selectable.is-selected a { color: var(--tdbg-color-accent-text) !important; background-color: var(--tdbg-color-accent) !important; }';
  556. // 背景を伴うアクション時
  557. style += '.lst-profile a:hover i, .lst-profile a:hover span, .lst-group a:hover strong, .lst-group a:hover span { color: var(--tdbg-color-accent) !important; background-color: var(--tdbg-color-accent-text) !important; }'
  558. // 各種テキスト
  559. style += '.txt-link, a:hover, a:active { color: var(--tdbg-color-accent) !important; }';
  560.  
  561.  
  562. //
  563. // style += '.txt-mute { color: var(--tdbg-color-subaccent) !important; }';
  564. style += '.lst-group .selected a:hover { color: var(--tdbg-color-subaccent) !important; }';
  565.  
  566. style += '.lst-group .selected strong, .lst-group .selected span { color: var(--tdbg-color-accent-text) !important; }';
  567.  
  568. // 引用ツイート
  569. style += '.quoted-tweet, .TwitterCard-container { background-color: rgba(0, 0, 0, 0.4) !important; }';
  570.  
  571. //
  572. style += 'input[type=text], .detail-view-inline-text { background-color: #000000A0 !important; }'
  573. })();
  574. // 背景を設定する部分
  575. (()=>{
  576. // 背景が無い部分ができないように覆う設定
  577. style += 'body { background-size: cover !important; background-position: center center !important; }';
  578. style += '.drawer { background-size: cover !important; background-position: center center !important; }';
  579. })();
  580. // 背景を表示するために透過色を設置する部分
  581. (()=>{
  582. // 全体の透過
  583. style += '.app-content, .app-columns-container { background-color: transparent !important; }';
  584. // ポップアップの透過
  585. style += '.mdl-column-med, .med-fullpanel, .prf-meta { background-color: transparent !important; }'
  586. // ドロワーの透過
  587. style += '.compose, .old-composer-footer,';
  588. style += 'html .accounts-drawer, html.dark .accounts-drawer { background-color: transparent !important; }';
  589. // カラムの透過
  590. style += '.column-panel, .column-content { background-color: transparent !important; }';
  591. // カラムヘッダーの透過
  592. style += '.button-tray, .facet-type { background-color: transparent !important; }';
  593. // ツイート関係の透過
  594. // ツイートの透過
  595. style += 'article { background-color: transparent !important; }';
  596. // ツイート詳細の透過
  597. style += '.column-scroller, .tweet-detail-wrapper, .detail-view-inline { background-color: transparent !important; }';
  598. style += '.inline-reply, .reply-triangle { background-color: transparent !important; }';
  599. // カラム透過
  600. style += '.column { background-color: transparent !important; }';
  601. // ポップアップにカラムが表示される際の透過
  602. style += '.column-header-temp { background-color: transparent !important; }';
  603. // 404ページ用
  604. style += '.srt-holder .container { padding: 12px; background-color: rgba(0, 0, 0, 0.5); border-radius: 15px; }';
  605. })();
  606. // 背景設定画面
  607. (()=>{
  608. style += '#tdbg-settings .control-group { height: 1em; }';
  609. style += '.tdbg-radio-group label { float: left; padding-top: 6px; }';
  610. style += '.tdbg-radio-group.item-3 label { width: 33%; }';
  611.  
  612. style += '.tdbg-colorpicker { float: left !important; padding-right: 10px; }';
  613. style += '.tdbg-colorpicker input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }';
  614. style += '.tdbg-colorpicker input[type="color"]::-webkit-color-swatch { border: none; }';
  615. style += `.tdbg-colorpicker input[type="color"] {
  616. -webkit-appearance: none;
  617. padding: 0;
  618. background-color: rgba(0,0,0,0);
  619. width: 1em;
  620. height: 1em;
  621. margin-left: 3px;
  622. border-radius: 0;
  623. border: 1px solid #FFFFFF;
  624. float: right;
  625. }`;
  626.  
  627. // panelの色は透過度も設定できるようにしたいのでスライダーも存在する
  628. style += '#tdbg-setting-color .tdbg-flex .tdbg-colorpicker { vertical-align: middle; height: 2em; width: 115px; }';
  629. style += '#tdbg-setting-color .tdbg-flex .tdbg-colorslider { vertical-align: middle; height: 2em; }';
  630.  
  631. style += '.tdbg-input-container label { padding: 4px 8px;}';
  632. style += '.tdbg-input-container .tdbg-input-preview { object-fit: cover; border: solid 1px; }';
  633. style += '.tdbg-input-container div { padding-bottom: 10px; }';
  634. style += '.tdbg-input-container .tdbg-control-group { float: left; width: 360px; margin-top: 5px; }';
  635. style += '.tdbg-input-container .tdbg-controls { float: right; }';
  636.  
  637. style += '.tdbg-input-container label > input[type=file]::-webkit-file-upload-button { display: none; }';
  638. style += '.tdbg-input-container label > input[type=file] { flex: 1; }';
  639. style += '.tdbg-input-container label { padding: 0; display: flex; }';
  640. style += '.tdbg-input-container label div { position: absolute; padding: 6px; }';
  641. style += '.tdbg-input-container label > input[type=file] { padding-left: 25px; }';
  642.  
  643. style += '.tdbg-flex { display: flex; }';
  644. style += '.tdbg-flex-row { flex-direction: row; }';
  645. style += '.tdbg-flex-column { flex-direction: column; }';
  646. style += '.tdbg-flex-auto { flex-grow: 1; }';
  647. })();
  648. // 見栄えの為の調整
  649. (()=>{
  650. // ロードアイコンがgifなので背景色が背景を透過した際に見栄えが悪いため非表示
  651. // style += '.login-container img:not(.tdbg-img) { display: none; }';
  652. // カラムの角を丸める
  653. style += '.column { border-radius: 15px !important; }';
  654. // カラム間の余白を詰めて上下にも余白を取る
  655. style += '.app-columns { padding: 2px 0 2px 2px !important; }';
  656. style += '.column { margin: 0 2px 0 0 !important; }';
  657. // ナビゲーションバーのアイコン間の余白を詰める
  658. style += '.column-nav-item { height: var(--tdbg-navicon-space) !important; }';
  659. // ナビゲーションバーのスクロールバーが透明だと使いづらいので半透明に修正
  660. style += '.antiscroll-scrollbar { opacity: 0.5 !important; }';
  661. style += '.antiscroll-scrollbar-shown { opacity: 1.0 !important; }';
  662. //
  663. style += '.app-header:not(.is-condensed) .app-search-fake .icon { opacity: 0; }';
  664. // インラインリプライの余白を詰める
  665. style += 'html .inline-reply > div, html.dark .inline-reply > div { padding-top: 0 !important; }';
  666. style += '.compose-text-container { margin-top: 0px !important; }';
  667. // リプライ入力ボックスの透過度を揃える
  668. style += '.detail-view-inline-text { background-color: rgba(0,0,0,0.4) !important; }';
  669. // ボーダー統一
  670. style += '[dir=ltr] [role=button], .r-aaos50, .column { border: solid 1px !important; }';
  671. style += '.new-composer-bottom-button, .r-18qmn74 { border: solid 1px !important; background-color: rgba(0, 0, 0, .5) !important; border-radius: 30px !important; }';
  672. style += '.compose-reply-tweet { border: solid 1px !important; }';
  673. style += '.app-content *:not(span):not(.replyto-caret), ::-webkit-scrollbar-track, [dir=ltr] [role=button], .r-aaos50, .r-18qmn74 { border-color: #808080 !important; }';
  674. // ポップアップを開いた際に後ろの画面を透過
  675. style += 'html:not(.btd-on) .med-fullpanel { background-color: #00000000 !important; }';
  676. style += 'html:not(.btd-on) .ovl, html:not(.btd-on) .overlay { background: #000000CC !important; }';
  677.  
  678. // 404ページのheightが0になっていて背景の表示がおかしくなるのを修正
  679. style += 'html { height: 100%; }';
  680. })();
  681. // TeamInvitationsカラムが見えちゃうことがあるため、開いてないときは見えないようにする
  682. style += '.js-team-invitations-container, .drawer:not(.is-shifted-1) .js-contributor-manager-container { display: none !important; }';
  683. // アニメーション
  684. (()=>{
  685. if (!flagUIAnimation) return;
  686. // ナビゲーションバーのアイコン間の余白が変わったときに滑らかに推移
  687. style += '.column-nav-item { transition: height 0.3s ease-out; }';
  688. // ページロード時にカラムポップアップ演出
  689. // style += '@keyframes fadeIn { 0% { transform: scale(0,0); opacity: 0; animation-timing-function: cubic-bezier(.3,1.52,.51,.89); } 100% { transform: scale(1); opacity: 1; }}';
  690. // style += '.column { animation-name: fadeIn; animation-duration: 0.8s; animation-fill-mode: backwards; }';
  691. // const columns = document.querySelectorAll('.app-columns > section');
  692. // columns.forEach(function(e,i){ style += '.column:nth-of-type(' + (i + 1) + ') { animation-delay: ' + 0.07 * i + 's; }'; });
  693. })();
  694. // BetterTweetdeck導入時の修正
  695. (()=>{
  696. // BetterTweetdeckの設定画面を開くナビアイコンが増えたことにより、ナビアイコン列の最後が隠れてしまうのを修正
  697. style += '.btd-loaded .column-navigator.column-navigator-overflow { height: calc(100% - 409px) !important; }';
  698. // ツイート詳細画面のリプライ吹き出しに不自然なマージンが取られているのを修正
  699. style += '.btd-loaded .inline-reply, .detail-view-inline { margin-top: 0px !important; }';
  700. // カラム折りたたみ時にボーダー線が重なるのを修正
  701. style += '.btd-column-collapsed .column-header { border: solid 0px !important }';
  702. // カラム折りたたみ時にカラム透過色が見えてしまうのを修正(不自然な横幅を修正)
  703. style += 'html .app-content section.column.btd-column-collapsed { width: 50px !important; }';
  704. // 設定画面の一時的に影を消す設定を有効にしたときの補正
  705. style += 'html.btd-on #settings-modal.ovl.tdbg-bgtransparent { background: transparent !important; }';
  706. })();
  707. tag.innerText = style;
  708. }
  709. })();