- // ==UserScript==
- // @name LZT Stats
- // @namespace lzt-stats
- // @version 1.01
- // @description Detailed statistics of your activity
- // @author Toil
- // @license MIT
- // @match https://zelenka.guru/*
- // @match https://lolz.live/*
- // @match https://lolz.guru/*
- // @match https://lzt.market/*
- // @match https://lolz.market/*
- // @match https://zelenka.market/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=zelenka.guru
- // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js
- // @supportURL https://zelenka.guru/toil/
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @grant GM_listValues
- // @grant GM_addStyle
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // * STYLES
- GM_addStyle(`
- .LZTStatsTabs {
- width: 100%;
- box-sizing: border-box;
- padding: 0 10px;
- border: none !important;
- margin: 15px auto !important;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- @media screen and (max-width: 699px) {
- .LZTStatsTabs {
- flex-direction: column;
- }
- }
-
- .LZTStatsTab {
- position: relative;
- padding: 10px;
- margin: 0 4px;
- float: left;
- font-weight: 600;
- list-style: none !important;
- font-size: 14px;
- }
-
- .LZTStatsTab:hover {
- cursor: pointer;
- }
-
- .LZTStatsTab.active {
- box-shadow: inset 0px -2px 0px 0px #0daf77;
- transform: translateY(-1px);
- transition: .2s;
- }
-
- .LZTStatsTab:not(.active):hover {
- box-shadow: inset 0px -2px 0px 0px rgb(54, 54, 54);
- }
-
- .LZTStatsInfo {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- }
-
- @media screen and (max-width: 699px) {
- .LZTStatsInfo {
- flex-direction: column;
- align-content: center;
- }
- }
-
- .LZTStatsInfo .LZTStatsItem {
- height: 63px;
- background: #2D2D2D;
- border-radius: 8px;
- padding: 12px;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- gap: 12px;
- white-space: normal;
- }
-
- @media screen and (min-width: 700px) {
- .LZTStatsInfo .LZTStatsItem {
- width: 32%;
- }
- }
-
- @media screen and (max-width: 699px) {
- .LZTStatsInfo .LZTStatsItem {
- width: 90%;
- }
- }
-
- .LZTStatsInfo .LZTStatsItem i {
- width: 24px;
- height: 24px;
- font-size: 24px;
- }
-
- .LZTStatsInfo .LZTStatsItem p {
- font-weight: 400;
- font-size: 13px;
- line-height: 16px;
- color: #D6D6D6;
- opacity: 0.8;
- margin-bottom: 6px;
- max-width: 95px;
- }
-
- .LZTStatsInfo .LZTStatsItem .LZTStatsChangeInfo {
- height: 16px;
- margin-left: auto;
- display: flex;
- }
-
- .LZTStatsInfo .LZTStatsItem .LZTStatsChangeInfo i {
- width: 16px;
- height: 16px;
- font-size: 16px;
- margin-left: 2px;
- }
-
- #LZTStatsModalTitle {
- text-align: center;
- padding: 16px;
- font-size: 20px;
- font-weight: bold;
- }
-
- .LZTStatsChatComment {
- background: rgb(54, 54, 54);
- margin: 5px 15px;
- padding: 10px 15px;
- border-radius: 10px;
- }
-
- .LZTStatsSectionItem {
- max-width: 580px;
- flex-basis: 50%;
- flex-grow: 1;
- height: 64px;
- display: flex;
- align-items: center;
-
- transition: all 0.5s ease;
- }
-
- .LZTStatsSectionItem:hover {
- background: rgba(54, 54, 54, 0.75);
- border-radius: 8px;
- cursor: pointer;
- }
-
- .LZTUpSectionTextContainer {
- display: flex;
- flex-direction: column;
- justify-content: center;
- flex: 1 1 auto;
- max-width: 100%;
- }
-
- .LZTStatsSectionItem i {
- width: 28px;
- height: 28px;
- margin: 20px;
- font-size: 28px;
- color: #0daf77;
- }
-
- .LZTStatsSectionTitle {
- display: block;
- margin-right: 20px;
- font-size: 15px;
- font-weight: bold;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- }
-
- #LZTStatsLoadChatHistory, .LZTStatsTotalMessages {
- margin: 15px;
- }
- `)
-
- // * CONSTANTS
- const HOUR_IN_SECS = 3600;
- const DAY_IN_SECS = HOUR_IN_SECS * 24;
- const MONTH_IN_SECS = DAY_IN_SECS * 30;
-
- // * REGEXES
- const SEND_IN_THREAD = /^threads\/([^d]+)\/add-reply$/
- const COMMENT_IN_THREAD = /^posts\/([^d]+)\/comment$/
-
- const DELETE_MESSAGE_IN_THREAD = /^posts\/([^d]+)\/delete$/
- const DELETE_COMMENT_IN_THREAD = /^posts\/comments\/([^d]+)\/delete$/
-
- const EDITED_MESSAGE_IN_THREAD = /^posts\/([^d]+)\/save-inline$/
- const EDITED_COMMENT_IN_THREAD = /^posts\/comments\/([^d]+)\/save-inline$/
-
- const PARTICIPATE_IN_CONTEST = /^threads\/([^d]+)\/participate/ // ! DON'T ADD ANYTHING TO THE END!! THERE'S A CAPTCHA GOING ON
- const SYMPATHY_IN_THREAD = /\/posts\/([^d]+)\/like$/
- const REPORT_IN_THREAD = /^posts\/([^d]+)\/report$/
-
- const SEND_IN_PROFILE_USERID = /^members\/([^d]+)\/post$/
- const SEND_IN_PROFILE_TAG = /^([-\w]+)\/post$/
- const COMMENT_IN_PROFILE = /^profile-posts\/([^d]+)\/comment$/
-
- const POLL_IN_THREAD = /^threads\/([^d]+)\/poll\/vote/
-
- const SEND_IN_CHAT = /^\/chatbox\/post-message$/
- const EDIT_IN_CHAT = /^\/chatbox\/([^d]+)\/edit$/
- const DELETE_IN_CHAT = /^\/chatbox\/([^d]+)\/delete$/
-
- const CREATE_NEW_THREAD = /^forums\/([-\w]+)\/add-thread$/
- const EDIT_THREAD = /^threads\/([^d]+)\/save$/
- const DELETE_THREAD = /^threads\/([^d]+)\/delete$/
-
- const SEND_IN_CONVERSATION = /^conversations\/([^d]+)\/insert-reply$/
- const EDIT_IN_CONVERSATION = /^conversations\/([^d]+)\/save-message/ // ! DON'T ADD ANYTHING TO THE END!! THERE GOES THE MESSAGE NUMBER
-
- const FIND_THREAD_ID = /^threads\/([^d]+)\//
- const FIND_POST_ID = /^posts\/([^d]+)\//
- const FIND_USER_POST = /^([-\w]+)\/post$/
- const FIND_USER_ID = /^members\/([^d]+)\//
- const FIND_COMMENT_POST_ID = /^posts\/comments\/([^d]+)\//
- const FIND_FORUM_ID = /^forums\/([-\w]+)\//
- const FIND_POST_ID_IN_FULL_URL = /\/posts\/([^d]+)\//
- const FIND_CHAT_ID = /^\/chatbox\/([^d]+)\//
- const FIND_CONVERSATION_ID = /^conversations\/([^d]+)\//
-
- // * HOOKS
- function initHooks() {
- // reference: https://github.com/LOLZHelper/LOLZHelperReborn/blob/main/src/common/hooks.js
- const XF_AJAX = XenForo.ajax;
-
- XenForo.ajax = function () {
- console.debug('[LZTStats.initHooks] [XenForo.ajax]', arguments);
- const url = arguments[0];
- const data = arguments[1];
- const success = arguments[2];
- const options = arguments[3];
-
- const timestamp = getTimestamp();
- switch (true) {
- case SEND_IN_THREAD.test(url):
- {
- const threadId = getThreadIdByURL(url);
- saveStats({
- id: threadId,
- timestamp
- }, 'stats-messages')
- break;
- }
- case COMMENT_IN_THREAD.test(url):
- {
- const postId = getPostIdByURL(url);
- saveStats({
- id: postId,
- timestamp
- }, 'stats-comments')
- break;
- }
- case PARTICIPATE_IN_CONTEST.test(url):
- {
- const threadId = getThreadIdByURL(url);
- saveStats({
- id: threadId,
- timestamp
- }, 'stats-participates');
- break;
- }
- case SYMPATHY_IN_THREAD.test(url):
- {
- // ! SYMPATHIES AND LIKES HAVE THE SAME URL
- const postId = getPostIdByFullURL(url);
- saveStats({
- id: postId,
- timestamp
- }, 'stats-sympathies');
- break;
- }
- case REPORT_IN_THREAD.test(url):
- {
- const postId = getPostIdByURL(url);
- saveStats({
- id: postId,
- // message: data?.[1] || '', // weighs a lot
- timestamp
- }, 'stats-reports');
- break;
- }
- case CREATE_NEW_THREAD.test(url):
- {
- const forumId = getForumIdByURL(url);
- saveStats({
- id: forumId,
- timestamp
- }, 'stats-threads-created')
- break;
- }
- case EDIT_THREAD.test(url):
- {
- const threadId = getThreadIdByURL(url);
- saveStats({
- id: threadId,
- timestamp
- }, 'stats-threads-edited')
- break;
- }
- case DELETE_THREAD.test(url):
- {
- const threadId = getThreadIdByURL(url);
- saveStats({
- id: threadId,
- timestamp
- }, 'stats-threads-deleted')
- break;
- }
- case (DELETE_COMMENT_IN_THREAD.test(url) && data.length > 0):
- {
- const postId = getCommentPostIdByURL(url);
- saveStats({
- id: postId,
- timestamp
- }, 'stats-comments-deleted')
- break;
- }
- case (DELETE_MESSAGE_IN_THREAD.test(url) && data.length > 0):
- {
- // ! Do not switch places with comments or you will have to fix the regex
- const postId = getPostIdByURL(url);
- saveStats({
- id: postId,
- timestamp
- }, 'stats-messages-deleted')
- break;
- }
- case EDITED_COMMENT_IN_THREAD.test(url):
- {
- const postId = getCommentPostIdByURL(url);
- saveStats({
- id: postId,
- timestamp
- }, 'stats-comments-edited')
- break;
- }
- case (EDITED_MESSAGE_IN_THREAD.test(url)):
- {
- // ! Do not switch places with comments or you will have to fix the regex
- const postId = getPostIdByURL(url);
- saveStats({
- id: postId,
- timestamp
- }, 'stats-messages-edited')
- break;
- }
- case (POLL_IN_THREAD.test(url) && data.length > 0):
- {
- const threadId = getThreadIdByURL(url);
- saveStats({
- id: threadId,
- timestamp
- }, 'stats-polls')
- break;
- }
- case (SEND_IN_PROFILE_USERID.test(url) || (SEND_IN_PROFILE_TAG.test(url) && (window.location.pathname.replace('/', '') + '/post') === url)):
- {
- // Check by ID, if the user has a tag, then check so that the path matches the name in the link
- const userId = getUserIdByURL(url); // it can also return the tag
- saveStats({
- id: userId,
- timestamp
- }, 'stats-messages');
- break;
- }
- case (COMMENT_IN_PROFILE.test(url)):
- {
- const postId = getCommentPostIdByURL(url);
- saveStats({
- id: postId,
- timestamp
- }, 'stats-comments');
- break;
- }
- case (SEND_IN_CHAT.test(url)):
- {
- saveStats({
- id: 0,
- message: data?.message,
- timestamp
- }, 'stats-chat');
- break;
- }
- case (EDIT_IN_CHAT.test(url)):
- {
- const chatId = getChatIdByURL(url);
- saveStats({
- id: chatId,
- timestamp
- }, 'stats-chat-edited');
- break;
- }
- case (DELETE_IN_CHAT.test(url)):
- {
- const chatId = getChatIdByURL(url);
- saveStats({
- id: chatId,
- timestamp
- }, 'stats-chat-deleted');
- break;
- }
- case (SEND_IN_CONVERSATION.test(url)):
- {
- const conversationId = getConversationIdByURL(url);
- saveStats({
- id: conversationId,
- timestamp
- }, 'stats-conversation');
- break;
- }
- case (EDIT_IN_CONVERSATION.test(url)):
- {
- const conversationId = getConversationIdByURL(url);
- saveStats({
- id: conversationId,
- timestamp
- }, 'stats-conversation-edited');
- break;
- }
- default:
- break;
- }
-
- return XF_AJAX.apply(this, arguments);
- }
- }
-
- // * REGISTERS
- function regMenuBtn(name) {
- const menuBtn = document.createElement('li');
-
- const link = document.createElement('a');
- link.classList.add('bold');
- link.style.color = '#0daf77';
- link.innerText = name;
-
- const separator = document.createElement('div');
- separator.classList.add('account-menu-sep');
-
- menuBtn.appendChild(link);
-
- const latestMenuItem = document.querySelector('#AccountMenu > .blockLinksList > li:last-child');
- latestMenuItem.insertAdjacentElement('beforebegin', menuBtn);
- latestMenuItem.insertAdjacentElement('beforebegin', separator);
-
- return menuBtn;
- }
-
- function regModal(name, mainEl = '') {
- return XenForo.alert(mainEl, name, null, () => {
- document.querySelector('div.modal.fade').remove();
- })
- }
-
- function setMenuTitle(modal, title) {
- const modalOverlay = modal?.[0];
- const modalTitle = modalOverlay?.querySelector('h2.heading');
- modalTitle.id = 'LZTStatsModalTitle';
- modalTitle.innerText = title;
- }
-
- // * CLASSES
- class Tab {
- /**
- *
- * @constructor
- * @param {string} name - name of the tab
- * @param {string} tabId - id of the tab
- * @param {string} sectionId - id of the section
- * @param {boolean} active - status of tab
- */
-
- constructor(name, tabId, sectionId, active) {
- this.name = name;
- this.tabId = tabId;
- this.sectionId = sectionId;
- this.active = active;
- }
-
- createElement() {
- const tab = document.createElement('li');
- tab.classList.add('LZTStatsTab');
- tab.id = this.tabId;
-
- const span = document.createElement('span');
- span.innerText = this.name;
-
- tab.appendChild(span);
- tab.addEventListener('click', () => this.setActive());
- return tab;
- }
-
- setActive() {
- document.querySelectorAll('.LZTStatsTab').forEach(tab => tab.classList.remove('active'));
-
- document.getElementById(this.tabId).classList.add('active');
-
- document.querySelectorAll('.LZTStatsModalContent > .LZTStatsSection').forEach(section => section.style.display = 'none');
-
- document.getElementById(this.sectionId).style.display = '';
- }
- }
-
-
- // * HELPERS
- function renderMenu() {
- const menuBtn = regMenuBtn('LZT Stats');
- menuBtn.addEventListener('click', async () => {
- // * CREATE MODAL
- const modal = regModal('LZT Stats', '<div class="LZTStatsModalContent"></div>');
- setMenuTitle(modal, 'LZT Stats');
-
- const modalContent = document.querySelector('.LZTStatsModalContent');
-
- const tabsContainer = document.createElement('ul');
- tabsContainer.classList.add('LZTStatsTabs');
-
- /**
- * items - list of objects (usually: id, timestamp)
- * label - title of the item
- * data - converted items to work with graph
- * icon - icon of the item
- * changeValue - Value compared to last time
- * hidden - default visibility in graph
- */
- const statsData = [
- {
- items: await GM_getValue('stats-messages') || [],
- label: 'Сообщений',
- icon: 'far fa-comment-alt',
- data: [],
- changeValue: null,
- hidden: false
- },
- {
- items: await GM_getValue('stats-messages-edited') || [],
- label: 'Изменено сообщений',
- icon: 'far fa-comment-alt-edit',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-messages-deleted') || [],
- label: 'Удалено сообщений',
- icon: 'far fa-comment-alt-times',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-comments') || [],
- label: 'Комментариев',
- icon: 'far fa-comment-alt-dots',
- data: [],
- changeValue: null,
- hidden: false
- },
- {
- items: await GM_getValue('stats-comments-edited') || [],
- label: 'Изменено комментариев',
- icon: 'far fa-comment-alt-edit',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-comments-deleted') || [],
- label: 'Удалено комментариев',
- icon: 'far fa-comment-alt-times',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-chat') || [],
- label: 'Сообщений в чате',
- icon: 'far fa-comments',
- data: [],
- changeValue: null,
- hidden: false
- },
- {
- items: await GM_getValue('stats-chat-edited') || [],
- label: 'Изменено в чате',
- icon: 'far fa-comment-edit',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-chat-deleted') || [],
- label: 'Удалено из чата',
- icon: 'far fa-comment-times',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-threads-created') || [],
- label: 'Создано тем',
- icon: 'far fa-file-alt',
- data: [],
- changeValue: null,
- hidden: false
- },
- {
- items: await GM_getValue('stats-threads-edited') || [],
- label: 'Изменено тем',
- icon: 'far fa-file-edit',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-threads-deleted') || [],
- label: 'Удалено тем',
- icon: 'far fa-file-times',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-participates') || [],
- label: 'Участий в розыгрышах',
- icon: 'far fa-gift',
- data: [],
- changeValue: null,
- hidden: false,
- },
- {
- items: await GM_getValue('stats-sympathies') || [],
- label: 'Поставлено лайков/симп',
- icon: 'far fa-heart',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-reports') || [],
- label: 'Репортов',
- icon: 'far fa-bullhorn',
- data: [],
- changeValue: null,
- hidden: false
- },
- {
- items: await GM_getValue('stats-polls') || [],
- label: 'Пройдено опросов',
- icon: 'far fa-poll',
- data: [],
- changeValue: null,
- hidden: true
- },
- {
- items: await GM_getValue('stats-conversation') || [],
- label: 'Сообщений в ЛС',
- icon: 'far fa-comments-alt',
- data: [],
- changeValue: null,
- hidden: false
- },
- {
- items: await GM_getValue('stats-conversation-edited') || [],
- label: 'Изменено в ЛС',
- icon: 'far fa-comment-alt-edit',
- data: [],
- changeValue: null,
- hidden: true
- },
- ]
-
- // ** STATS BY ALL TIME SECTION
- const allTimeSection = document.createElement('div');
- allTimeSection.classList.add('LZTStatsSection')
- allTimeSection.id = 'LZTStatsAllSection';
- await createTimeSection(allTimeSection, statsData, 12, MONTH_IN_SECS);
-
- // ** STATS BY ALL MONTH SECTION
- const monthTimeSection = document.createElement('div');
- monthTimeSection.classList.add('LZTStatsSection')
- monthTimeSection.id = 'LZTStatsMonthSection';
- await createTimeSection(monthTimeSection, statsData, 30, DAY_IN_SECS);
-
- // ** STATS BY ALL WEEK SECTION
- const weekTimeSection = document.createElement('div');
- weekTimeSection.classList.add('LZTStatsSection')
- weekTimeSection.id = 'LZTStatsWeekSection';
- await createTimeSection(weekTimeSection, statsData, 7, DAY_IN_SECS);
-
- // ** STATS BY ALL DAY SECTION
- const dayTimeSection = document.createElement('div');
- dayTimeSection.classList.add('LZTStatsSection')
- dayTimeSection.id = 'LZTStatsDaySection';
- await createTimeSection(dayTimeSection, statsData, 24, HOUR_IN_SECS);
-
- // ** CHAT SECTION
- const chatSection = document.createElement('div');
- chatSection.classList.add('LZTStatsSection');
- chatSection.id = 'LZTStatsChatSection';
-
- const loadChatHistory = document.createElement('button');
- loadChatHistory.classList.add('button', 'primary', 'fit');
- loadChatHistory.id = 'LZTStatsLoadChatHistory'
- loadChatHistory.innerText = 'Загрузить историю чата';
- loadChatHistory.onclick = async () => {
- loadChatHistory.disabled = true;
- loadChatHistory.classList.add('disabled');
- loadChatHistory.innerText = 'Загружено';
-
- const chatMessages = await GM_getValue('stats-chat') || [];
- const messagesEl = []
- const messagesTotalEl = document.createElement('div');
- messagesTotalEl.classList.add('LZTStatsTotalMessages');
- messagesTotalEl.innerText = `Всего сообщений: ${chatMessages.length}`
-
- for (const msg of chatMessages.reverse()) {
- const chatEl = document.createElement('p');
- const chatTimeEl = document.createElement('p');
- chatEl.classList.add('LZTStatsChatComment')
- chatEl.innerHTML = msg?.message || 'текст не найден';
- const chatDate = new Date(msg.timestamp * 1000);
- chatTimeEl.innerText = `\n${formatDate(chatDate.getHours())}:${formatDate(chatDate.getMinutes())} ${formatDate(chatDate.getDate())}.${formatDate(chatDate.getMonth() + 1)}.${formatDate(chatDate.getFullYear())}`;
- chatEl.append(chatTimeEl);
- messagesEl.push(chatEl);
- }
-
- chatSection.append(messagesTotalEl, ...messagesEl)
- }
- chatSection.append(loadChatHistory);
-
- // ** SETTINGS SECTION
- const settingsSection = document.createElement('div');
- settingsSection.classList.add('LZTStatsSection');
- settingsSection.id = 'LZTStatsSettingsSection';
-
- const settingsDownloadEl = document.createElement('div');
- settingsDownloadEl.classList.add('LZTStatsSectionItem');
-
- const sectionIcon = document.createElement('i');
- sectionIcon.classList.add('far', 'fa-file-download')
-
- const textContainer = document.createElement('div');
- textContainer.classList.add('LZTStatsSectionTextContainer');
-
- const textEl = document.createElement('span');
- textEl.classList.add('LZTStatsSectionTitle');
- textEl.innerText = 'Скачать собранные данные';
-
- textContainer.append(textEl);
- settingsDownloadEl.append(sectionIcon, textContainer);
- settingsSection.append(settingsDownloadEl)
-
- settingsDownloadEl.onclick = () => {
- downloadJSONFile(JSON.stringify(statsData.map(e => e.items)), 'LZTStats')
- }
-
- // ** MODAL EXECUTORS
- modalContent.append(tabsContainer, allTimeSection, monthTimeSection, weekTimeSection, dayTimeSection, chatSection, settingsSection);
-
- const tabs = [
- new Tab('За год', 'LZTStatsAllTab', 'LZTStatsAllSection', true),
- new Tab('За месяц', 'LZTStatsMonthTab', 'LZTStatsMonthSection', false),
- new Tab('За неделю', 'LZTStatsWeekTab', 'LZTStatsWeekSection', false),
- new Tab('За день', 'LZTStatsDayTab', 'LZTStatsDaySection', false),
- new Tab('Чат', 'LZTStatsChatTab', 'LZTStatsChatSection', false),
- new Tab('Настройки', 'LZTStatsSettingsTab', 'LZTStatsSettingsSection', false),
- ]
-
- for (const tab of tabs) {
- tabsContainer.appendChild(tab.createElement());
- tab.active ? tab.setActive() : null;
- }
- })
- }
-
- async function createTimeSection(containerEl, statsData, GRAPH_LENGTH, GRAPH_TIME_FORMAT) {
- statsData.forEach(s => s.data = getSumValuesByTime(calcSumByTime(s.items, GRAPH_TIME_FORMAT, GRAPH_LENGTH)))
- const currentTimestamp = getTimestamp();
-
- const calculatedGraphTime = GRAPH_TIME_FORMAT * GRAPH_LENGTH;
- const lastPageDate = currentTimestamp - (calculatedGraphTime * 2)
- const thisPageDate = currentTimestamp - calculatedGraphTime
-
- if (GRAPH_TIME_FORMAT !== MONTH_IN_SECS) {
- // skip year format info
- statsData.forEach(s => s.changeValue = s.items.filter(i => i.timestamp > thisPageDate).length - s.items.filter(i => i.timestamp > lastPageDate && i.timestamp < thisPageDate).length);
- }
-
- const timeArray = getTimeArray(GRAPH_TIME_FORMAT, GRAPH_LENGTH);
-
- const statsContainer = document.createElement('div');
- statsContainer.classList.add('LZTStatsInfo')
-
- const statsItems = statsData.map(s => createStatsItem(s.label, s.items.filter(i => i.timestamp > thisPageDate).length, s.icon, s.changeValue));
- statsContainer.append(...statsItems);
-
- const graph = document.createElement('canvas');
- if (document.querySelector('.xenOverlay.slim')) {
- graph.width = 300;
- graph.height = 600;
- } else {
- graph.width = 600;
- graph.height = 450;
- }
- graph.id = `LZTStatsGraph-${GRAPH_LENGTH}-${GRAPH_TIME_FORMAT}`;
-
- containerEl.append(statsContainer, graph);
-
- new Chart(graph, {
- type: 'line',
- data: {
- labels: timeArray.reverse(),
- datasets: statsData.map(s => ({
- label: s.label,
- data: s.data,
- borderWidth: 1,
- hidden: s.hidden
- }))
- },
- options: {
- scales: {
- y: {
- beginAtZero: true
- }
- },
- onClick: (e) => {
- console.log(e)
- }
- }
- });
- }
-
- function createStatsItem(title, value, iconClasses = '', changeValue = null) {
- const item = document.createElement('div');
- item.classList.add('LZTStatsItem');
-
- const iconContainer = document.createElement('div');
- const icon = document.createElement('i');
- icon.classList.add(...iconClasses.split(' ')); // convert to the view that FontAwesome works with
- iconContainer.appendChild(icon)
-
- const textContainer = document.createElement('div');
- textContainer.innerText = value
- const textTitle = document.createElement('p');
- textTitle.innerText = title;
- textContainer.insertAdjacentElement('afterbegin', textTitle)
-
- item.append(iconContainer, textContainer)
-
- if (typeof changeValue === 'number') {
- const changeContainer = document.createElement('div');
- changeContainer.classList.add('LZTStatsChangeInfo')
- changeContainer.innerText = changeValue;
- const changeIcon = document.createElement('i');
-
- // Set icon and style by number that has changed
- // default: Just like last time
- let changeIconClasses = 'far fa-arrows-alt-v'
- let changeIconStyle = '#D6D6D6';
- if (changeValue < 0) {
- // Less than last time
- changeIconClasses = 'fas fa-caret-down'
- changeIconStyle = '#ea4c4c';
- } else if (changeValue > 0) {
- // More than last time
- changeIconClasses = 'fas fa-caret-up'
- changeIconStyle = '#0daf77';
- }
-
- changeIcon.classList.add(...changeIconClasses.split(' ')); // convert to the view that FontAwesome works with
- changeIcon.style = `color: ${changeIconStyle}`;
- changeContainer.appendChild(changeIcon)
- item.append(changeContainer);
- }
-
- return item
- }
-
- // * UTILS
- function getTimestamp() {
- return Math.floor(Date.now() / 1000);
- }
-
- function roundMinutes(date) {
- // https://stackoverflow.com/questions/7293306/how-to-round-to-nearest-hour-using-javascript-date-object
- date.setHours(date.getHours() + Math.round(date.getMinutes() / 60));
- date.setMinutes(0, 0, 0); // Resets also seconds and milliseconds
-
- return date;
- }
-
- function formatDate(dateString) {
- // format X hours, minutes and etc to 0X. Ex. 1 -> 01, 12 -> 12
- return ('0' + dateString).slice(-2);
- }
-
- function getThreadIdByURL(threadURL) {
- const threadId = threadURL.match(FIND_THREAD_ID)?.[1]?.split('/')?.[0];
- return Number(threadId) || 0;
- }
-
- function getPostIdByURL(postURL) {
- const postId = postURL.match(FIND_POST_ID)?.[1]?.split('/')?.[0];
- return Number(postId) || 0;
- }
-
- function getUserIdByURL(userURL) {
- const matched = userURL.match(FIND_USER_POST) || userURL.match(FIND_USER_ID);
- const userId = matched?.[1]?.split('/')?.[0];
- return String(userId) || 0;
- }
-
- function getCommentPostIdByURL(postURL) {
- const postId = postURL.match(FIND_COMMENT_POST_ID)?.[1]?.split('/')?.[0];
- return Number(postId) || 0;
- }
-
- function getForumIdByURL(forumURL) {
- const forumId = forumURL.match(FIND_FORUM_ID)?.[1]?.split('/')?.[0];
- return String(forumId) || 0;
- }
-
- function getPostIdByFullURL(postURL) {
- const postId = postURL.match(FIND_POST_ID_IN_FULL_URL)?.[1]?.split('/')?.[0];
- return Number(postId) || 0;
- }
-
- function getChatIdByURL(chatURL) {
- const chatId = chatURL.match(FIND_CHAT_ID)?.[1]?.split('/')?.[0];
- return Number(chatId) || 0;
- }
-
- function getConversationIdByURL(conversationURL) {
- const conversationId = conversationURL.match(FIND_CONVERSATION_ID)?.[1]?.split('/')?.[0];
- return Number(conversationId) || 0;
- }
-
- function calcSumByTime(data, timeFormat = DAY_IN_SECS, maxLength = 7) {
- let sepatedData = {};
- let timestamp = getTimestamp();
-
- while (Object.keys(sepatedData).length < maxLength) {
- const temp = data.filter(m => m.timestamp > timestamp - timeFormat && m.timestamp < timestamp);
- const date = roundMinutes(new Date((timestamp - timeFormat) * 1000));
- let dateString = `${formatDate(date.getDate())}.${formatDate(date.getMonth() + 1)}`;
- if (timeFormat === HOUR_IN_SECS) {
- dateString = `${formatDate(date.getHours())}:${formatDate(date.getMinutes())}`;
- }
-
- sepatedData[dateString] = temp;
- timestamp -= timeFormat;
- }
-
- return sepatedData;
- }
-
- function getSumValuesByTime(sumData) {
- return Object.values(sumData).map(m => m.length).reverse();
- }
-
- function getTimeArray(timeFormat = DAY_IN_SECS, maxLength = 7) {
- let timeArray = [];
- let timestamp = getTimestamp();
-
- for (let i = 0; i < maxLength; i++) {
- const date = roundMinutes(new Date((timestamp - timeFormat) * 1000));
- if (timeFormat === HOUR_IN_SECS) {
- timeArray.push(`${formatDate(date.getHours())}:${formatDate(date.getMinutes())}`);
- } else {
- timeArray.push(`${formatDate(date.getDate())}.${formatDate(date.getMonth() + 1)}`); // month start with 0
- }
-
- timestamp -= timeFormat;
- }
-
- return timeArray;
- }
-
- function downloadJSONFile(data, name) {
- const blob = new Blob([data], {
- type: 'application/json'
- });
- const link = document.createElement('a');
- link.href = window.URL.createObjectURL(blob);
- link.download = `${name}.json`;
- link.click();
- return link;
- }
-
- /**
- * Save stats in GM Storage
- *
- * @param {object} stats - object of stats (ex.: id, message, timestamp)
- * @param {string} valueName - name of storage
- */
- function saveStats(stats, valueName) {
- console.debug("[LZTStats.saveStats]", stats)
- let oldData = GM_getValue(valueName);
- if (oldData === undefined) {
- oldData = []
- }
-
- oldData.push(stats)
-
- GM_setValue(valueName, oldData);
- }
-
-
- // * MAIN
- function init() {
- initHooks()
- renderMenu()
- }
-
- init()
- })();