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:
+12
@@ -1,5 +1,17 @@
|
||||
## 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
|
||||
- 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.
|
||||
|
||||
+8
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "replaywebpage",
|
||||
"productName": "ReplayWeb.page",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"description": "Serverless Web Archive Replay",
|
||||
"repository": "https://github.com/webrecorder/replayweb.page",
|
||||
"homepage": "https://replayweb.page/",
|
||||
@@ -9,10 +9,11 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"@webrecorder/wabac": "^2.1.5",
|
||||
"@webrecorder/wabac": "^2.2.0",
|
||||
"bulma": "^0.9.0",
|
||||
"fetch-ndjson": "^1.1.0",
|
||||
"flexsearch": "^0.6.32",
|
||||
"ipfs-core": "^0.2.0",
|
||||
"keyword-mark-element": "^0.1.2",
|
||||
"lit-element": "^2.3.1",
|
||||
"marked": "^1.0.0",
|
||||
@@ -23,6 +24,7 @@
|
||||
"split.js": "^1.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^6.3.0",
|
||||
"css-loader": "^3.5.3",
|
||||
"electron": "^10.1.3",
|
||||
"electron-builder": "^22.7.0",
|
||||
@@ -35,7 +37,7 @@
|
||||
"style-loader": "^1.2.1",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"to-string-loader": "^1.1.6",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
},
|
||||
@@ -56,6 +58,7 @@
|
||||
"start-electron": "NODE_ENV=development electron ./dist/electron.js $1",
|
||||
"start-jekyll": "yarn run build-docs && bundle exec jekyll serve",
|
||||
"dist": "yarn run build && yarn run pack",
|
||||
"dist-dev": "yarn run build-dev && yarn run pack",
|
||||
"release": "yarn run build && electron-builder"
|
||||
},
|
||||
"build": {
|
||||
@@ -70,6 +73,8 @@
|
||||
"files": [
|
||||
"!**/node_modules/**/*",
|
||||
"dist/*.js",
|
||||
"dist/prebuilds/${platform}-${arch}/*",
|
||||
"dist/build/*",
|
||||
"*.js",
|
||||
"*.html",
|
||||
"webmanifest.json",
|
||||
|
||||
+82
-80
@@ -1,14 +1,11 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss, rwpLogo, IS_APP, clickOnSpacebarPress } from './misc';
|
||||
|
||||
import { registerSW, initDBWorker } from './pageutils';
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { registerSW } from './pageutils';
|
||||
|
||||
import fasHelp from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
import fasArrowLeft from '@fortawesome/fontawesome-free/svgs/solid/arrow-left.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();
|
||||
this.sourceUrl = null;
|
||||
this.collInfo = null;
|
||||
this.showTerms = false;
|
||||
this.showAbout = false;
|
||||
this.pageParams = {};
|
||||
|
||||
this.inited = false;
|
||||
this.navMenuShown = false;
|
||||
|
||||
registerSW(__SW_NAME__);
|
||||
initDBWorker();
|
||||
|
||||
this.safariKeyframes();
|
||||
}
|
||||
@@ -37,7 +33,7 @@ class App extends LitElement
|
||||
pageParams: { type: Object },
|
||||
sourceUrl: { type: String },
|
||||
navMenuShown: { type: Boolean },
|
||||
showTerms: { type: Boolean },
|
||||
showAbout: { type: Boolean },
|
||||
collInfo: { type: Object },
|
||||
loadInfo: { type: Object },
|
||||
embed: { type: String },
|
||||
@@ -112,12 +108,19 @@ class App extends LitElement
|
||||
`);
|
||||
}
|
||||
|
||||
renderNavBar() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.inited) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const showNavBar = !this.collInfo && (!this.embed || this.embed === "full");
|
||||
|
||||
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>
|
||||
<nav class="navbar has-background-info" aria-label="main">
|
||||
<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"> Go Forward</span>
|
||||
</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: <b>${this.sourceUrl}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Filename: <b>${this.collInfo.filename}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Total Size: <b>${prettyBytes(Number(this.collInfo.size || 0))}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Loading Mode: <b>${this.collInfo.onDemand ? "Download On-Demand" : "Fully Local"}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Collection Id: <b>${this.collInfo.coll}</b>
|
||||
</div>
|
||||
<hr class="navbar-divider">
|
||||
<div class="navbar-item">
|
||||
ReplayWeb.page Version: ${__VERSION__}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ``}
|
||||
</div>
|
||||
${!this.embed ? html`
|
||||
<div class="navbar-end">
|
||||
@@ -196,10 +168,10 @@ class App extends LitElement
|
||||
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span> User Docs</span>
|
||||
</a>
|
||||
<!--
|
||||
-- NB: the Terms 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?
|
||||
-- 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 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>
|
||||
</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}"
|
||||
embed="${this.embed}"
|
||||
@replay-favicons=${this.onFavIcons}
|
||||
@coll-loaded=${this.onCollLoaded}></wr-coll>
|
||||
@coll-loaded=${this.onCollLoaded}
|
||||
@about-show=${(e) => this.showAbout = true}></wr-coll>
|
||||
` : html`
|
||||
|
||||
<wr-coll-index>
|
||||
@@ -222,42 +195,8 @@ class App extends LitElement
|
||||
</wr-coll-index>
|
||||
`}
|
||||
|
||||
${this.showTerms ? html`
|
||||
<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>
|
||||
` : ``} `
|
||||
${this.showAbout ? this.renderAbout() : ``}
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
@@ -344,7 +283,7 @@ class App extends LitElement
|
||||
this.embed = this.pageParams.get("embed") || "";
|
||||
|
||||
if (this.pageParams.has("terms")) {
|
||||
this.showTerms = true;
|
||||
this.showAbout = true;
|
||||
}
|
||||
|
||||
if (this.pageParams.get("config")) {
|
||||
@@ -438,6 +377,69 @@ class App extends LitElement
|
||||
document.head.appendChild(style);
|
||||
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);
|
||||
|
||||
+45
-6
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
@@ -12,6 +12,14 @@ class Chooser extends LitElement
|
||||
|
||||
this.fileDisplayName = "";
|
||||
this.file = null;
|
||||
|
||||
this.hasNativeFS = !!window.showOpenFilePicker && !IS_APP;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
fileDisplayName: { type: String }
|
||||
}
|
||||
}
|
||||
|
||||
onChooseFile(event) {
|
||||
@@ -22,7 +30,32 @@ class Chooser extends LitElement
|
||||
this.file = event.currentTarget.files[0];
|
||||
// file.path only available in electron app
|
||||
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) {
|
||||
@@ -37,12 +70,17 @@ class Chooser extends LitElement
|
||||
const url = new URL(__APP_FILE_SERVE_PREFIX__);
|
||||
url.searchParams.set("filename", this.file.path);
|
||||
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 {
|
||||
loadInfo.loadUrl = URL.createObjectURL(this.file);
|
||||
loadInfo.blob = this.file;
|
||||
loadInfo.noCache = false;
|
||||
}
|
||||
loadInfo.size = this.file.size;
|
||||
loadInfo.noCache = this.file.path !== undefined;
|
||||
loadInfo.name = this.fileDisplayName;
|
||||
}
|
||||
|
||||
@@ -116,10 +154,11 @@ class Chooser extends LitElement
|
||||
<div class="extra-padding panel-block file has-name">
|
||||
<form class="content is-flex" @submit="${this.onStartLoad}">
|
||||
<label class="file-label">
|
||||
${!this.hasNativeFS ? html`
|
||||
<input class="file-input"
|
||||
@click="${(e) => e.currentTarget.value = null}"
|
||||
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">
|
||||
<span class="file-cta">
|
||||
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">` : ``}
|
||||
<span class="file-cta" @click="${this.onChooseNativeFile}">
|
||||
<span class="file-icon">
|
||||
<fa-icon size="0.9em" .svg=${fasUpload} aria-hidden="true"></fa-icon>
|
||||
</span>
|
||||
@@ -133,7 +172,7 @@ class Chooser extends LitElement
|
||||
<p class="control is-expanded">
|
||||
<input class="file-name input" type="text"
|
||||
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}"
|
||||
@input="${this.onInput}"
|
||||
autocomplete="off"
|
||||
|
||||
+139
-39
@@ -195,23 +195,6 @@ class CollIndex extends LitElement
|
||||
.icon.is-left {
|
||||
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 {
|
||||
position: relative;
|
||||
}
|
||||
@@ -220,10 +203,6 @@ class CollIndex extends LitElement
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#sort-select::after {
|
||||
display: none;
|
||||
}
|
||||
@@ -279,23 +258,7 @@ class CollIndex extends LitElement
|
||||
<div class="coll-list">
|
||||
${this.sortedColls.map((coll, i) => html`
|
||||
<div class="coll-block panel-block">
|
||||
<div class="columns">
|
||||
<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} </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>
|
||||
<wr-coll-info .coll=${coll}></wr-coll-info>
|
||||
${!this._deleting[coll.sourceUrl] ? html`
|
||||
<button class="delete" aria-label="Unload Collection" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button>
|
||||
` : html`
|
||||
@@ -314,6 +277,138 @@ class CollIndex extends LitElement
|
||||
</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} </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) {
|
||||
event.preventDefault();
|
||||
@@ -321,9 +416,14 @@ class CollIndex extends LitElement
|
||||
navigator.clipboard.writeText(sourceUrl);
|
||||
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);
|
||||
|
||||
export { CollIndex };
|
||||
|
||||
+107
-30
@@ -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 farResources from '@fortawesome/fontawesome-free/svgs/solid/puzzle-piece.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 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 fasAngleRight from '@fortawesome/fontawesome-free/svgs/solid/angle-right.svg';
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
import Split from 'split.js';
|
||||
|
||||
|
||||
@@ -53,7 +56,7 @@ class Coll extends LitElement
|
||||
this.url = "";
|
||||
this.ts = "";
|
||||
|
||||
this.tabNames = ["pages", "story", "resources"];
|
||||
this.tabNames = ["pages", "story", "resources", "info"];
|
||||
|
||||
this.menuActive = false;
|
||||
|
||||
@@ -97,7 +100,7 @@ class Coll extends LitElement
|
||||
|
||||
isVisible: { type: Boolean },
|
||||
|
||||
favIconUrl: {type: String }
|
||||
favIconUrl: {type: String },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +428,7 @@ class Coll extends LitElement
|
||||
static get replayBarStyles() {
|
||||
return css`
|
||||
.replay-bar {
|
||||
padding: 1em 0em 1em 1em;
|
||||
padding: 0.5em 0em 0.5em 0.5em;
|
||||
max-width: none;
|
||||
border-bottom: solid .1rem #97989A;
|
||||
width: 100%;
|
||||
@@ -439,7 +442,8 @@ class Coll extends LitElement
|
||||
.favicon img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
filter: drop-shadow(1px 1px 2px grey);
|
||||
margin: 8px;
|
||||
/*filter: drop-shadow(1px 1px 2px grey);*/
|
||||
}
|
||||
|
||||
#datetime {
|
||||
@@ -484,12 +488,13 @@ class Coll extends LitElement
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
top: 174px;
|
||||
.narrow {
|
||||
padding: calc(0.5em - 1px) 0.8em;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.gutter.gutter-horizontal {
|
||||
@@ -506,6 +511,18 @@ class Coll extends LitElement
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-bg {
|
||||
background-color: whitesmoke;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.is-list {
|
||||
margin: 1.0em;
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
#contents.full-pages {
|
||||
width: 100% !important;
|
||||
}
|
||||
@@ -581,7 +598,7 @@ class Coll extends LitElement
|
||||
<div id="contents" class="is-light ${isSidebar ? 'sidebar' : (isReplay ? 'is-hidden' : 'full-pages')}"
|
||||
role="${isSidebar ? "complementary" : ""}" aria-label="${isSidebar ? "Browse Contents" : ""}">
|
||||
${this.renderTabHeader(isSidebar)}
|
||||
${this.renderCollTabs(isSidebar)}
|
||||
${isSidebar || !isReplay ? this.renderCollTabs(isSidebar) : html``}
|
||||
</div>
|
||||
|
||||
${isReplay && this.isVisible ? html`
|
||||
@@ -644,6 +661,13 @@ class Coll extends LitElement
|
||||
</a>
|
||||
</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`
|
||||
<li class="sidebar-nav right">
|
||||
<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>
|
||||
<nav class="replay-bar" aria-label="replay">
|
||||
<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"}">
|
||||
<span class="icon is-small">
|
||||
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
|
||||
</span>
|
||||
</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">
|
||||
<span class="icon is-small">
|
||||
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasLeft}"></fa-icon>
|
||||
</span>
|
||||
</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">
|
||||
<span class="icon is-small">
|
||||
<fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRight}"></fa-icon>
|
||||
</span>
|
||||
</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">
|
||||
<span class="icon is-small">
|
||||
${!this.isLoading ? html`
|
||||
@@ -697,7 +730,7 @@ class Coll extends LitElement
|
||||
` : ``}
|
||||
</span>
|
||||
</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">
|
||||
<span class="icon is-small">
|
||||
<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 class="dropdown-menu" id="menu-dropdown">
|
||||
<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}">
|
||||
<span class="icon is-small">
|
||||
<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">
|
||||
<span class="menu-head">Capture Date</span>${dateStr}
|
||||
</div>` : ``}
|
||||
${this.embed ? html`
|
||||
<hr class="dropdown-divider">
|
||||
<a href="https://replayweb.page/" target="_blank" class="dropdown-item info">
|
||||
<fa-icon class="menu-logo" size="1.0rem" aria-hidden="true" .svg=${rwpLogo}></fa-icon>
|
||||
<a href="#" role="button" class="dropdown-item" @click="${this.onAbout}">
|
||||
<span> About ReplayWeb.page</span>
|
||||
<span class="menu-version">(${__VERSION__})</span>
|
||||
</a>
|
||||
` : ``}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -796,12 +835,26 @@ class Coll extends LitElement
|
||||
}
|
||||
|
||||
renderCollTabs(isSidebar) {
|
||||
const isStory = this.tabData.view === 'story';
|
||||
const isStory = this.hasStory && this.tabData.view === 'story';
|
||||
const isPages = this.tabData.view === 'pages';
|
||||
const isResources = this.tabData.view === 'resources';
|
||||
const isInfo = this.tabData.view === 'info';
|
||||
|
||||
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}"
|
||||
.active="${isStory}"
|
||||
currList="${this.tabData.currList || 0}"
|
||||
@@ -812,6 +865,7 @@ class Coll extends LitElement
|
||||
>
|
||||
</wr-coll-story>` : ''}
|
||||
|
||||
${isResources ? html`
|
||||
<wr-coll-resources .collInfo="${this.collInfo}"
|
||||
.active="${isResources}"
|
||||
query="${this.tabData.query || ""}"
|
||||
@@ -822,8 +876,9 @@ class Coll extends LitElement
|
||||
class="is-paddingless ${isResources ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"
|
||||
role="${isSidebar ? '' : 'main'}"
|
||||
>
|
||||
</wr-coll-resources>
|
||||
</wr-coll-resources>` : ``}
|
||||
|
||||
${isPages ? html`
|
||||
<wr-page-view
|
||||
.collInfo="${this.collInfo}"
|
||||
.active="${isPages}"
|
||||
@@ -838,7 +893,7 @@ class Coll extends LitElement
|
||||
class="${isPages ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"
|
||||
role="${isSidebar ? '' : 'main'}"
|
||||
>
|
||||
</wr-page-view>
|
||||
</wr-page-view>` : ``}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -911,28 +966,46 @@ class Coll extends LitElement
|
||||
this.showSidebar = false;
|
||||
}
|
||||
|
||||
onFavIcons(event) {
|
||||
this.favIconUrl = "";
|
||||
event.detail.icons.map(async (icon) => {
|
||||
const res = await fetch(icon.href);
|
||||
if (res.status === 200) {
|
||||
async onFavIcons(event) {
|
||||
for (const icon of event.detail.icons) {
|
||||
const resp = await fetch(icon.href);
|
||||
if (resp.status === 200) {
|
||||
const ct = resp.headers.get("Content-Type");
|
||||
if (ct && !ct.startsWith("text/")) {
|
||||
this.favIconUrl = icon.href;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onPurgeCache(event) {
|
||||
// not found
|
||||
this.favIconUrl = "";
|
||||
}
|
||||
|
||||
onPurgeCache(event) {
|
||||
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}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (resp.status === 200 && window.parent) {
|
||||
window.parent.location.reload();
|
||||
} else {
|
||||
if (resp.status !== 200) {
|
||||
console.warn("purge failed: " + resp.status);
|
||||
}
|
||||
|
||||
if (!reload && !this.embed) {
|
||||
window.location.search = "";
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.hash = "";
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
onSubmit(event) {
|
||||
@@ -1019,6 +1092,10 @@ class Coll extends LitElement
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
onAbout() {
|
||||
this.dispatchEvent(new CustomEvent("about-show"));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-coll", Coll);
|
||||
|
||||
@@ -8,7 +8,7 @@ import fs from 'fs';
|
||||
|
||||
import { ArchiveResponse, Rewriter } from '@webrecorder/wabac/src/rewrite';
|
||||
|
||||
import { PassThrough } from 'stream';
|
||||
import { PassThrough, Readable } from 'stream';
|
||||
|
||||
import mime from 'mime-types';
|
||||
|
||||
@@ -21,6 +21,10 @@ const REPLAY_PREFIX = STATIC_PREFIX + "wabac/";
|
||||
|
||||
const URL_RX = /([^\/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/;
|
||||
|
||||
let IPFS = null;
|
||||
let ipfs = null;
|
||||
let initingIPFS = null;
|
||||
|
||||
|
||||
// ============================================================================
|
||||
class ElectronReplayApp
|
||||
@@ -95,7 +99,7 @@ class ElectronReplayApp
|
||||
app.commandLine.appendSwitch('ppapi-flash-path', this.pluginPath);
|
||||
|
||||
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('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() {
|
||||
//ElectronReplayApp.doInitIPFS();
|
||||
|
||||
this.screenSize = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
@@ -213,7 +248,14 @@ class ElectronReplayApp
|
||||
|
||||
if (request.url.startsWith(__APP_FILE_SERVE_PREFIX__)) {
|
||||
const parsedUrl = new URL(request.url);
|
||||
|
||||
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);
|
||||
|
||||
const stat = await fs.promises.lstat(filename);
|
||||
@@ -224,16 +266,44 @@ class ElectronReplayApp
|
||||
|
||||
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 data = request.method === "HEAD" ? null : fs.createReadStream(filename, {start, end});
|
||||
|
||||
callback({statusCode, headers, data});
|
||||
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
|
||||
@@ -324,6 +394,9 @@ class ElectronReplayApp
|
||||
const range = reqHeaders.get("range");
|
||||
|
||||
if (!range) {
|
||||
if (headers) {
|
||||
headers["Content-Length"] = "" + size;
|
||||
}
|
||||
return {statusCode};
|
||||
}
|
||||
|
||||
@@ -337,6 +410,7 @@ class ElectronReplayApp
|
||||
statusCode = 206;
|
||||
if (headers) {
|
||||
headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
|
||||
headers["Content-Length"] = `${end - start + 1}`;
|
||||
}
|
||||
return {statusCode, start, end};
|
||||
}
|
||||
|
||||
+34
-8
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { wrapCss } from './misc';
|
||||
import { wrapCss, IS_APP } from './misc';
|
||||
|
||||
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.totalSize = 0;
|
||||
|
||||
this.dbworker = initDBWorker();
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -47,7 +45,11 @@ class Loader extends LitElement
|
||||
}
|
||||
|
||||
initMessages() {
|
||||
this.dbworker.addEventListener("message", (event) => {
|
||||
if (!navigator.serviceWorker) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
switch (event.data.msg_type) {
|
||||
case "collProgress":
|
||||
if (event.data.name === this.coll) {
|
||||
@@ -85,7 +87,13 @@ class Loader extends LitElement
|
||||
|
||||
if (!navigator.serviceWorker) {
|
||||
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)";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,8 +124,18 @@ You can select a file to upload from the main page by clicking the \'Choose File
|
||||
|
||||
source = this.loadInfo;
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -149,7 +173,9 @@ You can select a file to upload from the main page by clicking the \'Choose File
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
const p = new Promise((res, rej) => {
|
||||
@@ -28,7 +28,7 @@ async function registerSW(name = "sw.js", scope = "./") {
|
||||
}
|
||||
});
|
||||
|
||||
await p;
|
||||
return p;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
+67
-31
@@ -1,5 +1,5 @@
|
||||
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.reauthWait = null;
|
||||
this.authFileHandle = null;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -36,20 +37,37 @@ class Replay extends LitElement
|
||||
iframeUrl: { type: String },
|
||||
|
||||
showAuth: { type: Boolean },
|
||||
authFileHandle: { type: Object }
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
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.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) {
|
||||
await this.reauthWait;
|
||||
} else {
|
||||
this.showAuth = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
doSetIframeUrl() {
|
||||
@@ -58,9 +76,6 @@ class Replay extends LitElement
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("sourceUrl") || changedProperties.has("collInfo")) {
|
||||
this.isAuthable = (this.sourceUrl.startsWith("googledrive://") &&
|
||||
this.collInfo && this.collInfo.onDemand);
|
||||
|
||||
this.reauthWait = null;
|
||||
}
|
||||
|
||||
@@ -70,6 +85,8 @@ class Replay extends LitElement
|
||||
|
||||
this.replayUrl = this.url;
|
||||
this.replayTS = this.ts;
|
||||
this.showAuth = false;
|
||||
this.reauthWait = null;
|
||||
this.doSetIframeUrl();
|
||||
}
|
||||
|
||||
@@ -121,17 +138,28 @@ class Replay extends LitElement
|
||||
|
||||
onReAuthed(event) {
|
||||
this.reauthWait = (async () => {
|
||||
|
||||
if (!this.authFileHandle) {
|
||||
// google drive reauth
|
||||
const headers = event.detail.headers;
|
||||
|
||||
const resp = await fetch(`${this.collInfo.apiPrefix}/updateAuth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({headers})
|
||||
});
|
||||
} else {
|
||||
if (await this.authFileHandle.requestPermission({mode: "read"}) !== "granted") {
|
||||
this.reauthWait = null;
|
||||
return;
|
||||
}
|
||||
this.authFileHandle = null;
|
||||
}
|
||||
|
||||
if (this.showAuth) {
|
||||
this.refresh();
|
||||
this.showAuth = false;
|
||||
this.reauthWait = null;
|
||||
}
|
||||
this.refresh();
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -191,18 +219,24 @@ class Replay extends LitElement
|
||||
|
||||
.intro-panel .panel-heading {
|
||||
font-size: 1.0em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#wrlogo {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.intro-panel .panel-block {
|
||||
padding: 1.0em;
|
||||
flex-direction: column;
|
||||
line-height: 2.5em;
|
||||
}
|
||||
|
||||
div.intro-panel.panel {
|
||||
min-width: 40%;
|
||||
display: flex;
|
||||
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>
|
||||
|
||||
${this.iframeUrl ? html`
|
||||
<iframe @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
|
||||
src="${this.iframeUrl}" title="${title}"></iframe>
|
||||
` : html`
|
||||
${!this.iframeUrl ? html`
|
||||
<div class="panel intro-panel">
|
||||
<p class="panel-heading">Replay Web Page</p>
|
||||
<div class="panel-block">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>` : html`
|
||||
|
||||
${this.isAuthable ? html`
|
||||
<div class="modal ${this.showAuth ? 'is-active' : ''}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Auth Needed</p>
|
||||
<button class="delete" aria-label="close"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="container has-text-centered">
|
||||
<wr-gdrive .sourceUrl=${this.sourceUrl} .state="${this.showAuth ? 'trymanual' : 'implicitonly'}" .reauth="${true}" @load-ready=${this.onReAuthed}/>
|
||||
${this.showAuth ? html`
|
||||
<div class="panel intro-panel">
|
||||
<p class="panel-heading">
|
||||
<fa-icon id="wrlogo" size="1.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
|
||||
Authorization Needed
|
||||
</p>
|
||||
<div class="panel-block">
|
||||
${this.authFileHandle ? html`
|
||||
<p>This archive is loaded from a local file: <b>${this.authFileHandle.name}</b></p>
|
||||
<p>The browser needs to confirm your permission to continue loading from this file.</p>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ``}
|
||||
`;
|
||||
</div>` : html`
|
||||
|
||||
<iframe @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
|
||||
src="${this.iframeUrl}" title="${title}"></iframe>
|
||||
`}
|
||||
`}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
import INDEX_HTML from '../index.html';
|
||||
|
||||
if (self.registration) {
|
||||
const staticData = new Map();
|
||||
import { SWReplay } from '@webrecorder/wabac/src/swmain';
|
||||
|
||||
const prefix = self.registration.scope;
|
||||
const staticData = new Map();
|
||||
|
||||
staticData.set(prefix, {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});
|
||||
const prefix = self.registration.scope;
|
||||
|
||||
const { SWReplay } = require('@webrecorder/wabac/src/swmain');
|
||||
self.sw = new SWReplay(staticData);
|
||||
staticData.set(prefix, {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});
|
||||
|
||||
//const { SWReplay } = require('@webrecorder/wabac/src/swmain');
|
||||
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);
|
||||
}
|
||||
+23
-9
@@ -1,6 +1,7 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
// fake url used in app to serve files
|
||||
// can not use custom scheme due to service worker issues
|
||||
@@ -25,9 +26,16 @@ const electronMainConfig = (env, argv) => {
|
||||
entry: {
|
||||
'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: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
filename: '[name].js',
|
||||
},
|
||||
node: {
|
||||
__dirname: false,
|
||||
@@ -38,8 +46,18 @@ const electronMainConfig = (env, argv) => {
|
||||
__APP_FILE_SERVE_PREFIX__ : JSON.stringify(APP_FILE_SERVE_PREFIX),
|
||||
__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: {
|
||||
'preload': './src/electron-preload.js',
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.BannerPlugin(BANNER_TEXT)
|
||||
],
|
||||
@@ -111,15 +125,15 @@ const browserConfig = (env, argv) => {
|
||||
rules: [
|
||||
{
|
||||
test: /\.svg$/,
|
||||
loader: 'svg-inline-loader'
|
||||
use: ['svg-inline-loader'],
|
||||
},
|
||||
{
|
||||
test: /main.scss$/,
|
||||
loaders: ['css-loader', 'sass-loader']
|
||||
use: ['css-loader', 'sass-loader']
|
||||
},
|
||||
{
|
||||
test: /wombat.js|wombatWorkers.js|index.html$/i,
|
||||
loaders: ['raw-loader'],
|
||||
use: ['raw-loader'],
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user