254 lines
8.0 KiB
JavaScript
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);
|
|
})();
|