diff --git a/src/frontend.js b/src/frontend.js index a3ea706..ce9321a 100644 --- a/src/frontend.js +++ b/src/frontend.js @@ -17,41 +17,23 @@ class AppMain extends LitElement constructor() { super(); - this.coll = ""; - this.collInfo = {}; - this.loadInfo = {}; - this.updateRoute(); + this.sourceUrl = null; + this.pageParams = {}; } - static get properties() { return { - pageState: { type: String }, pageParams: { type: Object }, - coll: { type: String }, - collInfo: { type: Object } + sourceUrl: { type: String }, } } - async updateRoute() { + firstUpdated() { + this.initRoute(); + } + + initRoute() { this.pageParams = new URLSearchParams(window.location.search); - const source = this.pageParams.get("source"); - if (source) { - const {url, coll} = await this.sourceToId(source); - - this.coll = coll; - - const resp = await fetch(`/wabac/api/${coll}`); - if (resp.status != 200) { - this.sourceUrl = source; - this.pageState = "load"; - } else { - const json = await resp.json(); - this.collInfo = json; - this.pageState = "coll"; - } - return; - } // Google Drive let state = this.pageParams.get("state"); @@ -72,53 +54,22 @@ class AppMain extends LitElement } } - - this.pageState = "index"; - } - - 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}; - } - - onCollLoaded(event) { - this.updateRoute(); + this.sourceUrl = this.pageParams.get("source") || ""; } render() { return html` -
+

Webrecorder Viewer

- ${this.renderContent()} + ${this.sourceUrl ? html` + + ` : this.sourceUrl === "" ? html`` : html``} `; } - - renderContent() { - switch (this.pageState) { - case "load": - return html``; - - case "coll": - return html``; - - case "index": - return html``; - - default: - return html``; - } - } - - //createRenderRoot() { return this; } } // =========================================================================== @@ -247,8 +198,6 @@ class WrLoader extends LitElement return html``; } } - - //createRenderRoot() { return this; } } // =========================================================================== @@ -266,33 +215,7 @@ class WrIndex extends LitElement } static get styles() { - return css` - fa-icon { - cursor: pointer; - display: inline-flex; - } - - fa-icon.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; } - } - - @-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } - - @keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } } - `; + return css``; } firstUpdated() { @@ -353,8 +276,6 @@ class WrIndex extends LitElement
`; } - - //createRenderRoot() { return this; } } // =========================================================================== @@ -362,145 +283,310 @@ class WrColl extends LitElement { constructor() { super(); + this.sourceUrl = null; + + this.baseApiPrefix = "/wabac/api"; + this.baseReplayPrefix = "/wabac"; + this.apiPrefix = ""; + this.replayPrefix = ""; + this.coll = ""; - this.offset = 0; - this.currList = 0; - this.bookmarks = {}; - this.collInfo = {lists: []}; - this.currList = null; - this.lastST = 0; + + this.collInfo = null; this.hasCurated = false; - this.currTab = ""; - this.clickTime = 0; + this.tabData = {}; } static get properties() { return { - hasCurated: { type: Boolean }, + sourceUrl: { type: String }, + collInfo: { type: Object }, coll: { type: String }, - bookmarks: { type: Object }, - currList: { type: Number }, - currTab: { type: String } - } - } - async doLoadBookmarks() { - this.hasCurated = (this.collInfo.lists && this.collInfo.lists.length); - this.bookmarks = {}; + hasCurated: { type: Boolean }, - if (!this.hasCurated) { - return; - } - - const resp = await fetch(`/wabac/api/${this.coll}/bookmarks?offset=${this.offset}`); - const json = await resp.json(); - - this.total = json.total; - - for (const bookmark of json.bookmarks) { - if (!this.bookmarks[bookmark.list]) { - this.bookmarks[bookmark.list] = []; - } - this.bookmarks[bookmark.list].push(bookmark); + tabData: { type: Object } } } firstUpdated() { - window.addEventListener("hashchange", () => this.onHashChange()); + window.addEventListener("hashchange", (event) => this.onHashChange(event)); + + this.doUpdateInfo().then(() => this.onHashChange()); + } + + 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")) { + this._manualHash = true; + // don't add empty params to shorten query + Object.keys(this.tabData). + forEach(key => !this.tabData[key] && delete this.tabData[key]); + window.location.hash = "#" + new URLSearchParams(this.tabData).toString(); + } + } + + 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.doLoadBookmarks().then(() => this.onHashChange()); + this.hasCurated = (this.collInfo.lists && this.collInfo.lists.length); } - onHashChange() { - const hash = window.location.hash; - if (!hash) { - this.currTab = this.hasCurated ? "curated" : "resources"; + onCollLoaded(event) { + this.doUpdateInfo(); + } + + onHashChange(event) { + if (this._manualHash) { + this._manualHash = false; return; } - - if (hash === "#resources") { - this.currTab = "resources"; - } else if (hash === "#curated") { - this.currTab = "curated"; + const hash = window.location.hash; + if (hash) { + this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries()); } - - if (hash.startsWith("#list-")) { - const id = hash.slice(6); - this.currTab = "curated"; - this.scrollToList(id); - } - } - - updated(changedProperties) { - if (changedProperties.get("lists")) { - this.doLoadBookmarks(); + if (!this.tabData.currTab) { + this.tabData = {...this.tabData, currTab: this.hasCurated ? "curated" : "resources"}; } } onTabClick(event) { event.preventDefault(); const hash = event.target.getAttribute("href"); - //this.currTab = hash.slice(1); - window.location.hash = hash; + this.tabData = {...this.tabData, currTab: hash.slice(1)}; return false; } + onCollTabNav(event) { + this.tabData = {...this.tabData, ...event.detail}; + } + render() { return html` - + ${this.renderColl()} `; } + renderColl() { + if (this.collInfo && !this.collInfo.coll) { + return html``; + } else if (this.collInfo) { + return html` + `; + } else { + return html``; + } + } + + renderCollTabs() { + return html` + + + + + + ${this.tabData.currTab === "replay" ? html` + + + ` : ``} + `; + + + switch (this.tabData.currTab) { + case "curated": + return html` + `; + + case "resources": + return html` + + `; + + default: + 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 }, + } + } + + static get styles() { + return css` + .column { + max-height: calc(100vh - 145px); + } + + #content { + margin-top: 10px; + max-height: calc(100vh - 155px); + } + `; + } + + firstUpdated() { + this.doLoadCurated(); + } + + updated(changedProperties) { + if (changedProperties.get("collInfo")) { + this.doLoadCurated(); + } + } + + async doLoadCurated() { + this.curatedPages = {}; + + const resp = await fetch(`${this.collInfo.apiPrefix}/curatedPages?offset=${this.offset}`); + const json = await resp.json(); + + this.total = json.total; + + 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); + } + } + + render() { + return html` + +
+
+ +
+
+
+

