// ==UserScript==
// @name Distributed Proofreaders - Re-format Project Table
// @namespace dp-scripts
// @version 1.03
// @description Re-writes the Project Table for readability and accessibility of its commonly used parts
// @author IsolationBooth @ Distributed Proofreaders
// @match https://www.pgdp.net/c/project.php?*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @require https://code.jquery.com/jquery-3.7.1.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/cdn.min.js
// @grant none
// ==/UserScript==
// This script was written by someone who knows how to program, but hasn't used JQuery, or even much Javascript & CSS,
// functional programming, etc. Laugh all you want! It's still better than the default.
(function() {
'use strict';
var $ = window.jQuery.noConflict(true); // this became necessary after adding date-fns
var doc = document.documentElement.outerHTML
var url = window.location.href
var projectStateFull, projectState, projectStateDesc, title, author, scanSource, authorFirstLast, language, genre, difficulty,
projectID, clearanceLine, source, pm, pp, ppv, credits, lastUpdate, lastProofreadSpan, wordLists, characterSuites, lastEditSpan, lastStateChangeSpan,
lastPostSpan, forum, pageDetail, pageBrowser, unsavedPages, savedPages, projectCommentsHeader, projectComments, startProofing,
pagesText, pgNumber, visibilityClass
// default values for variables that may be referenced later but not assigned text
pp = "<i>none yet</i>"
ppv = ""
pageBrowser = ""
pagesText = ""
clearanceLine = ""
lastProofreadSpan = ""
visibilityClass = ""
function parseDate(dateString) {
let myDate = dateFns.parse(dateString, "EEEE, MMMM d, yyyy 'at' HH:mm", new Date()); //couldn't get .parse "XX"= e.g. "-400" (offset/tz) to work here https://date-fns.org/docs/parse
let tempDate = myDate
tempDate.setHours(tempDate.getHours() + 4);
myDate = new Date(dateFns.format(tempDate, "yyyy-MM-dd'T'HH:mm:00'Z'"))
// as a newbie, I spent untold hours trying to add 4 hours to "server time" (EST/EDT), have it understood as UTC and compare it to "now UTC"
// apparently all I can do is use date-fns to add 4, spit out the date format [that says "UTC" ("Z") explicitly] that JS understands as a string, and make a date out of it again
//I could not import date-fns-tz here, to start. Of course four hours is a hack; I am unaware of other options.
return myDate
}
function getDateSpan(myDate, myLiteralDate) { // re second argument--help--with the time zone issue "fixed" above, lastPostFormatted is not what the string originally said
// let lastPostFormatted = dateFns.format(myDate, "MMM d/yy, h:mm a");
let now = new Date()
let lastPostRelative = dateFns.formatDistance(myDate, now, { addSuffix: true });
let mySpan = `<span class='tooltip' title='${myLiteralDate} (server time)'>${lastPostRelative}</span>`
return mySpan
}
function getScanSource(projectComments) {
let start = `<br/><a id="scanSource" title="Copied from a Project Comments link" href="`
let end = `">[view scan source]</a>`
let capture = /<a href=["'](https?:\/\/archive\.org\/details\/[^"']+|https?:\/\/.*\.?hathitrust\.org\/[^"']+|https?:\/\/hdl.handle.net\/[^"']+|https?:\/\/books\.google\.com\/books\?[^"']+)/
let matches = projectComments.match(capture) // the above regex has four "or"s, and finds when the link stops via "not a quotation mark ending the link"
if ( matches !== null ) {
return start + matches[1] + end }
return ""
}
function processProjectTable(tableId) {
const table = document.getElementById(tableId);
if (!table) {
console.error('Table not found');
return 0;
}
// Get all rows from the table
const rows = table.getElementsByTagName('tr');
for (let row of rows) {
// Get all cells in the current row
const cells = row.querySelectorAll('td,th');
// Extract the text content from each cell
let value, field
if ( row.childElementCount == 2 ) { // this is a "Field Name" - "Field Contents" duple in the original table
field = row.childNodes[0].innerHTML
value = row.childNodes[1].innerHTML
}
switch (field) {
case "PG etext number":
var matches = value.match(/^\d+/)
pgNumber = matches[0]
console.log(pgNumber)
break
case "Project State":
projectStateFull = value;
if ( projectStateFull.includes(":") ) {
projectState = projectStateFull.slice(0, projectStateFull.indexOf(":"))
projectStateDesc = projectStateFull.slice(projectStateFull.indexOf(":")+2, )
} else {
projectState = projectStateFull
projectStateDesc = projectStateFull
}
break
case "Title": title = value; break
case "Author": author = value; break
case "Language": language = value; break
case "Genre": genre = value; break
case "Difficulty": difficulty = value; break
case "Project ID": projectID = value; break
case "Clearance Line": clearanceLine = "Clearance:" + value; break
case "Image Source": source = value; break
case "Project Manager": pm = value; break
case "Post Processor":
pp = `<a href="https://www.pgdp.net/c/tools/search.php?show=search&checkedoutby=${value}&state[]=proj_post_first_checked_out">` + value + `</a>`
break
case "PP Verifier": ppv = "PP verifier: " + value; break
case "Credits line so far": credits = value; break
case "Last Edit of Project Info":
value = value.slice(0, value.indexOf("&")) // remove " ...Current Time...")
lastEditSpan = "Last project edit: " + getDateSpan(parseDate(value), value)
break
case "Last State Change":
lastStateChangeSpan = "Last state change: " + getDateSpan(parseDate(value), value)
break
case "Last Proofread":
if ( value !== "Project has not been proofread in this round." ) {
lastProofreadSpan = "Last proofread: " + getDateSpan(parseDate(value), value) }
else { lastProofreadSpan = "Not proofread in this round." }
break
case "Word Lists": wordLists = value; break
case "Character Suites": characterSuites = value; break
case "Last Forum Post": lastPostSpan = getDateSpan(parseDate(value), value); break
case "Forum": forum = value; break // TODO: add a "last page" link based on "\d+ replies" and the URL https://www.pgdp.net/phpBB3/viewtopic.php?t=\d+&start=\d\d if \d+ greater than # of comments per page
case "Page Detail": pageDetail = value; break
case "Page Browser": pageBrowser = value; break
}
}
// get the often-large Project Comments, which spans columns in the original table
projectComments = $('tr').has('td.project-comments').next('tr').find('td')[0].innerHTML //2nd try here accommodates tables in proj comments, which prev method didn't
projectCommentsHeader = $('td.project-comments')[0].innerHTML
if ( projectState.includes("Completed and Posted") ) { // add ebook # if complete. Haven't had time to organize project states and descriptions somewhere
projectState = `<a title="PG book #${pgNumber}" href="https://www.gutenberg.org/ebooks/${pgNumber}">` + projectState + `</a>`
}
if ( /(Processing|Unavailable|Bad |Waiting|Complete|Delete)/.test(projectState) ) {
visibilityClass = "hidden" //for things we don't want to show in the non-"active" states
}
if (( doc.includes("<b>Advanced</b>") || doc.includes("<b>Everything</b>")) && /Round \d: Available/.test(doc) ) {
// this number of pages information is only available in 2 of the 4 views
let pagesLeft = ''
let totalPages = ''
let matches = doc.match(/(\d+)<\/td><td>Pages Total/);
if ( matches !== null ) { totalPages = matches[1] }
matches = doc.match(/(\d+)<\/td><td>in ..\.page_avail<\/td><\/tr>/);
if ( matches !== null ) { pagesLeft = matches[1] }
if ( pagesLeft !== '' ) { // a special case when the HTML does have Total pages but the Available/left is not in HTML because it's zero (basically just-finished rounds still IN the round)
pagesText = `ℹ️ ${pagesLeft} left of ${totalPages} pages`
}
else {
pagesText = `ℹ️ ${totalPages} pages`
}
// see if we can provide the "next page" nnn.png, if the file-naming system is a simple, zero-padded increment - and make a link for it
let firstPage = ''
let pagePrefixLength = ''
matches = doc.match(/imagefile=(\d+)\.png(?:&|&)mode=image/)
if ( matches !== null ) {
firstPage = +matches[1];
pagePrefixLength = matches[1].length;
let nextPage = +totalPages - +pagesLeft + firstPage; //+variable casts to integer; if firstPage is not 1, adjust accordingly
pagesText = pagesText + ` · <a href='https://www.pgdp.net/c/tools/page_browser.php?project=` + projectID + `&imagefile=`
+ nextPage.toString().padStart(pagePrefixLength,'0') + `.png&mode=image'><span title="approximate!">view next page in round</span></a>`
}
}
// convert author to first-last for some author searches
if ( author.includes(",") ) {
authorFirstLast = author.slice(author.indexOf(",")+2, author.length) + " " + author.slice(0,author.indexOf(","))
}
else { authorFirstLast = author }
// get the scan source in project comments, if any, to give it a predictable location in the table
scanSource = getScanSource(projectComments)
// GET IN-PROGRESS AND "DONE" PAGES - the current table shows up to five of each
// TODO: make a new interface element with a VIEW image+text link to the LAST DONE page - I would use this - clicking on the green ones opens the proofing i-face, not wanted
let capture = /(<tr><td class=["']center-align["']><a href="https:\/\/www.pgdp.net\/c\/tools\/proofers\/proof\.php\?projectid=projectID.{13}&imagefile=.{1,6}\.png&proj_state=..\.proj_avail&page_state=..\.page_(out|temp)">\w\w\w \d\d: .{1,6}\.png<\/a>(?:.*?)?<\/td><\/tr>)/
matches = doc.match(capture);
if ( matches ) {
unsavedPages = matches[0].replace(/td/g, "span").replace(/<\/?tr>/g, "").replace(/"center-align"/g, '"unsavedPage"') // take the td-based HTML and replace them with spans and give a new class
} else { unsavedPages = "none" }
// now repeat the same logic without one function for both, for "saved" pages ("out" in url string becomes "saved"; different class assigned)
// the optional part toward the end is to catch WordCheck stuff that isn't present in formatting rounds, conveniently carrying the HTML through as is
capture = /(<tr><td class=["']center-align["']><a href="https:\/\/www.pgdp.net\/c\/tools\/proofers\/proof\.php\?projectid=projectID.{13}&imagefile=.{1,6}\.png&proj_state=..\.proj_avail&page_state=..\.page_saved">\w\w\w \d\d: .{1,6}\.png<\/a>(?:.*?)?<\/td><\/tr>)/
matches = doc.match(capture);
if ( matches ) {
savedPages = matches[0].replace(/td/g, "span").replace(/<\/?tr>/g, "").replace(/"center-align"/g, '"savedPage"') // capturing tr was necessary to avoid getting the next unrelated row, and non-greediness on the outside didn't work for that
} else { savedPages = "none" }
return 1;
}
// START THE SHOW
$('#project_info_table').hide();
if ( processProjectTable('project_info_table') ) {
// now remember that yours truly doesn't know anything about div-based CSS layout at the page level, other than "it's the modern way!"
// and we are after all replacing a c. 2000 _field-value table_
var newTable = $(`
<table id="pi">
<colgroup>
<col style="width: 70%;" />
<col style="width: 30%;" />
</colgroup>
<tbody>
<tr>
<td id="left">
<div id="title">
${title}
</div>
<div id="author">by ${author}</div>
<div id="authorSearch">
Author search:
<a href="https://www.pgdp.net/c/tools/search.php?show=search&author=${author}">DP</a>
· <a href="https://archive.org/search?query=creator%3A%22${author}%22">Internet Archive</a>
· <a href="https://www.gutenberg.org/ebooks/authors/search/?query=${author}">Project Gutenberg</a>
· <a href="https://en.wikipedia.org/w/index.php?search=${authorFirstLast}&title=Special%3ASearch&ns0=1">Wikipedia</a>
· <a href="https://catalog.hathitrust.org/Search/Home?lookfor=${authorFirstLast}+&searchtype=author&ft=ft&setft=true">HathiTrust</a>
· <a href="https://www.google.com/search?q=${authorFirstLast}">Google</a>
</div>
<div class="subhead" id="myPagesHeader">My pages</div>
<div id="pageDetail">📋 ${pageDetail}</div>
<div id="unsavedPages" class="${visibilityClass}">⏳ Pages in progress: <span id="unsavedPage">${unsavedPages}</span></div>
<div id="savedPages" class="${visibilityClass}">✔️ Pages saved recently: <span id="savedPage">${savedPages}</span></div>
<div class="subhead" id="pageInfoHeader">Page information</div>
<div id="pageBrowser">📄 ${pageBrowser}</div>
<div id="pages">${pagesText}</div>
<div class="subhead" id="forumHeader">Forum</div>
<div id="forum">💬 ${forum}</span> · Last post: <span id="lastPost">${lastPostSpan}</div>
</td>
<td id="right">
<span id="projectState" title="${projectStateDesc}">${projectState}</span>
<div id="bookMeta"><span id="language" title="language">${language}</span> ·
<span id="genre" title="genre"><a href=
"https://www.pgdp.net/c/tools/search.php?show=search&genre%5B%5D=${genre}&state%5B%5D=P1.proj_avail&state%5B%5D=P2.proj_avail&state%5B%5D=P3.proj_avail&state%5B%5D=F1.proj_avail&state%5B%5D=F2.proj_avail&state%5B%5D=proj_post_first_available">
${genre}</a></span> · <span id="difficulty" title="difficulty">${difficulty}</span>
</div>
<hr>
<div id="source" title="Book source"><i>via</i> ${source} ${scanSource}</div>
<hr>
<div id="pm">Project Manager: <a href="https://www.pgdp.net/c/tools/search.php?show=search&project_manager=${pm}&state%5B0%5D=P1.proj_avail&state%5B1%5D=P2.proj_avail&state%5B2%5D=P3.proj_avail&state%5B3%5D=F1.proj_avail&state%5B4%5D=F2.proj_avail&state%5B5%5D=proj_post_first_available&sec_sort=genre&results_offset=0&sort=stateA#head">${pm}</a></div>
<div id="pp">Post processor: ${pp}</div>
<div id="ppv">${ppv}</div>
<div id="projectID" title="Unique identifier">${projectID}</div>
<div id="clearance" title="PG clearance">${clearanceLine}</div>
<hr>
<div id="divCharacterSuites">Character suites: <span id="characterSuites">${characterSuites}</span></div>
<hr>
<div id="wordLists">${wordLists}</div>
<hr>
<div id="dates">${lastProofreadSpan}<br/>
${lastStateChangeSpan}<br/>
${lastEditSpan}
</div>
<hr>
<div id="credits">Credit line so far: <span id="credits">"${credits}"</div>
</td>
</tr>
<tr>
<td colspan="2"><div id="projectCommentsHeader">${projectCommentsHeader}</div></td>
</tr>
<tr id="trProjectComments">
<td colspan="2"><span id="projectComments">${projectComments}</span></td>
</tr>
</tbody>
</table>`);
var table_css = `
#content-container h1 {
display: none;
/* redundant "Project page for..."*/
}
#left hr {
color: #d8d8d8;
margin: 1em;
}
#right hr {
color: #d6d6d6;
margin: 0.4em auto;
width: 80%;
}
.hidden {
display: none;
}
table#pi {
border: 4px solid #336633;
border-radius: 15px;
border-collapse: separate;
max-width: 1000px;
margin-bottom: 1em;
border-spacing: 0;
}
#pi td {
padding: 0.2em;
}
table#pi a:link,
table#pi a:hover {
text-decoration: none;
}
.tooltip {
border-bottom: 0.1px dashed #6f6e6e;
}
table.blurb-box {
max-width: 1000px;
border: 2px solid #336633;
border-collapse: separate;
border-radius: 15px;
background-color: #e0e8dd;
}
html[data-darkreader-scheme = "dark"] .blurb-box {
background-color: #335566b0;
}
.blurb-box a {
display: inline-block;
margin-top: 0.5em;
font-size: large;
border: 3px solid;
border-radius: 15px;
border-spacing: 0px;
padding: 5px;
color: #3192e3;
text-decoration: none;
}
html[data-darkreader-scheme = "dark"] .blurb-box a {
color: #76b0e0;
}
#projectCommentsHeader {
background-color: #e0e8dd;
text-align: center;
border: 2px solid #336633;
border-collapse: separate;
border-radius: 15px;
}
html[data-darkreader-scheme = "dark"] #projectCommentsHeader {
background-color: #335566b0;
text-align: center;
}
#left {
vertical-align: text-top;
}
#author {
font-size: 1.1em;
font-weight: bold;
color: #002680;
text-align: center;
margin: 0;
}
html[data-darkreader-scheme = "dark"] #author {
color: #a8e2fb;
}
#authorSearch {
font-size: small;
font-weight: bold;
margin-top: 0.5em;
text-align: center;
}
#title,
#projectState {
display: block;
color: black;
text-align: center;
font-size: x-large;
border: 2px black solid;
border-radius: 8px;
background-color: #c3e2fd;
padding: 3px;
margin-bottom: 0.3em;
font-weight: bold;
}
html[data-darkreader-scheme = "dark"] :is(#projectState, #title) {
background-color: #a8e2fb;
}
#title {
font-style: italic;
}
#right {
vertical-align: top;
}
#bookMeta {
text-align: center;
}
#scanSource {
font-weight: bold;
}
#source {
text-align: center;
font-size: small;
}
.subhead {
text-align: center;
font-size: large;
font-weight: bold;
border-radius: 8px;
background-color: rgb(233, 233, 233);
padding: 4px;
margin: 0.8em 0 0.9em 0;
font-variant: small-caps;
}
html[data-darkreader-scheme = "dark"] .subhead {
background-color: rgb(182, 181, 181);
color: black;
}
#pages {
font-size: medium;
padding-top: 4px;
font-weight: bold;
margin: 0.5em;
}
#source, #dates {
font-size: small;
}
#pageBrowser {
font-weight: bold;
margin: 0.5em;
}
#pageDetail {
font-weight: bold;
margin: 0.5em;
}
#unsavedPages {
font-weight: bold;
margin: 0.5em;
}
.unsavedPage {
margin-top: 0.5em;
display: inline-block;
padding: 3px;
font-size: x-small;
background-color: #f8f878;
border: 1px dotted;
}
#savedPages {
font-weight: bold;
margin: 0.5em;
}
.savedPage {
margin-top: 0.5em;
display: inline-block;
padding: 3px;
font-size: x-small;
background-color: #8df4a2;
border: 1px dotted;
}
html[data-darkreader-scheme = "dark"] .savedPage {
background-color: #194319;
}
#forum {
font-weight: bold;
margin-bottom: 0.8em;
}
#credits {
font-size: small;
}
`;
$('#project_info_table').before(newTable);
// Re-add the 'bookmark' icon implemented by DP on 2024-09-11, inside the original 'h1' heading, which this script hides. It is wrapped in a FORM element.
$('#title').prepend($('h1 form'))
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = table_css
document.head.appendChild(style)
}
else {
$('#project_info_table').show(); // function returned 0, it didn't work, and hiding the old table was the first thing we did
console.log("TamperMonkey script for DP Project Table failed. Table not found by its ID.")
}
})();