${this.colls.length ? this.colls.map((coll, i) => html`
Size: ${prettyBytes(Number(coll.size || 0))}
${!this._deleting[coll.sourceUrl] ? html`
` : html`
`}
`) : 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._locUpdateNeeded = false;
this._locationHash = "";
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));
}
updated(changedProperties) {
if (changedProperties.has("sourceUrl")) {
this.doUpdateInfo();
}
if (changedProperties.has("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]);
const newHash = "#" + new URLSearchParams(this.tabData).toString();
if (newHash !== this._locationHash) {
this._locationHash = newHash;
if (this._replaceLoc) {
const newLoc = new URL(window.location.href);
newLoc.hash = this._locationHash;
window.history.replaceState({}, "", newLoc.href);
this._replaceLoc = false;
} else {
window.location.hash = this._locationHash;
}
}
this._locUpdateNeeded = false;
}
}
async doUpdateInfo() {
const {url, coll} = await 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 = this.collInfo.filename;
}
this.hasCurated = (this.collInfo.lists && this.collInfo.lists.length);
this.dispatchEvent(new CustomEvent("coll-loaded", {detail: {alreadyLoaded: true}}));
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"};
//this.replaceLoc = true;
}
}
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._locationHash) {
this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries());
this._locationHash = hash;
}
}
onTabClick(event) {
event.preventDefault();
const hash = event.currentTarget.getAttribute("href");
this.tabData = {...this.tabData, view: hash.slice(1)};
return false;
}
onCollTabNav(event) {
if (event.target.id === this.tabData.view) {
this.tabData = {view: this.tabData.view, ...event.detail.data};
this._replaceLoc = !this._locUpdateNeeded && event.detail.replaceLoc;
this._locUpdateNeeded = true;
}
}
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-light {
background-color: #f0fff8";
}
`);
}
render() {
if (this.collInfo && !this.collInfo.coll) {
return html`
`;
} else if (this.collInfo) {
return html`
`;
} else {
return html``;
}
}
renderCollTabs() {
return html`
${this.tabData.view === 'replay' ? 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.has("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`
.columns {
width: 100vw;
}
.column {
max-height: calc(100vh - 92px);
}
#content {
margin-top: 10px;
max-height: calc(100vh - 102px);
}
ul.menu-list a.is-active {
background-color: #55be6f;
}
`);
}
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`
-
${p.title || p.url}
${p.url}
${new Date(p.date).toLocaleString()}
${p.desc}
`) : html``}
`)}
`;
}
onReplay(event) {
event.preventDefault();
const data = {
url: event.currentTarget.getAttribute("data-url"),
ts: 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);
// lists are 1 based, 0 is header, 1 is first list
if (this.currList > this.collInfo.lists.length) {
this.currList = 0;
}
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
{
static get filters() {
return [
{name: "HTML", filter: "text/html,text/xhtml"},
{name: "Images", filter: "image/"},
{name: "Audio/Video", filter: "audio/,video/"},
{name: "PDF", filter: "application/pdf"},
{name: "Javascript", filter: "application/javascript,application/x-javascript"},
{name: "CSS", filter: "text/css"},
{name: "Fonts", filter: "font/,application/font-woff"},
{name: "Plain Text", filter: "text/plain"},
{name: "JSON", filter: "application/json"},
{name: "DASH/HLS", filter: "application/dash+xml,application/x-mpegURL,application/vnd.apple.mpegurl"},
{name: "All URLs", filter: "all"}
];
}
constructor() {
super();
this.collInfo = null;
this.currMime = "";
this.urlSearch = "";
this.urlSearchType = "";
this.filteredResults = [];
this.results = [];
this.tryMore = false;
this.loading = false;
}
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();
}
updated(changedProperties) {
if (changedProperties.has("urlSearch") ||
changedProperties.has("urlSearchType") ||
changedProperties.has("currMime")) {
if (!this.currMime) {
this.currMime = "text/html,text/xhtml";
}
this.doLoadResources();
const data = {
urlSearch: this.urlSearch,
urlSearchType: this.urlSearchType,
currMime: this.currMime
};
const replaceLoc = !changedProperties.has("currMime") && !changedProperties.has("urlSearchType");
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {replaceLoc, data}}));
}
}
async doLoadResources() {
const count = 100;
this.loading = true;
let url = (this.urlSearchType !== "" ? 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 mime = this.currMime === "all" ? "" : this.currMime;
const params = new URLSearchParams({
mime,
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.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 !== "" ? this.urlSearch : "");
const prefix = url && this.urlSearchType === "prefix" ? 1 : 0;
const mime = this.currMime === "all" ? "" : this.currMime;
const last = this.results[this.results.length - 1];
const params = new URLSearchParams({
mime,
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 === "" ? 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 - 199px);
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`
Search:
| URL |
Timestamp |
Mime |
Status |
${this.filteredResults.length ?
this.filteredResults.map((result) => html`
| ${result.url} |
${new Date(result.date).toLocaleString()} |
${result.mime} |
${result.status} |
`) : html`No Results Found.
`}
`;
}
onReplay(event) {
event.preventDefault();
const data = {
url: event.currentTarget.getAttribute("data-url"),
ts: event.currentTarget.getAttribute("data-ts"),
view: "replay"
};
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
return false;
}
}
// ===========================================================================
class WrReplayPage extends LitElement
{
constructor() {
super();
this.isLoading = false;
this.replayUrl = "";
this.replayTS = "";
this.url = "";
this.ts = "";
this.title = "";
this.showAuth = false;
}
static get properties() {
return {
collInfo: {type: Object },
sourceUrl: { type: String },
// external url set by parent
url: { type: String },
ts: { type: String },
// actual replay url
replayUrl: { type: String },
replayTS: { type: String },
title: { type: String },
iframeUrl: { type: String },
isLoading: { type: Boolean },
showAuth: { type: Boolean }
}
}
firstUpdated() {
window.addEventListener("message", (event) => this.onReplayMessage(event));
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data.type === "authneeded" && this.collInfo && event.data.coll === this.collInfo.coll) {
this.showAuth = true;
}
});
}
doSetIframeUrl() {
this.iframeUrl = this.url ? `${this.collInfo.replayPrefix}/${this.ts || ""}mp_/${this.url}` : "";
}
updated(changedProperties) {
if (changedProperties.has("sourceUrl") || changedProperties.has("collInfo")) {
this.isAuthable = (this.sourceUrl.startsWith("googledrive://") &&
this.collInfo && this.collInfo.onDemand);
}
if (this.url &&
((this.replayUrl != this.url) || (this.replayTS != this.ts)) &&
(changedProperties.has("url") || changedProperties.has("ts"))) {
this.replayUrl = this.url;
this.replayTS = this.ts;
this.doSetIframeUrl();
}
if (this.iframeUrl && changedProperties.has("iframeUrl")) {
this.isLoading = true;
}
if (this.replayUrl && changedProperties.has("replayUrl")) {
const data = {
url: this.replayUrl,
ts: this.replayTS,
};
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {replaceLoc: true, data}}));
}
}
onReplayMessage(event) {
const iframe = this.renderRoot.querySelector("iframe");
if (iframe && event.source === iframe.contentWindow) {
if (event.data.wb_type === "load" || event.data.wb_type === "replace-url") {
this.replayTS = event.data.ts;
this.replayUrl = event.data.url;
this.title = event.data.title || this.title;
this.isLoading = false;
if (event.data.icons) {
const icons = event.data.icons;
this.dispatchEvent(new CustomEvent("replay-favicons", {bubbles: true, composed: true, detail: {icons}}));
}
} else if (event.data.wb_type === "title") {
this.title = event.data.title;
}
}
}
onSubmit(event) {
event.preventDefault();
const value = this.renderRoot.querySelector("input").value;
//this.replayUrl = value;
this.url = value;
return false;
}
onRefresh(event, forceReload) {
if (event) {
event.preventDefault();
}
if (this.isLoading && !forceReload) {
return;
}
const iframe = this.renderRoot.querySelector("iframe");
if (iframe) {
this.isLoading = true;
iframe.contentWindow.location.reload();
}
}
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;
}
.embed-bar {
padding: 0.25em;
max-width: none;
border-bottom: solid .1rem #97989A;
background-color: aliceblue;
text-align: center;
height: 44px;
}
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;
}
#refresh {
border: 0px;
}
.modal {
top: 174px;
}
`);
}
render() {
return html`
${!this.embed ? html`
` : html`
`}
${this.iframeUrl ? html`
` : ``}
${this.isAuthable ? html`
Auth Needed
` : ``}
`;
}
async onReAuthed(event) {
const headers = event.detail.headers;
const resp = await fetch(`/wabac/api/${this.collInfo.coll}/updateAuth`, {
method: 'POST',
body: JSON.stringify({headers})
});
if (this.showAuth) {
this.onRefresh(null, true);
this.showAuth = false;
}
}
}
// ===========================================================================
class WrGdrive extends LitElement
{
constructor() {
super();
this.state = "trypublic";
this.sourceUrl = "";
this.scriptLoaded = false;
this.error = false;
}
static get properties() {
return {
state: { type: String },
sourceUrl: { type: String },
error: { type: Boolean },
reauth: { type: Boolean }
}
}
updated(changedProperties) {
if (changedProperties.has("sourceUrl")) {
this.error = false;
this.state = "trypublic";
this.tryPublicAccess().then((result) => {
if (!result) {
this.state = "tryauto";
this.requestUpdate();
}
});
}
}
async tryPublicAccess() {
try {
const sourceUrl = this.sourceUrl;
const fileId = sourceUrl.slice("googledrive://".length);
const publicCheckUrl = `https://gdrive-proxy.webrecorder.workers.dev/g/${fileId}`;
let resp = null;
try {
resp = await fetch(publicCheckUrl);
} catch (e) {
return false;
}
const json = await resp.json();
if (!json.url || !json.name || !json.size) {
return false;
}
// only allow for small downloads that will not result in partial download
if (json.size > 15000000) {
return false;
}
const publicUrl = json.url;
try {
const abort = new AbortController();
const signal = abort.signal;
resp = await fetch(publicUrl, {signal});
abort.abort();
if (resp.status != 200) {
return false;
}
} catch(e) {
return false;
}
const name = json.name;
const extra = {publicUrl};
const size = Number(json.size);
this.dispatchEvent(new CustomEvent("load-ready", {detail: {name, extra, size, sourceUrl}}));
return true;
} catch (e) {
return false;
}
}
onLoad() {
this.scriptLoaded = true;
this.gauth('none', (response) => {
if (response.error === "immediate_failed") {
if (this.state !== "implicitonly") {
this.state = "trymanual";
}
} else {
this.authed(response);
}
});
}
onClickAuth() {
this.gauth('select_account', (response) => {
if (!response.error) {
this.authed(response);
}
});
}
async authed(response) {
const sourceUrl = this.sourceUrl;
const fileId = sourceUrl.slice("googledrive://".length);
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,size", {headers});
if ((resp.status === 404 || resp.status == 403)) {
if (this.state !== "implicitonly") {
this.state = "trymanual";
}
this.error = true;
return;
}
this.error = false;
const metadata = await resp.json();
const name = metadata.name;
const size = Number(metadata.size);
this.dispatchEvent(new CustomEvent("load-ready", {detail: {sourceUrl, headers, size, name}}));
}
static get styles() {
return wrapCss(css``);
}
render() {
return html`
${this.script()}
${this.state !== "trymanual" ? html`
Connecting to Google Drive...
` : html`
${this.error ? html`
${this.reauth ? 'Some resources are loaded on demand from Google Drive, which requires reauthorization.' :
'Could not access this file with the current Google Drive account.'}
If you have multiple Google Drive accounts, be sure to select the correct one.
` : ``}
`}`;
}
script() {
if (this.state === "trypublic" || this.scriptLoaded) {
return html``;
}
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: GDRIVE_CLIENT_ID,
scope: "https://www.googleapis.com/auth/drive.file",
response_type: "token",
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`
`;
}
}
// ===========================================================================
// ===========================================================================
function registerSW(url) {
let resolve, reject;
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
register(url, {
registrationOptions: { scope: '/' },
ready (registration) {
console.log('Service worker is active.');
resolve();
},
error (error) {
console.error('Error during service worker registration:', error);
reject();
}
});
}
async function main() {
//const swPromise = initSW(__SW_PATH__ + "?replayPrefix=wabac&stats=true");
const swPromise = registerSW(__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();