LZT Stats

Detailed statistics of your activity

目前为 2023-09-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name LZT Stats
  3. // @namespace lzt-stats
  4. // @version 1.01
  5. // @description Detailed statistics of your activity
  6. // @author Toil
  7. // @license MIT
  8. // @match https://zelenka.guru/*
  9. // @match https://lolz.live/*
  10. // @match https://lolz.guru/*
  11. // @match https://lzt.market/*
  12. // @match https://lolz.market/*
  13. // @match https://zelenka.market/*
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=zelenka.guru
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js
  16. // @supportURL https://zelenka.guru/toil/
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_deleteValue
  20. // @grant GM_listValues
  21. // @grant GM_addStyle
  22. // ==/UserScript==
  23.  
  24. (function() {
  25. 'use strict';
  26.  
  27. // * STYLES
  28. GM_addStyle(`
  29. .LZTStatsTabs {
  30. width: 100%;
  31. box-sizing: border-box;
  32. padding: 0 10px;
  33. border: none !important;
  34. margin: 15px auto !important;
  35. display: flex;
  36. align-items: center;
  37. justify-content: center;
  38. }
  39.  
  40. @media screen and (max-width: 699px) {
  41. .LZTStatsTabs {
  42. flex-direction: column;
  43. }
  44. }
  45.  
  46. .LZTStatsTab {
  47. position: relative;
  48. padding: 10px;
  49. margin: 0 4px;
  50. float: left;
  51. font-weight: 600;
  52. list-style: none !important;
  53. font-size: 14px;
  54. }
  55.  
  56. .LZTStatsTab:hover {
  57. cursor: pointer;
  58. }
  59.  
  60. .LZTStatsTab.active {
  61. box-shadow: inset 0px -2px 0px 0px #0daf77;
  62. transform: translateY(-1px);
  63. transition: .2s;
  64. }
  65.  
  66. .LZTStatsTab:not(.active):hover {
  67. box-shadow: inset 0px -2px 0px 0px rgb(54, 54, 54);
  68. }
  69.  
  70. .LZTStatsInfo {
  71. display: flex;
  72. flex-wrap: wrap;
  73. gap: 12px;
  74. }
  75.  
  76. @media screen and (max-width: 699px) {
  77. .LZTStatsInfo {
  78. flex-direction: column;
  79. align-content: center;
  80. }
  81. }
  82.  
  83. .LZTStatsInfo .LZTStatsItem {
  84. height: 63px;
  85. background: #2D2D2D;
  86. border-radius: 8px;
  87. padding: 12px;
  88. box-sizing: border-box;
  89. display: flex;
  90. align-items: center;
  91. gap: 12px;
  92. white-space: normal;
  93. }
  94.  
  95. @media screen and (min-width: 700px) {
  96. .LZTStatsInfo .LZTStatsItem {
  97. width: 32%;
  98. }
  99. }
  100.  
  101. @media screen and (max-width: 699px) {
  102. .LZTStatsInfo .LZTStatsItem {
  103. width: 90%;
  104. }
  105. }
  106.  
  107. .LZTStatsInfo .LZTStatsItem i {
  108. width: 24px;
  109. height: 24px;
  110. font-size: 24px;
  111. }
  112.  
  113. .LZTStatsInfo .LZTStatsItem p {
  114. font-weight: 400;
  115. font-size: 13px;
  116. line-height: 16px;
  117. color: #D6D6D6;
  118. opacity: 0.8;
  119. margin-bottom: 6px;
  120. max-width: 95px;
  121. }
  122.  
  123. .LZTStatsInfo .LZTStatsItem .LZTStatsChangeInfo {
  124. height: 16px;
  125. margin-left: auto;
  126. display: flex;
  127. }
  128.  
  129. .LZTStatsInfo .LZTStatsItem .LZTStatsChangeInfo i {
  130. width: 16px;
  131. height: 16px;
  132. font-size: 16px;
  133. margin-left: 2px;
  134. }
  135.  
  136. #LZTStatsModalTitle {
  137. text-align: center;
  138. padding: 16px;
  139. font-size: 20px;
  140. font-weight: bold;
  141. }
  142.  
  143. .LZTStatsChatComment {
  144. background: rgb(54, 54, 54);
  145. margin: 5px 15px;
  146. padding: 10px 15px;
  147. border-radius: 10px;
  148. }
  149.  
  150. .LZTStatsSectionItem {
  151. max-width: 580px;
  152. flex-basis: 50%;
  153. flex-grow: 1;
  154. height: 64px;
  155. display: flex;
  156. align-items: center;
  157.  
  158. transition: all 0.5s ease;
  159. }
  160.  
  161. .LZTStatsSectionItem:hover {
  162. background: rgba(54, 54, 54, 0.75);
  163. border-radius: 8px;
  164. cursor: pointer;
  165. }
  166.  
  167. .LZTUpSectionTextContainer {
  168. display: flex;
  169. flex-direction: column;
  170. justify-content: center;
  171. flex: 1 1 auto;
  172. max-width: 100%;
  173. }
  174.  
  175. .LZTStatsSectionItem i {
  176. width: 28px;
  177. height: 28px;
  178. margin: 20px;
  179. font-size: 28px;
  180. color: #0daf77;
  181. }
  182.  
  183. .LZTStatsSectionTitle {
  184. display: block;
  185. margin-right: 20px;
  186. font-size: 15px;
  187. font-weight: bold;
  188. text-overflow: ellipsis;
  189. white-space: nowrap;
  190. overflow: hidden;
  191. }
  192.  
  193. #LZTStatsLoadChatHistory, .LZTStatsTotalMessages {
  194. margin: 15px;
  195. }
  196. `)
  197.  
  198. // * CONSTANTS
  199. const HOUR_IN_SECS = 3600;
  200. const DAY_IN_SECS = HOUR_IN_SECS * 24;
  201. const MONTH_IN_SECS = DAY_IN_SECS * 30;
  202.  
  203. // * REGEXES
  204. const SEND_IN_THREAD = /^threads\/([^d]+)\/add-reply$/
  205. const COMMENT_IN_THREAD = /^posts\/([^d]+)\/comment$/
  206.  
  207. const DELETE_MESSAGE_IN_THREAD = /^posts\/([^d]+)\/delete$/
  208. const DELETE_COMMENT_IN_THREAD = /^posts\/comments\/([^d]+)\/delete$/
  209.  
  210. const EDITED_MESSAGE_IN_THREAD = /^posts\/([^d]+)\/save-inline$/
  211. const EDITED_COMMENT_IN_THREAD = /^posts\/comments\/([^d]+)\/save-inline$/
  212.  
  213. const PARTICIPATE_IN_CONTEST = /^threads\/([^d]+)\/participate/ // ! DON'T ADD ANYTHING TO THE END!! THERE'S A CAPTCHA GOING ON
  214. const SYMPATHY_IN_THREAD = /\/posts\/([^d]+)\/like$/
  215. const REPORT_IN_THREAD = /^posts\/([^d]+)\/report$/
  216.  
  217. const SEND_IN_PROFILE_USERID = /^members\/([^d]+)\/post$/
  218. const SEND_IN_PROFILE_TAG = /^([-\w]+)\/post$/
  219. const COMMENT_IN_PROFILE = /^profile-posts\/([^d]+)\/comment$/
  220.  
  221. const POLL_IN_THREAD = /^threads\/([^d]+)\/poll\/vote/
  222.  
  223. const SEND_IN_CHAT = /^\/chatbox\/post-message$/
  224. const EDIT_IN_CHAT = /^\/chatbox\/([^d]+)\/edit$/
  225. const DELETE_IN_CHAT = /^\/chatbox\/([^d]+)\/delete$/
  226.  
  227. const CREATE_NEW_THREAD = /^forums\/([-\w]+)\/add-thread$/
  228. const EDIT_THREAD = /^threads\/([^d]+)\/save$/
  229. const DELETE_THREAD = /^threads\/([^d]+)\/delete$/
  230.  
  231. const SEND_IN_CONVERSATION = /^conversations\/([^d]+)\/insert-reply$/
  232. const EDIT_IN_CONVERSATION = /^conversations\/([^d]+)\/save-message/ // ! DON'T ADD ANYTHING TO THE END!! THERE GOES THE MESSAGE NUMBER
  233.  
  234. const FIND_THREAD_ID = /^threads\/([^d]+)\//
  235. const FIND_POST_ID = /^posts\/([^d]+)\//
  236. const FIND_USER_POST = /^([-\w]+)\/post$/
  237. const FIND_USER_ID = /^members\/([^d]+)\//
  238. const FIND_COMMENT_POST_ID = /^posts\/comments\/([^d]+)\//
  239. const FIND_FORUM_ID = /^forums\/([-\w]+)\//
  240. const FIND_POST_ID_IN_FULL_URL = /\/posts\/([^d]+)\//
  241. const FIND_CHAT_ID = /^\/chatbox\/([^d]+)\//
  242. const FIND_CONVERSATION_ID = /^conversations\/([^d]+)\//
  243.  
  244. // * HOOKS
  245. function initHooks() {
  246. // reference: https://github.com/LOLZHelper/LOLZHelperReborn/blob/main/src/common/hooks.js
  247. const XF_AJAX = XenForo.ajax;
  248.  
  249. XenForo.ajax = function () {
  250. console.debug('[LZTStats.initHooks] [XenForo.ajax]', arguments);
  251. const url = arguments[0];
  252. const data = arguments[1];
  253. const success = arguments[2];
  254. const options = arguments[3];
  255.  
  256. const timestamp = getTimestamp();
  257. switch (true) {
  258. case SEND_IN_THREAD.test(url):
  259. {
  260. const threadId = getThreadIdByURL(url);
  261. saveStats({
  262. id: threadId,
  263. timestamp
  264. }, 'stats-messages')
  265. break;
  266. }
  267. case COMMENT_IN_THREAD.test(url):
  268. {
  269. const postId = getPostIdByURL(url);
  270. saveStats({
  271. id: postId,
  272. timestamp
  273. }, 'stats-comments')
  274. break;
  275. }
  276. case PARTICIPATE_IN_CONTEST.test(url):
  277. {
  278. const threadId = getThreadIdByURL(url);
  279. saveStats({
  280. id: threadId,
  281. timestamp
  282. }, 'stats-participates');
  283. break;
  284. }
  285. case SYMPATHY_IN_THREAD.test(url):
  286. {
  287. // ! SYMPATHIES AND LIKES HAVE THE SAME URL
  288. const postId = getPostIdByFullURL(url);
  289. saveStats({
  290. id: postId,
  291. timestamp
  292. }, 'stats-sympathies');
  293. break;
  294. }
  295. case REPORT_IN_THREAD.test(url):
  296. {
  297. const postId = getPostIdByURL(url);
  298. saveStats({
  299. id: postId,
  300. // message: data?.[1] || '', // weighs a lot
  301. timestamp
  302. }, 'stats-reports');
  303. break;
  304. }
  305. case CREATE_NEW_THREAD.test(url):
  306. {
  307. const forumId = getForumIdByURL(url);
  308. saveStats({
  309. id: forumId,
  310. timestamp
  311. }, 'stats-threads-created')
  312. break;
  313. }
  314. case EDIT_THREAD.test(url):
  315. {
  316. const threadId = getThreadIdByURL(url);
  317. saveStats({
  318. id: threadId,
  319. timestamp
  320. }, 'stats-threads-edited')
  321. break;
  322. }
  323. case DELETE_THREAD.test(url):
  324. {
  325. const threadId = getThreadIdByURL(url);
  326. saveStats({
  327. id: threadId,
  328. timestamp
  329. }, 'stats-threads-deleted')
  330. break;
  331. }
  332. case (DELETE_COMMENT_IN_THREAD.test(url) && data.length > 0):
  333. {
  334. const postId = getCommentPostIdByURL(url);
  335. saveStats({
  336. id: postId,
  337. timestamp
  338. }, 'stats-comments-deleted')
  339. break;
  340. }
  341. case (DELETE_MESSAGE_IN_THREAD.test(url) && data.length > 0):
  342. {
  343. // ! Do not switch places with comments or you will have to fix the regex
  344. const postId = getPostIdByURL(url);
  345. saveStats({
  346. id: postId,
  347. timestamp
  348. }, 'stats-messages-deleted')
  349. break;
  350. }
  351. case EDITED_COMMENT_IN_THREAD.test(url):
  352. {
  353. const postId = getCommentPostIdByURL(url);
  354. saveStats({
  355. id: postId,
  356. timestamp
  357. }, 'stats-comments-edited')
  358. break;
  359. }
  360. case (EDITED_MESSAGE_IN_THREAD.test(url)):
  361. {
  362. // ! Do not switch places with comments or you will have to fix the regex
  363. const postId = getPostIdByURL(url);
  364. saveStats({
  365. id: postId,
  366. timestamp
  367. }, 'stats-messages-edited')
  368. break;
  369. }
  370. case (POLL_IN_THREAD.test(url) && data.length > 0):
  371. {
  372. const threadId = getThreadIdByURL(url);
  373. saveStats({
  374. id: threadId,
  375. timestamp
  376. }, 'stats-polls')
  377. break;
  378. }
  379. case (SEND_IN_PROFILE_USERID.test(url) || (SEND_IN_PROFILE_TAG.test(url) && (window.location.pathname.replace('/', '') + '/post') === url)):
  380. {
  381. // Check by ID, if the user has a tag, then check so that the path matches the name in the link
  382. const userId = getUserIdByURL(url); // it can also return the tag
  383. saveStats({
  384. id: userId,
  385. timestamp
  386. }, 'stats-messages');
  387. break;
  388. }
  389. case (COMMENT_IN_PROFILE.test(url)):
  390. {
  391. const postId = getCommentPostIdByURL(url);
  392. saveStats({
  393. id: postId,
  394. timestamp
  395. }, 'stats-comments');
  396. break;
  397. }
  398. case (SEND_IN_CHAT.test(url)):
  399. {
  400. saveStats({
  401. id: 0,
  402. message: data?.message,
  403. timestamp
  404. }, 'stats-chat');
  405. break;
  406. }
  407. case (EDIT_IN_CHAT.test(url)):
  408. {
  409. const chatId = getChatIdByURL(url);
  410. saveStats({
  411. id: chatId,
  412. timestamp
  413. }, 'stats-chat-edited');
  414. break;
  415. }
  416. case (DELETE_IN_CHAT.test(url)):
  417. {
  418. const chatId = getChatIdByURL(url);
  419. saveStats({
  420. id: chatId,
  421. timestamp
  422. }, 'stats-chat-deleted');
  423. break;
  424. }
  425. case (SEND_IN_CONVERSATION.test(url)):
  426. {
  427. const conversationId = getConversationIdByURL(url);
  428. saveStats({
  429. id: conversationId,
  430. timestamp
  431. }, 'stats-conversation');
  432. break;
  433. }
  434. case (EDIT_IN_CONVERSATION.test(url)):
  435. {
  436. const conversationId = getConversationIdByURL(url);
  437. saveStats({
  438. id: conversationId,
  439. timestamp
  440. }, 'stats-conversation-edited');
  441. break;
  442. }
  443. default:
  444. break;
  445. }
  446.  
  447. return XF_AJAX.apply(this, arguments);
  448. }
  449. }
  450.  
  451. // * REGISTERS
  452. function regMenuBtn(name) {
  453. const menuBtn = document.createElement('li');
  454.  
  455. const link = document.createElement('a');
  456. link.classList.add('bold');
  457. link.style.color = '#0daf77';
  458. link.innerText = name;
  459.  
  460. const separator = document.createElement('div');
  461. separator.classList.add('account-menu-sep');
  462.  
  463. menuBtn.appendChild(link);
  464.  
  465. const latestMenuItem = document.querySelector('#AccountMenu > .blockLinksList > li:last-child');
  466. latestMenuItem.insertAdjacentElement('beforebegin', menuBtn);
  467. latestMenuItem.insertAdjacentElement('beforebegin', separator);
  468.  
  469. return menuBtn;
  470. }
  471.  
  472. function regModal(name, mainEl = '') {
  473. return XenForo.alert(mainEl, name, null, () => {
  474. document.querySelector('div.modal.fade').remove();
  475. })
  476. }
  477.  
  478. function setMenuTitle(modal, title) {
  479. const modalOverlay = modal?.[0];
  480. const modalTitle = modalOverlay?.querySelector('h2.heading');
  481. modalTitle.id = 'LZTStatsModalTitle';
  482. modalTitle.innerText = title;
  483. }
  484.  
  485. // * CLASSES
  486. class Tab {
  487. /**
  488. *
  489. * @constructor
  490. * @param {string} name - name of the tab
  491. * @param {string} tabId - id of the tab
  492. * @param {string} sectionId - id of the section
  493. * @param {boolean} active - status of tab
  494. */
  495.  
  496. constructor(name, tabId, sectionId, active) {
  497. this.name = name;
  498. this.tabId = tabId;
  499. this.sectionId = sectionId;
  500. this.active = active;
  501. }
  502.  
  503. createElement() {
  504. const tab = document.createElement('li');
  505. tab.classList.add('LZTStatsTab');
  506. tab.id = this.tabId;
  507.  
  508. const span = document.createElement('span');
  509. span.innerText = this.name;
  510.  
  511. tab.appendChild(span);
  512. tab.addEventListener('click', () => this.setActive());
  513. return tab;
  514. }
  515.  
  516. setActive() {
  517. document.querySelectorAll('.LZTStatsTab').forEach(tab => tab.classList.remove('active'));
  518.  
  519. document.getElementById(this.tabId).classList.add('active');
  520.  
  521. document.querySelectorAll('.LZTStatsModalContent > .LZTStatsSection').forEach(section => section.style.display = 'none');
  522.  
  523. document.getElementById(this.sectionId).style.display = '';
  524. }
  525. }
  526.  
  527.  
  528. // * HELPERS
  529. function renderMenu() {
  530. const menuBtn = regMenuBtn('LZT Stats');
  531. menuBtn.addEventListener('click', async () => {
  532. // * CREATE MODAL
  533. const modal = regModal('LZT Stats', '<div class="LZTStatsModalContent"></div>');
  534. setMenuTitle(modal, 'LZT Stats');
  535.  
  536. const modalContent = document.querySelector('.LZTStatsModalContent');
  537.  
  538. const tabsContainer = document.createElement('ul');
  539. tabsContainer.classList.add('LZTStatsTabs');
  540.  
  541. /**
  542. * items - list of objects (usually: id, timestamp)
  543. * label - title of the item
  544. * data - converted items to work with graph
  545. * icon - icon of the item
  546. * changeValue - Value compared to last time
  547. * hidden - default visibility in graph
  548. */
  549. const statsData = [
  550. {
  551. items: await GM_getValue('stats-messages') || [],
  552. label: 'Сообщений',
  553. icon: 'far fa-comment-alt',
  554. data: [],
  555. changeValue: null,
  556. hidden: false
  557. },
  558. {
  559. items: await GM_getValue('stats-messages-edited') || [],
  560. label: 'Изменено сообщений',
  561. icon: 'far fa-comment-alt-edit',
  562. data: [],
  563. changeValue: null,
  564. hidden: true
  565. },
  566. {
  567. items: await GM_getValue('stats-messages-deleted') || [],
  568. label: 'Удалено сообщений',
  569. icon: 'far fa-comment-alt-times',
  570. data: [],
  571. changeValue: null,
  572. hidden: true
  573. },
  574. {
  575. items: await GM_getValue('stats-comments') || [],
  576. label: 'Комментариев',
  577. icon: 'far fa-comment-alt-dots',
  578. data: [],
  579. changeValue: null,
  580. hidden: false
  581. },
  582. {
  583. items: await GM_getValue('stats-comments-edited') || [],
  584. label: 'Изменено комментариев',
  585. icon: 'far fa-comment-alt-edit',
  586. data: [],
  587. changeValue: null,
  588. hidden: true
  589. },
  590. {
  591. items: await GM_getValue('stats-comments-deleted') || [],
  592. label: 'Удалено комментариев',
  593. icon: 'far fa-comment-alt-times',
  594. data: [],
  595. changeValue: null,
  596. hidden: true
  597. },
  598. {
  599. items: await GM_getValue('stats-chat') || [],
  600. label: 'Сообщений в чате',
  601. icon: 'far fa-comments',
  602. data: [],
  603. changeValue: null,
  604. hidden: false
  605. },
  606. {
  607. items: await GM_getValue('stats-chat-edited') || [],
  608. label: 'Изменено в чате',
  609. icon: 'far fa-comment-edit',
  610. data: [],
  611. changeValue: null,
  612. hidden: true
  613. },
  614. {
  615. items: await GM_getValue('stats-chat-deleted') || [],
  616. label: 'Удалено из чата',
  617. icon: 'far fa-comment-times',
  618. data: [],
  619. changeValue: null,
  620. hidden: true
  621. },
  622. {
  623. items: await GM_getValue('stats-threads-created') || [],
  624. label: 'Создано тем',
  625. icon: 'far fa-file-alt',
  626. data: [],
  627. changeValue: null,
  628. hidden: false
  629. },
  630. {
  631. items: await GM_getValue('stats-threads-edited') || [],
  632. label: 'Изменено тем',
  633. icon: 'far fa-file-edit',
  634. data: [],
  635. changeValue: null,
  636. hidden: true
  637. },
  638. {
  639. items: await GM_getValue('stats-threads-deleted') || [],
  640. label: 'Удалено тем',
  641. icon: 'far fa-file-times',
  642. data: [],
  643. changeValue: null,
  644. hidden: true
  645. },
  646. {
  647. items: await GM_getValue('stats-participates') || [],
  648. label: 'Участий в розыгрышах',
  649. icon: 'far fa-gift',
  650. data: [],
  651. changeValue: null,
  652. hidden: false,
  653. },
  654. {
  655. items: await GM_getValue('stats-sympathies') || [],
  656. label: 'Поставлено лайков/симп',
  657. icon: 'far fa-heart',
  658. data: [],
  659. changeValue: null,
  660. hidden: true
  661. },
  662. {
  663. items: await GM_getValue('stats-reports') || [],
  664. label: 'Репортов',
  665. icon: 'far fa-bullhorn',
  666. data: [],
  667. changeValue: null,
  668. hidden: false
  669. },
  670. {
  671. items: await GM_getValue('stats-polls') || [],
  672. label: 'Пройдено опросов',
  673. icon: 'far fa-poll',
  674. data: [],
  675. changeValue: null,
  676. hidden: true
  677. },
  678. {
  679. items: await GM_getValue('stats-conversation') || [],
  680. label: 'Сообщений в ЛС',
  681. icon: 'far fa-comments-alt',
  682. data: [],
  683. changeValue: null,
  684. hidden: false
  685. },
  686. {
  687. items: await GM_getValue('stats-conversation-edited') || [],
  688. label: 'Изменено в ЛС',
  689. icon: 'far fa-comment-alt-edit',
  690. data: [],
  691. changeValue: null,
  692. hidden: true
  693. },
  694. ]
  695.  
  696. // ** STATS BY ALL TIME SECTION
  697. const allTimeSection = document.createElement('div');
  698. allTimeSection.classList.add('LZTStatsSection')
  699. allTimeSection.id = 'LZTStatsAllSection';
  700. await createTimeSection(allTimeSection, statsData, 12, MONTH_IN_SECS);
  701.  
  702. // ** STATS BY ALL MONTH SECTION
  703. const monthTimeSection = document.createElement('div');
  704. monthTimeSection.classList.add('LZTStatsSection')
  705. monthTimeSection.id = 'LZTStatsMonthSection';
  706. await createTimeSection(monthTimeSection, statsData, 30, DAY_IN_SECS);
  707.  
  708. // ** STATS BY ALL WEEK SECTION
  709. const weekTimeSection = document.createElement('div');
  710. weekTimeSection.classList.add('LZTStatsSection')
  711. weekTimeSection.id = 'LZTStatsWeekSection';
  712. await createTimeSection(weekTimeSection, statsData, 7, DAY_IN_SECS);
  713.  
  714. // ** STATS BY ALL DAY SECTION
  715. const dayTimeSection = document.createElement('div');
  716. dayTimeSection.classList.add('LZTStatsSection')
  717. dayTimeSection.id = 'LZTStatsDaySection';
  718. await createTimeSection(dayTimeSection, statsData, 24, HOUR_IN_SECS);
  719.  
  720. // ** CHAT SECTION
  721. const chatSection = document.createElement('div');
  722. chatSection.classList.add('LZTStatsSection');
  723. chatSection.id = 'LZTStatsChatSection';
  724.  
  725. const loadChatHistory = document.createElement('button');
  726. loadChatHistory.classList.add('button', 'primary', 'fit');
  727. loadChatHistory.id = 'LZTStatsLoadChatHistory'
  728. loadChatHistory.innerText = 'Загрузить историю чата';
  729. loadChatHistory.onclick = async () => {
  730. loadChatHistory.disabled = true;
  731. loadChatHistory.classList.add('disabled');
  732. loadChatHistory.innerText = 'Загружено';
  733.  
  734. const chatMessages = await GM_getValue('stats-chat') || [];
  735. const messagesEl = []
  736. const messagesTotalEl = document.createElement('div');
  737. messagesTotalEl.classList.add('LZTStatsTotalMessages');
  738. messagesTotalEl.innerText = `Всего сообщений: ${chatMessages.length}`
  739.  
  740. for (const msg of chatMessages.reverse()) {
  741. const chatEl = document.createElement('p');
  742. const chatTimeEl = document.createElement('p');
  743. chatEl.classList.add('LZTStatsChatComment')
  744. chatEl.innerHTML = msg?.message || 'текст не найден';
  745. const chatDate = new Date(msg.timestamp * 1000);
  746. chatTimeEl.innerText = `\n${formatDate(chatDate.getHours())}:${formatDate(chatDate.getMinutes())} ${formatDate(chatDate.getDate())}.${formatDate(chatDate.getMonth() + 1)}.${formatDate(chatDate.getFullYear())}`;
  747. chatEl.append(chatTimeEl);
  748. messagesEl.push(chatEl);
  749. }
  750.  
  751. chatSection.append(messagesTotalEl, ...messagesEl)
  752. }
  753. chatSection.append(loadChatHistory);
  754.  
  755. // ** SETTINGS SECTION
  756. const settingsSection = document.createElement('div');
  757. settingsSection.classList.add('LZTStatsSection');
  758. settingsSection.id = 'LZTStatsSettingsSection';
  759.  
  760. const settingsDownloadEl = document.createElement('div');
  761. settingsDownloadEl.classList.add('LZTStatsSectionItem');
  762.  
  763. const sectionIcon = document.createElement('i');
  764. sectionIcon.classList.add('far', 'fa-file-download')
  765.  
  766. const textContainer = document.createElement('div');
  767. textContainer.classList.add('LZTStatsSectionTextContainer');
  768.  
  769. const textEl = document.createElement('span');
  770. textEl.classList.add('LZTStatsSectionTitle');
  771. textEl.innerText = 'Скачать собранные данные';
  772.  
  773. textContainer.append(textEl);
  774. settingsDownloadEl.append(sectionIcon, textContainer);
  775. settingsSection.append(settingsDownloadEl)
  776.  
  777. settingsDownloadEl.onclick = () => {
  778. downloadJSONFile(JSON.stringify(statsData.map(e => e.items)), 'LZTStats')
  779. }
  780.  
  781. // ** MODAL EXECUTORS
  782. modalContent.append(tabsContainer, allTimeSection, monthTimeSection, weekTimeSection, dayTimeSection, chatSection, settingsSection);
  783.  
  784. const tabs = [
  785. new Tab('За год', 'LZTStatsAllTab', 'LZTStatsAllSection', true),
  786. new Tab('За месяц', 'LZTStatsMonthTab', 'LZTStatsMonthSection', false),
  787. new Tab('За неделю', 'LZTStatsWeekTab', 'LZTStatsWeekSection', false),
  788. new Tab('За день', 'LZTStatsDayTab', 'LZTStatsDaySection', false),
  789. new Tab('Чат', 'LZTStatsChatTab', 'LZTStatsChatSection', false),
  790. new Tab('Настройки', 'LZTStatsSettingsTab', 'LZTStatsSettingsSection', false),
  791. ]
  792.  
  793. for (const tab of tabs) {
  794. tabsContainer.appendChild(tab.createElement());
  795. tab.active ? tab.setActive() : null;
  796. }
  797. })
  798. }
  799.  
  800. async function createTimeSection(containerEl, statsData, GRAPH_LENGTH, GRAPH_TIME_FORMAT) {
  801. statsData.forEach(s => s.data = getSumValuesByTime(calcSumByTime(s.items, GRAPH_TIME_FORMAT, GRAPH_LENGTH)))
  802. const currentTimestamp = getTimestamp();
  803.  
  804. const calculatedGraphTime = GRAPH_TIME_FORMAT * GRAPH_LENGTH;
  805. const lastPageDate = currentTimestamp - (calculatedGraphTime * 2)
  806. const thisPageDate = currentTimestamp - calculatedGraphTime
  807.  
  808. if (GRAPH_TIME_FORMAT !== MONTH_IN_SECS) {
  809. // skip year format info
  810. statsData.forEach(s => s.changeValue = s.items.filter(i => i.timestamp > thisPageDate).length - s.items.filter(i => i.timestamp > lastPageDate && i.timestamp < thisPageDate).length);
  811. }
  812.  
  813. const timeArray = getTimeArray(GRAPH_TIME_FORMAT, GRAPH_LENGTH);
  814.  
  815. const statsContainer = document.createElement('div');
  816. statsContainer.classList.add('LZTStatsInfo')
  817.  
  818. const statsItems = statsData.map(s => createStatsItem(s.label, s.items.filter(i => i.timestamp > thisPageDate).length, s.icon, s.changeValue));
  819. statsContainer.append(...statsItems);
  820.  
  821. const graph = document.createElement('canvas');
  822. if (document.querySelector('.xenOverlay.slim')) {
  823. graph.width = 300;
  824. graph.height = 600;
  825. } else {
  826. graph.width = 600;
  827. graph.height = 450;
  828. }
  829. graph.id = `LZTStatsGraph-${GRAPH_LENGTH}-${GRAPH_TIME_FORMAT}`;
  830.  
  831. containerEl.append(statsContainer, graph);
  832.  
  833. new Chart(graph, {
  834. type: 'line',
  835. data: {
  836. labels: timeArray.reverse(),
  837. datasets: statsData.map(s => ({
  838. label: s.label,
  839. data: s.data,
  840. borderWidth: 1,
  841. hidden: s.hidden
  842. }))
  843. },
  844. options: {
  845. scales: {
  846. y: {
  847. beginAtZero: true
  848. }
  849. },
  850. onClick: (e) => {
  851. console.log(e)
  852. }
  853. }
  854. });
  855. }
  856.  
  857. function createStatsItem(title, value, iconClasses = '', changeValue = null) {
  858. const item = document.createElement('div');
  859. item.classList.add('LZTStatsItem');
  860.  
  861. const iconContainer = document.createElement('div');
  862. const icon = document.createElement('i');
  863. icon.classList.add(...iconClasses.split(' ')); // convert to the view that FontAwesome works with
  864. iconContainer.appendChild(icon)
  865.  
  866. const textContainer = document.createElement('div');
  867. textContainer.innerText = value
  868. const textTitle = document.createElement('p');
  869. textTitle.innerText = title;
  870. textContainer.insertAdjacentElement('afterbegin', textTitle)
  871.  
  872. item.append(iconContainer, textContainer)
  873.  
  874. if (typeof changeValue === 'number') {
  875. const changeContainer = document.createElement('div');
  876. changeContainer.classList.add('LZTStatsChangeInfo')
  877. changeContainer.innerText = changeValue;
  878. const changeIcon = document.createElement('i');
  879.  
  880. // Set icon and style by number that has changed
  881. // default: Just like last time
  882. let changeIconClasses = 'far fa-arrows-alt-v'
  883. let changeIconStyle = '#D6D6D6';
  884. if (changeValue < 0) {
  885. // Less than last time
  886. changeIconClasses = 'fas fa-caret-down'
  887. changeIconStyle = '#ea4c4c';
  888. } else if (changeValue > 0) {
  889. // More than last time
  890. changeIconClasses = 'fas fa-caret-up'
  891. changeIconStyle = '#0daf77';
  892. }
  893.  
  894. changeIcon.classList.add(...changeIconClasses.split(' ')); // convert to the view that FontAwesome works with
  895. changeIcon.style = `color: ${changeIconStyle}`;
  896. changeContainer.appendChild(changeIcon)
  897. item.append(changeContainer);
  898. }
  899.  
  900. return item
  901. }
  902.  
  903. // * UTILS
  904. function getTimestamp() {
  905. return Math.floor(Date.now() / 1000);
  906. }
  907.  
  908. function roundMinutes(date) {
  909. // https://stackoverflow.com/questions/7293306/how-to-round-to-nearest-hour-using-javascript-date-object
  910. date.setHours(date.getHours() + Math.round(date.getMinutes() / 60));
  911. date.setMinutes(0, 0, 0); // Resets also seconds and milliseconds
  912.  
  913. return date;
  914. }
  915.  
  916. function formatDate(dateString) {
  917. // format X hours, minutes and etc to 0X. Ex. 1 -> 01, 12 -> 12
  918. return ('0' + dateString).slice(-2);
  919. }
  920.  
  921. function getThreadIdByURL(threadURL) {
  922. const threadId = threadURL.match(FIND_THREAD_ID)?.[1]?.split('/')?.[0];
  923. return Number(threadId) || 0;
  924. }
  925.  
  926. function getPostIdByURL(postURL) {
  927. const postId = postURL.match(FIND_POST_ID)?.[1]?.split('/')?.[0];
  928. return Number(postId) || 0;
  929. }
  930.  
  931. function getUserIdByURL(userURL) {
  932. const matched = userURL.match(FIND_USER_POST) || userURL.match(FIND_USER_ID);
  933. const userId = matched?.[1]?.split('/')?.[0];
  934. return String(userId) || 0;
  935. }
  936.  
  937. function getCommentPostIdByURL(postURL) {
  938. const postId = postURL.match(FIND_COMMENT_POST_ID)?.[1]?.split('/')?.[0];
  939. return Number(postId) || 0;
  940. }
  941.  
  942. function getForumIdByURL(forumURL) {
  943. const forumId = forumURL.match(FIND_FORUM_ID)?.[1]?.split('/')?.[0];
  944. return String(forumId) || 0;
  945. }
  946.  
  947. function getPostIdByFullURL(postURL) {
  948. const postId = postURL.match(FIND_POST_ID_IN_FULL_URL)?.[1]?.split('/')?.[0];
  949. return Number(postId) || 0;
  950. }
  951.  
  952. function getChatIdByURL(chatURL) {
  953. const chatId = chatURL.match(FIND_CHAT_ID)?.[1]?.split('/')?.[0];
  954. return Number(chatId) || 0;
  955. }
  956.  
  957. function getConversationIdByURL(conversationURL) {
  958. const conversationId = conversationURL.match(FIND_CONVERSATION_ID)?.[1]?.split('/')?.[0];
  959. return Number(conversationId) || 0;
  960. }
  961.  
  962. function calcSumByTime(data, timeFormat = DAY_IN_SECS, maxLength = 7) {
  963. let sepatedData = {};
  964. let timestamp = getTimestamp();
  965.  
  966. while (Object.keys(sepatedData).length < maxLength) {
  967. const temp = data.filter(m => m.timestamp > timestamp - timeFormat && m.timestamp < timestamp);
  968. const date = roundMinutes(new Date((timestamp - timeFormat) * 1000));
  969. let dateString = `${formatDate(date.getDate())}.${formatDate(date.getMonth() + 1)}`;
  970. if (timeFormat === HOUR_IN_SECS) {
  971. dateString = `${formatDate(date.getHours())}:${formatDate(date.getMinutes())}`;
  972. }
  973.  
  974. sepatedData[dateString] = temp;
  975. timestamp -= timeFormat;
  976. }
  977.  
  978. return sepatedData;
  979. }
  980.  
  981. function getSumValuesByTime(sumData) {
  982. return Object.values(sumData).map(m => m.length).reverse();
  983. }
  984.  
  985. function getTimeArray(timeFormat = DAY_IN_SECS, maxLength = 7) {
  986. let timeArray = [];
  987. let timestamp = getTimestamp();
  988.  
  989. for (let i = 0; i < maxLength; i++) {
  990. const date = roundMinutes(new Date((timestamp - timeFormat) * 1000));
  991. if (timeFormat === HOUR_IN_SECS) {
  992. timeArray.push(`${formatDate(date.getHours())}:${formatDate(date.getMinutes())}`);
  993. } else {
  994. timeArray.push(`${formatDate(date.getDate())}.${formatDate(date.getMonth() + 1)}`); // month start with 0
  995. }
  996.  
  997. timestamp -= timeFormat;
  998. }
  999.  
  1000. return timeArray;
  1001. }
  1002.  
  1003. function downloadJSONFile(data, name) {
  1004. const blob = new Blob([data], {
  1005. type: 'application/json'
  1006. });
  1007. const link = document.createElement('a');
  1008. link.href = window.URL.createObjectURL(blob);
  1009. link.download = `${name}.json`;
  1010. link.click();
  1011. return link;
  1012. }
  1013.  
  1014. /**
  1015. * Save stats in GM Storage
  1016. *
  1017. * @param {object} stats - object of stats (ex.: id, message, timestamp)
  1018. * @param {string} valueName - name of storage
  1019. */
  1020. function saveStats(stats, valueName) {
  1021. console.debug("[LZTStats.saveStats]", stats)
  1022. let oldData = GM_getValue(valueName);
  1023. if (oldData === undefined) {
  1024. oldData = []
  1025. }
  1026.  
  1027. oldData.push(stats)
  1028.  
  1029. GM_setValue(valueName, oldData);
  1030. }
  1031.  
  1032.  
  1033. // * MAIN
  1034. function init() {
  1035. initHooks()
  1036. renderMenu()
  1037. }
  1038.  
  1039. init()
  1040. })();