import { LitElement, html, css, unsafeCSS } from 'lit-element'; import { unsafeHTML } from 'lit-html/directives/unsafe-html'; import { unsafeSVG } from 'lit-html/directives/unsafe-svg'; import { styleMap } from 'lit-html/directives/style-map'; import { initSW, digestMessage, getTS, tsToDate } from './pageutils'; import prettyBytes from 'pretty-bytes'; import marked from 'marked'; import '../scss/base.scss'; import allCssRaw from '../scss/main.scss'; //import fasArrowLeft from '@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg'; //import fasInfo from '@fortawesome/fontawesome-free/svgs/solid/info-circle.svg'; import fasHelp from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg'; import farListAlt from '@fortawesome/fontawesome-free/svgs/solid/list-alt.svg'; import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg'; import fasFile from '@fortawesome/fontawesome-free/svgs/solid/file.svg'; import farPlayCircle from '@fortawesome/fontawesome-free/svgs/regular/play-circle.svg'; import fabGoogleDrive from '@fortawesome/fontawesome-free/svgs/brands/google-drive.svg'; import fasUpload from '@fortawesome/fontawesome-free/svgs/solid/upload.svg' // =========================================================================== const allCss = unsafeCSS(allCssRaw); function wrapCss(custom) { return [allCss, custom]; } // =========================================================================== class AppMain extends LitElement { constructor() { super(); this.sourceUrl = null; this.pageParams = {}; } static get properties() { return { pageParams: { type: Object }, sourceUrl: { type: String }, navShown: { type: Boolean } } } static get styles() { return wrapCss(css` #logo { max-height: 2.5rem; margin-right: 8px; } .has-allcaps { font-variant-caps: all-small-caps; } `); } render() { return html`
${this.sourceUrl ? html` ` : this.sourceUrl === "" ? html` ` : html``} `; } firstUpdated() { this.initRoute(); } onNavMenu() { this.navShown = !this.navShown; } initRoute() { this.pageParams = new URLSearchParams(window.location.search); // Google Drive let state = this.pageParams.get("state"); if (state) { try { state = JSON.parse(state); if ((state.ids instanceof Array) && state.userId && state.action === "open") { this.pageParams.set("source", "googledrive://" + state.ids[0]); this.pageParams.delete("state"); window.location.search = this.pageParams.toString(); return; } } catch (e) { console.log(e); } } this.sourceUrl = this.pageParams.get("source") || ""; } onStartLoad(event) { // just redirect right away? if (!event.detail.blobUrl) { this.pageParams.set("source", event.detail.sourceUrl); window.location.search = this.pageParams.toString(); return; } this.sourceUrl = event.detail.sourceUrl; this.loadInfo = event.detail; } onCollLoaded(event) { this.loadInfo = null; this.initRoute(); if (event.detail.sourceUrl !== this.sourceUrl) { this.pageParams.set("source", event.detail.sourceUrl); window.location.search = this.pageParams.toString(); } } } // =========================================================================== class WrLoader extends LitElement { constructor() { super(); this.progress = 0; this.total = 0; this.percent = 0; this.coll = ""; this.state = "waiting"; this.loadInfo = null; this.worker = new Worker(__SW_PATH__); } static get properties() { return { sourceUrl: { type: String }, loadInfo: { type: Object }, state: { type: String }, progress: { type: Number }, percent: { type: Number }, error: { type: String}, total: { type: Number }, status: { type: String }, coll: { type: String }, } } firstUpdated(props) { this.initMessages(); this.doLoad(); } initMessages() { this.worker.addEventListener("message", (event) => { switch (event.data.msg_type) { case "collProgress": if (event.data.name === this.coll) { this.percent = event.data.percent; if (event.data.error) { this.error = event.data.error; this.state = "errored"; } } break; case "collAdded": if (event.data.name === this.coll) { if (!this.total) { this.total = 100; } this.progress = this.total; this.percent = 100; this.dispatchEvent(new CustomEvent("coll-loaded", {detail: event.data})); } break; } }); } async doLoad() { let sourceUrl = this.sourceUrl; let source = null; // custom protocol handlers here... try { const sup = new URL(this.sourceUrl); let firstSlash = null; if (!sup.host) { firstSlash = sup.pathname.indexOf("/", 2); } switch (sup.protocol) { case "googledrive:": this.state = "googledrive"; if (firstSlash < 0) { firstSlash = undefined; } this.fileId = sup.pathname.slice(2, firstSlash); source = await this.googledriveInit(); break; case "s3:": sourceUrl = `https://${sup.pathname.slice(2, firstSlash)}.s3.amazonaws.com${sup.pathname.slice(firstSlash)}`; source = {sourceUrl, sourceId: this.sourceUrl, name: this.sourceUrl}; break; case "file:": if (!this.loadInfo) { this.state = "errored"; this.error = "File URLs are local and can not be shared. You can choose a file to upload from the main page."; return; } source = { sourceUrl: this.loadInfo.blobUrl, sourceId: this.loadInfo.name, name: this.loadInfo.name } break; } } catch (e) {} if (!source) { source = {sourceUrl}; } this.state = "started"; const msg = {"msg_type": "addColl", "name": this.coll, skipExisting: true, file: source}; this.worker.postMessage(msg); } googledriveInit() { this._gdWait = new Promise((resolve) => this._gdResolve = resolve); return this._gdWait; } async onLoadReady(event) { if (this._gdResolve) { //const digest = await digestMessage(url, 'SHA-256'); //this.coll = "id-" + digest.slice(0, 12); this._gdResolve(event.detail); } } updated(changedProperties) { if (changedProperties.get("sourceUrl")) { this.doLoad(); } } static get styles() { return wrapCss(css` .logo { width: 96px; height: 96px; margin: 1em; } progress:after { content: attr(data-percent)'%'; position: absolute; -webkit-transform: translate(-50%, -100%); -ms-transform: translate(-50%, -100%); transform: translateX(-50%, -100%); font-size: calc(1.5rem / 1.5); line-height: 1.5rem; } `); } render() { return html`

Loading ${this.sourceUrl}...

${this.renderContent()}
`; } renderContent() { switch (this.state) { case "googledrive": return html`` case "started": return html` `; case "errored": return html`
${this.error}
`; case "waiting": default: return html``; } } } // =========================================================================== class WrIndex extends LitElement { constructor() { super(); this.colls = []; this.fileDisplayName = ""; this.file = null; } static get properties() { return { colls: { type: Array }, fileDisplayName: { type: String } } } firstUpdated() { this.loadColls(); } async loadColls() { const resp = await fetch("/wabac/api/index"); const json = await resp.json(); this.colls = json.colls; } async deleteColl(event) { event.preventDefault(); const index = Number(event.currentTarget.getAttribute("data-coll-index")); const coll = this.colls[index]; if (!coll || coll.deleting) { return; } this.colls[index].deleting = true; const resp = await fetch(`/wabac/api/${coll.id}`, {method: 'DELETE'}); if (resp.status === 200) { const json = await resp.json(); this.colls = json.colls; } return false; } onChooseFile(event) { if (event.currentTarget.files.length === 0) { return; } this.file = event.currentTarget.files[0]; this.fileDisplayName = "file://" + this.file.name; this.requestUpdate(); } onStartLoad(event) { event.preventDefault(); const detail = {sourceUrl: this.fileDisplayName}; if (this.file) { detail.blobUrl = URL.createObjectURL(this.file); detail.name = this.fileDisplayName; } this.dispatchEvent(new CustomEvent("load-start", {detail})); return false; } onInput(event) { this.fileDisplayName = event.currentTarget.value; if (this.file && this.fileDisplayName && this.fileDisplayName.startsWith("file://")) { this.file = null; this.fileDisplayName = ""; } } static get styles() { return wrapCss(css` .size { margin-right: 20px; } .extra-padding { padding: 2em; } .no-top-padding { padding-top: 1.0em; } div.field.has-addons { flex: auto; } `); } render() { return html`
`; } } // =========================================================================== class WrColl extends LitElement { constructor() { super(); this.sourceUrl = null; this.baseApiPrefix = "/wabac/api"; this.baseReplayPrefix = "/wabac"; this.apiPrefix = ""; this.replayPrefix = ""; this.coll = ""; this.collInfo = null; this.hasCurated = false; this.replaceLoc = false; this.tabData = {}; } static get properties() { return { sourceUrl: { type: String }, loadInfo: { type: Object }, collInfo: { type: Object }, coll: { type: String }, hasCurated: { type: Boolean }, tabData: { type: Object }, locationHash: { type: String } } } firstUpdated() { this.doUpdateInfo(); window.addEventListener("hashchange", (event) => this.onHashChange(event)); } async sourceToId(url) { try { url = new URL(url, window.location.origin).href; } catch(e) {} const digest = await digestMessage(url, 'SHA-256'); const coll = "id-" + digest.slice(0, 12); return {url, coll}; } updated(changedProperties) { if (changedProperties.get("sourceUrl")) { this.doUpdateInfo(); } if (changedProperties.get("tabData")) { if (this.collInfo && !this.collInfo.coll) { return } // don't add empty params to shorten query Object.keys(this.tabData). forEach(key => !this.tabData[key] && delete this.tabData[key]); this.locationHash = new URLSearchParams(this.tabData).toString(); } if (changedProperties.get("locationHash")) { if (this.replaceLoc) { const newLoc = new URL(window.location.href); newLoc.hash = "#" + this.locationHash; window.history.replaceState({}, "", newLoc.href); } else { window.location.hash = "#" + this.locationHash; } } } async doUpdateInfo() { const {url, coll} = await this.sourceToId(this.sourceUrl); this.coll = coll; const apiPrefix = this.baseApiPrefix + "/" + coll; const replayPrefix = this.baseReplayPrefix + "/" + coll; const resp = await fetch(apiPrefix); if (resp.status != 200) { this.collInfo = {}; this.hasCurated = false; return; } const json = await resp.json(); this.collInfo = {apiPrefix, replayPrefix, coll, ...json}; if (!this.collInfo.title) { this.collInfo.title = "Archive from " + this.collInfo.displayName; } this.hasCurated = (this.collInfo.lists && this.collInfo.lists.length); const hash = window.location.hash; if (hash) { this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries()); } if (this.collInfo.coll && !this.tabData.view) { this.tabData = {...this.tabData, view: this.hasCurated ? "curated" : "resources"}; } } onCollLoaded(event) { this.doUpdateInfo(); this.loadInfo = null; this.dispatchEvent(new CustomEvent("coll-loaded", {detail: {sourceUrl: this.sourceUrl}})); } onHashChange(event) { const hash = window.location.hash; if (hash && hash !== this._lastHash) { this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries()); this._lastHash = hash; } } onTabClick(event) { event.preventDefault(); const hash = event.currentTarget.getAttribute("href"); this.tabData = {...this.tabData, view: hash.slice(1)}; return false; } onCollTabNav(event) { //this.tabData = {...this.tabData, ...event.detail}; this.tabData = {view: this.tabData.view, ...event.detail.data}; this.replaceLoc = event.detail.replaceLoc || false; //this.tabData = event.detail; } static get styles() { return wrapCss(css` .icon { vertical-align: text-top; } .back fa-icon { width: 1.5em; vertical-align: bottom; line-height: 0.5em; } .is-active { font-weight: bold; } .header-info { font-size: initial; font-weight: initial; margin-right: 20px; } .bg-alice-blue { background-color: aliceblue; } `); } render() { if (this.collInfo && !this.collInfo.coll) { return html``; } else if (this.collInfo) { return html` `; } else { return html``; } } renderCollTabs() { return html` `; } } // =========================================================================== class WrCuratedPages extends LitElement { constructor() { super(); this.collInfo = null; this.currList = 0; this.curatedPages = {}; this.offset = 0; this.lastST = 0; this.clickTime = 0; } static get properties() { return { collInfo: { type: Object }, curatedPages: { type: Object }, currList: { type: Number }, } } firstUpdated() { this.doLoadCurated(); } updated(changedProperties) { if (changedProperties.get("collInfo")) { this.doLoadCurated(); } } async doLoadCurated() { const resp = await fetch(`${this.collInfo.apiPrefix}/curatedPages?offset=${this.offset}`); const json = await resp.json(); this.total = json.total; this.curatedPages = {}; for (const curated of json.curatedPages) { if (!this.curatedPages[curated.list]) { this.curatedPages[curated.list] = []; } this.curatedPages[curated.list].push(curated); } if (this.currList) { this.scrollToList(this.currList); } } static get styles() { return wrapCss(css` .column { max-height: calc(100vh - 145px); } #content { margin-top: 10px; max-height: calc(100vh - 155px); } `); } render() { return html`

