- // ==UserScript==
- // @name 314n.org improver
- // @name:ru 314n.org improver
- // @namespace 314n.org
- // @version 1.0.2
- // @match https://314n.org/*
- // @match https://314n.ru/*
- // @description Improves 314n.org user-expiriense.
- // @description:ru Улучшает взаимодействие с 314n.org.
- // @icon https://314n.org/f1.png
- // @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
- // @run-at document-end
- // @license GPLv3
- // ==/UserScript==
-
- /*jshint esnext: true */
-
- const TEXT_COLOR_RGB = 'rgb(95, 191, 255)';
- const TEXT_COLOR_HEX = '#5FBFFF';
- const TEXT_HIGHLIGHTED_COLOR = '#eee';
- const EVENT_SUBMIT_ACCOUNT = 'event-submit-account';
- const CLASS_BOARD_LINK = 'board-link';
- const CLASS_THREAD_LINK = 'thread-link';
-
- const CONTEXT_EMPTY = '';
- const CONTEXT_LOGGED = 'logged';
- const CONTEXT_BOARDS = 'boards';
- const CONTEXT_BOARD = 'board';
- const CONTEXT_TOPIC = 'topic';
-
- var scriptElements = [];
- var loginForm;
- var regForm;
- var log = console.log;
- var ce = document.createElement.bind(document);
- var cn = document.createTextNode.bind(document);
- var boardEreg = /\[(\d+)\]\s+(.+)\s+/;
- var bracketsEreg = /\[(\d+)\]/;
- let underliner = (e)=>e.target.style.setProperty('text-decoration','underline');
- let unUnderliner = (e)=>e.target.style.removeProperty('text-decoration');
- let pointer = (e)=>e.target.style.cursor = 'pointer';
- let unPointer = (e)=>e.target.style.removeProperty('cursor');
- let textHighlight = (e)=>e.target.style.color = TEXT_HIGHLIGHTED_COLOR;
- let unTextHighlight = (e)=>e.target.style.removeProperty('color');
- var currentContext = '';
- let commandsMap = new Map(); /* String -> Command*/
- let checkCommandEReg = null;
- let lastEreg = /last/i;
- let lastPageNum = 9999;
-
- $(document).ready(init);
-
- function init(){
-
- initCommands();
-
- regForm = regEl(createRegForm());
- let reg = createPanelButton('reg', regForm.id);
- reg.addEventListener('click', showAccountForm);
-
- loginForm = regEl(createLoginForm());
- let login = createPanelButton('log-in', loginForm.id);
- login.addEventListener('click', showAccountForm);
-
- // create menu buttons wrapper
- let btnWrap = regEl(ce('div'));
-
- btnWrap.style.display = 'flex';
- btnWrap.style.setProperty('display','flex');
- btnWrap.style.setProperty('flex-flow','row nowrap');
- btnWrap.style.setProperty('justify-content','flex-start');
- btnWrap.style.position = 'absolute';
- btnWrap.style.bottom = '100%';
- btnWrap.style.right = '0';
- btnWrap.style.zIndex = '10';
- btnWrap.appendChild($(createPanelButton('boards')).click(e=>simulateInput('BOARDS')).get(0));
- btnWrap.appendChild(reg);
- btnWrap.appendChild(login);
- btnWrap.appendChild($(createPanelButton('log-out')).click(e => simulateInput('LOGOUT')).get(0));
- btnWrap.appendChild($(createPanelButton(' ? ')).click(e => simulateInput('HELP')).get(0));
- $('#board').append(btnWrap);
- $('#board').click(globalClick);
-
- reinitCmd();
-
- setTimeout(focusCmd, 1);
-
- currentContext = CONTEXT_EMPTY;
- }
-
- function initCommands(){
-
- // no args
- addCommand(new CommandHelp());
- addCommand(new CommandBoards());
- addCommand(new Command('LOGOUT'));
- addCommand(new CommandNav('NEXT'));
- addCommand(new CommandNav('PREV'));
- addCommand(new CommandNav('FIRST'));
- addCommand(new CommandNav('LAST'));
- addCommand(new CommandNav('REFRESH'));
-
- // one arg
- addCommand(new CommandTimezone());
- addCommand(new CommandBoard());
- addCommand(new CommandRvt());
- addCommand(new CommandReply());
- addCommand(new CommandOneArg('DELETE','p'));
- addCommand(new CommandOneArg('EDIT','p'));
- addCommand(new CommandPage());
-
- // two args
- addCommand(new CommandAccount('LOGIN','u','p'));
- addCommand(new CommandAccount('REGISTER','u','p'));
- addCommand(new CommandTopic());
- ///NOTE No NEWTOPIC command - there is no simple way
- // to separate title from content without keys
-
- //construct ereg to match command name
- rebuildCommandsEReg();
- }
-
- function addCommand(cmd,/*Bool*/rebuild){
- commandsMap.set(cmd.name,cmd);
- if(rebuild)
- rebuildCommandsEReg();
- }
-
- function rebuildCommandsEReg(){
- let cmdsStr = '';
- commandsMap.forEach((val,key)=>{
- cmdsStr+= (key+'|');
- });
- cmdsStr = cmdsStr.substr(0, cmdsStr.length-1); // remove last | char from string
- checkCommandEReg = new RegExp(`^\\s*(${cmdsStr})`,'i');
- log(checkCommandEReg);
- }
-
- function reinitCmd(){
- let cmd = el('#cmd');
- let newCmd = cmd.cloneNode();
- newCmd.dataset.new = 'yes';
- cmd.parentElement.appendChild(newCmd);
- $(cmd).remove();
- cmd = newCmd;
-
- $(cmd).keydown((e)=>{
- if (e.keyCode == 38) {
- if (command_number > 0) {
- if (command_number == commands.length)
- commands.push(e.currentTarget.value);
- command_number--;
- e.currentTarget.value = commands[command_number];
- }
- return false;
- }
-
- if (e.keyCode == 40) {
- if (command_number < commands.length - 1) {
- command_number++;
- e.currentTarget.value = commands[command_number];
- }
- return false;
- }
-
- if((e.keyCode == 13 || e.key=='Enter' || e.code=='Enter') && !e.shiftKey){
-
- let command = getCommandObj(cmd.value);
-
- logInput(cmd.value);
- sendCommand(command.processInput(cmd.value), command.processOutput.bind(command));
- return false;
- }
- });
-
- return cmd;
- }
-
- function getCommandObj(input){
- let match = checkCommandEReg.exec(input);
- if(match!==null){
- let cmd = commandsMap.get(match[1].toLowerCase());
- if(cmd) return cmd;
- }
- return new Command(input);
- }
-
- function globalClick(e){
- if(e.target.classList.contains(CLASS_BOARD_LINK)){
- simulateInput(`BOARD -n ${e.target.dataset.boardNumber}`);
- }
- else if(e.target.classList.contains(CLASS_THREAD_LINK)){
- simulateInput(`TOPIC -n ${e.target.dataset.threadNumber}`);
- }
- }
-
- function createLoginForm(){
- return createAccountForm('LOGIN');
- }
-
- function createRegForm(){
-
- let form = createAccountForm('REGISTER');
-
- form.addEventListener(EVENT_SUBMIT_ACCOUNT,(e)=>{
- let login = id('login-form');
- let reg = e.currentTarget;
- if(login!==null){
- let lname = el('[type=text]',login);
- let lpass = el('[type=password]',login);
- let rname = el('[type=text]',reg);
- let rpass = el('[type=password]',reg);
- lname.value = rname.value;
- lname.innerText = rname.innerText;
- lpass.value = rpass.value;
- lpass.innerText = rpass.innerText;
- }
- });
-
- return form;
- }
-
- function createAccountForm(commandText){
- let form = ce('div');
-
- function getInput(type, name){
- var inp = ce('input');
- inp.setAttribute('type',type);
- inp.setAttribute('name',name);
- sanitizeInputStyle(inp);
- inp.style.color = TEXT_COLOR_HEX;
- inp.style.paddingBottom = '2px';
- inp.style.borderBottom = `2px solid ${TEXT_COLOR_HEX}`;
- inp.style.width = '10em';
- return inp;
- }
-
- let nameInput = getInput('text','name');
- let nameLabel = ce('label');
- nameLabel.appendChild(createReverse('name: '));
- nameLabel.appendChild(nameInput);
-
- let passInput = getInput('password','pass');
- let passLabel = ce('label');
- passLabel.appendChild(createReverse('password: '));
- passLabel.appendChild(passInput);
-
- let label = createReverse(`[ ${commandText.toUpperCase()} ]`);
- label.style.outline='none';
- let ok = ce('button');
- ok.appendChild(label);
- ok.style.margin = '0 auto';
- ok.style.marginTop = '15px';
- ok.style.color = TEXT_COLOR_HEX;
- ok.style.display = 'block';
- sanitizeInputStyle(ok);
- $(ok).hover((e)=>{
- e.currentTarget.querySelector('span').style.backgroundColor = '#eee';
- ok.style.cursor = 'pointer';
- }, (e)=>{
- e.currentTarget.querySelector('span').style.removeProperty('background-color');
- ok.style.removeProperty('cursor');
- });
-
- function submit(e){
- let name = el('[type=text]',form);
- let pass = el('[type=password]',form);
- simulateInput(`${commandText.toLowerCase()} -u ${name.value} -p ${pass.value}`);
- form.style.display = 'none';
- form.dispatchEvent(new Event(EVENT_SUBMIT_ACCOUNT));
- }
-
- ok.addEventListener('click',submit);
- passInput.addEventListener('keydown',(evt)=>{
- if(evt.keyCode===13)
- submit();
- });
-
- form.id = `${commandText.toLowerCase()}-form`;
- form.classList.add('acc-form');
- form.style.outline = '1px solid rgb(0,255,255)';
- form.style.padding = '15px';
- form.style.position = 'absolute';
- form.style.top = '0';
- form.style.right = '0';
- form.style.textAlign = 'right';
- form.style.backgroundColor = '#000';
- form.style.display = 'none';
- form.appendChild(nameLabel);
- form.appendChild(ce('br'));
- form.appendChild(passLabel);
- form.appendChild(ce('br'));
- form.appendChild(ok);
-
- id('board').appendChild(form);
-
- return form;
- }
-
- function sanitizeInputStyle(el){
- el.style.outline = 'none';
- el.style.border = 'none';
- el.style.background = 'transparent';
- return el;
- }
-
- function createPanelButton(text, forId){
- let ret = createReverse(text);
- $(ret).hover((e)=>{ret.style.cursor='pointer';
- ret.style.backgroundColor='#eee';},
- (e)=>{ret.style.cursor='initial';
- ret.style.removeProperty('background-color')});
- if(forId)
- ret.dataset.forId = forId;
- return ret;
- }
-
- function createReverse(text){
- var ret = ce('span');
- ret.classList.add('reverse');
- ret.style.padding = '2px 4px';
- ret.style.marginRight = '5px';
- ret.innerText = text;
- return ret;
- }
-
- function regEl(el){
- scriptElements.push(el);
- return el;
- }
-
- function removeRegistered(){
- scriptElements.forEach(el=>{
- $(el).remove();
- });
- scriptElements = [];
- }
-
- function simulateInput(command){
- let cmd = el('#cmd');
- cmd.value = command;
- cmd.innerText = command;
- cmd.dispatchEvent(new KeyboardEvent('keydown',{keyCode:13, shiftKey:false}));
- }
-
- function showAccountForm(e){
- let form = id(e.currentTarget.dataset.forId);
- if(form){
- if(form.parentElement===null || form.style.display === 'none'){
- let board = id('board');
- els('.acc-form',board).forEach((elt)=>elt.style.display='none');
- board.appendChild(form);
- form.style.display = 'block';
- }else{
- form.style.display = 'none';
- }
- }
- }
-
- function logInput(/*String*/command){
- commands.push(command);
- command_number = commands.length;
- }
-
- function sendCommand(/*String*/command,/*Function*/callback){
- el('#cmd').value = '';
- el('#cmd').innerText = '';
- $.ajax({
- type:'POST',
- url:'console.php',
- dataType:'json',
- data:{input: command},
- success: callback
- // success: (r)=>{callback(r);}
- });
- }
-
- function focusCmd(){
- el('#cmd').focus();
- }
-
- function removeEl(from,selector){
- var els = from.querySelectorAll(selector);
- els.forEach((v,i,l)=>{
- if(v.parentElement!==null)
- v.parentElement.removeChild(v);
- })
- }
-
- function id(id){
- return document.getElementById(id);
- }
-
- function el(/*String*/selector,/*Element*/el){
- if(el) return el.querySelector(selector);
- else return document.querySelector(selector);
- }
-
- function els(/*String*/selector,/*Element*/el){
- if(el) return el.querySelectorAll(selector);
- else return document.querySelectorAll(selector);
- }
-
- function loading(onComplete){
- str = 'Loading...';
- nextchar = str.charAt($('#loading').html().length);
- if ($('#loading').html().length < 10) {
- $('#loading').html($('#loading').html()+nextchar);
- if(onComplete)
- setTimeout(loading, 40, onComplete);
- else
- setTimeout(loading, 40);
- } else {
- $('.content').css('display', 'block');
- if(onComplete)
- onComplete();
- else
- nav_down();
- }
- }
-
- function empty(){}
-
- //---------------------------------------------------
- // COMMANDS
- //---------------------------------------------------
- /*
- NOTE How commands works.
-
- There is two ways - WITH keys and WITHOUT keys.
- If input has at least one key like '-k'
- then it passes to server without any changes.
- Otherwise there is an attempt to extract values
- and rebuild input with concrete key-value data,
- then new input passed to server.
- Otherwise input passes to server without any changes.
-
- */
-
- /** Base command */
- function Command(name){
- this.name = name?name:'';
- this.name = this.name.toLowerCase();
- this.argReg = /[^\\]-(.+?)(?= -|$)/i; // is need only test, so no GLOBAL flag
- }
- Command.prototype.processInput = function(input){return input;};
- Command.prototype.processOutput = function(response){
- if (response.edit) {
- $('#path').html(response.path+' > ');
- $('#cmd').val(response.edittext);
- } else {
- if (response.clear) $('#content').html('');
- $('#content').append(response.message);
- if (response.path) $('#path').html(response.path+' > ');
-
- $('.content').css('display', 'block');
- if (response.clear) loading();
- else nav_down();
- }
- focusCmd();
- };
- Command.processOutputWithContext = function(response){
- switch(currentContext){
- case CONTEXT_BOARD:
- // log('board context');
- commandsMap.get('board').processOutput(response);
- break;
-
- case CONTEXT_TOPIC:
- // log('topic context');
- commandsMap.get('topic').processOutput(response);
- break;
-
- default:
- // log('no ctx');
- Object.getPrototypeOf(Object.getPrototypeOf(this)).processOutput(response);
- }
- };
- Command.getLastPage = function(pageStr){
- return lastEreg.test(pageStr)?lastPageNum:pageStr;
- };
-
- //---------------------------------------------------
- // NO arg commands
- //---------------------------------------------------
-
- function CommandHelp(){
- Command.call(this, 'help');
- this.boardReg = /(BOARD +)(-n)/;
- this.topicReg = /TOPIC -n <number> [-p <page>]/;
-
- this.keysReg = /Before the parameter.+\./;
- this.newKeysInfo = 'You can write commands* with** or without** keys (keys looks like "-k").<br><br>* with the exception of <span class=reverse style="padding:0 4px"> NEWTOPIC </span><br>** you cannot combine both ways - it is possible to use only one at a time';
- }
- CommandHelp.prototype = Object.create(Command.prototype);
- CommandHelp.prototype.constructor = CommandHelp;
- CommandHelp.prototype.processInput = function(input){return input;};
- CommandHelp.prototype.processOutput = function(response){
-
- if(response.clear) $('#content').html('');
- let msg = response.message.replace(this.keysReg, this.newKeysInfo);
-
- $('#content').append(msg);
- if (response.path) $('#path').html(response.path+' > ');
- if (response.clear) loading();
- else nav_down();
- };
-
- function CommandBoards(){
- Command.call(this,'boards');
- }
- CommandBoards.prototype = Object.create(Command.prototype);
- CommandBoards.prototype.constructor = CommandBoards;
- CommandBoards.prototype.processOutput = function(response){
-
- if (response.clear) $('#content').html('');
- let els = $.parseHTML(response.message);
- els.forEach((el)=>{
-
- let nodes = Array.prototype.slice.call(el.childNodes);
- nodes = nodes.map((n)=>{
- if(n.nodeType!==3)
- return n;
- else{
- let matches = boardEreg.exec(n.textContent);
- if(matches!==null){
- let num = matches[1];
- let name = matches[2];
- let b = ce('span');
- b.classList.add(CLASS_BOARD_LINK);
- b.innerText = `[${num}] ${name} `;
- b.dataset.boardNumber = num;
- $(b).hover(underliner,unUnderliner);
- $(b).hover(pointer,unPointer);
- $(b).hover(textHighlight,unTextHighlight);
- return b;
- }
- else return n;
- }
- });
-
- while(el.firstChild!==null)
- el.removeChild(el.firstChild);
- nodes.forEach(n=>el.appendChild(n));
- });
-
- els.forEach((el)=>$('#content').append(el));
-
- if (response.path) $('#path').html(response.path+' > ');
- if (response.clear) loading();
- else nav_down();
- focusCmd();
- currentContext = CONTEXT_BOARDS;
- };
-
- function CommandNav(name){
- Command.call(this, name);
- }
- CommandNav.prototype = Object.create(Command.prototype);
- CommandNav.prototype.constructor = CommandNav;
- CommandNav.prototype.processOutput = function(response){
- Command.processOutputWithContext.call(this,response);
- };
-
- //---------------------------------------------------
- // ONE args commands
- //---------------------------------------------------
-
- function CommandOneArg(name,arg){
- Command.call(this, name);
- this.arg = arg;
- this.ereg = new RegExp(`${this.name} +(.+)`,'i');
- }
- CommandOneArg.prototype = Object.create(Command.prototype);
- CommandOneArg.prototype.constructor = CommandOneArg;
- CommandOneArg.prototype.processInput = function(input){
- let match = this.argReg.exec(input);
- if(match){
- return input;
- }
-
- match = this.ereg.exec(input);
- if(match){
- return `${this.name} -${this.arg} ${match[1]}`;
- }
-
- return input;
- };
-
- function CommandTimezone(){
- CommandOneArg.call(this, 'timezone', 'u');
- }
- CommandTimezone.prototype = Object.create(CommandOneArg.prototype);
- CommandTimezone.prototype.constructor = CommandTimezone;
-
- function CommandBoard(){
- CommandOneArg.call(this,'board', 'n');
- }
- CommandBoard.prototype = Object.create(CommandOneArg.prototype);
- CommandBoard.prototype.constructor = CommandBoard;
- CommandBoard.prototype.processOutput = function(response){
- if (response.clear) $('#content').html('');
-
- let wrap = ce('div');
- let els = $.parseHTML(response.message);
- els.forEach((elt)=>wrap.appendChild(elt));
-
- let numSelector = 'tr > td.postsnumber';
- let nameSelector = 'td > span.reverse';
- let nums = wrap.querySelectorAll(numSelector);
- let names = wrap.querySelectorAll(nameSelector);
- nums.forEach((elt,i)=>{
-
- let matches = bracketsEreg.exec(elt.innerText);
- let threadNumber = matches[1];
- let span = ce('span');
- span.classList.add(CLASS_THREAD_LINK);
- span.innerText = threadNumber;
- span.dataset.threadNumber = threadNumber;
- $(span).hover(underliner, unUnderliner);
- $(span).hover(pointer, unPointer);
- $(span).hover(textHighlight, unTextHighlight);
-
- elt.innerText = '';
- elt.appendChild(cn('['));
- elt.appendChild(span);
- elt.appendChild(cn(']'));
-
- let nameElt = names[i];
- nameElt.classList.add(CLASS_THREAD_LINK);
- nameElt.dataset.threadNumber = threadNumber;
- $(nameElt).hover(pointer, unPointer);
- $(nameElt).hover((e)=>e.currentTarget.style.backgroundColor='#eee',
- (e)=>e.currentTarget.style.removeProperty('background-color'));
-
- });
-
- els.forEach((elt)=>el('#content').appendChild(elt));
-
- if (response.path) $('#path').html(response.path+' > ');
- if (response.clear) loading(empty);
-
- focusCmd();
- currentContext = CONTEXT_BOARD;
- };
-
- function CommandRvt(){
- CommandOneArg.call(this,'rvt','p');
- this.ereg = new RegExp(`${this.name}( +(\\w+))?`,'i');
- }
- CommandRvt.prototype = Object.create(CommandOneArg.prototype);
- CommandRvt.prototype.constructor = CommandRvt;
- CommandRvt.prototype.processInput = function(input){
- let match = this.argReg.exec(input);
- if(match)
- return input;
-
- match = this.ereg.exec(input);
- if(match){
- let page = match[1];
- let ret;
- if(page){
- page = Command.getLastPage(match[2]);
- ret = `rvt -p ${page}`;
- }else
- ret = `rvt`;
-
- return ret;
- }
-
- return ret;
- };
- CommandRvt.prototype.processOutput = CommandBoard.prototype.processOutput;
-
- function CommandReply(){
- CommandOneArg.call(this,'reply','m');
- this.argReg = /[^\\]-(\w)(?= |$)/i;
- }
- CommandReply.prototype = Object.create(CommandOneArg.prototype);
- CommandReply.prototype.constructor = CommandReply;
-
- function CommandPage(){
- CommandOneArg.call(this,'page','p');
- }
- CommandPage.prototype = Object.create(CommandOneArg.prototype);
- CommandPage.prototype.constructor = CommandPage;
- CommandPage.prototype.processOutput = function(r){
- Command.processOutputWithContext.call(this, r);
- };
-
- //---------------------------------------------------
- // TWO args commands
- //---------------------------------------------------
-
- function CommandTwoArgs(name,arg1,arg2){
- CommandOneArg.call(this,name,arg1);
- this.arg2 = arg2;
- this.ereg = new RegExp(`${this.name} +(\\w+) +(.+)`,'i');
- }
- CommandTwoArgs.prototype = Object.create(CommandOneArg.prototype);
- CommandTwoArgs.prototype.constructor = CommandTwoArgs;
- CommandTwoArgs.prototype.processInput = function(input){
- // check if input has key-value like '-u username'
- // if so do nothing and return input as is
- // otherwise check if input have no keys like 'login username password'
- // if so - build command with appropriate keys and values
- // else return input as is
- let match = this.argReg.exec(input);
- if(match){
- // it is a key-value variant
- return input;
- }
-
- match = this.ereg.exec(input);
- if(match){
- // no keys variant
- return `${this.name} -${this.arg} ${match[1]} -${this.arg2} ${match[2]}`;
- }
-
- return input;
- };
-
- function CommandAccount(name, arg1, arg2){
- CommandTwoArgs.call(this,name,arg1,arg2);
- }
- CommandAccount.prototype = Object.create(CommandTwoArgs.prototype);
- CommandAccount.prototype.constructor = CommandAccount;
-
- function CommandTopic(){
- CommandTwoArgs.call(this,'topic','n','p');
- this.ereg = new RegExp(`${this.name} +(\\w+)( +(\\w+))?`,'i');
- }
- CommandTopic.prototype = Object.create(CommandTwoArgs.prototype);
- CommandTopic.prototype.constructor = CommandTopic;
- CommandTopic.prototype.processInput = function(input){
- let match = this.argReg.exec(input);
- if(match)
- return input;
-
- match = this.ereg.exec(input);
- if(match){
- let num = match[1];
- let page = match[2];
- let ret;
- if(page){
- page = Command.getLastPage(match[3]);
- ret = `${this.name} -n ${num} -p ${page}`;
- }else{
- ret = `${this.name} -n ${num}`;
- }
- return ret;
- }
-
- return input;
- };
- CommandTopic.prototype.processOutput = function(response){
- Object.getPrototypeOf(CommandTopic.prototype).processOutput.call(this, response);
- currentContext = CONTEXT_TOPIC;
- };
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-