/searcher.user.js Secret
Last active
October 5, 2024 09:30
4chan searcher
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name 4chan search mega | |
// @namespace https://github.com/saxamaphone69/ss21 | |
// @version 2.6 | |
// @description Companion userscript | |
// @author saxamaphone69 | |
// @match https://*.4chan.org/* | |
// @connect https://boards.4chan.org | |
// @connect https://find.4chan.org | |
// @grant GM.xmlHttpRequest | |
// @grant GM.addStyle | |
// @run-at document-start | |
// ==/UserScript== | |
(async () => { | |
"use strict"; | |
const d = document; | |
function addStyle() { | |
const css = ` | |
#shortcut-search { | |
all: unset; | |
margin-left: 3px; | |
} | |
dialog { | |
padding: 0; | |
border: 1px solid var(--xt-border); | |
background-color: var(--xt-background); | |
} | |
dialog::backdrop { | |
background-color: rgba(0,0,0,.75); | |
} | |
search { | |
max-width: 50vw; | |
white-space: normal; | |
} | |
.search_panel { | |
max-height: 50vh; | |
text-align: left; | |
overflow-y: auto; | |
} | |
.search-post { | |
display: flow-root; | |
background-color: #d6daf0; | |
border: 1px solid var(--xt-border); | |
border-radius: 4px; | |
padding: 4px; | |
.quote { | |
color: olive; | |
} | |
.name { | |
color: #117743; | |
font-weight: 700; | |
} | |
.subject { | |
color: #0f0c5d; | |
font-weight: 700; | |
} | |
.fileThumb { | |
float: left; | |
margin: 5px; | |
} | |
} | |
`; | |
GM.addStyle(css); | |
} | |
function makeRequest(url, method = 'GET', headers = {}, callback) { | |
var xhr = new XMLHttpRequest(); | |
xhr.open(method, url, true); | |
// Set headers if provided | |
for (var header in headers) { | |
if (headers.hasOwnProperty(header)) { | |
xhr.setRequestHeader(header, headers[header]); | |
} | |
} | |
// Setup the callback | |
xhr.onload = function () { | |
if (xhr.status >= 200 && xhr.status < 300) { | |
callback(null, xhr); | |
} else { | |
callback(new Error(`HTTP Status: ${xhr.status}`), null); | |
} | |
}; | |
xhr.onerror = function () { | |
callback(new Error('Network Error'), null); | |
}; | |
xhr.send(); | |
} | |
function addSearchShortcut() { | |
let searchIcon = d.createElement('button'); | |
searchIcon.id = 'shortcut-search'; | |
searchIcon.classList.add('shortcut'); | |
searchIcon.setAttribute("onclick", "document.querySelector('dialog').show();"); | |
searchIcon.innerHTML = ` | |
<a href="javascript:;" title="Search"> | |
<span class="icon--alt-text">Search</span> | |
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 512 512"> | |
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" fill="currentColor" /> | |
</svg> | |
</a>`; | |
let searchIconParent = d.getElementById('shortcuts'); | |
let searchIconPlacement = d.getElementById('shortcut-watcher'); | |
searchIconParent.insertBefore(searchIcon, searchIconPlacement); | |
} | |
function addSearchDialog() { | |
let searchDialog = d.createElement('dialog'); | |
searchDialog.id = 'dialog-search'; | |
//searchDialog.setAttribute("inert", ""); | |
searchDialog.innerHTML = ` | |
<form method="dialog"> | |
<search> | |
<section class="search_wrap"> | |
<input type="search" id="four-search" autocomplete="off"><button id="search-submit" type="submit" value="search">Search</button><button onclick="this.closest('dialog').close('cancel')">Close</button> | |
</section> | |
<section class="search_panel"> | |
</section> | |
</search> | |
</form>`; | |
d.querySelector('#shortcut-search').appendChild(searchDialog); | |
} | |
function actuallySearch() { | |
let searchInput = d.querySelector('#four-search'); | |
let searchButton = d.querySelector('#search-submit'); | |
let searchPanel = d.querySelector('.search_panel'); | |
searchButton.addEventListener('click', (e) => { | |
e.preventDefault(); | |
grabSearch(searchInput.value); | |
}); | |
function grabSearch(term) { | |
let url = `https://find.4chan.org/api?q=${term}`; | |
makeRequest(url, 'GET', { 'Accept': 'application/json' }, function (err, response) { | |
if (err) { | |
console.error('Error:', err); | |
searchPanel.innerHTML = `<div class="warning">Whoops! Seems there was an error:<br><span>${err}</span></div><div>Try <a href="${url}">going to your query in a new tab</a>, come back, and try again.`; | |
} else { | |
let searchResults = JSON.parse(response.responseText); | |
//console.log(searchResults, response.responseText); | |
function escapeQuotesInComField(jsonString) { | |
if (typeof jsonString !== 'string') { | |
throw new TypeError("Input must be a string"); | |
} | |
// Match the com field and escape quotes within its value | |
return jsonString.replace(/"com": "(.*?)"/g, (match, p1) => { | |
const escapedCom = p1.replace(/"/g, '\\"'); | |
return `"com": "${escapedCom}"`; | |
}); | |
} | |
// Convert the response object to a JSON string | |
const jsonString = JSON.stringify(searchResults); | |
let escapedJsonString = escapeQuotesInComField(jsonString); | |
const data = JSON.parse(escapedJsonString); | |
let html = `<div>There was ${data.nhits} hits for "${data.query}"!</div>`; | |
// Loop over each thread in the threads array | |
data.threads.forEach(thread => { | |
// Create a variable to hold the modified posts | |
let modifiedPosts = ''; | |
// Loop over each post in the thread's posts array | |
thread.posts.forEach(post => { | |
// Replace the search term with a marked version in the com field | |
const modifiedCom = post.com.replace(new RegExp(term, 'gi'), match => `<mark>${match}</mark>`); | |
// Use template literals to create HTML for each post | |
modifiedPosts += ` | |
<article class="search-post"> | |
${post.filename ? `<div class="file"> | |
<div class="fileText">${post.filename}${post.ext}, ${post.w}x${post.h}</div> | |
<a class="fileThumb" href="//is2.4chan.org/${thread.board}/${post.tim}${post.ext}.png"><img data-md5="${post.md5}" src="//i.4cdn.org/${thread.board}/${post.tim}s.jpg" style="height: ${post.tn_h}px; width: ${post.tn_w}px;"></a> | |
</div>` : ``} | |
<div class="search-postInfo">${post.sub ? `<span class="subject">${post.sub}</span>` : ``}<span class="name">${post.name}</span><span class="dateTime">${post.now}</span><a href="/${thread.board}/thread/${thread.thread.substring(1)}/#p${post.no}" class="post-no">#${post.no}</a></div> | |
<blockquote class="post-com">${modifiedCom}</blockquote> | |
</article> | |
`; | |
}); | |
// Use template literals to create HTML for each thread | |
html += `<div class="search-thread"> | |
<div class="board"><a href="/${thread.board}/thread/${thread.thread.substring(1)}/">Posted on ${thread.board}</a></div> | |
<div class="posts"> | |
${modifiedPosts} | |
</div> | |
</div>`; | |
}); | |
searchPanel.innerHTML = html; | |
} | |
}); | |
} | |
} | |
function init() { | |
addStyle(); | |
addSearchShortcut(); | |
addSearchDialog(); | |
actuallySearch(); | |
} | |
//d.addEventListener("DOMContentLoaded", init); | |
d.addEventListener("4chanXInitFinished", init); | |
/* | |
function ready(fn) { | |
if (document.readyState != 'loading'){ | |
fn(); | |
} else if (document.addEventListener) { | |
document.addEventListener('DOMContentLoaded', fn); | |
} else { | |
document.attachEvent('onreadystatechange', function() { | |
if (document.readyState != 'loading') { | |
fn(); | |
} | |
}); | |
} | |
} | |
*/ | |
//ready(init); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment