// ==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.result);
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 result = 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);
}
})();