1
0
folklife2024-userscript/Folklife 2024 schedule fixes.user.js
Daniel Perelman 28b9b9b9e8 Version 3.0. Fix for minor data format change.
Signed-off-by: Daniel Perelman <perelman@cs.washington.edu>
2024-05-25 00:55:03 -07:00

254 lines
8.0 KiB
JavaScript

// ==UserScript==
// @name Folklife 2024 schedule fixes
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Show schedule as a grid.
// @author Daniel Perelman (perelman@aweirdimagination.net)
// @match https://app.nwfolklife.org/embeddable/events/2/schedule
// @icon https://www.google.com/s2/favicons?sz=64&domain=nwfolklife.org
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const dayOfWeek = new Intl.DateTimeFormat("en-US", { weekday: "long" })
let data = null;
const w = window.wrappedJSObject || window;
const ourSetData = json => {
if (!data) data = [];
const newData = JSON.parse(json).data;
const newBlocks = newData.blocks;
if (newBlocks) data.push(...newBlocks);
const newVenues = newData.venues;
if (newVenues) {
// To avoid rewriting the code below, just rewrite into list of blocks.
for (const venue of newVenues) {
const name = venue.name
const blocks = venue.blocks
for (const b of blocks) {
b.venue = { name }
}
data.push(...blocks)
}
}
}
if (window.wrappedJSObject) {
exportFunction(ourSetData, window, { defineAs: "shareDataWithFixes" })
w.eval("window.originalFetch = window.fetch")
// From https://stackoverflow.com/a/69521684
w.eval(`window.fetch = ${async (...args) => {
let [resource, config ] = args;
// request interceptor here
const response = await window.originalFetch(resource, config);
// response interceptor here
window.shareDataWithFixes(await response.clone().text())
return response;
}}`);
} else {
// From https://blog.logrocket.com/intercepting-javascript-fetch-api-requests-responses/
const { fetch: originalFetch } = window;
w.fetch = async (...args) => {
let [resource, config ] = args;
// request interceptor here
const response = await originalFetch(resource, config);
// response interceptor here
if (!data) data = [];
ourSetData(await response.clone().text());
return response;
};
}
let gridParent = null
let existingGrid = null
let newContainer = null
let currentTableDay = null
const buildTable = {}
document.body.style.overflow = "scroll"
let categories = null
function setupCategories() {
if (categories) return
const categoriesDiv = document.querySelector(".categories")
if (!categoriesDiv) return
categories = {}
for (const cat of categoriesDiv.querySelectorAll(".cat")) {
const name = cat.classList[0] == "cat" ? cat.classList[1] : cat.classList[0];
const catInfo = categories[name] = {
div: cat,
name,
visible: true,
}
cat.addEventListener('click', () => {
catInfo.visible = !catInfo.visible
cat.style["text-decoration"] = catInfo.visible ? "" : "line-through"
const newDisplay = catInfo.visible ? "" : "none";
for (const div of document.querySelectorAll("." + name)) {
if (div.classList.contains("block")) div.style.display = newDisplay
}
});
}
}
function updateTable() {
const daySpan = document.querySelector(".rs-picker-toggle-value");
if (!daySpan) return
const day = daySpan.innerText;
if (buildTable[day]) {
buildTable[day]()
return
}
for (const t of document.getElementsByTagName("table")) {
t.parentNode.removeChild(t)
}
if (newContainer) {
const loadingTable = document.createElement("table")
loadingTable.innerText = "Loading..."
newContainer.appendChild(loadingTable)
}
existingGrid = document.querySelector("div.venues-grid.schedule")
gridParent = existingGrid.parentNode
const blocks = existingGrid.querySelectorAll(".block")
const venues = [...existingGrid.querySelectorAll("p.venue-name")]
if (!venues || venues.length == 0) {
console.log("No data displayed for " + day)
return
}
if (!newContainer) {
let node = existingGrid.parentNode
let prevNode = null
while (node != document.body) {
const newNode = document.createElement("div")
if (!newContainer) newContainer = newNode
else newNode.appendChild(prevNode)
newNode.className = node.className
if (node.parentNode == document.body) node.appendChild(prevNode)
prevNode = newNode
node = node.parentNode
}
}
const blocksByTitle = Object.fromEntries([...blocks].map(b => [b.querySelector(".title").innerText.trim(), b]))
if (!data) {
data = Object.values(w.__NEXT_DATA__.props.pageProps.urqlState)
.flatMap(v => JSON.parse(v.data).blocks || [])
}
for (const entry of data) {
entry.startsAt = new Date(entry.startsAt)
entry.endsAt = new Date(entry.endsAt)
}
const dataForDay = data.filter(x => dayOfWeek.format(x.startsAt) == day)
if (dataForDay.length == 0) {
console.log("No data loaded for " + day)
return
}
const dayStartsAt = new Date(Math.min.apply(null, dataForDay.map(x => x.startsAt)))
const dayEndsAt = new Date(Math.max.apply(null, dataForDay.map(x => x.endsAt)))
const fiveMinSegments = (dayEndsAt - dayStartsAt) / 1000 / 60 / 5
for (const entry of dataForDay) {
entry.rowStart = (entry.startsAt - dayStartsAt) / 1000 / 60 / 5
entry.rowEnd = -1 + (entry.endsAt - dayStartsAt) / 1000 / 60 / 5
}
buildTable[day] = function() {
if (day == currentTableDay) return;
let error = false
for (const t of document.getElementsByTagName("table")) {
t.parentNode.removeChild(t)
}
setupCategories()
const newGrid = document.createElement("table")
const columns = []
const headerTR = document.createElement("tr")
for (const venueP of venues) {
const venueName = venueP.innerText
const th = document.createElement("th")
th.appendChild(venueP.cloneNode(true))
headerTR.appendChild(th)
const dataForVenue = dataForDay.filter(x => x.venue.name == venueName)
const columnEntries = new Array(fiveMinSegments)
for (let i = 0; i < fiveMinSegments; i++) {
columnEntries[i] = dataForVenue.find(x => x.rowStart <= i && i <= x.rowEnd)
}
columns.push(columnEntries)
}
newGrid.appendChild(headerTR)
const trClass = existingGrid.querySelector(".blocks").className
for (let i = 0; i < fiveMinSegments; i++) {
const tr = document.createElement('tr')
tr.className = trClass
tr.style.display = "table-row"
for (const col of columns) {
const cell = col[i]
if (!cell) {
tr.appendChild(document.createElement('td'))
} else if (cell.rowStart == i) {
const td = document.createElement('td')
td.className = "jJAqXz"
td.style.display = "table-cell"
td.rowSpan = cell.rowEnd - cell.rowStart + 1
const block = blocksByTitle[cell.title.trim()]
if (!block) {
console.log("No block with title: " + cell.title)
error = true
buildTable[day] = null
}
else {
const blockClone = block.cloneNode(true)
if (categories) {
blockClone.classList.forEach(cls => {
if (categories[cls] && !categories[cls].visible) {
blockClone.style.display = "none"
}
})
}
td.appendChild(blockClone)
}
tr.appendChild(td)
}
}
newGrid.appendChild(tr)
}
newGrid.style.display = "table"
newGrid.className = existingGrid.className
document.getElementById("__next").childNodes[0].style.height = document.querySelectorAll(".layout-row")[0].getBoundingClientRect().bottom + "px"
existingGrid.style.display = "none"
newContainer.appendChild(newGrid)
currentTableDay = error ? null : day
}
buildTable[day]()
}
setInterval(updateTable, 500);
})();