${this.collInfo.title}

${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ''}
${this.renderContent()}
`; } renderContent() { return html` ${this.collInfo.lists.map((list, i) => html`

${list.title}


${list.desc}
    ${this.curatedPages[list.id] ? this.curatedPages[list.id].map((p) => html`
  1. `) : html``}
`)} `; } onReplay(event) { event.preventDefault(); const data = { replayUrl: event.currentTarget.getAttribute("data-url"), replayTS: event.currentTarget.getAttribute("data-ts"), view: "replay" }; this.sendChangeEvent(data); return false; } sendChangeEvent(data) { this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}})); } onClickScroll(event) { event.preventDefault(); this.scrollToList(event.currentTarget.getAttribute("data-list")); return false; } scrollToList(id) { this.currList = Number(id); this.sendChangeEvent({currList: this.currList || undefined}); const opts = {behavior: "smooth", block: "nearest", inline: "nearest"}; this.clickTime = new Date().getTime(); this.renderRoot.getElementById("list-" + this.currList).scrollIntoView(opts); } onScroll(event) { const scrollable = event.currentTarget; const curr = this.renderRoot.getElementById("list-" + this.currList); if (!curr) { return; } let next = curr; const target = scrollable.offsetTop; const currST = scrollable.scrollTop; if (currST > this.lastST) { while (next.nextElementSibling && next.nextElementSibling.getBoundingClientRect().top < target) { next = next.nextElementSibling; } } else { while (next.previousElementSibling && next.previousElementSibling.getBoundingClientRect().bottom >= target) { next = next.previousElementSibling; } } this.lastST = currST; if (next && next != curr) { if (next.id.startsWith("list-")) { this.currList = Number(next.id.slice(5)); this.sendChangeEvent({currList: this.currList}); } } if ((new Date().getTime() - this.clickTime) < 1000) { return; } const sel = this.renderRoot.querySelector(`a[data-list="${this.currList}"]`); if (sel) { const opts = {behavior: "smooth", block: "nearest", inline: "nearest"}; sel.scrollIntoView(opts); } } } // =========================================================================== class WrResources extends LitElement { constructor() { super(); this.collInfo = null; this.currMime = "text/html,text/xhtml"; this.urlSearch = ""; this.urlSearchType = ""; this.filteredResults = []; this.results = []; this.tryMore = false; this.loading = false; this.filters = [ {name: "HTML", filter: "text/html,text/xhtml"}, {name: "Images", filter: "image/"}, {name: "Audio/Video", filter: "audio/,video/"}, {name: "PDF", filter: "application/pdf"}, {name: "All", filter: ""} ] } static get properties() { return { collInfo: { type: Object }, currMime: { type: String }, urlSearch: { type: String }, urlSearchType: { type: String }, filteredResults: { type: Array }, loading: { type: Boolean } } } firstUpdated() { this.doLoadResources(); } async doLoadResources() { const count = 100; this.loading = true; let url = (this.urlSearchType !== "contains" ? this.urlSearch : ""); const prefix = url && this.urlSearchType === "prefix" ? 1 : 0; // optimization: if not starting with http, likely won't have a match here, so just add https:// if (url && !url.startsWith("http")) { url = "https://" + url; } const data = { urlSearch: this.urlSearch, currMime: this.currMime, urlSearchType: this.urlSearchType }; const params = new URLSearchParams({ mime: this.currMime, url, prefix, count }).toString(); let resp = await fetch(`${this.collInfo.apiPrefix}/urls?${params}`); resp = await resp.json(); this.results = resp.urls; this.tryMore = (resp.urls.length === count); this.filter(); this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}})); this.loading = false; } async doLoadMore() { const count = 100; if (!this.tryMore || !this.results.length) { return; } if (this.loading) { return; } this.loading = true; const url = (this.urlSearchType !== "contains" ? this.urlSearch : ""); const prefix = url && this.urlSearchType === "prefix" ? 1 : 0; const last = this.results[this.results.length - 1]; const params = new URLSearchParams({ mime: this.currMime, url, prefix, fromMime: last.mime, fromUrl: last.url, fromTs: new Date(last.date).getTime(), count }).toString(); let resp = await fetch(`${this.collInfo.apiPrefix}/urls?${params}`); resp = await resp.json(); this.results = this.results.concat(resp.urls); this.tryMore = (resp.urls.length === count); this.filter(); this.loading = false; } onChangeTypeSearch(event) { this.currMime = event.currentTarget.value; this.doLoadResources(); } onChangeUrlSearch(event) { this.urlSearch = event.currentTarget.value; this.doLoadResources(); } onClickUrlType(event) { this.urlSearchType = event.currentTarget.value; this.doLoadResources(); } filter() { const filteredResults = []; const filterText = (this.urlSearchType === "contains" ? this.urlSearch : ""); for (const result of this.results) { if (!filterText || result.url.indexOf(filterText) >= 0) { filteredResults.push(result); } } this.filteredResults = filteredResults; } onScroll(event) { const element = event.currentTarget; const diff = (element.scrollHeight - element.scrollTop) - element.clientHeight; if (this.tryMore && diff < 40) { this.doLoadMore(); } } static get styles() { return wrapCss(css` :host { display: flex; flex-direction: column; } .notification { width: 100%; } .main-scroll { max-height: calc(100vh - 255px); width: 100vw; } table { table-layout: fixed; word-wrap: break-word; text-overflow: ellipsis; } tbody > tr { display: table; width: 100%; } .col-url { width: 66%; max-width: 66%; min-width: 66%; word-break: break-word; } .col-ts { width: 16%; max-width: 16%; min-width: 16%; word-break: break-word; } td.col-mime { width: 10%; word-break: break-word; } td.col-status { text-align: center; } .loading-cog { width: 100vw; display: flex; } .flex-column { display: flex; flex: auto; max-width: 80%; flex-direction: column; } .flex-auto { flex: auto; } `); } render() { return html`
Showing ${this.filteredResults.length} Results
Resource Type:   ${this.filters.map((filter) => html` `)}
${this.filteredResults.length ? this.filteredResults.map((result) => html` `) : html`
No Results Found.
`}
URL Timestamp Mime Status
${result.url} ${new Date(result.date).toLocaleString()} ${result.mime} ${result.status}
`; } onReplay(event) { event.preventDefault(); const data = { replayUrl: event.currentTarget.getAttribute("data-url"), replayTS: event.currentTarget.getAttribute("data-ts"), view: "replay" }; this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}})); return false; } } // =========================================================================== class WrReplayPage extends LitElement { static get properties() { return { collInfo: {type: Object }, replayUrl: { type: String }, replayTS: { type: String }, iframeUrl: { type: String } } } firstUpdated() { window.addEventListener("message", (event) => this.onReplayMessage(event)); } updated(changedProperties) { if (changedProperties.get("replayUrl") || changedProperties.get("replayTS")) { this.iframeUrl = `${this.collInfo.replayPrefix}/${this.replayTS || ""}mp_/${this.replayUrl}`; } } onReplayMessage(event) { const iframe = this.renderRoot.querySelector("iframe"); if (iframe && event.source === iframe.contentWindow) { if (event.data.wb_type === "load") { this.replayTS = event.data.ts; this.replayUrl = event.data.url; this.sendChangeEvent(); } } } onSubmit(event) { event.preventDefault(); const value = this.renderRoot.querySelector("input").value; this.replayUrl = value; this.sendChangeEvent(); return false; } sendChangeEvent() { const data = { replayUrl: this.replayUrl, replayTS: this.replayTS, }; this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {replaceLoc: true, data}})); } static get styles() { return wrapCss(css` host: { display: flex; } iframe { width: 100vw; height: calc(100vh - 150px); border: 0px; } .replay-bar { padding: 1em; max-width: none; border-bottom: solid .1rem #97989A; } input#url { border-radius: 4px; } #datetime { position: absolute; right: 1em; z-index: 10; background: linear-gradient(90deg, rgba(255, 255, 255, 0), #FFF 15%, #FFF); margin: 5px 0.75em 5px 0; line-height: 2; } `); } render() { return html`

${tsToDate(this.replayTS).toLocaleString()}

${this.iframeUrl ? html` ` : ``} `; } } // =========================================================================== class WrGdrive extends LitElement { constructor() { super(); this.manual = false; this.fileId = ""; } static get properties() { return { manual: { type: Boolean }, fileId: { type: String } } } onLoad() { this.gauth('none', (response) => { if (response.error === "immediate_failed") { this.manual = true; } else { this.authed(response); } }); } onClickAuth() { this.gauth(undefined, (response) => { if (!response.error) { this.authed(response); } }); } async authed(response) { const fileId = this.fileId; const metadataUrl = `https://www.googleapis.com/drive/v3/files/${fileId}`; const authToken = response.access_token; const headers = {"Authorization": `Bearer ${authToken}`}; const resp = await fetch(metadataUrl + "?fields=name", {headers}); const metadata = await resp.json(); const sourceId = `googledrive://${fileId}`; const name = metadata.name; const displayName = `Google Drive file: ` + metadata.name; const sourceUrl = metadataUrl + "?alt=media"; this.dispatchEvent(new CustomEvent("load-ready", {detail: {name, displayName, sourceId, sourceUrl, headers}})); } render() { return html` ${this.script()} ${!this.manual ? html`

Connecting to Google Drive...

` : html` `}`; } script() { const script = document.createElement('script'); script.onload = (() => this.onLoad()); script.src = 'https://apis.google.com/js/platform.js'; return script; } gauth(prompt, callback) { gapi.load('auth2', () => { gapi.auth2.authorize({ client_id: "758792702348-kokjbpv1leqid7ac7tjq7pbp37cg05kq.apps.googleusercontent.com", scope: "https://www.googleapis.com/auth/drive.file", prompt }, callback); }); } } // =========================================================================== class WrCollProxy extends LitElement { constructor() { super(); this.view = "replay"; this.replayTS = ""; } static get properties() { return { replayUrl: { type: String }, replayTS: { type: String }, source: { type: String }, paramString: { type: String } } } firstUpdated() { this.paramString = new URLSearchParams({ replayUrl: this.replayUrl, replayTS: this.replayTS, source: this.source, view: this.view }); } render() { return html` `; } } // =========================================================================== class WrFaIcon extends LitElement { constructor() { super(); this.size = "1.1em"; } static get properties() { return { svg: { type: String }, size: { type: String } } } static get styles() { return css` :host { display: inline-block; padding: 0; margin: 0; line-height: 1.0em; } :host svg { fill: var(--fa-icon-fill-color, currentcolor); width: var(--fa-icon-width, 19px); height: var(--fa-icon-height, 19px); } `; } render() { if (!this.svg) { return html``; } const styles = {width: this.size, height: this.size}; return html`${unsafeSVG(this.svg)}`; } } // =========================================================================== // =========================================================================== async function main() { const swPromise = initSW(__SW_PATH__ + "?replayPrefix=wabac&stats=true"); await swPromise; customElements.define("app-main", AppMain); customElements.define("wr-index", WrIndex); customElements.define("wr-loader", WrLoader); customElements.define("wr-coll", WrColl); customElements.define("wr-coll-proxy", WrCollProxy); customElements.define("wr-coll-resources", WrResources); customElements.define("wr-coll-curated", WrCuratedPages); customElements.define("wr-replay-page", WrReplayPage); customElements.define("wr-gdrive", WrGdrive); customElements.define("fa-icon", WrFaIcon); } main();