// ==UserScript== // @name 4-ch.net Refresher // @namespace Violentmonkey Scripts // @match https://4-ch.net/* // @version 1.0 // @author - // @description Shows how many new posts there are // @grant GM_setValue // @grant GM_getValue // ==/UserScript== "use strict"; const boards_list = ["general", "hobby", "personal", "req", "tv", "japan", "games", "music", "book", "ascii", "dqn", "tech", "iaa", "language", "nihongo", "current"] var board_is_shift_jis = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0] var refresh_list1 = [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] //var refresh_list1 = [ 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0] //var refresh_list1 = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] //var refresh_list2 = [ 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1] var fetching = 0 // if fetching > 0, we are currently grabbing katalog data for some boards var fetch = null // this is the [fetch] button var katalog_all = new Array(boards_list.length) //katalog_all is recently fetched and parsed katalog. Array of array of thread data. var katalog_all_old = new Array(boards_list.length) //katalog_all_old is previous version of katalog, we keep it to compare old amount of replies to new one. //katalog elements are [thread_id, wasnt_deleted_yet, amount_of_replies, thread_title] const kat_thread_id = 0, kat_wasnt_deleted_yet = 1, kat_amount_of_replies = 2, kat_thread_title = 3 var changes_all = new Array(boards_list.length) //changes_all is //changes elements are [changes_type, the rest] //if changes_type == 0, this is a "new thread" element, and the rest are [thread_id, amount_of_replies, thread_title] //if changes_type == 1, this is a "updated thread", and the rest are [index_in_katalog, new_amount_of_replies] //if changes_type == 2, this is a "deleted thread", and the rest are [index_in_katalog] const changes_type_new = 0, changes_type_updated = 1, changes_type_deleted = 2 const changes_type = 0, changes_thread_id = 1, changes_amount_of_replies = 2, changes_thread_title = 3, changes_index_in_katalog = 1, changes_new_amount_of_replies = 2 var board_el_all = new Array(boards_list.length) var refreshbox_peek = null var sync // sync is array of numbers from 0 to 255. // first one indicates if 4-c--changes was updated in another tab // the rest indicates if any of the katalog was updated in another tab. //TODO // *) The most important one is faving and hiding threads. The rest is feature creep. // *) And maybe keep track of last updated threads, I sometimes click merge too fast and regret doing it. // *) Maybe move [merge] button somewhere. And add merge to each thread. Add [mark all as read] // *) Add [options-]. Add list of: List board / Follow board / Ignore unfaved threads // *) Display thread as /dqn/1:Thread Name, show its position so that I can bump it in time. // This will require changing the way threads are currently stored, will need to store their position too. // Oh, I know, I can reuse kat_wasn't deleted yet, when 1 is first, 2 is second, etc, and 3 is deleted. // *) Options screen is a bunch of /board/ [_][X][_] checkboxes. Top row is description and will set all boards. // *) Option to show current thread position on board. // *) Option to autodelete unused threads. // *) A way to open katalog without actually going to the board of interest. // *) Save options as json, and all faved/hidden threads as json, so that the user can paste all of it into a single text field for the user to copypaste. // *) Get rid of refresh_list1 // *) Add a way to show faved threads even if there are no new posts. // *) Add the date to the sync, and compare the last fetch date. I need to stop old fetch from overwriting new fetch. // I just need to add buttons [refresh][by new-][fav first-][show hidden-][board list-] // [mark all as read] // and [thread list style:1] function start() { var stylebox = document.getElementById("stylebox") if (!stylebox) { return } stylebox.style.display="none" sync = sync_load() var l_changes_all = GM_getValue("4-c--changes") if (l_changes_all) { changes_all = JSON.parse(l_changes_all) } var titlebox = document.getElementById("stylebox") if (!titlebox) { error() return } var refreshboxouter = document.createElement("div") refreshboxouter.id = "refreshbox" refreshboxouter.classList.add("outerbox") titlebox.parentNode.insertBefore(refreshboxouter, titlebox) var refreshbox_base = document.createElement("div") refreshbox_base.id = "refreshbox_base" refreshbox_base.classList.add("innerbox") refreshboxouter.appendChild(refreshbox_base) var refreshbox = document.createElement("div") refreshbox_base.appendChild(refreshbox) refreshbox_peek = document.createElement("div") refreshbox_base.appendChild(refreshbox_peek) var style_show = document.createElement("a") style_show.href = "#" style_show.textContent = "[st-]" refreshbox.appendChild(style_show) style_show.onclick = function(evt) { if (stylebox.style.display == "") { stylebox.style.display = "none" style_show.textContent = "[st-]" } else { stylebox.style.display = "" style_show.textContent = "[st+]" } evt.stopPropagation();evt.preventDefault();return false; } fetch = document.createElement("a") fetch.href = "#" fetch.textContent = "[fetch]" refreshbox.appendChild(fetch) fetch.onclick = function(evt) { if (fetching != 0) { evt.stopPropagation();evt.preventDefault();return false; } var l_fetching = 0 for (var is_refreshed of refresh_list1) { l_fetching += is_refreshed } fetching = l_fetching fetch.textContent = "[fetching...]" for (var board = 0; board < boards_list.length; board++) { if (refresh_list1[board]) { katalog_request(board) } } evt.stopPropagation();evt.preventDefault();return false; } var merge = document.createElement("a") merge.href = "#" merge.textContent = "[merge]" refreshbox.appendChild(merge) merge.onclick = function(evt) { katalog_merge() evt.stopPropagation();evt.preventDefault();return false; } for (var i = 0; i < boards_list.length; i++) { var board = boards_list[i] refreshbox.appendChild(document.createTextNode(" ")) var boardel = document.createElement("a") boardel.href = "/" + board + "/" var b = changes_all[i] var updated = 0 var deleted = 0 if (b) for (var j = 0; j < b.length; j++) { if (b[j][changes_type] == changes_type_deleted) { deleted++ } else { updated++ } } var name = boards_list[i] if (updated) name += "(" + updated + ")" if (deleted) name += "(-" + deleted + ")" boardel.textContent = name refreshbox.appendChild(boardel) board_el_all[i] = boardel } document.addEventListener("focus", function(evt) { var new_sync = sync_load() for (var board = 0; board < boards_list.length; board++ ) { if (sync[board+1] != new_sync[board+1]) { katalog_all_old[board] = null katalog_get_old(board) } } if (sync[0] != new_sync[0]) { changes_all = JSON.parse(GM_getValue("4-c--changes")) board_redraw() } sync = new_sync }) inject() board_peek() } function sync_load() { var new_sync var l_sync = GM_getValue("4-c--sync") if (l_sync) { new_sync = JSON.parse(l_sync) if (new_sync.length != (boards_list.length+1)) new_sync = null } if (!new_sync) { new_sync = new Array(boards_list.length+1) for (var i = 0; i < (boards_list.length+1); i++ ) { new_sync[i] = 0 } GM_setValue("4-c--sync", JSON.stringify(new_sync)) } return new_sync } function sync_save() { GM_setValue("4-c--sync", JSON.stringify(sync)) } function sync_add(index) { sync[index]++ if (sync[index] > 255) sync[index] = 0 } function inject() { var threads = document.getElementsByClassName("thread") for (var thread_i = 0; thread_i < threads.length; thread_i++) { let thread = threads[thread_i] var threadlinks = thread.getElementsByClassName("threadlinks") if (threadlinks.length == 0) continue threadlinks = threadlinks[0] if (threadlinks.getElementsByTagName("a").length == 0) continue let favbottom = document.createElement("a") favbottom.innerText = "[Fav]" favbottom.href = "#" let hidebottom = document.createElement("a") hidebottom.innerText = "[Hide]" hidebottom.href = "#" threadlinks.appendChild(document.createTextNode(" ")) threadlinks.appendChild(favbottom) threadlinks.appendChild(document.createTextNode(" ")) threadlinks.appendChild(hidebottom) var threadnavigation = thread.getElementsByClassName("threadnavigation") if (threadnavigation.length == 0) continue threadnavigation = threadnavigation[0] let favtop = document.createElement("a") favtop.innerText = "[Fav]" favtop.href = "#" let hidetop = document.createElement("a") hidetop.innerText = "[Hide]" hidetop.href = "#" hidetop.onclick = hidebottom.onclick = function(evt) { if (hidetop.textContent == "[Hide]") { thread.style.height = "1.25em" thread.style.overflow = "hidden" hidetop.textContent = "[Unhide]" } else { thread.style.height = "" thread.style.overflow = "" hidetop.textContent = "[Hide]" } evt.stopPropagation();evt.preventDefault();return false; } add_before(threadnavigation.firstChild, hidetop) add_before(threadnavigation.firstChild, document.createTextNode(" ")) add_before(threadnavigation.firstChild, favtop) add_before(threadnavigation.firstChild, document.createTextNode(" ")) } } function board_peek() { refreshbox_peek.innerHTML = "" for (var board = 0; board < boards_list.length; board++ ) { var changes = changes_all[board] if (!changes) continue var katalog_old = katalog_get_old(board) if (!katalog_old) continue for (var change of changes) { var thread_id var title var how_many_posts var indicator var posts_before var posts_now var element_type = change[0] if (element_type == changes_type_new) { thread_id = change[changes_thread_id] title = change[changes_thread_title] how_many_posts = change[changes_amount_of_replies] indicator = "NEW:" posts_before = 0 posts_now = how_many_posts } else if (element_type == changes_type_updated) { var katalog_entry = katalog_old[change[changes_index_in_katalog]] thread_id = katalog_entry[kat_thread_id] title = katalog_entry[kat_thread_title] how_many_posts = change[changes_amount_of_replies] - katalog_entry[kat_amount_of_replies] indicator = "" posts_before = katalog_entry[kat_amount_of_replies] posts_now = change[changes_amount_of_replies] } else if (element_type == changes_type_deleted) { var katalog_entry = katalog_old[entry[changes_index_in_katalog]] thread_id = katalog_entry[kat_thread_id] title = katalog_entry[kat_thread_title] how_many_posts = katalog_entry[kat_amount_of_replies] indicator = "DEL:" posts_before = katalog_entry[kat_amount_of_replies] posts_now = change[kat_amount_of_replies] } var div = document.createElement("div") refreshbox_peek.appendChild(div) var a = document.createElement("a") div.appendChild(a) a.href = "/" + boards_list[board] + "/kareha.pl/" + thread_id + "/l50" a.textContent = "/" + boards_list[board] + "/" + title + "(" + indicator + how_many_posts + ")" var space = document.createTextNode(" ") div.appendChild(space) var a_newtab = document.createElement("a") div.appendChild(a_newtab) a_newtab.href = "/" + boards_list[board] + "/kareha.pl/" + thread_id + "/l50" a_newtab.setAttribute("target", "_blank") a_newtab.textContent = "[newtab]" if (element_type == changes_type_deleted) continue div.appendChild(document.createTextNode(" ")) let a_peek = document.createElement("a") div.appendChild(a_peek) a_peek.href = "#" a_peek.textContent = "[peek-]" let a_peek_div = document.createElement("div") a_peek_div.style.backgroundColor="#EFEFEF" div.appendChild(a_peek_div) let a_peek_board = board let a_peek_url if (how_many_posts == 1) { a_peek_url = "https://4-ch.net/" + boards_list[board] + "/kareha.pl/" + thread_id + "/" + posts_now } else { a_peek_url = "https://4-ch.net/" + boards_list[board] + "/kareha.pl/" + thread_id + "/n" + (posts_before+1) + "-" + posts_now } a_peek.onclick = function(evt) { if (a_peek.textContent == "[peek-]") { a_peek.textContent = "[peek+]" thread_request(a_peek, a_peek_div, a_peek_url) } else { a_peek.textContent = "[peek-]" a_peek_div.innerHTML = "" } evt.stopPropagation();evt.preventDefault();return false; } } } } function thread_request(a_peek, a_peek_div, url) { var xmlhttp = new XMLHttpRequest(); var address_full = url xmlhttp.open("GET", address_full, true) var board = 0 //LATER: need to do something about shift-jis being badly supported by TextDecoder if (board_is_shift_jis[board]) { xmlhttp.responseType = 'arraybuffer' } xmlhttp.onload = function(e) { var response = "" if (board_is_shift_jis[board]) { var uInt8Array = new Uint8Array(this.response) var codepage = "shift_jis" var shift_jis = new TextDecoder(codepage, {fatal: true}) response = shift_jis.decode(uInt8Array) } else { response = this.responseText } response = response.split(/]*>((?:.|\n|\r)*)<\/body>/i)[1] var page = document.createElement('div') page.innerHTML = response page = document.evaluate('//*[@class="allreplies"]', page, null, 9, null).singleNodeValue a_peek_div.innerHTML = page.innerHTML } xmlhttp.send(); } function board_redraw() { for (var i = 0; i < boards_list.length; i++ ) { var b = changes_all[i] var updated = 0 var deleted = 0 if (b) for (var j = 0; j < b.length; j++) { if (b[j][changes_type] == changes_type_deleted) { deleted++ } else { updated++ } } var name = boards_list[i] if (updated) name += "(" + updated + ")" if (deleted) name += "(-" + deleted + ")" board_el_all[i].textContent = name } if (fetching == 0) { fetch.textContent = "[fetch]" GM_setValue("4-c--changes", JSON.stringify(changes_all)) sync_add(0) sync_save() } board_peek() } function katalog_merge() { for (var board = 0; board < boards_list.length; board++ ) { //LATER var changes = changes_all[board] if (!changes) continue var katalog_old = katalog_get_old(board) if (!katalog_old) continue for (var change of changes) { if (change[changes_type] == changes_type_new) { if (katalog_old[katalog_old.length - 1][kat_thread_id] >= change[changes_thread_id]) { //I assume this will only ever happen if some thread will be moved from some other board //if it is already in the old_katalog, this is not an issue in the slightest //overwise, yeah, ring the bell //LATER: I should walk through the entire old katalog and check if it is present or not, but I'm lazy right now //this will break only if there are two threads added at once, and you merge them from an other tab. Quite unlikely if (katalog_old[katalog_old.length - 1][kat_thread_id] == change[changes_thread_id]) { continue; } else { alert("two or more threads at once, oh no") return } } var new_entry = [change[changes_thread_id], 1, change[changes_amount_of_replies], change[changes_thread_title]] katalog_old.push(new_entry) } else if (change[changes_type] == changes_type_updated) { katalog_old[change[changes_thread_id]][kat_amount_of_replies] = change[changes_amount_of_replies] } else if (change[changes_type] == changes_type_deleted) { katalog_old[change[changes_thread_id]][kat_wasnt_deleted_yet] = 0 } } changes_all[board] = null sync_add(board+1) katalog_set_old(board) } GM_setValue("4-c--changes", JSON.stringify(changes_all)) sync_add(0) //sync_add(1) sync_save() board_redraw() } function katalog_request(board) { var xmlhttp = new XMLHttpRequest(); var address_full = "https://4-ch.net/" + boards_list[board] + "/subback.html" xmlhttp.open("GET", address_full, true) if (board_is_shift_jis[board]) { xmlhttp.responseType = 'arraybuffer' } xmlhttp.onload = function(e) { var response = "" if (board_is_shift_jis[board]) { var uInt8Array = new Uint8Array(this.response) var codepage = "shift_jis" var shift_jis = new TextDecoder(codepage, {fatal: true}) response = shift_jis.decode(uInt8Array) } else { response = this.responseText } response = response.split(/]*>((?:.|\n|\r)*)<\/body>/i)[1] var page = document.createElement('div') page.innerHTML = response page = document.evaluate('//*[@id="oldthreadlist"]', page, null, 9, null).singleNodeValue katalog_parse(board, page) } xmlhttp.send(); } function katalog_parse(board, tl) { var tr_all = tl.getElementsByTagName("tr") var katalog = new Array(tr_all.length-1) var ii = 0 for (var i = tr_all.length - 1; i != 0; i--) { var tr = tr_all[i] var a_all = tr.getElementsByTagName("a") var a0 = a_all[0] var thread_url = a0.getAttribute('href') var thread_num_str = thread_url.split("/")[3] var thread_num = parseInt(thread_num_str) var thread_title = a0.textContent var a1 = a_all[1] var thread_posts = parseInt(a1.textContent) katalog[ii] = [thread_num, 1, thread_posts, thread_title] var j = ii; if (j > 0) do { //from oldest to newest if (katalog[j][0] > katalog[j-1][0]) break var temp = katalog[j] katalog[j] = katalog[j-1] katalog[j-1] = temp } while (--j > 0) ii++ } katalog_all[board] = katalog //alert(katalog[0][3]) //alert(JSON.stringify(katalog)) katalog_compare(board) } function katalog_compare(board) { //alert("got here") var katalog_old = katalog_get_old(board) var katalog = katalog_all[board] if (!katalog_old) { katalog_all_old[board] = katalog katalog_set_old(board) return } var changed = [] if (!katalog || !katalog_old || katalog.length == 0 || katalog_old.length == 0) { alert("something went wrong in katalog_compare") return } var i = 0 var o = 0 while (true) { var ii = katalog[i] var oo = katalog_old[o] if (ii[kat_thread_id] == oo[kat_thread_id]) { if (ii[kat_amount_of_replies] != oo[kat_amount_of_replies]) { changed.push([changes_type_updated, o, ii[kat_amount_of_replies]]) } i++ o++ } else if (ii[kat_thread_id] > oo[kat_thread_id]) { if (oo[kat_wasnt_deleted_yet]) { changed.push([changes_type_deleted, o]) } o++ } else { changed.push([changes_type_new, ii[kat_thread_id], ii[kat_amount_of_replies], ii[kat_thread_title]]) i++ } if (i >= katalog.length) { while (o < katalog_old.length) { oo = katalog_old[o] if (oo[1]) { changed.push([changes_type_deleted, o]) } o++ } break } if (o >= katalog_old.length) { while (i < katalog.length) { ii = katalog[i] changed.push([changes_type_new, ii[kat_thread_id], ii[kat_amount_of_replies], ii[kat_thread_title]]) i++ } break } } if (changed.length) { changes_all[board] = changed } //alert("hi") fetching-- board_redraw() //alert("hi") //alert(katalog[0][3]) //katalog_all_old[board] = katalog //katalog_set_old(board) } function katalog_set_old(board) { var katalog = katalog_all_old[board] var res = "" var i = 0 for (var ka of katalog) { res += katalog[i][kat_thread_id] + "," + katalog[i][kat_wasnt_deleted_yet] + "," + katalog[i][kat_amount_of_replies] + "," + katalog[i][kat_thread_title] + "\n" i++ } GM_setValue(boards_list[board], res) } function katalog_get_old(board) { var res = katalog_all_old[board] if (res !== undefined) return res res = GM_getValue(boards_list[board]) if (res === undefined) return res var res2 = [] res = res.split("\n") for (var line of res) { if (line) { var parsed_line = [] var j = line.indexOf(",") parsed_line.push(parseInt(line.substring(0,j))) j++ var k = line.indexOf(",",j) parsed_line.push(parseInt(line.substring(j,k))) j = k+1 k = line.indexOf(",",j) parsed_line.push(parseInt(line.substring(j,k))) parsed_line.push(line.substring(k+1)) res2.push(parsed_line) } } katalog_all_old[board] = res2 return res2 } function error() { alert("4-ch.net userscript broke") } function add_after(referenceNode, newNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling) } function add_before(referenceNode, newNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode) } start()