${this.collInfo.title}

+ ${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ''} +
+ ${this.renderContent()} +
+
+ `; + } + + onReplay(event) { + event.preventDefault(); + const detail = {replayUrl: event.currentTarget.getAttribute("data-url"), + replayTS: event.currentTarget.getAttribute("data-ts"), + currTab: "replay" + } + this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail})); + return false; + } + renderContent() { return html` ${this.collInfo.lists.map((list, i) => html` @@ -510,15 +596,15 @@ class WrColl extends LitElement
${list.desc}
    - ${this.bookmarks[list.id] ? this.bookmarks[list.id].map((b) => html` + ${this.curatedPages[list.id] ? this.curatedPages[list.id].map((p) => html`
  1. `) : html``} @@ -530,11 +616,14 @@ class WrColl extends LitElement } onClickScroll(event) { + event.preventDefault(); this.scrollToList(event.target.getAttribute("data-list")); + return false; } scrollToList(id) { this.currList = Number(id); + this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {currList: this.currList || undefined}})); const opts = {behavior: "smooth", block: "start", inline: "nearest"}; this.clickTime = new Date().getTime(); this.renderRoot.getElementById("list-" + this.currList).scrollIntoView(opts); @@ -565,6 +654,7 @@ class WrColl extends LitElement if (next && next != curr) { if (next.id.startsWith("list-")) { this.currList = Number(next.id.slice(5)); + this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {currList: this.currList}})); } } @@ -578,12 +668,6 @@ class WrColl extends LitElement sel.scrollIntoView(opts); } } - - bookmarkUrl(b) { - return `/wabac/${this.coll}/${getTS(b.date)}/${b.url}`; - } - - //createRenderRoot() { return this; } } // =========================================================================== @@ -591,15 +675,19 @@ class WrResources extends LitElement { constructor() { super(); + this.collInfo = null; + this.currMime = "text/html,text/xhtml"; - this.results = []; + this.urlSearch = ""; + this.urlSearchType = ""; + this.filteredResults = []; + + this.results = []; + this.tryMore = false; this.loading = false; - this.urlSearch = ""; - this.urlSearchType = "exact"; - this.filters = [ {name: "HTML", filter: "text/html,text/xhtml"}, {name: "Images", filter: "image/"}, @@ -611,17 +699,27 @@ class WrResources extends LitElement static get properties() { return { - coll: { type: String }, + collInfo: { type: Object }, currMime: { type: String }, - //filterText: { type: String }, + urlSearch: { type: String }, + urlSearchType: { type: String }, filteredResults: { type: Array }, - //loading: { type: Boolean } } } static get styles() { return css` - + :host { + display: flex; + flex-direction: column; + } + .notification { + width: 100%; + } + .main-scroll { + max-height: calc(100vh - 295px); + width: 100vw; + } table { table-layout: fixed; word-wrap: break-word; @@ -640,8 +738,16 @@ class WrResources extends LitElement .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; @@ -658,13 +764,20 @@ class WrResources extends LitElement const url = (this.urlSearchType !== "contains" ? this.urlSearch : ""); const prefix = url && this.urlSearchType === "prefix" ? 1 : 0; + const detail = {urlSearch: this.urlSearch, + currMime: this.currMime, + urlSearchType: this.urlSearchType}; + + this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail})); + const params = new URLSearchParams({ mime: this.currMime, url, prefix, count }).toString(); - let resp = await fetch(`/wabac/api/${this.coll}/urls?${params}`); + + let resp = await fetch(`${this.collInfo.apiPrefix}/urls?${params}`); resp = await resp.json(); this.results = resp.urls; this.tryMore = (resp.urls.length === count); @@ -696,7 +809,7 @@ class WrResources extends LitElement count }).toString(); - let resp = await fetch(`/wabac/api/${this.coll}/urls?${params}`); + let resp = await fetch(`${this.collInfo.apiPrefix}/urls?${params}`); resp = await resp.json(); this.results = this.results.concat(resp.urls); console.log(resp.urls.length); @@ -705,7 +818,7 @@ class WrResources extends LitElement this.loading = false; } - onChangeSearch(event) { + onChangeTypeSearch(event) { this.currMime = event.target.value; this.doLoadResources(); } @@ -746,9 +859,9 @@ class WrResources extends LitElement
    - +
    - +
    @@ -761,7 +874,7 @@ class WrResources extends LitElement
    `; } + + onReplay(event) { + event.preventDefault(); + const detail = {replayUrl: event.currentTarget.getAttribute("data-url"), + replayTS: event.currentTarget.getAttribute("data-ts"), + currTab: "replay" + } + this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail})); + return false; + } +} + +// =========================================================================== +class WrReplayPage extends LitElement +{ + static get properties() { + return { + collInfo: {type: Object }, + + replayUrl: { type: String }, + replayTS: { type: String } + } + } + + static get styles() { + return css` + host: { + display: flex; + } + + iframe { + width: 100vw; + height: calc(100vh - 150px); + } + `; + } + + render() { + return html` + + `; + } } // =========================================================================== @@ -876,18 +1031,56 @@ class WrGdrive extends LitElement } } +// =========================================================================== +class WrCollProxy extends LitElement +{ + constructor() { + super(); + this.currTab = "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, + currTab: this.currTab + }); + } + + render() { + return html` + + `; + } +} + // =========================================================================== // =========================================================================== async function main() { - const swPromise = initSW("swonly.js?replayPrefix=wabac&stats=true"); + const swPromise = initSW("./dist/swonly.js?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); }