[Library] - GS ENCH

Library For GameSense 2.1

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/518265/1602254/%5BLibrary%5D%20-%20GS%20ENCH.js

  1. class GSEnhancedUI {
  2. constructor() {
  3. if (window.GSEnhancedUIInstance) {
  4. return window.GSEnhancedUIInstance;
  5. }
  6.  
  7. window.GSEnhancedUIInstance = this;
  8.  
  9. this.cache = {};
  10. this.currentTheme = GM_getValue('theme', 'default');
  11.  
  12. const styles = `
  13. :root {
  14. --gs-primary: #e61515;
  15. --gs-secondary: #353534;
  16. --gs-background: #272726;
  17. --gs-text: #ffffff;
  18. --gs-border: #454545;
  19. --gs-hover: #404040;
  20. }
  21.  
  22. .subscribelink {
  23. display: none !important;
  24. }
  25.  
  26. .blocktable h2 {
  27. transition: background-color 0.3s ease;
  28. cursor: pointer;
  29. }
  30.  
  31. .fa-magnet {
  32. margin-right: 8px;
  33. font-size: 14px;
  34. opacity: 0.7;
  35. transition: all 0.3s ease;
  36. }
  37.  
  38. .blocktable h2:hover .fa-magnet {
  39. opacity: 1;
  40. }
  41.  
  42. .fa-eye, .fa-eye-slash, .fa-desktop {
  43. font-size: 14px;
  44. padding: 2px;
  45. opacity: 0.7;
  46. transition: opacity 0.2s;
  47. }
  48.  
  49. .fa-eye:hover, .fa-eye-slash:hover, .fa-desktop:hover {
  50. opacity: 1;
  51. }
  52.  
  53. .blockform table td .button {
  54. padding: 3px 10px;
  55. font-size: 0.9em;
  56. white-space: nowrap;
  57. }
  58.  
  59. .blockform table td a:not(.button) {
  60. text-decoration: none;
  61. color: var(--gs-primary);
  62. }
  63.  
  64. .blockform table td a:not(.button):hover {
  65. text-decoration: underline;
  66. }
  67.  
  68. .section-header {
  69. user-select: none;
  70. }
  71.  
  72. .section-header .fa-magnet {
  73. opacity: 0.7;
  74. font-size: 14px;
  75. }
  76.  
  77. .section-header:hover .fa-magnet {
  78. opacity: 1;
  79. }
  80.  
  81. [data-theme="light-red"] .pun a:link,
  82. [data-theme="light-red"] .pun a:visited,
  83. [data-theme="light-red"] .pun .tcl h3 a,
  84. [data-theme="light-red"] #brdmenu a:link,
  85. [data-theme="light-red"] #brdmenu a:visited {
  86. color: #ff4444 !important;
  87. }
  88.  
  89. [data-theme="light-red"] .pun a:hover,
  90. [data-theme="light-red"] .pun a:active,
  91. [data-theme="light-red"] .pun .tcl h3 a:hover,
  92. [data-theme="light-red"] #brdmenu a:hover {
  93. color: #ff6666 !important;
  94. }
  95.  
  96. [data-theme="light-orange"] .pun a:link,
  97. [data-theme="light-orange"] .pun a:visited,
  98. [data-theme="light-orange"] .pun .tcl h3 a,
  99. [data-theme="light-orange"] #brdmenu a:link,
  100. [data-theme="light-orange"] #brdmenu a:visited {
  101. color: #ffa500 !important;
  102. }
  103.  
  104. [data-theme="light-orange"] .pun a:hover,
  105. [data-theme="light-orange"] .pun a:active,
  106. [data-theme="light-orange"] .pun .tcl h3 a:hover,
  107. [data-theme="light-orange"] #brdmenu a:hover {
  108. color: #ffc04d !important;
  109. }
  110.  
  111. .inform {
  112. margin-bottom: 12px;
  113. }
  114.  
  115. .infldset {
  116. padding: 12px;
  117. }
  118.  
  119. .button-group {
  120. display: flex;
  121. gap: 8px;
  122. margin-top: 8px;
  123. }
  124.  
  125. .button-group .button {
  126. display: inline-flex;
  127. align-items: center;
  128. gap: 5px;
  129. }
  130.  
  131. .button-group .button i {
  132. font-size: 12px;
  133. }
  134.  
  135. .input-with-button {
  136. display: flex;
  137. gap: 8px;
  138. align-items: center;
  139. }
  140.  
  141. .input-with-button input {
  142. flex: 1;
  143. }
  144.  
  145. .contains-error {
  146. border-color: #ff4444 !important;
  147. }
  148.  
  149. .status-text {
  150. display: flex;
  151. align-items: center;
  152. gap: 5px;
  153. margin-bottom: 8px;
  154. }
  155.  
  156. .status-text i {
  157. color: var(--gs-primary);
  158. font-size: 14px;
  159. }
  160.  
  161. .chat-export-btn {
  162. cursor: pointer;
  163. margin-right: 5px;
  164. font-size: 16px;
  165. display: inline-block;
  166. vertical-align: middle;
  167. padding: 0 5px;
  168. transition: opacity 0.2s;
  169. }
  170.  
  171. .chat-export-btn:hover {
  172. opacity: 0.7;
  173. }
  174.  
  175. .export-status {
  176. position: fixed;
  177. top: 20px;
  178. right: 20px;
  179. background: var(--gs-secondary);
  180. color: var(--gs-text);
  181. padding: 10px 15px;
  182. border-radius: 5px;
  183. border: 1px solid var(--gs-border);
  184. z-index: 10000;
  185. display: none;
  186. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  187. }
  188.  
  189. .export-status.show {
  190. display: block;
  191. animation: slideIn 0.3s ease;
  192. }
  193.  
  194. @keyframes slideIn {
  195. from { transform: translateX(100%); opacity: 0; }
  196. to { transform: translateX(0); opacity: 1; }
  197. }
  198.  
  199. .config-download-btn {
  200. display: inline-block;
  201. padding: 3px 10px;
  202. font-size: 0.9em;
  203. white-space: nowrap;
  204. text-decoration: none;
  205. border: none;
  206. cursor: pointer;
  207. background: inherit;
  208. color: inherit;
  209. }
  210.  
  211. .config-download-btn:hover {
  212. text-decoration: underline;
  213. }
  214.  
  215. .config-replaced {
  216. background: var(--gs-secondary);
  217. border: 1px dashed var(--gs-border);
  218. padding: 15px;
  219. border-radius: 8px;
  220. text-align: center;
  221. margin: 10px 0;
  222. }
  223.  
  224. .config-info {
  225. color: #888;
  226. font-size: 0.9em;
  227. margin-top: 5px;
  228. }
  229.  
  230. .privacy-censored {
  231. background: #333 !important;
  232. color: #666 !important;
  233. border-radius: 3px;
  234. padding: 0 5px;
  235. font-family: monospace;
  236. }
  237.  
  238. .privacy-censored::before {
  239. content: "••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••";
  240. font-size: 0.8em;
  241. }
  242.  
  243. .mention-suggestions {
  244. position: absolute;
  245. background: var(--gs-secondary);
  246. border: 1px solid var(--gs-border);
  247. border-radius: 3px;
  248. max-height: 120px;
  249. overflow-y: auto;
  250. z-index: 1000;
  251. box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  252. display: none;
  253. font-size: 0.85em;
  254. }
  255.  
  256. .mention-suggestion {
  257. padding: 4px 8px;
  258. cursor: pointer;
  259. border-bottom: 1px solid var(--gs-border);
  260. transition: background-color 0.2s;
  261. }
  262.  
  263. .mention-suggestion:last-child {
  264. border-bottom: none;
  265. }
  266.  
  267. .mention-suggestion:hover,
  268. .mention-suggestion.selected {
  269. background: var(--gs-hover);
  270. }
  271.  
  272. .mention-suggestion .username {
  273. font-weight: bold;
  274. color: var(--gs-primary);
  275. font-size: 0.9em;
  276. }
  277.  
  278. .mention-suggestion .user-info {
  279. font-size: 0.75em;
  280. color: #888;
  281. margin-top: 1px;
  282. }
  283. `;
  284.  
  285. if (!document.getElementById('gs-enhanced-styles')) {
  286. const styleElement = document.createElement('style');
  287. styleElement.id = 'gs-enhanced-styles';
  288. styleElement.textContent = styles;
  289. document.head.appendChild(styleElement);
  290. }
  291.  
  292. if (!window.GSEnhancedUIInitialized) {
  293. this.init();
  294. window.GSEnhancedUIInitialized = true;
  295. }
  296. }
  297.  
  298. init() {
  299. this.addThemeToggle();
  300. this.addCollapsibleCategories();
  301. this.removeSubscribeLink();
  302. this.addRollButton();
  303. this.addChatExportButton();
  304. this.setupConfigDownloads();
  305. this.setupUndercoverMode();
  306. this.setupPrivacyMode();
  307. this.setupAutoMentionCompletion();
  308. this.setupResellerList();
  309. this.setupPremiumUI();
  310. document.body.setAttribute('data-theme', this.currentTheme);
  311. }
  312.  
  313. removeSubscribeLink() {
  314. const subscribeLink = document.querySelector('.subscribelink');
  315. if (subscribeLink) {
  316. subscribeLink.remove();
  317. }
  318. }
  319.  
  320. addThemeToggle() {
  321. if (document.getElementById('theme-toggle')) return;
  322.  
  323. const logoutLink = document.querySelector('#navlogout');
  324. if (!logoutLink) return;
  325.  
  326. const themeToggle = document.createElement('li');
  327. themeToggle.id = 'theme-toggle';
  328. themeToggle.innerHTML = this.getThemeIcon(this.currentTheme);
  329.  
  330. themeToggle.addEventListener('click', () => {
  331. this.cycleTheme();
  332. themeToggle.innerHTML = this.getThemeIcon(this.currentTheme);
  333. });
  334.  
  335. logoutLink.parentNode.insertBefore(themeToggle, logoutLink.nextSibling);
  336. }
  337.  
  338. getThemeIcon(theme) {
  339. const icons = {
  340. 'default': 'fa-adjust',
  341. 'light-red': 'fa-fire',
  342. 'light-orange': 'fa-sun-o'
  343. };
  344. return `<i class="fa ${icons[theme]} theme-icon"></i>`;
  345. }
  346.  
  347. cycleTheme() {
  348. const themes = ['default', 'light-red', 'light-orange'];
  349. const currentIndex = themes.indexOf(this.currentTheme);
  350. const newTheme = themes[(currentIndex + 1) % themes.length];
  351. this.currentTheme = newTheme;
  352. GM_setValue('theme', newTheme);
  353. document.body.setAttribute('data-theme', newTheme);
  354. }
  355.  
  356. addRollButton() {
  357. if (document.querySelector('.chat-roll')) return;
  358.  
  359. const emojiSelector = document.querySelector('#emojiselector');
  360. if (emojiSelector) {
  361. const rollButton = document.createElement('div');
  362. rollButton.className = 'chat-roll';
  363. rollButton.innerHTML = '🎲';
  364. rollButton.style.cssText = `
  365. cursor: pointer;
  366. margin-right: 5px;
  367. font-size: 16px;
  368. display: inline-block;
  369. vertical-align: middle;
  370. padding: 0 5px;
  371. `;
  372.  
  373. rollButton.addEventListener('click', () => {
  374. const chatInput = document.querySelector('#shouttext');
  375. if (chatInput) {
  376. chatInput.value = '/roll';
  377. const event = new KeyboardEvent('keydown', {
  378. key: 'Enter',
  379. code: 'Enter',
  380. keyCode: 13,
  381. which: 13,
  382. bubbles: true
  383. });
  384. chatInput.dispatchEvent(event);
  385. }
  386. });
  387.  
  388. emojiSelector.parentNode.insertBefore(rollButton, emojiSelector);
  389. }
  390. }
  391.  
  392. addChatExportButton() {
  393. if (document.querySelector('.chat-export-btn')) return;
  394.  
  395. const emojiSelector = document.querySelector('#emojiselector');
  396. if (emojiSelector) {
  397. const exportButton = document.createElement('div');
  398. exportButton.className = 'chat-export-btn';
  399. exportButton.innerHTML = '💾';
  400. exportButton.title = 'Export chat messages to JSON';
  401.  
  402. exportButton.addEventListener('click', () => {
  403. this.exportChatMessages();
  404. });
  405.  
  406. emojiSelector.parentNode.insertBefore(exportButton, emojiSelector);
  407. }
  408. }
  409.  
  410. exportChatMessages() {
  411. const chatContainer = document.querySelector('#shout > div');
  412. if (!chatContainer) {
  413. this.showExportStatus('No chat messages found!', 'error');
  414. return;
  415. }
  416.  
  417. const messages = [];
  418. const messageElements = chatContainer.querySelectorAll('p');
  419.  
  420. messageElements.forEach((element, index) => {
  421. const timeElement = element.querySelector('.dateTime');
  422. const userLink = element.querySelector('a[href*="profile.php"]');
  423.  
  424. if (timeElement && userLink) {
  425. const timestamp = timeElement.textContent.trim();
  426. const username = userLink.textContent.trim();
  427. const userClass = userLink.className;
  428. const userId = userLink.href.match(/id=(\d+)/)?.[1] || null;
  429.  
  430. const fullText = element.textContent;
  431. const messageStart = fullText.indexOf(': ') + 2;
  432. const messageContent = messageStart > 1 ? fullText.substring(messageStart) : '';
  433.  
  434. messages.push({
  435. id: index + 1,
  436. timestamp: timestamp,
  437. username: username,
  438. userId: userId,
  439. userClass: userClass,
  440. message: messageContent,
  441. rawHTML: element.innerHTML,
  442. exportedAt: new Date().toISOString()
  443. });
  444. }
  445. });
  446.  
  447. if (messages.length === 0) {
  448. this.showExportStatus('No valid chat messages found!', 'error');
  449. return;
  450. }
  451.  
  452. const exportData = {
  453. metadata: {
  454. exportedAt: new Date().toISOString(),
  455. totalMessages: messages.length,
  456. source: 'GameSense Chat',
  457. url: window.location.href,
  458. userAgent: navigator.userAgent
  459. },
  460. messages: messages
  461. };
  462.  
  463. try {
  464. const jsonString = JSON.stringify(exportData, null, 2);
  465. const blob = new Blob([jsonString], { type: 'application/json' });
  466. const url = URL.createObjectURL(blob);
  467.  
  468. const a = document.createElement('a');
  469. a.href = url;
  470. a.download = `gamesense_chat_${new Date().toISOString().split('T')[0]}_${Date.now()}.json`;
  471. document.body.appendChild(a);
  472. a.click();
  473. document.body.removeChild(a);
  474. URL.revokeObjectURL(url);
  475.  
  476. this.showExportStatus(`Successfully exported ${messages.length} messages!`, 'success');
  477. } catch (error) {
  478. console.error('Export failed:', error);
  479. this.showExportStatus('Export failed! Check console for details.', 'error');
  480. }
  481. }
  482.  
  483. setupConfigDownloads() {
  484.  
  485. const posts = document.querySelectorAll('.postmsg p');
  486.  
  487. posts.forEach(post => {
  488. const text = post.textContent.trim();
  489.  
  490. if (this.isConfigCode(text)) {
  491. this.replaceConfigWithButton(post, text);
  492. }
  493. });
  494. }
  495.  
  496. isConfigCode(text) {
  497.  
  498. if (text.length < 500) return false;
  499.  
  500. const base64Pattern = /^[A-Za-z0-9+/=]+$/;
  501. const hasMinimalSpaces = (text.split(' ').length - 1) < (text.length * 0.02);
  502. const containsConfigPatterns = /[A-Za-z0-9]{50,}/.test(text);
  503.  
  504. return (base64Pattern.test(text.replace(/\s/g, '')) || hasMinimalSpaces) && containsConfigPatterns;
  505. }
  506.  
  507. replaceConfigWithButton(postElement, configData) {
  508.  
  509. const postContainer = postElement.closest('.blockpost');
  510. const authorElement = postContainer.querySelector('.postleft dt strong a');
  511. const timestampElement = postContainer.querySelector('h2 a');
  512.  
  513. const author = authorElement ? authorElement.textContent.trim() : 'Unknown';
  514. const timestamp = timestampElement ? timestampElement.textContent.trim() : 'Unknown';
  515. const postId = postContainer.id || 'unknown';
  516.  
  517. const configContainer = document.createElement('div');
  518. configContainer.className = 'config-replaced';
  519. configContainer.innerHTML = `
  520. <div>
  521. <a class="config-download-btn button" onclick="this.nextElementSibling.click()">
  522. Download Config
  523. </a>
  524. <a style="display: none;" download="gamesense_config_${author}_${postId}.txt" href="data:text/plain;charset=utf-8,${encodeURIComponent(configData)}"></a>
  525. <div class="config-info">
  526. <i class="fa fa-user"></i> ${author}
  527. <i class="fa fa-clock-o"></i> ${timestamp}
  528. <i class="fa fa-file-text-o"></i> ${Math.round(configData.length / 1024)}KB
  529. </div>
  530. </div>
  531. `;
  532.  
  533. postElement.innerHTML = '';
  534. postElement.appendChild(configContainer);
  535.  
  536. const downloadBtn = configContainer.querySelector('.config-download-btn');
  537. downloadBtn.addEventListener('click', () => {
  538. this.showExportStatus(`Config downloaded from ${author}!`, 'success');
  539. });
  540. }
  541.  
  542. showExportStatus(message, type = 'info') {
  543.  
  544. const existingStatus = document.querySelector('.export-status');
  545. if (existingStatus) {
  546. existingStatus.remove();
  547. }
  548.  
  549. const statusDiv = document.createElement('div');
  550. statusDiv.className = 'export-status';
  551. statusDiv.innerHTML = `
  552. <i class="fa ${type === 'success' ? 'fa-check' : type === 'error' ? 'fa-times' : 'fa-info'}"></i>
  553. ${message}
  554. `;
  555.  
  556. if (type === 'success') {
  557. statusDiv.style.borderColor = '#4CAF50';
  558. statusDiv.style.color = '#4CAF50';
  559. } else if (type === 'error') {
  560. statusDiv.style.borderColor = '#f44336';
  561. statusDiv.style.color = '#f44336';
  562. }
  563.  
  564. document.body.appendChild(statusDiv);
  565.  
  566. setTimeout(() => {
  567. statusDiv.classList.add('show');
  568. }, 10);
  569.  
  570. setTimeout(() => {
  571. statusDiv.classList.remove('show');
  572. setTimeout(() => {
  573. if (statusDiv.parentNode) {
  574. statusDiv.parentNode.removeChild(statusDiv);
  575. }
  576. }, 300);
  577. }, 3000);
  578. }
  579.  
  580. setupUndercoverMode() {
  581. const loggedInSpan = document.querySelector('#brdwelcome .conl li:first-child span');
  582. if (!loggedInSpan || loggedInSpan.querySelector('.fa-eye')) return;
  583.  
  584. const usernameElement = loggedInSpan.querySelector('strong');
  585. if (usernameElement) {
  586. GM_setValue('username', usernameElement.textContent.trim());
  587. }
  588.  
  589. const eyeButton = document.createElement('i');
  590. eyeButton.className = 'fa fa-eye';
  591. eyeButton.style.cssText = `
  592. cursor: pointer;
  593. margin-left: 5px;
  594. opacity: 0.7;
  595. `;
  596. eyeButton.title = 'Toggle Undercover Mode';
  597.  
  598. const isUndercover = GM_getValue('undercover', false);
  599. if (isUndercover) {
  600. this.enableUndercoverMode();
  601. eyeButton.className = 'fa fa-eye-slash';
  602. }
  603.  
  604. eyeButton.addEventListener('click', () => {
  605. const currentState = GM_getValue('undercover', false);
  606. GM_setValue('undercover', !currentState);
  607.  
  608. if (!currentState) {
  609. this.enableUndercoverMode();
  610. eyeButton.className = 'fa fa-eye-slash';
  611. } else {
  612. this.disableUndercoverMode();
  613. eyeButton.className = 'fa fa-eye';
  614. }
  615. });
  616.  
  617. loggedInSpan.appendChild(eyeButton);
  618. }
  619.  
  620. setupPrivacyMode() {
  621. const loggedInSpan = document.querySelector('#brdwelcome .conl li:first-child span');
  622. if (!loggedInSpan || loggedInSpan.querySelector('.fa-desktop')) return;
  623.  
  624. const privacyButton = document.createElement('i');
  625. privacyButton.className = 'fa fa-desktop';
  626. privacyButton.style.cssText = `
  627. cursor: pointer;
  628. margin-left: 5px;
  629. opacity: 0.7;
  630. `;
  631. privacyButton.title = 'Toggle Privacy Mode (Screen Share Safe)';
  632.  
  633. const isPrivacyMode = GM_getValue('privacyMode', false);
  634. if (isPrivacyMode) {
  635. this.enablePrivacyMode();
  636. privacyButton.style.color = '#4CAF50';
  637. }
  638.  
  639. privacyButton.addEventListener('click', () => {
  640. const currentState = GM_getValue('privacyMode', false);
  641. GM_setValue('privacyMode', !currentState);
  642.  
  643. if (!currentState) {
  644. this.enablePrivacyMode();
  645. privacyButton.style.color = '#4CAF50';
  646. this.showExportStatus('Privacy Mode enabled - sensitive data hidden', 'success');
  647. } else {
  648. this.disablePrivacyMode();
  649. privacyButton.style.color = '';
  650. this.showExportStatus('Privacy Mode disabled', 'info');
  651. }
  652. });
  653.  
  654. loggedInSpan.appendChild(privacyButton);
  655. }
  656.  
  657. enableUndercoverMode() {
  658. const username = GM_getValue('username');
  659. if (!username) return;
  660.  
  661. const selectors = [
  662. 'a[href*="profile.php"]',
  663. '#brdwelcome .conl li:first-child strong',
  664. '.username',
  665. '.user-name',
  666. '.author'
  667. ];
  668.  
  669. document.querySelectorAll(selectors.join(', ')).forEach(element => {
  670. if (element.textContent.trim() === username) {
  671. element.setAttribute('data-original', element.textContent);
  672. element.textContent = '<HIDDEN>';
  673. }
  674. });
  675. }
  676.  
  677. disableUndercoverMode() {
  678. document.querySelectorAll('[data-original]').forEach(element => {
  679. if (element.getAttribute('data-original')) {
  680. element.textContent = element.getAttribute('data-original');
  681. element.removeAttribute('data-original');
  682. }
  683. });
  684. }
  685.  
  686. enablePrivacyMode() {
  687.  
  688. const sensitiveSelectors = [
  689.  
  690. 'input[name="req_email"]',
  691. 'input[type="email"]',
  692.  
  693. 'input[name*="2fa"]',
  694. 'input[name*="recovery"]',
  695. 'input[name*="backup"]',
  696. 'input[name*="secret"]',
  697. 'input[name*="token"]',
  698.  
  699. '*'
  700. ];
  701.  
  702. document.querySelectorAll('input[name="req_email"], input[type="email"]').forEach(element => {
  703. if (!element.hasAttribute('data-original-value') && element.value) {
  704. element.setAttribute('data-original-value', element.value);
  705. element.value = element.value.replace(/^.+@/, '••••••••@');
  706. }
  707. });
  708.  
  709. document.querySelectorAll('p').forEach(element => {
  710. const text = element.textContent;
  711.  
  712. if (text.includes('Recovery code:') || text.includes('recovery code:')) {
  713. const codeMatch = text.match(/[a-f0-9]{32,}/i);
  714. if (codeMatch && !element.hasAttribute('data-original-text')) {
  715. element.setAttribute('data-original-text', element.textContent);
  716. element.innerHTML = element.innerHTML.replace(codeMatch[0],
  717. `<span class="privacy-censored" data-sensitive="true"></span>`);
  718. }
  719. }
  720.  
  721. const emailMatch = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
  722. if (emailMatch && !element.hasAttribute('data-original-text')) {
  723. element.setAttribute('data-original-text', element.textContent);
  724. element.innerHTML = element.innerHTML.replace(emailMatch[0],
  725. '••••••••@••••••••.•••');
  726. }
  727.  
  728. if (text.match(/Registered:\s*\d{4}-\d{2}-\d{2}/) && !element.hasAttribute('data-original-text')) {
  729. element.setAttribute('data-original-text', element.textContent);
  730. element.textContent = element.textContent.replace(/Registered:\s*\d{4}-\d{2}-\d{2}/, 'Registered: ••••-••-••');
  731. }
  732.  
  733. if (text.match(/Last post:\s*.+\d{2}:\d{2}:\d{2}/) && !element.hasAttribute('data-original-text')) {
  734. element.setAttribute('data-original-text', element.textContent);
  735. element.textContent = element.textContent.replace(/Last post:\s*.+\d{2}:\d{2}:\d{2}/, 'Last post: •••••• ••:••:••');
  736. }
  737.  
  738. if (text.match(/Last visit:\s*.+\d{2}:\d{2}:\d{2}/) && !element.hasAttribute('data-original-text')) {
  739. element.setAttribute('data-original-text', element.textContent);
  740. element.textContent = element.textContent.replace(/Last visit:\s*.+\d{2}:\d{2}:\d{2}/, 'Last visit: •••••• ••:••:••');
  741. }
  742.  
  743. if (text.match(/Posts:\s*\d+/) && !element.hasAttribute('data-original-text')) {
  744. element.setAttribute('data-original-text', element.textContent);
  745. element.textContent = element.textContent.replace(/Posts:\s*\d+/, 'Posts: •••');
  746. }
  747.  
  748. const keyMatch = text.match(/[A-Za-z0-9]{20,}/);
  749. if (keyMatch && !element.hasAttribute('data-original-text') &&
  750. !text.includes('http') && !text.includes('www') &&
  751. !text.includes('Registered:') && !text.includes('Last post:') &&
  752. !text.includes('Last visit:') && !text.includes('Posts:')) {
  753. element.setAttribute('data-original-text', element.textContent);
  754. element.innerHTML = element.innerHTML.replace(keyMatch[0],
  755. `<span class="privacy-censored" data-sensitive="true"></span>`);
  756. }
  757. });
  758.  
  759. document.querySelectorAll('p').forEach(element => {
  760. if (element.textContent.includes('Invited by') && !element.hasAttribute('data-original-text')) {
  761. element.setAttribute('data-original-text', element.textContent);
  762. const inviterLink = element.querySelector('a');
  763. if (inviterLink) {
  764. inviterLink.textContent = '••••••••';
  765. inviterLink.href = '#';
  766. }
  767. }
  768. });
  769.  
  770. document.querySelectorAll('input[type="text"], input[type="password"]').forEach(element => {
  771. if (element.value && element.value.length > 15 &&
  772. !element.hasAttribute('data-original-value')) {
  773. element.setAttribute('data-original-value', element.value);
  774. element.value = '••••••••••••••••••••••••••••••••';
  775. }
  776. });
  777. }
  778.  
  779. disablePrivacyMode() {
  780.  
  781. document.querySelectorAll('input[data-original-value]').forEach(element => {
  782. element.value = element.getAttribute('data-original-value');
  783. element.removeAttribute('data-original-value');
  784. });
  785.  
  786. document.querySelectorAll('[data-original-text]').forEach(element => {
  787. element.textContent = element.getAttribute('data-original-text');
  788. element.removeAttribute('data-original-text');
  789. });
  790.  
  791. document.querySelectorAll('.privacy-censored').forEach(element => {
  792. element.remove();
  793. });
  794. }
  795.  
  796. setupAutoMentionCompletion() {
  797. const chatInput = document.querySelector('#shouttext');
  798. if (!chatInput) return;
  799.  
  800. let suggestions = [];
  801. let selectedIndex = -1;
  802. let suggestionsContainer = null;
  803.  
  804. this.collectUsersFromPage();
  805.  
  806. const createSuggestionsContainer = () => {
  807. if (suggestionsContainer) return;
  808.  
  809. suggestionsContainer = document.createElement('div');
  810. suggestionsContainer.className = 'mention-suggestions';
  811. document.body.appendChild(suggestionsContainer);
  812. };
  813.  
  814. const positionSuggestions = () => {
  815. if (!suggestionsContainer) return;
  816.  
  817. const rect = chatInput.getBoundingClientRect();
  818. suggestionsContainer.style.left = rect.left + 'px';
  819. suggestionsContainer.style.top = (rect.top - suggestionsContainer.offsetHeight - 5) + 'px';
  820. suggestionsContainer.style.width = rect.width + 'px';
  821. };
  822.  
  823. const showSuggestions = (suggestionList) => {
  824. if (suggestionList.length === 0) {
  825. this.hideMentionSuggestions();
  826. return;
  827. }
  828.  
  829. createSuggestionsContainer();
  830.  
  831. suggestionsContainer.innerHTML = '';
  832. suggestionList.forEach((user, index) => {
  833. const suggestionDiv = document.createElement('div');
  834. suggestionDiv.className = 'mention-suggestion';
  835. suggestionDiv.innerHTML = `
  836. <div class="username">${user.username}</div>
  837. <div class="user-info">${user.source}</div>
  838. `;
  839.  
  840. suggestionDiv.addEventListener('click', () => {
  841. this.completeMention(chatInput, user.username);
  842. this.hideMentionSuggestions();
  843. });
  844.  
  845. suggestionsContainer.appendChild(suggestionDiv);
  846. });
  847.  
  848. suggestionsContainer.style.display = 'block';
  849. positionSuggestions();
  850. this.highlightSuggestion(0);
  851. };
  852.  
  853. chatInput.addEventListener('input', (e) => {
  854. const value = e.target.value;
  855. const cursorPos = e.target.selectionStart;
  856. const textBeforeCursor = value.substring(0, cursorPos);
  857. const atIndex = textBeforeCursor.lastIndexOf('@');
  858.  
  859. if (atIndex !== -1 && (atIndex === 0 || value[atIndex - 1] === ' ')) {
  860. const query = textBeforeCursor.substring(atIndex + 1);
  861. if (query.length > 0) {
  862. suggestions = this.getUserSuggestions(query);
  863. selectedIndex = 0;
  864. showSuggestions(suggestions);
  865. } else {
  866. this.hideMentionSuggestions();
  867. }
  868. } else {
  869. this.hideMentionSuggestions();
  870. }
  871. });
  872.  
  873. chatInput.addEventListener('keydown', (e) => {
  874. if (suggestions.length > 0 && suggestionsContainer && suggestionsContainer.style.display === 'block') {
  875. if (e.key === 'ArrowDown') {
  876. selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1);
  877. this.highlightSuggestion(selectedIndex);
  878. e.preventDefault();
  879. } else if (e.key === 'ArrowUp') {
  880. selectedIndex = Math.max(selectedIndex - 1, 0);
  881. this.highlightSuggestion(selectedIndex);
  882. e.preventDefault();
  883. } else if (e.key === 'Tab' || e.key === 'Enter') {
  884. if (selectedIndex >= 0 && suggestions[selectedIndex]) {
  885. this.completeMention(chatInput, suggestions[selectedIndex].username);
  886. this.hideMentionSuggestions();
  887. e.preventDefault();
  888. }
  889. } else if (e.key === 'Escape') {
  890. this.hideMentionSuggestions();
  891. }
  892. }
  893. });
  894.  
  895. document.addEventListener('click', (e) => {
  896. if (!chatInput.contains(e.target) && (!suggestionsContainer || !suggestionsContainer.contains(e.target))) {
  897. this.hideMentionSuggestions();
  898. }
  899. });
  900. }
  901.  
  902. collectUsersFromPage() {
  903. const users = new Set();
  904.  
  905. document.querySelectorAll('a[href*="profile.php?id="]').forEach(link => {
  906. const username = link.textContent.trim();
  907. if (username && username.length > 0 && username !== 'PM') {
  908. users.add(username);
  909. }
  910. });
  911.  
  912. let userCache = JSON.parse(GM_getValue('userCache', '[]'));
  913. users.forEach(user => {
  914. if (!userCache.some(cached => cached.username === user)) {
  915. userCache.push({
  916. username: user,
  917. lastSeen: Date.now(),
  918. source: 'page'
  919. });
  920. }
  921. });
  922.  
  923. userCache.sort((a, b) => b.lastSeen - a.lastSeen);
  924. userCache = userCache.slice(0, 200);
  925.  
  926. GM_setValue('userCache', JSON.stringify(userCache));
  927. }
  928.  
  929. getUserSuggestions(query) {
  930. const suggestions = [];
  931. const seenUsers = new Set();
  932. const queryLower = query.toLowerCase();
  933.  
  934. document.querySelectorAll('#shout a[href*="profile.php"]').forEach(link => {
  935. const username = link.textContent.trim();
  936. if (username && username.toLowerCase().includes(queryLower) && !seenUsers.has(username)) {
  937. seenUsers.add(username);
  938. suggestions.push({
  939. username: username,
  940. source: '💬 In chat',
  941. priority: 1
  942. });
  943. }
  944. });
  945.  
  946. document.querySelectorAll('.postleft dt strong a, .postright h3 a').forEach(link => {
  947. const username = link.textContent.trim();
  948. if (username && username.toLowerCase().includes(queryLower) && !seenUsers.has(username)) {
  949. seenUsers.add(username);
  950. suggestions.push({
  951. username: username,
  952. source: '📝 In topic',
  953. priority: 2
  954. });
  955. }
  956. });
  957.  
  958. const userCache = JSON.parse(GM_getValue('userCache', '[]'));
  959. userCache.forEach(user => {
  960. if (user.username.toLowerCase().includes(queryLower) && !seenUsers.has(user.username)) {
  961. seenUsers.add(user.username);
  962. const timeAgo = Math.floor((Date.now() - user.lastSeen) / (1000 * 60 * 60 * 24));
  963. suggestions.push({
  964. username: user.username,
  965. source: timeAgo === 0 ? '👁️ Today' : `👁️ ${timeAgo}d ago`,
  966. priority: 3
  967. });
  968. }
  969. });
  970.  
  971. return suggestions
  972. .sort((a, b) => a.priority - b.priority)
  973. .slice(0, 5);
  974. }
  975.  
  976. completeMention(inputElement, username) {
  977. const value = inputElement.value;
  978. const cursorPos = inputElement.selectionStart;
  979. const textBeforeCursor = value.substring(0, cursorPos);
  980. const textAfterCursor = value.substring(cursorPos);
  981. const atIndex = textBeforeCursor.lastIndexOf('@');
  982.  
  983. if (atIndex !== -1) {
  984. const beforeAt = value.substring(0, atIndex);
  985. const newValue = beforeAt + '@' + username + ' ' + textAfterCursor;
  986. inputElement.value = newValue;
  987.  
  988. const newCursorPos = atIndex + username.length + 2;
  989. inputElement.setSelectionRange(newCursorPos, newCursorPos);
  990. inputElement.focus();
  991. }
  992. }
  993.  
  994. highlightSuggestion(index) {
  995. const suggestions = document.querySelectorAll('.mention-suggestion');
  996. suggestions.forEach((suggestion, i) => {
  997. suggestion.classList.toggle('selected', i === index);
  998. });
  999. }
  1000.  
  1001. hideMentionSuggestions() {
  1002. const suggestionsContainer = document.querySelector('.mention-suggestions');
  1003. if (suggestionsContainer) {
  1004. suggestionsContainer.style.display = 'none';
  1005. }
  1006. }
  1007.  
  1008. setupResellerList() {
  1009. if (!window.location.href.includes('payment.php') || document.querySelector('.reseller-section')) return;
  1010.  
  1011. const extendGameSense = document.querySelector('.blockform');
  1012. if (!extendGameSense) return;
  1013.  
  1014. const resellerSection = document.createElement('div');
  1015. resellerSection.className = 'blockform reseller-section';
  1016. resellerSection.innerHTML = `
  1017. <h2>
  1018. <span>
  1019. <div style="display: flex; align-items: center; cursor: pointer;" class="section-header">
  1020. <i class="fa fa-magnet" style="margin-right: 8px; transition: transform 0.3s ease"></i>
  1021. Verified Resellers
  1022. </div>
  1023. </span>
  1024. </h2>
  1025. <div class="box">
  1026. <div class="fakeform">
  1027. <div class="inform">
  1028. <fieldset>
  1029. <legend>Alternative Payment Methods</legend>
  1030. <div class="fakeform">
  1031. <p>Below is a list of verified resellers. Please be careful and only deal with listed resellers to avoid scams.</p>
  1032. <table>
  1033. <tr>
  1034. <th class="tcl">Reseller</th>
  1035. <th class="tcl">Payment Methods</th>
  1036. <th class="tcl">Price</th>
  1037. <th class="tcl">Action</th>
  1038. </tr>
  1039. <tr>
  1040. <td><a href="profile.php?id=985">Sigma</a></td>
  1041. <td>Crypto, PayPal, CashApp</td>
  1042. <td>24 USD</td>
  1043. <td><a href="viewtopic.php?id=23385" class="button">Purchase</a></td>
  1044. </tr>
  1045. <tr>
  1046. <td><a href="profile.php?id=2933">death1989</a></td>
  1047. <td>花呗,微信,支付宝,QQ红包</td>
  1048. <td>135 RMB</td>
  1049. <td><a href="viewtopic.php?id=17427" class="button">Purchase</a></td>
  1050. </tr>
  1051. <tr>
  1052. <td><a href="profile.php?id=3031">484481617</a></td>
  1053. <td>支付宝/微信/QQ/QIWI/淘宝/PayPal</td>
  1054. <td>135 RMB</td>
  1055. <td><a href="viewtopic.php?id=17435" class="button">Purchase</a></td>
  1056. </tr>
  1057. <tr>
  1058. <td><a href="profile.php?id=1699">tiagovski</a></td>
  1059. <td>PayPal, Bank, Card, Crypto, PSC, Alipay, Pix</td>
  1060. <td>21 EUR</td>
  1061. <td><a href="viewtopic.php?id=25671" class="button">Purchase</a></td>
  1062. </tr>
  1063. <tr>
  1064. <td><a href="profile.php?id=10043">Margele</a></td>
  1065. <td>支付宝,微信</td>
  1066. <td>148.88 CNY</td>
  1067. <td><a href="viewtopic.php?id=45009" class="button">Purchase</a></td>
  1068. </tr>
  1069. <tr>
  1070. <td><a href="profile.php?id=12434">Samo</a></td>
  1071. <td>PayPal, Giropay, TF2, Crypto, Skrill</td>
  1072. <td>21 EUR</td>
  1073. <td><a href="viewtopic.php?id=43045" class="button">Purchase</a></td>
  1074. </tr>
  1075. <tr>
  1076. <td><a href="profile.php?id=16166">Cahira</a></td>
  1077. <td>QQ, PayPal, Card, WeChat, Alipay, Crypto</td>
  1078. <td>160 CNY</td>
  1079. <td><a href="viewtopic.php?id=45499" class="button">Purchase</a></td>
  1080. </tr>
  1081. <tr>
  1082. <td><a href="profile.php?id=16243">pguest</a></td>
  1083. <td>QQ, PayPal, Card, WeChat, Alipay</td>
  1084. <td>?? RMB</td>
  1085. <td><a href="viewtopic.php?id=45179" class="button">Purchase</a></td>
  1086. </tr>
  1087. <tr>
  1088. <td><a href="profile.php?id=9060">VKVKF</a></td>
  1089. <td>Cards RU/EU/KZ/UA/ASIA, All Crypto</td>
  1090. <td>30 USD</td>
  1091. <td><a href="viewtopic.php?id=27735" class="button">Purchase</a></td>
  1092. </tr>
  1093. </table>
  1094. <p>⚠️ Always verify the reseller's profile and reputation before making any payments. Be aware of scammers impersonating verified resellers.</p>
  1095. </div>
  1096. </fieldset>
  1097. </div>
  1098. </div>
  1099. </div>
  1100. `;
  1101.  
  1102. const firstBlockform = document.querySelector('.blockform');
  1103. firstBlockform.parentNode.insertBefore(resellerSection, firstBlockform.nextSibling);
  1104.  
  1105. this.addCollapseFunctionToSection(resellerSection);
  1106. }
  1107.  
  1108. addCollapseFunctionToSection(section) {
  1109. const header = section.querySelector('.section-header');
  1110. const content = section.querySelector('.box');
  1111. const icon = header.querySelector('.fa-magnet');
  1112.  
  1113. const isSectionCollapsed = GM_getValue(`section_${header.textContent.trim()}_collapsed`, false);
  1114. if (isSectionCollapsed) {
  1115. content.style.display = 'none';
  1116. icon.style.transform = 'rotate(180deg)';
  1117. }
  1118.  
  1119. header.addEventListener('click', () => {
  1120. const isCollapsed = content.style.display === 'none';
  1121. content.style.display = isCollapsed ? '' : 'none';
  1122. icon.style.transform = isCollapsed ? '' : 'rotate(180deg)';
  1123. GM_setValue(`section_${header.textContent.trim()}_collapsed`, !isCollapsed);
  1124. });
  1125. }
  1126.  
  1127. addCollapsibleCategories() {
  1128. const categories = document.querySelectorAll('.blocktable h2');
  1129.  
  1130. categories.forEach(category => {
  1131. if (category.querySelector('.fa-magnet')) return;
  1132.  
  1133. const magnetIcon = document.createElement('i');
  1134. magnetIcon.className = 'fa fa-magnet';
  1135. magnetIcon.style.cssText = `
  1136. margin-right: 8px;
  1137. transition: transform 0.3s ease;
  1138. `;
  1139.  
  1140. const headerWrapper = document.createElement('div');
  1141. headerWrapper.style.cssText = `
  1142. display: flex;
  1143. align-items: center;
  1144. cursor: pointer;
  1145. user-select: none;
  1146. `;
  1147.  
  1148. const span = category.querySelector('span');
  1149. if (!span) return;
  1150.  
  1151. const content = span.cloneNode(true);
  1152.  
  1153. headerWrapper.appendChild(magnetIcon);
  1154. headerWrapper.appendChild(content);
  1155.  
  1156. category.innerHTML = '';
  1157. category.appendChild(headerWrapper);
  1158.  
  1159. const categoryContent = category.closest('.blocktable');
  1160. const contentBox = categoryContent.querySelector('.box');
  1161.  
  1162. headerWrapper.addEventListener('click', () => {
  1163. const isCollapsed = contentBox.style.display === 'none';
  1164. contentBox.style.display = isCollapsed ? '' : 'none';
  1165. magnetIcon.style.transform = isCollapsed ? '' : 'rotate(180deg)';
  1166.  
  1167. const categoryText = content.textContent.trim();
  1168. GM_setValue(`category_${categoryText}_collapsed`, !isCollapsed);
  1169. });
  1170.  
  1171. const savedState = GM_getValue(`category_${content.textContent.trim()}_collapsed`, false);
  1172. if (savedState) {
  1173. contentBox.style.display = 'none';
  1174. magnetIcon.style.transform = 'rotate(180deg)';
  1175. }
  1176. });
  1177. }
  1178.  
  1179. setupPremiumUI() {
  1180. if (!window.location.href.includes('profile.php') ||
  1181. !window.location.href.includes('section=premium') ||
  1182. document.querySelector('#gs-premium-ui')) return;
  1183.  
  1184. const container = document.querySelector('.blockform .box');
  1185. if (!container) return;
  1186.  
  1187. const userId = window.location.href.match(/id=(\d+)/) ?
  1188. window.location.href.match(/id=(\d+)/)[1] :
  1189. document.querySelector('#brdwelcome .conl a[href*="profile.php"]')?.href.match(/id=(\d+)/)?.[1];
  1190.  
  1191. if (!userId) return;
  1192.  
  1193. const existingStatus = container.querySelector('.infldset p')?.textContent || 'No active subscription';
  1194.  
  1195. container.id = 'gs-premium-ui';
  1196. container.innerHTML = `
  1197. <form id="profile8" method="post" action="profile.php?section=premium&id=${userId}">
  1198. <input type="hidden" name="form_sent" value="1" />
  1199.  
  1200. <div class="inform">
  1201. <fieldset>
  1202. <legend>Subscription Status</legend>
  1203. <div class="infldset">
  1204. <div class="status-text">
  1205. <i class="fa fa-clock-o"></i>
  1206. <span>${existingStatus}</span>
  1207. </div>
  1208. <div class="button-group">
  1209. <a href="payment.php?game=csgo" class="button">
  1210. <i class="fa fa-refresh"></i>
  1211. ${existingStatus.includes('No active') ? 'Purchase Subscription' : 'Extend Subscription'}
  1212. </a>
  1213. </div>
  1214. </div>
  1215. </fieldset>
  1216. </div>
  1217.  
  1218. <div class="inform">
  1219. <fieldset>
  1220. <legend>Game Clients</legend>
  1221. <div class="infldset">
  1222. <div class="button-group">
  1223. ${existingStatus.includes('No active') ? `
  1224. <button type="button" disabled class="button">
  1225. <i class="fa fa-download"></i>
  1226. CS2 Client
  1227. </button>
  1228. <button type="button" disabled class="button">
  1229. <i class="fa fa-download"></i>
  1230. CS:GO Client
  1231. </button>
  1232. ` : `
  1233. <button type="submit" name="download_client" class="button">
  1234. <i class="fa fa-download"></i>
  1235. CS2 Client
  1236. </button>
  1237. <button type="submit" name="download_client_csgo" class="button">
  1238. <i class="fa fa-download"></i>
  1239. CS:GO Client
  1240. </button>
  1241. `}
  1242. </div>
  1243. </div>
  1244. </fieldset>
  1245. </div>
  1246.  
  1247. <div class="inform">
  1248. <fieldset>
  1249. <legend>Discord Management</legend>
  1250. <div class="infldset">
  1251. <div class="input-with-button">
  1252. <input id="discord_reset_reason" type="text"
  1253. name="discord_reset_reason"
  1254. placeholder="Enter reason for Discord ID reset"
  1255. maxlength="40"
  1256. ${existingStatus.includes('No active') ? 'disabled' : ''} />
  1257. <button type="submit" name="reset_discord" class="button"
  1258. ${existingStatus.includes('No active') ? 'disabled' : ''}>
  1259. <i class="fa fa-refresh"></i>
  1260. Reset
  1261. </button>
  1262. </div>
  1263. </div>
  1264. </fieldset>
  1265. </div>
  1266.  
  1267. <div class="inform">
  1268. <fieldset>
  1269. <legend>Invite Codes</legend>
  1270. <div class="infldset">
  1271. <p>You have no unused invitation codes.</p>
  1272. </div>
  1273. </fieldset>
  1274. </div>
  1275. </form>
  1276. `;
  1277.  
  1278. const form = container.querySelector('form');
  1279. form.querySelectorAll(':submit').forEach(button => {
  1280. button.addEventListener('click', function(e) {
  1281. const discordReason = document.getElementById('discord_reset_reason');
  1282.  
  1283. if (this.name === 'reset_discord' && discordReason.value.trim() === '') {
  1284. discordReason.classList.add('contains-error');
  1285. e.preventDefault();
  1286. return;
  1287. }
  1288.  
  1289. this.disabled = true;
  1290. const hiddenInput = document.createElement('input');
  1291. hiddenInput.type = 'hidden';
  1292. hiddenInput.name = this.name;
  1293. hiddenInput.value = this.value;
  1294. form.appendChild(hiddenInput);
  1295. });
  1296. });
  1297. }
  1298. }
  1299.  
  1300. new GSEnhancedUI();