IPFS Support + Loading Improvements (Better local file handling) + Streamlined Collection View (#25)

* support ipfs:// urls!
misc: better error message for loading from http://
tabs: don't render pages/story/urls if hidden

* ipfs work: remove ww only use sw

* electron + ipfs: add ipfs support to electron app, using ipfs-core
- route ipfs requests to files proxy with 'ipfs=' query param

* file loading:
- add support for new Chrome native file system api with file handles, which allows persistent access to files vial file handles
- if window.showOpenFilePicker() is available use that, include file handle in load message
- support asking permissions for file handles, using similar workflow as google drive auth
- attempt to ask for file permission directly, show auth button if fails (user gestured required)
- replace auth modal with iframe replacement insert to ask for auth for google drive and local file

* ui overhaul for 1.3.0 / condense collection view:
- don't show navbar in collection view
- add logo to back to home page to location bar
- condense location bar buttons
- convert terms to general about dialog, available from collection context menu
- add info tab to collection view in place of drop-down info
- make <wr-coll-info> component, used in both collection list and info tab with additional details
- update to wabac.js 2.2.0

* update CHANGES.md
This commit is contained in:
Ilya Kreymer
2020-11-16 18:31:15 -08:00
committed by GitHub
parent e839400017
commit 228e9bdff6
15 changed files with 4533 additions and 614 deletions
+12
View File
@@ -1,5 +1,17 @@
## CHANGES ## CHANGES
v1.3.0
- UI: Condense navbar and location bar: add logo to location bar, condense button spacing, add about link to context menu
- UI: Add collection info dropdown to be a separate tab instead
- UI: Convert Terms into general About dialog
- Loading: IPFS support! Load from `ipfs://` URLs, eg. `ipfs://hash/filename.wacz. Possible to specify extension for root with: `ipfs://hash#filename=.wacz`
- App: Electron App uses IPFS in node, proxies through file proxy.
- Loading: Native File System (Chrome-only) support: Loading local files on Chrome uses FileHandle, which allows persistent reuse of files with permission
- Loading: Optimize local file loading on all browsers: No more separate webworker, serviceworker caches entire blob into memory. Should fix Firefox loading issues streaming from blob.
- Loading/UI: Revamp permissions UI to replace iframe instead of popup
- UI: Favicon smoother update, better check for valid icon
- UI: Purge Cache / Full Reload reloads current frame, not parent
v1.2.1 v1.2.1
- Loading/UI: Fix Google Drive Loading/Reauthorization: Only trigger when reauth is needed. For rate-limit errors, attempt reloading more slowly (exponential backoff) - Loading/UI: Fix Google Drive Loading/Reauthorization: Only trigger when reauth is needed. For rate-limit errors, attempt reloading more slowly (exponential backoff)
- Loading: ReplayWeb.page App supports opening WACZ, WARC and HAR files via double-clicking from OS and via command-line. - Loading: ReplayWeb.page App supports opening WACZ, WARC and HAR files via double-clicking from OS and via command-line.
+8 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "replaywebpage", "name": "replaywebpage",
"productName": "ReplayWeb.page", "productName": "ReplayWeb.page",
"version": "1.2.3", "version": "1.3.0",
"description": "Serverless Web Archive Replay", "description": "Serverless Web Archive Replay",
"repository": "https://github.com/webrecorder/replayweb.page", "repository": "https://github.com/webrecorder/replayweb.page",
"homepage": "https://replayweb.page/", "homepage": "https://replayweb.page/",
@@ -9,10 +9,11 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^5.13.0",
"@webrecorder/wabac": "^2.1.5", "@webrecorder/wabac": "^2.2.0",
"bulma": "^0.9.0", "bulma": "^0.9.0",
"fetch-ndjson": "^1.1.0", "fetch-ndjson": "^1.1.0",
"flexsearch": "^0.6.32", "flexsearch": "^0.6.32",
"ipfs-core": "^0.2.0",
"keyword-mark-element": "^0.1.2", "keyword-mark-element": "^0.1.2",
"lit-element": "^2.3.1", "lit-element": "^2.3.1",
"marked": "^1.0.0", "marked": "^1.0.0",
@@ -23,6 +24,7 @@
"split.js": "^1.6.2" "split.js": "^1.6.2"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^6.3.0",
"css-loader": "^3.5.3", "css-loader": "^3.5.3",
"electron": "^10.1.3", "electron": "^10.1.3",
"electron-builder": "^22.7.0", "electron-builder": "^22.7.0",
@@ -35,7 +37,7 @@
"style-loader": "^1.2.1", "style-loader": "^1.2.1",
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
"to-string-loader": "^1.1.6", "to-string-loader": "^1.1.6",
"webpack": "^4.43.0", "webpack": "^4.44.2",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3" "webpack-dev-server": "^3.10.3"
}, },
@@ -56,6 +58,7 @@
"start-electron": "NODE_ENV=development electron ./dist/electron.js $1", "start-electron": "NODE_ENV=development electron ./dist/electron.js $1",
"start-jekyll": "yarn run build-docs && bundle exec jekyll serve", "start-jekyll": "yarn run build-docs && bundle exec jekyll serve",
"dist": "yarn run build && yarn run pack", "dist": "yarn run build && yarn run pack",
"dist-dev": "yarn run build-dev && yarn run pack",
"release": "yarn run build && electron-builder" "release": "yarn run build && electron-builder"
}, },
"build": { "build": {
@@ -70,6 +73,8 @@
"files": [ "files": [
"!**/node_modules/**/*", "!**/node_modules/**/*",
"dist/*.js", "dist/*.js",
"dist/prebuilds/${platform}-${arch}/*",
"dist/build/*",
"*.js", "*.js",
"*.html", "*.html",
"webmanifest.json", "webmanifest.json",
+82 -80
View File
@@ -1,14 +1,11 @@
import { LitElement, html, css } from 'lit-element'; import { LitElement, html, css } from 'lit-element';
import { wrapCss, rwpLogo, IS_APP, clickOnSpacebarPress } from './misc'; import { wrapCss, rwpLogo, IS_APP, clickOnSpacebarPress } from './misc';
import { registerSW, initDBWorker } from './pageutils'; import { registerSW } from './pageutils';
import prettyBytes from 'pretty-bytes';
import fasHelp from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg'; import fasHelp from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
import fasArrowLeft from '@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg'; import fasArrowLeft from '@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg';
import fasArrowRight from '@fortawesome/fontawesome-free/svgs/solid/arrow-right.svg'; import fasArrowRight from '@fortawesome/fontawesome-free/svgs/solid/arrow-right.svg';
import fasInfoIcon from '@fortawesome/fontawesome-free/svgs/solid/info-circle.svg';
// =========================================================================== // ===========================================================================
@@ -19,14 +16,13 @@ class App extends LitElement
super(); super();
this.sourceUrl = null; this.sourceUrl = null;
this.collInfo = null; this.collInfo = null;
this.showTerms = false; this.showAbout = false;
this.pageParams = {}; this.pageParams = {};
this.inited = false; this.inited = false;
this.navMenuShown = false; this.navMenuShown = false;
registerSW(__SW_NAME__); registerSW(__SW_NAME__);
initDBWorker();
this.safariKeyframes(); this.safariKeyframes();
} }
@@ -37,7 +33,7 @@ class App extends LitElement
pageParams: { type: Object }, pageParams: { type: Object },
sourceUrl: { type: String }, sourceUrl: { type: String },
navMenuShown: { type: Boolean }, navMenuShown: { type: Boolean },
showTerms: { type: Boolean }, showAbout: { type: Boolean },
collInfo: { type: Object }, collInfo: { type: Object },
loadInfo: { type: Object }, loadInfo: { type: Object },
embed: { type: String }, embed: { type: String },
@@ -112,12 +108,19 @@ class App extends LitElement
`); `);
} }
renderNavBar() {
}
render() { render() {
if (!this.inited) { if (!this.inited) {
return html``; return html``;
} }
const showNavBar = !this.collInfo && (!this.embed || this.embed === "full");
return html` return html`
${!this.embed || this.embed === "full" ? html` ${showNavBar ? html`
<a href="#skip-main-target" @click=${this.skipMenu} class="skip-link">Skip main navigation</a> <a href="#skip-main-target" @click=${this.skipMenu} class="skip-link">Skip main navigation</a>
<nav class="navbar has-background-info" aria-label="main"> <nav class="navbar has-background-info" aria-label="main">
<div class="navbar-brand"> <div class="navbar-brand">
@@ -158,37 +161,6 @@ class App extends LitElement
<fa-icon .svg="${fasArrowRight}" aria-hidden="true"></fa-icon><span class="menu-only">&nbsp;Go Forward</span> <fa-icon .svg="${fasArrowRight}" aria-hidden="true"></fa-icon><span class="menu-only">&nbsp;Go Forward</span>
</a> </a>
` : ``} ` : ``}
${this.sourceUrl && this.collInfo ? html`
<div class="navbar-item has-dropdown is-hoverable">
<span tabindex="0" class="info-menu navbar-link is-arrowless" aria-labelledby="about-this-archive">
<fa-icon size="1.2em" .svg="${fasInfoIcon}" aria-hidden="true"></fa-icon>
<span id="about-this-archive" class="menu-only">About this Archive</span>
</span>
<div class="navbar-dropdown">
<div class="navbar-item wide-only"><i>About this Archive</i></div>
<hr class="navbar-divider">
<div class="navbar-item">
Source:&nbsp;<b>${this.sourceUrl}</b>
</div>
<div class="navbar-item">
Filename:&nbsp;<b>${this.collInfo.filename}</b>
</div>
<div class="navbar-item">
Total Size:&nbsp;<b>${prettyBytes(Number(this.collInfo.size || 0))}</b>
</div>
<div class="navbar-item">
Loading Mode:&nbsp;<b>${this.collInfo.onDemand ? "Download On-Demand" : "Fully Local"}</b>
</div>
<div class="navbar-item">
Collection Id:&nbsp;<b>${this.collInfo.coll}</b>
</div>
<hr class="navbar-divider">
<div class="navbar-item">
ReplayWeb.page Version: ${__VERSION__}
</div>
</div>
</div>
` : ``}
</div> </div>
${!this.embed ? html` ${!this.embed ? html`
<div class="navbar-end"> <div class="navbar-end">
@@ -196,10 +168,10 @@ class App extends LitElement
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span>&nbsp;User Docs</span> <fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span>&nbsp;User Docs</span>
</a> </a>
<!-- <!--
-- NB: the Terms modal is currently inaccessible to people using keyboards or screen readers. -- NB: the About modal is currently inaccessible to people using keyboards or screen readers.
-- Should all the JS and infrastructure for accessible modals be added, or should Terms be a normal page? -- Should all the JS and infrastructure for accessible modals be added, or should About be a normal page?
--> -->
<a href="?terms" @click="${(e) => { e.preventDefault(); this.showTerms = true} }"class="navbar-item">Terms</a> <a href="?terms" @click="${(e) => { e.preventDefault(); this.showAbout = true} }"class="navbar-item">About</a>
</div>` : html``} </div>` : html``}
</div> </div>
</nav><p id="skip-main-target" tabindex="-1" class="is-sr-only">Skipped</p> </nav><p id="skip-main-target" tabindex="-1" class="is-sr-only">Skipped</p>
@@ -211,7 +183,8 @@ class App extends LitElement
sourceUrl="${this.sourceUrl}" sourceUrl="${this.sourceUrl}"
embed="${this.embed}" embed="${this.embed}"
@replay-favicons=${this.onFavIcons} @replay-favicons=${this.onFavIcons}
@coll-loaded=${this.onCollLoaded}></wr-coll> @coll-loaded=${this.onCollLoaded}
@about-show=${(e) => this.showAbout = true}></wr-coll>
` : html` ` : html`
<wr-coll-index> <wr-coll-index>
@@ -222,42 +195,8 @@ class App extends LitElement
</wr-coll-index> </wr-coll-index>
`} `}
${this.showTerms ? html` ${this.showAbout ? this.renderAbout() : ``}
<div class="modal is-active"> `;
<div class="modal-background" @click="${(e) => this.showTerms = false}"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Terms and Privacy</p>
<button class="delete" aria-label="close" @click="${(e) => this.showTerms = false}"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div class="content">
<p>This site is a static browser-based application that loads web archive files provided by the user
and renders them for replay in the browser.</p>
<p>The site is operated by the <a href="https://webrecorder.net/">Webrecorder Project</a></p>
<p>See the <a target="_blank" href="./docs">Docs</a> for more info on how it works.</p>
<h3>Privacy</h3>
<p><b>No data is uploaded anywhere and no information is collected.</b></p>
<p>All content rendered stays directly in your browser. When loading an archive from Google Drive,
the site may ask for account authorization to download the specified file only.</p>
<h4>Disclaimer of Warranties</h4>
<p>The application may not always be available. No guarantees!</p>
<p>Some legalese:</p>
<p style="font-size: 0.8rem">DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS SOFTWARE TO YOU "AS AVAILABLE"
AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE,
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
</p>
<a class="button is-warning" href="#" @click="${(e) => this.showTerms = false}">Close</a>
</div>
</div>
</section>
</div>
</div>
</div>
` : ``} `
} }
firstUpdated() { firstUpdated() {
@@ -344,7 +283,7 @@ class App extends LitElement
this.embed = this.pageParams.get("embed") || ""; this.embed = this.pageParams.get("embed") || "";
if (this.pageParams.has("terms")) { if (this.pageParams.has("terms")) {
this.showTerms = true; this.showAbout = true;
} }
if (this.pageParams.get("config")) { if (this.pageParams.get("config")) {
@@ -438,6 +377,69 @@ class App extends LitElement
document.head.appendChild(style); document.head.appendChild(style);
style.appendChild(document.createTextNode(css)); style.appendChild(document.createTextNode(css));
} }
renderAbout() {
return html`
<div class="modal is-active">
<div class="modal-background" @click="${this.onAboutClose}"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">About ReplayWeb.page ${IS_APP ? 'App' : ''}</p>
<button class="delete" aria-label="close" @click="${this.onAboutClose}"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div class="content">
<div style="display: flex">
<div class="has-text-centered" style="width: 220px">
<wr-anim-logo class="logo" size="48px"></wr-anim-logo>
<div style="font-size: smaller; margin-bottom: 1em">${IS_APP ? 'App' : ''} v${__VERSION__}</div>
</div>
${IS_APP ? html`
<p>ReplayWeb.page App is a standalone app for Mac, Windows and Linux that loads web archive files provided by the user
and renders them for replay.</p>
` : html`
<p><a href="https://replayweb.page" target="_blank">ReplayWeb.page</a> is a browser-based viewer that loads web archive files provided by the user
and renders them for replay in the browser.</p>`}
</div>
<p>Full source code is available at:
<a href="https://github.com/webrecorder/replayweb.page" target="_blank">https://github.com/webrecorder/replayweb.page</a>
</p>
<p>See the <a target="_blank" href="./docs">User Docs</a> or the GitHub README for more info on how it works.</p>
<p>ReplayWeb.page is part of the <a href="https://webrecorder.net/" target="_blank">Webrecorder Project</a>.</p>
<h3>Privacy</h3>
<p><b>No data is uploaded anywhere and no information is collected.</b></p>
<p>All content rendered stays directly in your browser.<br/>When loading an archive from Google Drive,
the site may ask for account authorization to download the specified file only.</p>
<h4>Disclaimer of Warranties</h4>
<p>The application is provided "as is" without any guarantees.</p>
<details>
<summary>Legalese:</summary>
<p style="font-size: 0.8rem">DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS SOFTWARE TO YOU "AS AVAILABLE"
AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE,
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
</details>
<div class="has-text-centered">
<a class="button is-warning" href="#" @click="${this.onAboutClose}">Close</a>
</div>
</div>
</div>
</section>
</div>
</div>
</div>`;
}
onAboutClose() {
this.showAbout = false;
}
} }
customElements.define("replay-app-main", App); customElements.define("replay-app-main", App);
+45 -6
View File
@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'lit-element'; import { LitElement, html, css } from 'lit-element';
import { wrapCss } from './misc'; import { IS_APP, wrapCss } from './misc';
import fasUpload from '@fortawesome/fontawesome-free/svgs/solid/upload.svg'; import fasUpload from '@fortawesome/fontawesome-free/svgs/solid/upload.svg';
@@ -12,6 +12,14 @@ class Chooser extends LitElement
this.fileDisplayName = ""; this.fileDisplayName = "";
this.file = null; this.file = null;
this.hasNativeFS = !!window.showOpenFilePicker && !IS_APP;
}
static get properties() {
return {
fileDisplayName: { type: String }
}
} }
onChooseFile(event) { onChooseFile(event) {
@@ -22,7 +30,32 @@ class Chooser extends LitElement
this.file = event.currentTarget.files[0]; this.file = event.currentTarget.files[0];
// file.path only available in electron app // file.path only available in electron app
this.fileDisplayName = "file://" + (this.file.path || this.file.name); this.fileDisplayName = "file://" + (this.file.path || this.file.name);
this.requestUpdate(); }
async onChooseNativeFile() {
const options = {
types: [
{
description: 'WARC, WACZ, HAR and WBN Files',
accept: {
'application/warc': ['.warc', '.gz'],
'application/har': ['.har'],
'application/wacz': ['.wacz'],
'application/wbn': ['.wbn']
}
}
]
}
const [fileHandle] = await window.showOpenFilePicker(options);
this.fileHandle = fileHandle;
this.file = await fileHandle.getFile();
this.fileDisplayName = "file://" + fileHandle.name;
}
randomId() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
} }
onStartLoad(event) { onStartLoad(event) {
@@ -37,12 +70,17 @@ class Chooser extends LitElement
const url = new URL(__APP_FILE_SERVE_PREFIX__); const url = new URL(__APP_FILE_SERVE_PREFIX__);
url.searchParams.set("filename", this.file.path); url.searchParams.set("filename", this.file.path);
loadInfo.loadUrl = url.href; loadInfo.loadUrl = url.href;
loadInfo.noCache = true;
} else if (this.fileHandle) {
loadInfo.loadUrl = this.fileDisplayName + "-" + this.randomId();
loadInfo.extra = {fileHandle: this.fileHandle};
loadInfo.noCache = false;
} else { } else {
loadInfo.loadUrl = URL.createObjectURL(this.file); loadInfo.loadUrl = URL.createObjectURL(this.file);
loadInfo.blob = this.file; loadInfo.blob = this.file;
loadInfo.noCache = false;
} }
loadInfo.size = this.file.size; loadInfo.size = this.file.size;
loadInfo.noCache = this.file.path !== undefined;
loadInfo.name = this.fileDisplayName; loadInfo.name = this.fileDisplayName;
} }
@@ -116,10 +154,11 @@ class Chooser extends LitElement
<div class="extra-padding panel-block file has-name"> <div class="extra-padding panel-block file has-name">
<form class="content is-flex" @submit="${this.onStartLoad}"> <form class="content is-flex" @submit="${this.onStartLoad}">
<label class="file-label"> <label class="file-label">
${!this.hasNativeFS ? html`
<input class="file-input" <input class="file-input"
@click="${(e) => e.currentTarget.value = null}" @click="${(e) => e.currentTarget.value = null}"
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload"> @change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">` : ``}
<span class="file-cta"> <span class="file-cta" @click="${this.onChooseNativeFile}">
<span class="file-icon"> <span class="file-icon">
<fa-icon size="0.9em" .svg=${fasUpload} aria-hidden="true"></fa-icon> <fa-icon size="0.9em" .svg=${fasUpload} aria-hidden="true"></fa-icon>
</span> </span>
@@ -133,7 +172,7 @@ class Chooser extends LitElement
<p class="control is-expanded"> <p class="control is-expanded">
<input class="file-name input" type="text" <input class="file-name input" type="text"
name="filename" id="filename" name="filename" id="filename"
pattern="((file|http|https|s3):\/\/.*\.(warc|warc.gz|zip|wacz|har|wbn))|(googledrive:\/\/.+)" pattern="((file|http|https|ipfs|s3):\/\/.*\.(warc|warc.gz|zip|wacz|har|wbn))|(googledrive:\/\/.+)"
.value="${this.fileDisplayName}" .value="${this.fileDisplayName}"
@input="${this.onInput}" @input="${this.onInput}"
autocomplete="off" autocomplete="off"
+139 -39
View File
@@ -195,23 +195,6 @@ class CollIndex extends LitElement
.icon.is-left { .icon.is-left {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.columns {
width: 100vw;
}
.column {
word-break: break-word;
position: relative;
}
.col-title:hover {
}
.col-title a {
display: block;
height: 100%;
}
.column:hover > .copy, .source-text:hover + .copy, .copy:hover {
display: inline;
}
.coll-block { .coll-block {
position: relative; position: relative;
} }
@@ -220,10 +203,6 @@ class CollIndex extends LitElement
top: 10px; top: 10px;
right: 10px; right: 10px;
} }
.minihead {
font-size: 10px;
font-weight: bold;
}
#sort-select::after { #sort-select::after {
display: none; display: none;
} }
@@ -279,23 +258,7 @@ class CollIndex extends LitElement
<div class="coll-list"> <div class="coll-list">
${this.sortedColls.map((coll, i) => html` ${this.sortedColls.map((coll, i) => html`
<div class="coll-block panel-block"> <div class="coll-block panel-block">
<div class="columns"> <wr-coll-info .coll=${coll}></wr-coll-info>
<div class="column col-title is-4">
<span class="subtitle has-text-weight-bold">
<a href="?source=${coll.sourceUrl}">${coll.title || coll.filename}</a>
</span>
</div>
<div class="column is-4">
<span class="source-text"><p class="minihead">Source</p>${coll.sourceUrl}&nbsp;</span>
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy"><fa-icon .svg="${fasCopy}"/></a>
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://") ? html`
<p><i>(${coll.filename})</i></p>` : ''}
</div>
<div class="column is-2"><p class="minihead">Date Loaded</p>${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}</div>
<div class="column is-2"><p class="minihead">Total Size</p>${prettyBytes(Number(coll.size || 0))}
</div>
</div>
${!this._deleting[coll.sourceUrl] ? html` ${!this._deleting[coll.sourceUrl] ? html`
<button class="delete" aria-label="Unload Collection" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button> <button class="delete" aria-label="Unload Collection" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button>
` : html` ` : html`
@@ -314,6 +277,138 @@ class CollIndex extends LitElement
</section> </section>
`; `;
} }
}
// ===========================================================================
class WrCollInfo extends LitElement
{
constructor() {
super();
this.detailed = false;
this.canDelete = false;
}
static get properties() {
return {
coll: { type: Object },
detailed: { type: Boolean },
canDelete: { type: Boolean }
}
}
static get styles() {
return wrapCss(css`
.columns {
width: 100%;
}
.column {
word-break: break-word;
position: relative;
}
:host {
width: 100%;
height: 100%;
min-width: 0px;
}
:host(.is-list) .columns {
display: flex !important;
flex-direction: column;
}
:host(.is-list) .column {
width: 100% !important;
}
.col-title:hover {
}
.col-title a {
display: block;
height: 100%;
}
.column:hover > .copy, .source-text:hover + .copy, .copy:hover {
display: inline;
}
.copy {
color: black;
margin: 0px;
margin: 0;
line-height: 0.4em;
padding: 6px;
border-radius: 10px;
display: none;
position: absolute;
}
.copy:active {
background-color: lightgray;
}
.minihead {
font-size: 10px;
font-weight: bold;
}
`);
}
render() {
const coll = this.coll;
const detailed = this.detailed;
return html`
<div class="columns">
<div class="column col-title is-4">
<span class="subtitle has-text-weight-bold">
${detailed ? html`
${coll.title || coll.filename}
` : html`
<a href="?source=${encodeURIComponent(coll.sourceUrl)}">${coll.title || coll.filename}</a>`}
</span>
</div>
${detailed && coll.desc ? html`
<div class="column">
<p class="minihead">Description</p>
${coll.desc}
</div>` : html`
`}
<div class="column is-4">
<span class="source-text"><p class="minihead">Source</p>${coll.sourceUrl}&nbsp;</span>
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy"><fa-icon .svg="${fasCopy}"/></a>
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://") ? html`
<p><i>(${coll.filename})</i></p>` : ''}
</div>
${detailed ? html`
<div class="column"><p class="minihead">Filename</p>${coll.filename}</div>` : html``}
<div class="column is-2"><p class="minihead">Date Loaded</p>${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}</div>
<div class="column is-2"><p class="minihead">Total Size</p>${prettyBytes(Number(coll.size || 0))}</div>
${detailed ? html`
<div class="column">
<p class="minihead">Loading Mode</p>
${coll.onDemand ? "Download On-Demand" : "Fully Local"}
</div>
<div class="column">
<p class="minihead">Collection id</p>
${coll.coll}
</div>
` : html``}
</div>`;
}
// purge + delete buttons
//
// <div class="column">
// <button @click="${(e) => this.onPurge(true)}" class="button">Purge Cache + Full Reload</button>
// </div>
// ${this.canDelete ? html`
// <div class="column">
// <button @click="${(e) => this.onPurge(false)}" class="button is-outlined is-danger">Delete Archive</button>
// </div>` : html``}
//
onCopy(event, sourceUrl) { onCopy(event, sourceUrl) {
event.preventDefault(); event.preventDefault();
@@ -321,9 +416,14 @@ class CollIndex extends LitElement
navigator.clipboard.writeText(sourceUrl); navigator.clipboard.writeText(sourceUrl);
return false; return false;
} }
onPurge(reload) {
const detail = {reload};
this.dispatchEvent(new CustomEvent("coll-purge", {detail}));
}
} }
customElements.define("wr-coll-info", WrCollInfo);
customElements.define("wr-coll-index", CollIndex); customElements.define("wr-coll-index", CollIndex);
export { CollIndex }; export { CollIndex };
+107 -30
View File
@@ -8,6 +8,7 @@ import fasBook from '@fortawesome/fontawesome-free/svgs/solid/book.svg';
import farListAlt from '@fortawesome/fontawesome-free/svgs/regular/list-alt.svg'; import farListAlt from '@fortawesome/fontawesome-free/svgs/regular/list-alt.svg';
import farResources from '@fortawesome/fontawesome-free/svgs/solid/puzzle-piece.svg'; import farResources from '@fortawesome/fontawesome-free/svgs/solid/puzzle-piece.svg';
import farPages from '@fortawesome/fontawesome-free/svgs/regular/file-image.svg'; import farPages from '@fortawesome/fontawesome-free/svgs/regular/file-image.svg';
import fasInfoIcon from '@fortawesome/fontawesome-free/svgs/solid/info-circle.svg';
import fasRefresh from '@fortawesome/fontawesome-free/svgs/solid/redo-alt.svg'; import fasRefresh from '@fortawesome/fontawesome-free/svgs/solid/redo-alt.svg';
import fasFullscreen from '@fortawesome/fontawesome-free/svgs/solid/desktop.svg'; import fasFullscreen from '@fortawesome/fontawesome-free/svgs/solid/desktop.svg';
@@ -20,6 +21,8 @@ import fasMenuV from '@fortawesome/fontawesome-free/svgs/solid/ellipsis-v.svg';
import fasAngleLeft from '@fortawesome/fontawesome-free/svgs/solid/angle-left.svg'; import fasAngleLeft from '@fortawesome/fontawesome-free/svgs/solid/angle-left.svg';
import fasAngleRight from '@fortawesome/fontawesome-free/svgs/solid/angle-right.svg'; import fasAngleRight from '@fortawesome/fontawesome-free/svgs/solid/angle-right.svg';
import prettyBytes from 'pretty-bytes';
import Split from 'split.js'; import Split from 'split.js';
@@ -53,7 +56,7 @@ class Coll extends LitElement
this.url = ""; this.url = "";
this.ts = ""; this.ts = "";
this.tabNames = ["pages", "story", "resources"]; this.tabNames = ["pages", "story", "resources", "info"];
this.menuActive = false; this.menuActive = false;
@@ -97,7 +100,7 @@ class Coll extends LitElement
isVisible: { type: Boolean }, isVisible: { type: Boolean },
favIconUrl: {type: String } favIconUrl: {type: String },
} }
} }
@@ -425,7 +428,7 @@ class Coll extends LitElement
static get replayBarStyles() { static get replayBarStyles() {
return css` return css`
.replay-bar { .replay-bar {
padding: 1em 0em 1em 1em; padding: 0.5em 0em 0.5em 0.5em;
max-width: none; max-width: none;
border-bottom: solid .1rem #97989A; border-bottom: solid .1rem #97989A;
width: 100%; width: 100%;
@@ -439,7 +442,8 @@ class Coll extends LitElement
.favicon img { .favicon img {
width: 20px; width: 20px;
height: 20px; height: 20px;
filter: drop-shadow(1px 1px 2px grey); margin: 8px;
/*filter: drop-shadow(1px 1px 2px grey);*/
} }
#datetime { #datetime {
@@ -484,12 +488,13 @@ class Coll extends LitElement
border: 0px; border: 0px;
} }
.modal { .narrow {
top: 174px; padding: calc(0.5em - 1px) 0.8em;
} }
form { form {
width: 100%; width: 100%;
margin-left: 0.5em;
} }
.gutter.gutter-horizontal { .gutter.gutter-horizontal {
@@ -506,6 +511,18 @@ class Coll extends LitElement
width: 100%; width: 100%;
} }
.info-bg {
background-color: whitesmoke;
width: 100%;
height: 100%;
display: flex;
}
.is-list {
margin: 1.0em;
background-color: whitesmoke;
}
#contents.full-pages { #contents.full-pages {
width: 100% !important; width: 100% !important;
} }
@@ -581,7 +598,7 @@ class Coll extends LitElement
<div id="contents" class="is-light ${isSidebar ? 'sidebar' : (isReplay ? 'is-hidden' : 'full-pages')}" <div id="contents" class="is-light ${isSidebar ? 'sidebar' : (isReplay ? 'is-hidden' : 'full-pages')}"
role="${isSidebar ? "complementary" : ""}" aria-label="${isSidebar ? "Browse Contents" : ""}"> role="${isSidebar ? "complementary" : ""}" aria-label="${isSidebar ? "Browse Contents" : ""}">
${this.renderTabHeader(isSidebar)} ${this.renderTabHeader(isSidebar)}
${this.renderCollTabs(isSidebar)} ${isSidebar || !isReplay ? this.renderCollTabs(isSidebar) : html``}
</div> </div>
${isReplay && this.isVisible ? html` ${isReplay && this.isVisible ? html`
@@ -644,6 +661,13 @@ class Coll extends LitElement
</a> </a>
</li> </li>
<li class="${this.tabData.view === 'info' ? 'is-active' : ''}">
<a @click="${this.onTabClick}" href="#info" class="is-size-6" aria-label="Archive Info" aria-current="${this.tabData.view === 'info' ? 'location' : ''}">
<span class="icon"><fa-icon .svg="${fasInfoIcon}" aria-hidden="true" title="Archive Info"></fa-icon></span>
<span class="tab-label ${isSidebar ? 'is-hidden' : ''}" title="Archive Info">Info</span>
</a>
</li>
${isSidebar ? html` ${isSidebar ? html`
<li class="sidebar-nav right"> <li class="sidebar-nav right">
<a role="button" href="#" @click="${this.onFullPageView}" @keyup="${clickOnSpacebarPress}" class="is-marginless is-size-6 is-paddingless"> <a role="button" href="#" @click="${this.onFullPageView}" @keyup="${clickOnSpacebarPress}" class="is-marginless is-size-6 is-paddingless">
@@ -671,25 +695,34 @@ class Coll extends LitElement
<a class="skip-link" href="#skip-replay-target" @click="${this.skipMenu}">Skip replay navigation</a> <a class="skip-link" href="#skip-replay-target" @click="${this.skipMenu}">Skip replay navigation</a>
<nav class="replay-bar" aria-label="replay"> <nav class="replay-bar" aria-label="replay">
<div class="field has-addons"> <div class="field has-addons">
<a href="#" role="button" id="fullscreen" class="button is-borderless is-hidden-touch" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}"
${!this.embed ? html`
<a href="/" role="button" class="button is-borderless is-hidden-mobile" aria-labelledby="home" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small">
<fa-icon id="wrlogo" size="1.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
<span class="is-sr-only">Home</span>
</span>
</a>` : ``}
<a href="#" role="button" class="button narrow is-borderless is-hidden-touch" id="fullscreen" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}"
title="${this.isFullscreen ? "Exit Full Screen" : "Full Screen"}" aria-label="${this.isFullscreen ? "Exit Fullscreen" : "Fullscreen"}"> title="${this.isFullscreen ? "Exit Full Screen" : "Full Screen"}" aria-label="${this.isFullscreen ? "Exit Fullscreen" : "Fullscreen"}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
</span> </span>
</a> </a>
<a href="#" role="button" class="button is-borderless is-hidden-mobile" @click="${this.onGoBack}" @keyup="${clickOnSpacebarPress}" <a href="#" role="button" class="button narrow is-borderless is-hidden-mobile" @click="${this.onGoBack}" @keyup="${clickOnSpacebarPress}"
title="Back" aria-label="Back"> title="Back" aria-label="Back">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasLeft}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasLeft}"></fa-icon>
</span> </span>
</a> </a>
<a href="#" role="button" class="button is-borderless is-hidden-mobile" @click="${this.onGoForward}" @keyup="${clickOnSpacebarPress}" <a href="#" role="button" class="button narrow is-borderless is-hidden-mobile" @click="${this.onGoForward}" @keyup="${clickOnSpacebarPress}"
title="Forward" aria-label="Forward"> title="Forward" aria-label="Forward">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRight}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRight}"></fa-icon>
</span> </span>
</a> </a>
<a href="#" role="button" id="refresh" class="button is-borderless ${this.isLoading ? 'is-loading' : 'is-hidden-mobile'}" @click="${this.onRefresh}" @keyup="${clickOnSpacebarPress}" <a href="#" role="button" class="button narrow is-borderless ${this.isLoading ? 'is-loading' : 'is-hidden-mobile'}" id="refresh" @click="${this.onRefresh}" @keyup="${clickOnSpacebarPress}"
title="Reload" aria-label="Reload"> title="Reload" aria-label="Reload">
<span class="icon is-small"> <span class="icon is-small">
${!this.isLoading ? html` ${!this.isLoading ? html`
@@ -697,7 +730,7 @@ class Coll extends LitElement
` : ``} ` : ``}
</span> </span>
</a> </a>
<a href="#" role="button" class="button is-borderless is-hidden-touch ${!isReplay ? 'grey-disabled' : ''}" @click="${this.onShowPages}" @keyup="${clickOnSpacebarPress}" <a href="#" role="button" class="button narrow is-borderless is-hidden-touch ${!isReplay ? 'grey-disabled' : ''}" @click="${this.onShowPages}" @keyup="${clickOnSpacebarPress}"
?disabled="${!isReplay}" title="Browse Contents" aria-label="Browse Contents" aria-controls="contents"> ?disabled="${!isReplay}" title="Browse Contents" aria-label="Browse Contents" aria-controls="contents">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${farListAlt}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${farListAlt}"></fa-icon>
@@ -725,6 +758,15 @@ class Coll extends LitElement
</div> </div>
<div class="dropdown-menu" id="menu-dropdown"> <div class="dropdown-menu" id="menu-dropdown">
<div class="dropdown-content"> <div class="dropdown-content">
${!this.embed ? html`
<a href="/" role="button" class="dropdown-item is-hidden-tablet" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${rwpLogo}"></fa-icon>
</span>
<span>Home</span>
</a>
<hr class="dropdown-divider is-hidden-tablet">
` : ``}
<a href="#" role="button" class="dropdown-item is-hidden-desktop" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}"> <a href="#" role="button" class="dropdown-item is-hidden-desktop" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
@@ -764,14 +806,11 @@ class Coll extends LitElement
<div class="dropdown-item info is-hidden-desktop"> <div class="dropdown-item info is-hidden-desktop">
<span class="menu-head">Capture Date</span>${dateStr} <span class="menu-head">Capture Date</span>${dateStr}
</div>` : ``} </div>` : ``}
${this.embed ? html`
<hr class="dropdown-divider"> <hr class="dropdown-divider">
<a href="https://replayweb.page/" target="_blank" class="dropdown-item info"> <a href="#" role="button" class="dropdown-item" @click="${this.onAbout}">
<fa-icon class="menu-logo" size="1.0rem" aria-hidden="true" .svg=${rwpLogo}></fa-icon>
<span>&nbsp;About ReplayWeb.page</span> <span>&nbsp;About ReplayWeb.page</span>
<span class="menu-version">(${__VERSION__})</span> <span class="menu-version">(${__VERSION__})</span>
</a> </a>
` : ``}
</div> </div>
</div> </div>
</div> </div>
@@ -796,12 +835,26 @@ class Coll extends LitElement
} }
renderCollTabs(isSidebar) { renderCollTabs(isSidebar) {
const isStory = this.tabData.view === 'story'; const isStory = this.hasStory && this.tabData.view === 'story';
const isPages = this.tabData.view === 'pages'; const isPages = this.tabData.view === 'pages';
const isResources = this.tabData.view === 'resources'; const isResources = this.tabData.view === 'resources';
const isInfo = this.tabData.view === 'info';
return html` return html`
${this.hasStory ? html`
${isInfo ? html`
<div class="info-bg">
<wr-coll-info
class="is-list"
.coll="${this.collInfo}"
?detailed="${true}"
?canDelete="${!this.embed}"
@coll-purge="${this.onPurgeCache}"
></wr-coll-info>
</div>
`: html``}
${isStory ? html`
<wr-coll-story .collInfo="${this.collInfo}" <wr-coll-story .collInfo="${this.collInfo}"
.active="${isStory}" .active="${isStory}"
currList="${this.tabData.currList || 0}" currList="${this.tabData.currList || 0}"
@@ -812,6 +865,7 @@ class Coll extends LitElement
> >
</wr-coll-story>` : ''} </wr-coll-story>` : ''}
${isResources ? html`
<wr-coll-resources .collInfo="${this.collInfo}" <wr-coll-resources .collInfo="${this.collInfo}"
.active="${isResources}" .active="${isResources}"
query="${this.tabData.query || ""}" query="${this.tabData.query || ""}"
@@ -822,8 +876,9 @@ class Coll extends LitElement
class="is-paddingless ${isResources ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}" class="is-paddingless ${isResources ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"
role="${isSidebar ? '' : 'main'}" role="${isSidebar ? '' : 'main'}"
> >
</wr-coll-resources> </wr-coll-resources>` : ``}
${isPages ? html`
<wr-page-view <wr-page-view
.collInfo="${this.collInfo}" .collInfo="${this.collInfo}"
.active="${isPages}" .active="${isPages}"
@@ -838,7 +893,7 @@ class Coll extends LitElement
class="${isPages ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}" class="${isPages ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"
role="${isSidebar ? '' : 'main'}" role="${isSidebar ? '' : 'main'}"
> >
</wr-page-view> </wr-page-view>` : ``}
`; `;
} }
@@ -911,28 +966,46 @@ class Coll extends LitElement
this.showSidebar = false; this.showSidebar = false;
} }
onFavIcons(event) { async onFavIcons(event) {
this.favIconUrl = ""; for (const icon of event.detail.icons) {
event.detail.icons.map(async (icon) => { const resp = await fetch(icon.href);
const res = await fetch(icon.href); if (resp.status === 200) {
if (res.status === 200) { const ct = resp.headers.get("Content-Type");
if (ct && !ct.startsWith("text/")) {
this.favIconUrl = icon.href; this.favIconUrl = icon.href;
return;
}
} }
});
} }
async onPurgeCache(event) { // not found
this.favIconUrl = "";
}
onPurgeCache(event) {
event.preventDefault(); event.preventDefault();
const reload = event.detail && event.detail.reload !== undefined ? event.detail.reload : true;
this.deleteFully(reload);
}
async deleteFully(reload = false) {
const resp = await fetch(`${this.collInfo.apiPrefix}`, { const resp = await fetch(`${this.collInfo.apiPrefix}`, {
method: 'DELETE', method: 'DELETE',
}); });
if (resp.status === 200 && window.parent) { if (resp.status !== 200) {
window.parent.location.reload();
} else {
console.warn("purge failed: " + resp.status); console.warn("purge failed: " + resp.status);
} }
if (!reload && !this.embed) {
window.location.search = "";
return;
}
window.location.hash = "";
window.location.reload();
} }
onSubmit(event) { onSubmit(event) {
@@ -1019,6 +1092,10 @@ class Coll extends LitElement
window.location.reload(); window.location.reload();
} }
} }
onAbout() {
this.dispatchEvent(new CustomEvent("about-show"));
}
} }
customElements.define("wr-coll", Coll); customElements.define("wr-coll", Coll);
+80 -6
View File
@@ -8,7 +8,7 @@ import fs from 'fs';
import { ArchiveResponse, Rewriter } from '@webrecorder/wabac/src/rewrite'; import { ArchiveResponse, Rewriter } from '@webrecorder/wabac/src/rewrite';
import { PassThrough } from 'stream'; import { PassThrough, Readable } from 'stream';
import mime from 'mime-types'; import mime from 'mime-types';
@@ -21,6 +21,10 @@ const REPLAY_PREFIX = STATIC_PREFIX + "wabac/";
const URL_RX = /([^\/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/; const URL_RX = /([^\/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/;
let IPFS = null;
let ipfs = null;
let initingIPFS = null;
// ============================================================================ // ============================================================================
class ElectronReplayApp class ElectronReplayApp
@@ -95,7 +99,7 @@ class ElectronReplayApp
app.commandLine.appendSwitch('ppapi-flash-path', this.pluginPath); app.commandLine.appendSwitch('ppapi-flash-path', this.pluginPath);
console.log('app path', this.appPath); console.log('app path', this.appPath);
console.log('dir name', this.__dirname); console.log('dir name', __dirname);
console.log('proj path', this.projPath); console.log('proj path', this.projPath);
console.log('plugin path', this.pluginPath); console.log('plugin path', this.pluginPath);
@@ -134,7 +138,38 @@ class ElectronReplayApp
}); });
} }
static async doInitIPFS() {
if (!IPFS) {
IPFS = require("ipfs-core");
}
ipfs = await IPFS.create({
//repo: "/tmp/test-ipfs"
init: {emptyRepo: true},
//preload: {enabled: false},
});
}
async initIPFS() {
if (!ipfs) {
try {
if (!initingIPFS) {
initingIPFS = ElectronReplayApp.doInitIPFS();
}
await initingIPFS;
} catch (e) {
console.warn(e);
}
}
return ipfs;
}
onAppReady() { onAppReady() {
//ElectronReplayApp.doInitIPFS();
this.screenSize = screen.getPrimaryDisplay().workAreaSize; this.screenSize = screen.getPrimaryDisplay().workAreaSize;
app.on('web-contents-created', (event, contents) => { app.on('web-contents-created', (event, contents) => {
@@ -213,7 +248,14 @@ class ElectronReplayApp
if (request.url.startsWith(__APP_FILE_SERVE_PREFIX__)) { if (request.url.startsWith(__APP_FILE_SERVE_PREFIX__)) {
const parsedUrl = new URL(request.url); const parsedUrl = new URL(request.url);
const filename = parsedUrl.searchParams.get("filename"); const filename = parsedUrl.searchParams.get("filename");
const ipfsCID = parsedUrl.searchParams.get("ipfs");
const headers = {"Content-Type": "application/octet-stream"};
const reqHeaders = new Headers(request.headers);
if (filename) {
console.log("file serve:", filename); console.log("file serve:", filename);
const stat = await fs.promises.lstat(filename); const stat = await fs.promises.lstat(filename);
@@ -224,16 +266,44 @@ class ElectronReplayApp
const size = stat.size; const size = stat.size;
const headers = {"Content-Length": "" + size, "Content-Type": "application/octet-stream"};
const reqHeaders = new Headers(request.headers);
const {statusCode, start, end} = this.parseRange(reqHeaders, headers, size); const {statusCode, start, end} = this.parseRange(reqHeaders, headers, size);
const data = request.method === "HEAD" ? null : fs.createReadStream(filename, {start, end}); const data = request.method === "HEAD" ? null : fs.createReadStream(filename, {start, end});
callback({statusCode, headers, data}); callback({statusCode, headers, data});
return; return;
} else if (ipfsCID) {
console.log("ipfs serve:", ipfsCID);
const ipfs = await this.initIPFS();
let size = 0;
for await (const file of ipfs.get(ipfsCID, {timeout: 20000, preload: false})) {
if (file.type !== "file") {
return this.notFound(ipfsCID, callback);
}
size = file.size;
break;
}
const {statusCode, start, end} = this.parseRange(reqHeaders, headers, size);
let data = null;
if (request.method === "GET") {
const offset = start || 0;
const length = end ? end - start + 1 : size;
data = Readable.from(ipfs.cat(ipfsCID, {offset, length}));
}
callback({statusCode, headers, data});
return;
} else {
return this.notFound("No Resource Specified", callback);
}
} }
// possible 'live leak' attempt, just return not found // possible 'live leak' attempt, just return not found
@@ -324,6 +394,9 @@ class ElectronReplayApp
const range = reqHeaders.get("range"); const range = reqHeaders.get("range");
if (!range) { if (!range) {
if (headers) {
headers["Content-Length"] = "" + size;
}
return {statusCode}; return {statusCode};
} }
@@ -337,6 +410,7 @@ class ElectronReplayApp
statusCode = 206; statusCode = 206;
if (headers) { if (headers) {
headers["Content-Range"] = `bytes ${start}-${end}/${size}`; headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
headers["Content-Length"] = `${end - start + 1}`;
} }
return {statusCode, start, end}; return {statusCode, start, end};
} }
+34 -8
View File
@@ -1,9 +1,9 @@
import { LitElement, html, css } from 'lit-element'; import { LitElement, html, css } from 'lit-element';
import { wrapCss } from './misc'; import { wrapCss, IS_APP } from './misc';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import { parseURLSchemeHostPath, initDBWorker } from './pageutils'; import { parseURLSchemeHostPath } from './pageutils';
// =========================================================================== // ===========================================================================
@@ -20,8 +20,6 @@ class Loader extends LitElement
this.currentSize = 0; this.currentSize = 0;
this.totalSize = 0; this.totalSize = 0;
this.dbworker = initDBWorker();
} }
static get properties() { static get properties() {
@@ -47,7 +45,11 @@ class Loader extends LitElement
} }
initMessages() { initMessages() {
this.dbworker.addEventListener("message", (event) => { if (!navigator.serviceWorker) {
return;
}
navigator.serviceWorker.addEventListener("message", (event) => {
switch (event.data.msg_type) { switch (event.data.msg_type) {
case "collProgress": case "collProgress":
if (event.data.name === this.coll) { if (event.data.name === this.coll) {
@@ -85,7 +87,13 @@ class Loader extends LitElement
if (!navigator.serviceWorker) { if (!navigator.serviceWorker) {
this.state = "errored"; this.state = "errored";
if (window.location.protocol === "http:") {
this.error = `\
Sorry, the ReplayWeb.page system must be loaded from an HTTPS URL, but was loaded from: ${window.location.host}.
Please try loading this page from an HTTPS URL`;
} else {
this.error = "Sorry, this browser is not supported. Please try a different browser\n(If you're using Firefox, try without Private Mode)"; this.error = "Sorry, this browser is not supported. Please try a different browser\n(If you're using Firefox, try without Private Mode)";
}
return; return;
} }
@@ -116,8 +124,18 @@ You can select a file to upload from the main page by clicking the \'Choose File
source = this.loadInfo; source = this.loadInfo;
break; break;
case "ipfs":
if (IS_APP) {
const url = new URL(__APP_FILE_SERVE_PREFIX__);
url.searchParams.set("ipfs", sourceUrl.slice("ipfs://".length));
source = {sourceUrl, loadUrl: url.href};
}
break;
}
} catch (e) {
console.log(e);
} }
} catch (e) {}
if (!source) { if (!source) {
source = {sourceUrl}; source = {sourceUrl};
@@ -131,7 +149,13 @@ You can select a file to upload from the main page by clicking the \'Choose File
msg.extraConfig = this.loadInfo.extraConfig; msg.extraConfig = this.loadInfo.extraConfig;
} }
this.dbworker.postMessage(msg); if (!navigator.serviceWorker.controller) {
navigator.serviceWorker.addEventListener("controllerchange", (event) => {
navigator.serviceWorker.controller.postMessage(msg);
});
} else {
navigator.serviceWorker.controller.postMessage(msg);
}
} }
googledriveInit() { googledriveInit() {
@@ -149,7 +173,9 @@ You can select a file to upload from the main page by clicking the \'Choose File
} }
onCancel() { onCancel() {
this.dbworker.postMessage({"msg_type": "cancelLoad", "name": this.coll}); if (navigator.serviceWorker && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({"msg_type": "cancelLoad", "name": this.coll});
}
} }
updated(changedProperties) { updated(changedProperties) {
+2 -2
View File
@@ -2,7 +2,7 @@ import { register } from 'register-service-worker';
// =========================================================================== // ===========================================================================
async function registerSW(name = "sw.js", scope = "./") { function registerSW(name = "sw.js", scope = "./") {
let resolve, reject; let resolve, reject;
const p = new Promise((res, rej) => { const p = new Promise((res, rej) => {
@@ -28,7 +28,7 @@ async function registerSW(name = "sw.js", scope = "./") {
} }
}); });
await p; return p;
} }
// =========================================================================== // ===========================================================================
+67 -31
View File
@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'lit-element'; import { LitElement, html, css } from 'lit-element';
import { wrapCss } from './misc'; import { wrapCss, rwpLogo } from './misc';
@@ -17,6 +17,7 @@ class Replay extends LitElement
this.showAuth = false; this.showAuth = false;
this.reauthWait = null; this.reauthWait = null;
this.authFileHandle = null;
} }
static get properties() { static get properties() {
@@ -36,20 +37,37 @@ class Replay extends LitElement
iframeUrl: { type: String }, iframeUrl: { type: String },
showAuth: { type: Boolean }, showAuth: { type: Boolean },
authFileHandle: { type: Object }
} }
} }
firstUpdated() { firstUpdated() {
window.addEventListener("message", (event) => this.onReplayMessage(event)); window.addEventListener("message", (event) => this.onReplayMessage(event));
navigator.serviceWorker.addEventListener("message", async (event) => { navigator.serviceWorker.addEventListener("message", (event) => this.handleAuthMessage(event));
}
async handleAuthMessage(event) {
if (event.data.type === "authneeded" && this.collInfo && event.data.coll === this.collInfo.coll) { if (event.data.type === "authneeded" && this.collInfo && event.data.coll === this.collInfo.coll) {
if (event.data.fileHandle) {
this.authFileHandle = event.data.fileHandle;
try {
if (await this.authFileHandle.requestPermission({mode: "read"}) === "granted") {
this.showAuth = false;
this.reauthWait = null;
this.refresh();
return;
}
} catch (e) {}
} else {
this.authFileHandle = null;
}
if (this.reauthWait) { if (this.reauthWait) {
await this.reauthWait; await this.reauthWait;
} else { } else {
this.showAuth = true; this.showAuth = true;
} }
} }
});
} }
doSetIframeUrl() { doSetIframeUrl() {
@@ -58,9 +76,6 @@ class Replay extends LitElement
updated(changedProperties) { updated(changedProperties) {
if (changedProperties.has("sourceUrl") || changedProperties.has("collInfo")) { if (changedProperties.has("sourceUrl") || changedProperties.has("collInfo")) {
this.isAuthable = (this.sourceUrl.startsWith("googledrive://") &&
this.collInfo && this.collInfo.onDemand);
this.reauthWait = null; this.reauthWait = null;
} }
@@ -70,6 +85,8 @@ class Replay extends LitElement
this.replayUrl = this.url; this.replayUrl = this.url;
this.replayTS = this.ts; this.replayTS = this.ts;
this.showAuth = false;
this.reauthWait = null;
this.doSetIframeUrl(); this.doSetIframeUrl();
} }
@@ -121,17 +138,28 @@ class Replay extends LitElement
onReAuthed(event) { onReAuthed(event) {
this.reauthWait = (async () => { this.reauthWait = (async () => {
if (!this.authFileHandle) {
// google drive reauth
const headers = event.detail.headers; const headers = event.detail.headers;
const resp = await fetch(`${this.collInfo.apiPrefix}/updateAuth`, { const resp = await fetch(`${this.collInfo.apiPrefix}/updateAuth`, {
method: 'POST', method: 'POST',
body: JSON.stringify({headers}) body: JSON.stringify({headers})
}); });
} else {
if (await this.authFileHandle.requestPermission({mode: "read"}) !== "granted") {
this.reauthWait = null;
return;
}
this.authFileHandle = null;
}
if (this.showAuth) { if (this.showAuth) {
this.refresh();
this.showAuth = false; this.showAuth = false;
this.reauthWait = null;
} }
this.refresh();
})(); })();
} }
@@ -191,18 +219,24 @@ class Replay extends LitElement
.intro-panel .panel-heading { .intro-panel .panel-heading {
font-size: 1.0em; font-size: 1.0em;
display: inline-block;
}
#wrlogo {
vertical-align: middle;
} }
.intro-panel .panel-block { .intro-panel .panel-block {
padding: 1.0em; padding: 1.0em;
flex-direction: column; flex-direction: column;
line-height: 2.5em;
} }
div.intro-panel.panel { div.intro-panel.panel {
min-width: 40%; min-width: 40%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: auto; margin: 3em auto;
} }
`); `);
} }
@@ -214,37 +248,39 @@ class Replay extends LitElement
<h1 id="replay-heading" class="is-sr-only">${title}</h1> <h1 id="replay-heading" class="is-sr-only">${title}</h1>
${this.iframeUrl ? html` ${!this.iframeUrl ? html`
<iframe @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
src="${this.iframeUrl}" title="${title}"></iframe>
` : html`
<div class="panel intro-panel"> <div class="panel intro-panel">
<p class="panel-heading">Replay Web Page</p> <p class="panel-heading">Replay Web Page</p>
<div class="panel-block"> <div class="panel-block">
<p>Enter a URL above to replay it from the web archive!</p> <p>Enter a URL above to replay it from the web archive!</p>
<p>(Or, check out <a href="#view=pages">Pages</a> or <a href="#view=resources">URLs</a> to explore the contents of this archive.)</p> <p>(Or, check out <a href="#view=pages">Pages</a> or <a href="#view=resources">URLs</a> to explore the contents of this archive.)</p>
</div> </div>
</div> </div>` : html`
`}
${this.isAuthable ? html` ${this.showAuth ? html`
<div class="modal ${this.showAuth ? 'is-active' : ''}"> <div class="panel intro-panel">
<div class="modal-background"></div> <p class="panel-heading">
<div class="modal-card"> <fa-icon id="wrlogo" size="1.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
<header class="modal-card-head"> Authorization Needed
<p class="modal-card-title">Auth Needed</p> </p>
<button class="delete" aria-label="close"></button> <div class="panel-block">
</header> ${this.authFileHandle ? html`
<section class="modal-card-body"> <p>This archive is loaded from a local file: <b>${this.authFileHandle.name}</b></p>
<div class="container has-text-centered"> <p>The browser needs to confirm your permission to continue loading from this file.</p>
<wr-gdrive .sourceUrl=${this.sourceUrl} .state="${this.showAuth ? 'trymanual' : 'implicitonly'}" .reauth="${true}" @load-ready=${this.onReAuthed}/> <button class="button is-warning is-rounded" @click="${this.onReAuthed}">Show Confirmation</button>
` : html`
<wr-gdrive
.sourceUrl=${this.sourceUrl}
.state="${this.showAuth ? 'trymanual' : 'implicitonly'}"
.reauth="${true}
@load-ready=${this.onReAuthed}/>`}
</div> </div>
</section> </div>` : html`
</div>
</div> <iframe @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
</div> src="${this.iframeUrl}" title="${title}"></iframe>
` : ``} `}
`; `}`;
} }
} }
+3 -9
View File
@@ -1,8 +1,7 @@
"use strict";
import INDEX_HTML from '../index.html'; import INDEX_HTML from '../index.html';
if (self.registration) { import { SWReplay } from '@webrecorder/wabac/src/swmain';
const staticData = new Map(); const staticData = new Map();
const prefix = self.registration.scope; const prefix = self.registration.scope;
@@ -11,11 +10,6 @@ if (self.registration) {
staticData.set(prefix + "index.html", {type: "text/html", content: INDEX_HTML}); staticData.set(prefix + "index.html", {type: "text/html", content: INDEX_HTML});
//staticData.set(prefix + "ui.js", {type: "application/javascript", content: UI_JS}); //staticData.set(prefix + "ui.js", {type: "application/javascript", content: UI_JS});
const { SWReplay } = require('@webrecorder/wabac/src/swmain'); //const { SWReplay } = require('@webrecorder/wabac/src/swmain');
self.sw = new SWReplay(staticData); self.sw = new SWReplay(staticData);
} else if (self.postMessage) {
// Inited as Web Worker
const { WorkerLoader } = require('@webrecorder/wabac/src/loaders');
const loader = new WorkerLoader(self);
}
+7 -5
View File
File diff suppressed because one or more lines are too long
+307 -203
View File
File diff suppressed because one or more lines are too long
+23 -9
View File
@@ -1,6 +1,7 @@
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyPlugin = require('copy-webpack-plugin');
// fake url used in app to serve files // fake url used in app to serve files
// can not use custom scheme due to service worker issues // can not use custom scheme due to service worker issues
@@ -25,9 +26,16 @@ const electronMainConfig = (env, argv) => {
entry: { entry: {
'electron': './src/electron-main.js', 'electron': './src/electron-main.js',
}, },
resolve: {
alias: {
"abort-controller": "abort-controller/dist/abort-controller.js",
"dlv": "dlv/dist/dlv.js",
"bignumber.js": "bignumber.js/bignumber.js"
}
},
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: '[name].js' filename: '[name].js',
}, },
node: { node: {
__dirname: false, __dirname: false,
@@ -38,8 +46,18 @@ const electronMainConfig = (env, argv) => {
__APP_FILE_SERVE_PREFIX__ : JSON.stringify(APP_FILE_SERVE_PREFIX), __APP_FILE_SERVE_PREFIX__ : JSON.stringify(APP_FILE_SERVE_PREFIX),
__HELPER_PROXY__ : JSON.stringify(HELPER_PROXY) __HELPER_PROXY__ : JSON.stringify(HELPER_PROXY)
}), }),
new webpack.BannerPlugin(BANNER_TEXT) new webpack.BannerPlugin(BANNER_TEXT),
new CopyPlugin({
patterns: [
{ from: 'node_modules/bcrypto/build/Release/bcrypto.node', to: 'build' },
{ from: 'node_modules/leveldown/prebuilds/', to: 'prebuilds' },
], ],
}),
],
externals: {
"bufferutil": "bufferutil",
"utf-8-validate": "utf-8-validate",
}
} }
}; };
@@ -51,10 +69,6 @@ const electronPreloadConfig = (env, argv) => {
entry: { entry: {
'preload': './src/electron-preload.js', 'preload': './src/electron-preload.js',
}, },
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [ plugins: [
new webpack.BannerPlugin(BANNER_TEXT) new webpack.BannerPlugin(BANNER_TEXT)
], ],
@@ -111,15 +125,15 @@ const browserConfig = (env, argv) => {
rules: [ rules: [
{ {
test: /\.svg$/, test: /\.svg$/,
loader: 'svg-inline-loader' use: ['svg-inline-loader'],
}, },
{ {
test: /main.scss$/, test: /main.scss$/,
loaders: ['css-loader', 'sass-loader'] use: ['css-loader', 'sass-loader']
}, },
{ {
test: /wombat.js|wombatWorkers.js|index.html$/i, test: /wombat.js|wombatWorkers.js|index.html$/i,
loaders: ['raw-loader'], use: ['raw-loader'],
} }
] ]
}, },
+3566 -132
View File
File diff suppressed because it is too large Load Diff