Camamba Chat Tweaks

tweaks layout of the chat

当前为 2021-05-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Camamba Chat Tweaks
  3. // @namespace dannysaurus.camamba
  4. // @version 0.5.8
  5. // @description tweaks layout of the chat
  6. // @license MIT License
  7. //
  8. // @include https://www.camamba.com/chat/
  9. // @include https://www.de.camamba.com/chat/
  10. //
  11. // @connect camamba.com
  12. // @grant GM_xmlhttpRequest
  13. //
  14. // @require https://greasyfork.org/scripts/405143-simplecache/code/SimpleCache.js
  15. // @require https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js
  16. // @require https://greasyfork.org/scripts/391854-enum/code/Enum.js
  17. // @require https://greasyfork.org/scripts/405699-camamba-user/code/Camamba%20User.js
  18. //
  19. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
  20. //
  21. // @require https://greasyfork.org/scripts/423722-camamba-chat-helpers-library/code/Camamba%20Chat%20Helpers%20Library.js?version=934149
  22. //
  23. // @grant GM.getValue
  24. // @grant GM.setValue
  25. // ==/UserScript==
  26.  
  27. /* jslint esnext: true */
  28. /* globals knownUsers, me */
  29. (function() {
  30. 'use strict';
  31. // --- initial sizes ---
  32. const SIZES = {
  33. FONT_EM: {
  34. userList: 1.2,
  35. chatBox: 1.8,
  36. },
  37. WIDTH_EM: {
  38. sidebarLeft: 10,
  39. sidebarRight: 14,
  40. },
  41. };
  42.  
  43. // --- HTML Selector Helpers ---
  44. const SELECTORS = {
  45. ID: {
  46. // original
  47. userList: 'userList',
  48. chatBox: 'chatBox',
  49. chatInput: 'chatInput',
  50. chatWindow: 'chatWindow',
  51. mediaContainer1 : 'mediaContainer1',
  52. mediaContainer2 : 'mediaContainer2',
  53. mediaContainer3 : 'mediaContainer3',
  54. mediaContainer4 : 'mediaContainer4',
  55. mediaContainer5 : 'mediaContainer5',
  56. mediaContainer6 : 'mediaContainer6',
  57. mediaContainer7 : 'mediaContainer7',
  58. mediaContainer8 : 'mediaContainer8',
  59.  
  60. // script
  61. cbCamslots: 'cb-camslots',
  62. spinnerUserlistFont: 'spinner-userlist-font',
  63. spinnerChatFont: 'spinner-chat-font',
  64. },
  65. CLASS: {
  66. noTextSelect: 'noTextSelect',
  67. borderBox: 'borderBox',
  68. camBox: 'camBox'
  69. }
  70. };
  71.  
  72. const containers = (() => {
  73. let userList, chatBox, sidebars, camslots;
  74.  
  75. return {
  76. get userList() {
  77. if (typeof userList === "undefined") {
  78. userList = document.getElementById(SELECTORS.ID.userList);
  79. }
  80. return userList;
  81. },
  82.  
  83. get chatBox() {
  84. if (typeof chatBox === "undefined") {
  85. chatBox = document.getElementById(SELECTORS.ID.chatBox);
  86. }
  87. return chatBox;
  88. },
  89.  
  90. get sidebars() {
  91. if (typeof sidebars === "undefined") {
  92. sidebars = document.getElementById(SELECTORS.ID.chatWindow).querySelectorAll(`.${SELECTORS.CLASS.noTextSelect}`);
  93. }
  94. return sidebars;
  95. },
  96.  
  97. get sidebarLeft() {
  98. return this.sidebars[0];
  99. },
  100.  
  101. get sidebarTop() {
  102. return this.sidebars[1];
  103. },
  104.  
  105. get sidebarRight() {
  106. return this.sidebars[2];
  107. },
  108.  
  109. get camslots() {
  110. if (typeof camslots === "undefined") {
  111. const parentContainers = [
  112. SELECTORS.ID.mediaContainer1,
  113. SELECTORS.ID.mediaContainer2,
  114. SELECTORS.ID.mediaContainer3,
  115. SELECTORS.ID.mediaContainer4,
  116. SELECTORS.ID.mediaContainer5,
  117. SELECTORS.ID.mediaContainer6,
  118. SELECTORS.ID.mediaContainer7,
  119. SELECTORS.ID.mediaContainer8,
  120. ]
  121. .map(id => document.getElementById(id))
  122. .filter(el => el !== null)
  123. .map(el => el.parentNode);
  124.  
  125. camslots = [ ...new Set(parentContainers)];
  126. }
  127. return camslots;
  128. }
  129. };
  130. })();
  131.  
  132. const layoutPatcher = new class {
  133. constructor() {
  134. this.historyCamslotsRemoved = [];
  135. }
  136.  
  137. patchSizes() {
  138. // this.setWidthOfSidebarLeft(`${SIZES.WIDTH_EM.sidebarLeft}em`);
  139. this.setWidthOfSidebarRight(`${SIZES.WIDTH_EM.sidebarRight}em`);
  140. return this;
  141. }
  142.  
  143. setFontSizeOfUserList(fontSize) {
  144. containers.userList.style.fontSize = fontSize;
  145. return this;
  146. }
  147.  
  148. setFontSizeOfChat(fontSize) {
  149. containers.chatBox.style.fontSize = fontSize;
  150. return this;
  151. }
  152.  
  153. setWidthOfSidebarLeft(width) {
  154. containers.sidebarLeft.style.width = width;
  155. return this;
  156. }
  157.  
  158. setWidthOfSidebarRight(width) {
  159. containers.sidebarLeft.style.width = width;
  160. return this;
  161. }
  162.  
  163. showCamslots() {
  164. for (let i = 0; i < this.historyCamslotsRemoved.length; i++) {
  165. const { parent, index, element } = this.historyCamslotsRemoved.pop();
  166. parent.insertBefore(element, parent.children[index]);
  167. }
  168. return this;
  169. }
  170.  
  171. hideCamslots() {
  172. for (let element of containers.camslots) {
  173. const parent = element.parentNode;
  174. if (parent) {
  175. let index = Array.from(parent.children).indexOf(element);
  176. parent.removeChild(element);
  177.  
  178. this.historyCamslotsRemoved.push({ parent, index, element });
  179. }
  180. }
  181. return this;
  182. }
  183. }();
  184.  
  185.  
  186. const controls = (() => {
  187. // --- HTML Create Element Helpers ---
  188. const createInput = ({
  189. id,
  190. parentElement = null,
  191. type = 'text',
  192. defaultValue = '',
  193. labelText = null,
  194. onValueChange = null,
  195. propertyNameValue = 'value',
  196. eventNameValueChange = 'input',
  197. }) => {
  198. const div = document.createElement('div');
  199.  
  200. const input = div.appendChild(document.createElement('input'));
  201. input.type = type;
  202. input.id = id;
  203. input.style.backgroundColor = 'rgba(39,62,77,1)';
  204.  
  205. if (labelText) {
  206. const label = div.appendChild(document.createElement('label'));
  207. label.htmlFor = id;
  208. label.appendChild(document.createTextNode(labelText));
  209. }
  210.  
  211. if (onValueChange) {
  212. let oldValue;
  213.  
  214. input.addEventListener(eventNameValueChange, () => {
  215. const newValue = input[propertyNameValue];
  216. if (oldValue !== newValue) {
  217. oldValue = newValue;
  218.  
  219. onValueChange(newValue);
  220. }
  221. });
  222. }
  223.  
  224. if (parentElement) {
  225. parentElement.appendChild(div);
  226. }
  227. return input;
  228. };
  229.  
  230. const createInputPersistent = ({
  231. id,
  232. parentElement = null,
  233. type = 'text',
  234. defaultValue = '',
  235. labelText = null,
  236. onValueChange = null,
  237. propertyNameValue = 'value',
  238. eventNameValueChange = 'input',
  239. }) => {
  240. const input = createInput({
  241. parentElement, type, id, defaultValue, labelText, propertyNameValue, eventNameValueChange,
  242. onValueChange: value => {
  243. GM.setValue(id, value);
  244. if (onValueChange) {
  245. onValueChange(value);
  246. }
  247. }
  248. });
  249.  
  250. input.setValue = value => {
  251. GM.setValue(id, value);
  252. input[propertyNameValue] = value;
  253. onValueChange(value);
  254. };
  255.  
  256. input.updateValue = () => GM.getValue(id, defaultValue).then(value => {
  257. input[propertyNameValue] = value;
  258. if (onValueChange) {
  259. onValueChange(value);
  260. }
  261. });
  262.  
  263. return input;
  264. };
  265.  
  266. const createCheckbox = ({
  267. id,
  268. parentElement = null,
  269. initialChecked = false,
  270. labelText = null,
  271. onValueChange = null,
  272. }) => {
  273. const checkbox = createInputPersistent({
  274. parentElement, id, labelText, onValueChange,
  275. defaultValue: !!initialChecked,
  276. type: 'checkbox',
  277. propertyNameValue: 'checked',
  278. eventNameValueChange: 'click',
  279. });
  280. return checkbox;
  281. };
  282.  
  283. const createSpinner = ({
  284. id, min, max, step,
  285. parentElement = null,
  286. defaultValue = 0,
  287. labelText = null,
  288. onValueChange = null,
  289. }) => {
  290. const spinner = createInputPersistent({
  291. parentElement, id, defaultValue, labelText, onValueChange,
  292. type: 'number',
  293. });
  294. spinner.min = min;
  295. spinner.max = max;
  296. spinner.step = step;
  297.  
  298. const buttonDec = spinner.parentNode.insertBefore(document.createElement('button'), spinner);
  299. buttonDec.type = 'button';
  300. buttonDec.innerHTML = '-';
  301. buttonDec.addEventListener('click', () => {
  302. spinner.stepDown();
  303. spinner.setValue(spinner.value);
  304. });
  305.  
  306. const buttonInc = spinner.parentNode.insertBefore(document.createElement('button'), spinner.nextSibling);
  307. buttonInc.type = 'button';
  308. buttonInc.innerHTML = '+';
  309. buttonInc.addEventListener('click', () => {
  310. spinner.stepUp();
  311. spinner.setValue(spinner.value);
  312. });
  313.  
  314. return spinner;
  315. };
  316.  
  317. const sidebarLeftCenter = containers.sidebarLeft.children[1];
  318. sidebarLeftCenter.innerHTML = "";
  319. const container = sidebarLeftCenter.appendChild(document.createElement('div'));
  320.  
  321. // checkbox camslots on/off
  322. const cbCamslots = createCheckbox({
  323. parentElement: container,
  324. id: SELECTORS.ID.cbCamslots,
  325. initialChecked: true,
  326. labelText: 'camslots',
  327. onValueChange: value => {
  328. if (value) {
  329. layoutPatcher.showCamslots();
  330. } else {
  331. layoutPatcher.hideCamslots();
  332. }
  333. },
  334. });
  335.  
  336. // spinner userlist font
  337. const spinnerUserlistFont = createSpinner({
  338. parentElement: container,
  339. id: SELECTORS.ID.spinnerUserlistFont,
  340. defaultValue: SIZES.FONT_EM.userList,
  341. min: 1.0,
  342. max: 2.2,
  343. step: 0.1,
  344. labelText: 'users',
  345. onValueChange: value => {
  346. const fontSize = `${value}em`;
  347. layoutPatcher.setFontSizeOfUserList(fontSize);
  348. },
  349. });
  350.  
  351. // spinner chat font
  352. const spinnerChatFont = createSpinner({
  353. parentElement: container,
  354. id: SELECTORS.ID.spinnerChatFont,
  355. defaultValue: SIZES.FONT_EM.chatBox,
  356. min: 1.0,
  357. max: 2.5,
  358. step: 0.1,
  359. labelText: 'chat',
  360. onValueChange: value => {
  361. const fontSize = `${value}em`;
  362. layoutPatcher.setFontSizeOfChat(fontSize);
  363. },
  364. });
  365.  
  366. const buttonKickFromCam = container.appendChild(document.createElement('button'));
  367. buttonKickFromCam.type = 'button';
  368. buttonKickFromCam.innerHTML = 'Kick from cam';
  369. buttonKickFromCam.addEventListener('click', () => {
  370. knownUsers.bySelected().stopViewing();
  371. });
  372.  
  373. if (me.admin) {
  374. const buttonRepeatBan = container.appendChild(document.createElement('button'));
  375. buttonRepeatBan.type = 'button';
  376. buttonRepeatBan.innerHTML = 'repeat last ban';
  377. buttonRepeatBan.addEventListener('click', () => {
  378. if (lastBanData && lastBanData.userId) {
  379. let { text, time, isPerma } = lastBanData;
  380.  
  381. knownUsers.byId(lastBanData.userId).ban(text, time, { isPerma });
  382. }
  383. });
  384. }
  385.  
  386. return {
  387. cbCamslots,
  388. spinnerUserlistFont,
  389. spinnerChatFont,
  390. };
  391. })();
  392.  
  393. const patchObject = ({ getExpected, doPatch, confirmAvailable = null, timeOutRetryMillis = 200, maxPeriodTryMillis = 5000 }) => {
  394. const expected = getExpected();
  395. const isAvailable = confirmAvailable ? confirmAvailable(expected) : !!expected;
  396. if (!isAvailable) {
  397. if (timeOutRetryMillis <= maxPeriodTryMillis) {
  398. setTimeout(() => {
  399. maxPeriodTryMillis -= timeOutRetryMillis;
  400. patchObject({ getExpected, doPatch, confirmAvailable, timeOutRetryMillis, maxPeriodTryMillis });
  401. }, timeOutRetryMillis);
  402. }
  403. return;
  404. }
  405. doPatch(expected);
  406. };
  407.  
  408. /* eslint-disable no-undef */
  409. patchObject({
  410. getExpected: () => initSettings,
  411.  
  412. doPatch: (original) => {
  413. initSettings = () => {
  414. original();
  415.  
  416. // Breite von Userliste anpassen
  417. layoutPatcher.patchSizes();
  418.  
  419. // weiterere Einstellungen überschreiben, bzw übernehmen
  420. for (let control of [ controls.cbCamslots, controls.spinnerUserlistFont, controls.spinnerChatFont ]) {
  421. control.updateValue();
  422. }
  423. };
  424. },
  425. });
  426.  
  427. let lastBanData = { userId : 0, text: '', time: 0, isPerma: false };
  428.  
  429. patchObject({
  430. getExpected: () => adminExec,
  431.  
  432. doPatch: (original) => {
  433. original();
  434.  
  435. if (currentAdminAction == "ban") {
  436. let userId, text, time, isPerma;
  437.  
  438. text = byId('adminMessageInput').value;
  439. if (!text || text.length <= 3 && byId('adminMessageSelect').selectedIndex) {
  440. text = adminMessages[currentAdminAction][byId('adminMessageSelect').value];
  441. }
  442.  
  443. userId = currentAdminTarget;
  444. time = parseInt(byId('banTime').value);
  445. isPerma = byId('permaBan') && byId('permaBan').checked;
  446.  
  447. if (userId && text > 3 && time) {
  448.  
  449. lastBanData = { userId, text, time, isPerma };
  450. }
  451. }
  452. }
  453. });
  454. patchObject({
  455. getExpected: () => {
  456. return document.getElementById(SELECTORS.ID.chatInput);
  457. },
  458. doPatch: (el) => {
  459. el.setAttribute('autoComplete', 'on');
  460. }
  461. });
  462. /* eslint-enable no-undef */
  463. })();