// ==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); })();