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
|
## 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
@@ -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
@@ -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"> Go Forward</span>
|
<fa-icon .svg="${fasArrowRight}" aria-hidden="true"></fa-icon><span class="menu-only"> 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: <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>
|
</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> User Docs</span>
|
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span> 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
@@ -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
@@ -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} </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} </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 };
|
||||||
|
|||||||
+108
-31
@@ -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> About ReplayWeb.page</span>
|
<span> 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");
|
||||||
this.favIconUrl = icon.href;
|
if (ct && !ct.startsWith("text/")) {
|
||||||
|
this.favIconUrl = icon.href;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// not found
|
||||||
|
this.favIconUrl = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async onPurgeCache(event) {
|
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);
|
||||||
|
|||||||
+91
-17
@@ -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,27 +248,62 @@ 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");
|
||||||
console.log("file serve:", filename);
|
const ipfsCID = parsedUrl.searchParams.get("ipfs");
|
||||||
|
|
||||||
const stat = await fs.promises.lstat(filename);
|
|
||||||
|
|
||||||
if (!stat.isFile()) {
|
|
||||||
return this.notFound(filename, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = stat.size;
|
|
||||||
|
|
||||||
const headers = {"Content-Length": "" + size, "Content-Type": "application/octet-stream"};
|
|
||||||
|
|
||||||
|
const headers = {"Content-Type": "application/octet-stream"};
|
||||||
const reqHeaders = new Headers(request.headers);
|
const reqHeaders = new Headers(request.headers);
|
||||||
|
|
||||||
const {statusCode, start, end} = this.parseRange(reqHeaders, headers, size);
|
if (filename) {
|
||||||
|
console.log("file serve:", filename);
|
||||||
|
|
||||||
const data = request.method === "HEAD" ? null : fs.createReadStream(filename, {start, end});
|
const stat = await fs.promises.lstat(filename);
|
||||||
|
|
||||||
callback({statusCode, headers, data});
|
if (!stat.isFile()) {
|
||||||
return;
|
return this.notFound(filename, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = stat.size;
|
||||||
|
|
||||||
|
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
|
// 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};
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-10
@@ -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";
|
||||||
this.error = "Sorry, this browser is not supported. Please try a different browser\n(If you're using Firefox, try without Private Mode)";
|
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;
|
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) {}
|
} catch (e) {
|
||||||
|
console.log(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) {
|
||||||
@@ -241,7 +267,7 @@ You can select a file to upload from the main page by clicking the \'Choose File
|
|||||||
case "errored":
|
case "errored":
|
||||||
return html`
|
return html`
|
||||||
<div class="has-text-left">
|
<div class="has-text-left">
|
||||||
<div class="error has-text-danger">${this.error}</div>
|
<div class="error has-text-danger">${this.error}</div>
|
||||||
<div>
|
<div>
|
||||||
${this.embed ? html`
|
${this.embed ? html`
|
||||||
<a class="button is-warning" @click=${(e) => window.parent.location.reload()}>Try Again</a>` : html`
|
<a class="button is-warning" @click=${(e) => window.parent.location.reload()}>Try Again</a>` : html`
|
||||||
|
|||||||
+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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
+78
-42
@@ -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));
|
||||||
if (event.data.type === "authneeded" && this.collInfo && event.data.coll === this.collInfo.coll) {
|
}
|
||||||
if (this.reauthWait) {
|
|
||||||
await this.reauthWait;
|
async handleAuthMessage(event) {
|
||||||
} else {
|
if (event.data.type === "authneeded" && this.collInfo && event.data.coll === this.collInfo.coll) {
|
||||||
this.showAuth = true;
|
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() {
|
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 () => {
|
||||||
const headers = event.detail.headers;
|
|
||||||
|
|
||||||
const resp = await fetch(`${this.collInfo.apiPrefix}/updateAuth`, {
|
if (!this.authFileHandle) {
|
||||||
method: 'POST',
|
// google drive reauth
|
||||||
body: JSON.stringify({headers})
|
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) {
|
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>
|
||||||
</div>
|
` : html`
|
||||||
</section>
|
<wr-gdrive
|
||||||
|
.sourceUrl=${this.sourceUrl}
|
||||||
|
.state="${this.showAuth ? 'trymanual' : 'implicitonly'}"
|
||||||
|
.reauth="${true}
|
||||||
|
@load-ready=${this.onReAuthed}/>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>` : html`
|
||||||
</div>
|
|
||||||
` : ``}
|
<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';
|
import INDEX_HTML from '../index.html';
|
||||||
|
|
||||||
if (self.registration) {
|
import { SWReplay } from '@webrecorder/wabac/src/swmain';
|
||||||
const staticData = new Map();
|
|
||||||
|
|
||||||
const prefix = self.registration.scope;
|
const staticData = new Map();
|
||||||
|
|
||||||
staticData.set(prefix, {type: "text/html", content: INDEX_HTML});
|
const prefix = self.registration.scope;
|
||||||
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');
|
staticData.set(prefix, {type: "text/html", content: INDEX_HTML});
|
||||||
self.sw = new SWReplay(staticData);
|
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 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'],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user