Add the ability to add notes to any post + add categories to post + hide posts\categories
// ==UserScript== // @name VK Post Notes // @namespace http://tampermonkey.net/ // @version 0.19 // @description Add the ability to add notes to any post + add categories to post + hide posts\categories // @author psxvoid // @match *://vk.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @noframes true // @license https://creativecommons.org/licenses/by-sa/4.0/ // @homepage https://github.com/psxvoid/vkpostnotes // @supportURL https://github.com/psxvoid/vkpostnotes/issues // ==/UserScript== (function () { 'use strict'; //Additional facts: //1) The code is pretty ugly and break multiple good programming principles. Take care of your eyes. //2) The script saves notes to a local storage. It is not optimized. //3) It has no backup support for now, do not use it to store an important information. //4) It partially uses es6 features (not in the best way), so ensure that your browser is supporting it. // Have fun! GM_registerMenuCommand('Export Notes (JSON)', () => { const a = document.createElement('a'); const data = notesStorage.getNotesAsString(); a.href = "data:text/plain," + escape(data); // content a.download = "vkpostnotes.txt"; // file name a.click(); // alert("Put script's main function here"); }, 'r'); GM_registerMenuCommand('Import Notes (JSON)', () => { const input = document.createElement('input'); input.type = 'file'; input.onchange = (e) => { var file = e.target.files[0]; var reader = new FileReader(); reader.onload = (onloadEvent) => { const content = unescape(onloadEvent.target.r###lt); const contentOject = JSON.parse(content); notesStorage.import(contentOject); alert('Notes are successfully imported!'); }; reader.readAsText(file); }; input.click(); }, 'r'); //Add google fonts. See: http://stackoverflow.com/questions/5751620/ways-to-add-javascript-files-dynamically-in-a-page let materialIconsStylesheet = document.createElement("link"); materialIconsStylesheet.setAttribute("rel", "stylesheet"); materialIconsStylesheet.setAttribute("type", "text/css"); materialIconsStylesheet.setAttribute("href", "https://fonts.googleapis.com/icon?family=Material+Icons"); document.getElementsByTagName("head")[0].appendChild(materialIconsStylesheet); //constants: const customNotesContainerClass = "post_custom_notes_container"; const gmStorageKey = "gm_vk_post_custom_note"; //helpers: let createElement = (htmlText) => { let element = document.createElement("div"); element.innerHTML = htmlText; return element.firstChild; }; let removeElement = (domElement) => { domElement.parentNode.removeChild(domElement); }; //notes save\restore class NotesStorage { constructor() { this.notesRuntimeCache = {}; this.notes = []; } addRuntimeNoteToCache(runtimeNote) { this.notesRuntimeCache[runtimeNote.noteId] = runtimeNote; //try to restore the note data let note = this.findNoteById(runtimeNote.noteId); if (note != null) { runtimeNote.text = note.text; runtimeNote.category = note.category; runtimeNote.isPostHidden = note.isPostHidden; } } save() { //see: http://stackoverflow.com/questions/15730216/how-where-to-store-data-in-a-chrome-tampermonkey-script debugger; for (var runtimeNoteId in this.notesRuntimeCache) { let runtimeNote = this.notesRuntimeCache[runtimeNoteId], noteIndex = this.getNoteIndex(runtimeNote.noteId), note = this.getNoteByIndex(noteIndex), noteShouldBeSaved = (runtimeNote.text && runtimeNote.text.length > 0) || (runtimeNote.category != null) || runtimeNote.isPostHidden === true, newNote = null; if (noteShouldBeSaved) { newNote = { noteId: runtimeNote.noteId, postId: runtimeNote.postId, text: runtimeNote.text, category: runtimeNote.category, isPostHidden: runtimeNote.isPostHidden }; } if (note != null) { //handle changed text: if (noteShouldBeSaved) { this.notes[noteIndex] = newNote; } else { //remove note this.notes.splice(noteIndex, 1); } } else { //add new note: if (noteShouldBeSaved) { this.notes.push(newNote); } } } let notesData = JSON.stringify(this.notes); GM_setValue(gmStorageKey, notesData); } getNotesAsString() { return JSON.stringify(this.notes); } //TODO: Broke single responsibility, move to other class hidePostsWithCategory(category) { let notesToHide = []; for (var runtimeNoteId in this.notesRuntimeCache) { let runtimeNote = this.notesRuntimeCache[runtimeNoteId]; if (runtimeNote.category === category && runtimeNote.postDomElement != null) { notesToHide.push(runtimeNote); } } for (let i = 0; i < notesToHide.length; ++i) { try { //notesToHide[i].postDomElement.parentNode can be null here => exception will be thrown removeElement(notesToHide[i].postDomElement); notesToHide[i].postDomElement = null; } catch (ex) { //try to delete it later setTimeout(() => { try { removeElement(notesToHide[i].postDomElement); notesToHide[i].postDomElement = null; } catch (ex) { console.log("!!! Can't remove post node !!!"); } }, 500); } } } findNoteById(noteId) { //let r###lt = this.notes.filter(function(note) { //return note.noteId === noteId; //return note.noteId === noteId && note.text && note.text.length > 0; //}); let index = this.getNoteIndex(noteId); if (index >= 0) { return this.notes[index]; } } getNoteIndex(noteId) { let index = -1; for (let i = 0; i < this.notes.length; ++i) { if (this.notes[i].noteId == noteId) { index = i; break; } } return index; } getNoteByIndex(index) { if (index != null && index >= 0) { return this.notes[index]; } } load() { try { let notes = JSON.parse(GM_getValue(gmStorageKey)); this.notes = notes; } catch (ex) { //GM_setValue(gmStorageKey, JSON.stringify([])); //this.notes = []; console.log("Failed to load notes!!!"); } } import(notesObject) { this.notes = notesObject; this.save(); } } class Lightbox { constructor() { this.element = null; this.postId = null; } setTextAreaElement(textAreaElement) { this.textAreaElement = textAreaElement; } show(runtimeNote) { this.postId = runtimeNote.postId; this.runtimeNote = runtimeNote; this.element.style.display = "block"; if (runtimeNote.text && runtimeNote.text.length > 0) { this.textAreaElement.value = runtimeNote.text; } else { this.textAreaElement.value = ""; } } hide() { this.postId = null; this.runtimeNote = null; this.element.style.display = "none"; } save() { if (this.postId == null) return; let text = this.textAreaElement.value; if (text.length > 0) { this.runtimeNote.text = text; //change icon this.runtimeNote.addNoteElement.innerHTML = "description"; } else { this.runtimeNote.text = null; this.runtimeNote.addNoteElement.innerHTML = "note_add"; } notesStorage.save(); this.hide(); } } class RuntimeCategoryManager { constructor() { this["hiddenCategories"] = []; } markCategoryAsHidden(category) { if (this["hiddenCategories"].indexOf(category) < 0) { this["hiddenCategories"].push(category); } } isCategoryHidden(category) { return this["hiddenCategories"].indexOf(category) >= 0; } } //'global' variables let notesStorage = new NotesStorage(), lightbox = new Lightbox(), categoryManager = new RuntimeCategoryManager(), isObserving = false, isNextProcessingRequested = false; // Get PostId // 26.03.2017 20:04 The format is following: post50101872_500 let getPostId = (post) => { return post.id; }; let buildContainerId = (postId) => { return postId + "-custom-notes-container"; }; let buildNoteId = (postId) => { return postId + "-custom-note"; }; //TODO: Check if note text is already saved. If yes, then load text. let createNote = (postId, postDomElement) => { const addNoteButtonClass = "post-custom-note"; const categoryButtonClass = "post-custom-note-change-category-button"; const hideButtonClass = "post-hide-note-button"; const blockButtonClass = "post-block-button"; let addNoteButtonId = postId + "-custom-notes-add-note-button"; let categoryButtonId = postId + "-custom-notes-category-note-button"; let hideButtonId = postId + "-custom-notes-hide-category-button"; let blockButtonId = postId + "-custom-notes-block-button"; let categoryIconId = postId + "-custom-notes-category-icon", categoryIconClass = "post-custom-note-category-icon"; let noteId = buildNoteId(postId); let noteContainerId = buildContainerId(postId); //Create "Custom Note" container // How to add? See : https://material.io/icons/#ic_note_add let containerElement = document.createElement("div"); containerElement.id = noteId; containerElement.className = customNotesContainerClass; //Get category icon by category name //TODO: refactor let getCategoryIconHtml = (category) => { if (category === "cancel") { return "<i id='" + categoryIconId + "' class='material-icons " + categoryIconClass + "'>cancel</i>"; } if (category === "done") { return "<i id='" + categoryIconId + "' class='material-icons " + categoryIconClass + "'>done</i>"; } if (category === "active") { return "<i id='" + categoryIconId + "' class='material-icons " + categoryIconClass + "'>airplanemode_active</i>"; } if (category === "question") { return "<i id='" + categoryIconId + "' class='material-icons " + categoryIconClass + "'>help_outline</i>"; } if (category === "car0") { return "<i id='" + categoryIconId + "' class='material-icons " + categoryIconClass + "'>directions_car</i>"; } if (category === "suspended") { return "<i id='" + categoryIconId + "' class='material-icons " + categoryIconClass + "'>hotel</i>"; } return ""; }; //"add note" button let note = notesStorage.findNoteById(noteId); let buttonsHtml = "", categoryIcon = null; if (note == null) { buttonsHtml = '<i id="' + addNoteButtonId + '"class="material-icons ' + addNoteButtonClass + '">note_add</i>'; } else { //note could be not empty when category is set that is why we need to verify "text" property if (note.text != null) { buttonsHtml = '<i id="' + addNoteButtonId + '"class="material-icons ' + addNoteButtonClass + '">description</i>'; } else { buttonsHtml = '<i id="' + addNoteButtonId + '"class="material-icons ' + addNoteButtonClass + '">note_add</i>'; } if (note.category != null) { categoryIcon = getCategoryIconHtml(note.category); } } buttonsHtml += '<i ' + hideButtonId + '" class="material-icons ' + hideButtonClass + '">visibility_off</i>'; buttonsHtml += '<i ' + categoryButtonId + '" class="material-icons ' + categoryButtonClass + '">group_work</i>'; buttonsHtml += '<i ' + blockButtonId + '" class="material-icons ' + blockButtonClass + '">block</i>'; if (categoryIcon != null) { buttonsHtml += categoryIcon; } containerElement.innerHTML = buttonsHtml; let addNoteButtonElement = containerElement.getElementsByClassName(addNoteButtonClass)[0]; let changeCategoryButtonElement = containerElement.getElementsByClassName(categoryButtonClass)[0]; let categoryIconElement = containerElement.getElementsByClassName(categoryIconClass)[0]; let hideButtomElement = containerElement.getElementsByClassName(hideButtonClass)[0]; let blockButtomElement = containerElement.getElementsByClassName(blockButtonClass)[0]; let runtimeNote = { "noteId": noteId, "postId": postId, "text": null, "category": null, "postDomElement": postDomElement, "containerElement": containerElement, "addNoteElement": addNoteButtonElement, "categoryElement": changeCategoryButtonElement, "categoryIconElement": categoryIconElement, "hideButtomElement": hideButtomElement, "blockButtomElement": blockButtomElement }; addNoteButtonElement.onclick = (e) => { e.stopPropagation(); lightbox.show(runtimeNote); }; hideButtomElement.onclick = (e) => { e.stopPropagation(); if (runtimeNote.category != null) { categoryManager.markCategoryAsHidden(runtimeNote.category); notesStorage.hidePostsWithCategory(runtimeNote.category); } }; blockButtomElement.onclick = (e) => { e.stopPropagation(); runtimeNote.isPostHidden = true; notesStorage.save(); removeElement(runtimeNote.postDomElement); }; changeCategoryButtonElement.onclick = (e) => { e.stopPropagation(); //TODO: Change category id //runtimeNote if (runtimeNote.category == null) { runtimeNote.category = "cancel"; } else if (runtimeNote.category === "cancel") { runtimeNote.category = "done"; } else if (runtimeNote.category === "done") { runtimeNote.category = "suspended"; } else if (runtimeNote.category === "suspended") { runtimeNote.category = "question"; } else if (runtimeNote.category === "question") { runtimeNote.category = "car0"; } else if (runtimeNote.category === "car0") { runtimeNote.category = null; } else { runtimeNote.category = null; } //process dom changes if (runtimeNote.category != null) { let newElement = createElement(getCategoryIconHtml(runtimeNote.category)); if (runtimeNote.categoryIconElement == null) { //there wasn't category set before containerElement.appendChild(newElement); } else { //a category was set before containerElement.replaceChild(newElement, runtimeNote.categoryIconElement); } runtimeNote.categoryIconElement = newElement; } else { containerElement.removeChild(runtimeNote.categoryIconElement); runtimeNote.categoryIconElement = null; } notesStorage.save(); }; //Add note object to notes: notesStorage.addRuntimeNoteToCache(runtimeNote); return runtimeNote; }; let createLightboxElement = () => { //see: http://stackoverflow.com/questions/11668111/how-do-i-pop-up-a-custom-form-dialog-in-a-greasemonkey-script let template = '<div id="gmPopupContainer">' + '<form> <!-- For true form use method="POST" action="YOUR_DESIRED_URL" -->' + //'<input type="text" id="myNumber1" value=""/>' + //'<input type="text" id="myNumber2" value=""/>' + '<textarea rows="15" cols="50" id="gm-dlg-post-custom-note-textarea"/></textarea>' + '<p id="myNumberSum"> </p>' + '<button id="gmSaveDlgBtn" class="gmSaveDlgBtnClass" type="button">Save</button>' + '<button id="gmCloseDlgBtn" class="gmCloseDlgBtnClass" type="button">Close popup</button>' + '</form>' + '</div>'; let lightboxElement = document.createElement("div"); lightboxElement.innerHTML = template; lightboxElement.style.display = "none"; let saveButtonElement = lightboxElement.getElementsByClassName("gmSaveDlgBtnClass")[0]; let closeButtonElement = lightboxElement.getElementsByClassName("gmCloseDlgBtnClass")[0]; closeButtonElement.onclick = (e) => { e.stopPropagation(); lightbox.hide(); }; saveButtonElement.onclick = (e) => { e.stopPropagation(); lightbox.save(); }; lightbox.element = lightboxElement; document.body.appendChild(lightboxElement); lightbox.setTextAreaElement(document.getElementById("gm-dlg-post-custom-note-textarea")); //--- CSS styles make it work... //see: http://stackoverflow.com/questions/1360194/gm-addstyle-not-working GM_addStyle("#gmPopupContainer{ position: fixed;top: 30%;left: 20%;padding: 2em;background: powderblue;border: 3pxdoubleblack;border-radius: 1ex;z-index: 777;} #gmPopupContainerbutton{cursor: pointer;margin: 1em1em0;border: 1pxoutsetbuttonface;}"); }; createLightboxElement(); let processDom = () => { isObserving = true; //Get all posts let posts = document.getElementsByClassName("post"); for (let i = 0; i < posts.length; ++i) { let postId = getPostId(posts[i]); let postHeader = posts[i].getElementsByClassName("post_header_info")[0]; let postNoteContainer = postHeader.getElementsByClassName(customNotesContainerClass)[0]; if (postNoteContainer == null) { //container is not created yet, create it: let runtimeNote = createNote(postId, posts[i]); if (runtimeNote.isPostHidden === true || categoryManager.isCategoryHidden(runtimeNote.category)) { removeElement(runtimeNote.postDomElement); } else { postHeader.appendChild(runtimeNote.containerElement); } } } setTimeout(() => { if (isNextProcessingRequested) { isNextProcessingRequested = false; processDom(); } }, 100); }; notesStorage.load(); processDom(); //See: https://greasyfork.org/en/scripts/22457-remove-ad-posts-from-vk-com/code //See: http://stackoverflow.com/a/14570614 var observeDOM = (function () { var MutationObserver = window.MutationObserver || window.WebKitMutationObserver, eventListenerSupported = window.addEventListener; return function (obj, callback) { if (MutationObserver) { // define a new observer var obs = new MutationObserver(function (mutations, observer) { //if(mutations[0].addedNodes.length || mutations[0].removedNodes.length) if (mutations[0].addedNodes.length) if (isObserving) { isNextProcessingRequested = true; return; } callback(); }); // have the observer observe foo for changes in children obs.observe(obj, { childList: true, subtree: true }); } else if (eventListenerSupported) { obj.addEventListener('DOMNodeInserted', callback, false); //obj.addEventListener('DOMNodeRemoved', callback, false); } }; })(); let containers = document.querySelectorAll('body'); let n = containers.length; for (let i = 0; i < n; ++i) { let d = containers[i]; //TODO: Uncomment, performance issues observeDOM(d, processDom); } })();