您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds an note field to each user on BitcoinTalk
// ==UserScript== // @name BitcoinTalk User Notes // @version 0.3.1 // @description Adds an note field to each user on BitcoinTalk // @author TryNinja // @match https://bitcointalk.org/* // @icon https://www.google.com/s2/favicons?sz=64&domain=bitcointalk.org // @grant GM.setValue // @grant GM.getValue // @grant GM_setValue // @grant GM_getValue // @namespace https://greasyfork.org/users/1070272 // ==/UserScript== const enableModal = 1; (async function() { 'use strict'; const addStyle = (css) => { const style = document.getElementById("GM_addStyleBy8626") || (() => { const style = document.createElement('style'); style.id = "GM_addStyleBy8626"; document.head.appendChild(style); return style; })(); const sheet = style.sheet; sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length); } if (enableModal) { addStyle(`.modal { position: fixed; width: 100vw; height: 100vh; top: 0; left: 0; display: flex; align-items: center; justify-content: center; }`); addStyle(`.modal-bg { position: absolute; width: 100%; height: 100%; }`); addStyle(`.modal-container { min-width: 30vh; border-radius: 10px; background: #fff; position: relative; padding: 10px; }`); addStyle(`.modal-close { position: absolute; right: 15px; top: 15px; outline: none; appearance: none; color: red; background: none; border: 0px; font-weight: bold; cursor: pointer; }`); }; const getValue = typeof GM_getValue === 'undefined' ? GM.getValue : GM_getValue; const setValue = typeof GM_setValue === 'undefined' ? GM.setValue : GM_setValue; const getParentNodeNth = (element, num) => { let parent = element; for (let i = 0; i < num; i++) { if (parent.parentNode) { parent = parent.parentNode; } } return parent; }; const getNotes = async () => { let notes; try { notes = JSON.parse(await getValue('notes') ?? '{}'); } catch (error) { notes = {}; }; return notes; }; const setNotes = async notes => { if (typeof notes === 'string') { try { JSON.parse(notes); await setValue('notes', notes); } catch (error) { console.error('Notes value is an invalid JSON format') }; } else if (typeof notes === 'object') { await setValue('notes', JSON.stringify(notes ?? {})); }; }; const getUserNote = async user => { const notes = await getNotes(); if (!notes) { return null; } return notes[user]; }; const setUserNote = async (user, note) => { const notes = await getNotes(); notes[user] = note; await setNotes(notes) }; const texts = { addNote: '<a style="cursor: pointer; font-weight: bold" href="javascript:;">📜 Add Note</a>', withNote: note => `<a style="cursor: pointer; font-weight: bold" href="javascript:;"><b>📜</b> ${note}</a>` }; const addNote = async (user, element) => { const note = prompt('Input the note (empty to remove):'); if (note) { element.innerHTML = texts.withNote(note); await setUserNote(user, note); } else if (note !== null) { element.innerHTML = texts.addNote; await setUserNote(user, note); } }; const exportNotesToInput = async () => { const notesInput = document.querySelector('#notesInput'); const notesImportExportDiv = document.querySelector('#notesImportExportDiv'); const doneImportButton = document.querySelector('#doneImportButton'); const notes = await getNotes(); const notesJsonString = JSON.stringify(Object.keys(notes) .filter(user => notes[user]).reduce((obj, user) => ({...obj, [user]: notes[user]}), {})); notesInput.value = notesJsonString; notesImportExportDiv.querySelector('span').innerText = 'Export (copy the code)'; notesImportExportDiv.style.display = 'flex'; doneImportButton.style.display = 'none'; }; const importNotesFromInput = async () => { const notesInput = document.querySelector('#notesInput'); const notesImportExportDiv = document.querySelector('#notesImportExportDiv'); const doneImportButton = document.querySelector('#doneImportButton'); notesInput.value = ''; notesImportExportDiv.querySelector('span').innerText = 'Import (paste the code)'; notesImportExportDiv.style.display = 'flex'; doneImportButton.style.display = 'inline-block'; }; const importNotesFromInputDone = async () => { const notesInput = document.querySelector('#notesInput'); const confirmImport = confirm('Are you sure you want to override your local notes?'); if (confirmImport && notesInput.value) { setNotes(notesInput.value); loadUserNotesList(); } }; const insertNotesModal = async () => { let notesModal = document.querySelector('#userNotesModal'); if (!notesModal) { const moreMenuBtn = document.querySelector('body'); notesModal = document.createElement('div'); notesModal.innerHTML = ` <div class="modal" id="modal-one"> <div class="modal-bg modal-exit"></div> <div class="modal-container"> <div style="margin-bottom: 5px;"> <b style="font-size: 2rem;">User Notes</b> <button class="modal-close modal-exit">X</button> </div> <div style="display: flex; align-items: center; margin-bottom: 5px;"> <button id="exportUserNotes">Export</button> <button id="importUserNotes">Import</button> </div> <div> <div style="display: none; flex-direction: column;" id="notesImportExportDiv"> <span id="notesInputText"></span> <input id="notesInput" /> <button id="doneImportButton" style="display: none;">Done</button> </div> </div> <div id="userNotesList" /> </div> </div>`; notesModal.classList.add('modal'); notesModal.style.visibility = 'hidden'; notesModal.setAttribute('id', 'userNotesModal'); moreMenuBtn.after(notesModal); const exportButton = document.querySelector('#exportUserNotes'); const importButton = document.querySelector('#importUserNotes'); const doneImportButton = document.querySelector('#doneImportButton'); exportButton.addEventListener('click', () => exportNotesToInput()); importButton.addEventListener('click', () => importNotesFromInput()); doneImportButton.addEventListener('click', () => importNotesFromInputDone()); }; return notesModal; }; const loadUserNotesList = async () => { const userNotesList = document.querySelector('#userNotesList'); const notes = await getNotes(); if (Object.keys(notes).length) { userNotesList.innerHTML = Object.keys(notes) .filter(user => notes[user]) .map((user) => `<a href="https://bitcointalk.org/index.php?action=profile;u=${user}" target="_blank">${user}</a>: ${notes[user]}`).join('<br/>'); } else { userNotesList.innerHTML = 'No notes...'; }; }; const insertUserNotesMenuButton = async () => { let notesBtn = document.querySelector('#userNotesMenuBtn'); const modal = await insertNotesModal(); const modalExit = modal.querySelectorAll('.modal-exit'); if (!notesBtn) { const moreMenuBtn = document.querySelector(`a[href='/more.php']`).parentNode; notesBtn = document.createElement('td'); notesBtn.innerHTML = '<td><a href="javascript:;" id="openUserNotes">User Notes</a></td>'; notesBtn.classList.add('maintab_back'); notesBtn.setAttribute('id', 'userNotesMenuBtn'); moreMenuBtn.after(notesBtn); const openUserNotes = document.querySelector('#openUserNotes') const notesImportExportDiv = document.querySelector('#notesImportExportDiv'); const notesInput = document.querySelector('#notesInput'); openUserNotes.addEventListener('click', () => { modal.style.visibility = 'visible'; modal.style.opacity = 1; notesImportExportDiv.style.display = 'none'; notesInput.value = ''; loadUserNotesList(); }); modalExit.forEach(el => el.addEventListener('click', () => { modal.style.visibility = 'hidden'; modal.style.opacity = 0; })); } return notesBtn; }; if (enableModal) { insertNotesModal(); insertUserNotesMenuButton(); }; if (window.location.href.match(/topic=\d+/)) { const targets = [...document.querySelectorAll('td.poster_info div a:last-child')] .filter(e => window.getComputedStyle(getParentNodeNth(e, 11)).display !== 'none'); targets.map(async target => { const [_, userId] = [...target.parentNode.parentNode.childNodes].find(childNode => childNode.innerHTML).innerHTML.match(/u=(\d+)/); const noteDiv = document.createElement('div'); const note = await getUserNote(userId); if (!note) { noteDiv.innerHTML = texts.addNote; } else { noteDiv.innerHTML = texts.withNote(note); } target.before(noteDiv); noteDiv.addEventListener('click', () => addNote(userId, noteDiv), false); }); } else if (window.location.href.match(/profile;u=\d+$/)) { const [_, userId] = window.location.href.match(/u=(\d+)/); const target = getParentNodeNth(document.querySelector('#bodyarea table tr td tbody tr:nth-child(2) tr:last-child').parentNode, 1); const noteDiv = document.createElement('div'); const note = await getUserNote(userId); if (!note) { noteDiv.innerHTML = texts.addNote; } else { noteDiv.innerHTML = texts.withNote(note); } target.before(noteDiv); noteDiv.addEventListener('click', () => addNote(userId, noteDiv), false); } })();