7 |
8 |
9 | ### Project Deprectated
10 |
11 | This project hasn't been maintained in ~4 years (at the time of this commit)
12 |
--------------------------------------------------------------------------------
/ui/progress.css:
--------------------------------------------------------------------------------
1 | .progress {
2 | height: 5px;
3 | width: 100%;
4 | background-color: #EEEEEE;
5 | display: none;
6 | position: fixed;
7 | }
8 |
9 | .progress .complete {
10 | height: 100%;
11 | width: 0%;
12 | background-color: #FF5722;
13 | }
14 |
15 | /* only display the progress bar if it's corresponding tab is selected and
16 | * it's corresponding webview is loading
17 | */
18 | .progress.selected.loading {
19 | display: block;
20 | }
21 |
--------------------------------------------------------------------------------
/ui/tabs.css:
--------------------------------------------------------------------------------
1 | .tab-add, .tab {
2 | border-right: solid white 2px !important;
3 | height: 40px;
4 | display: inline-block;
5 | }
6 |
7 | .tab {
8 | width: 0px;
9 | max-width: 180px;
10 | flex: 1;
11 | padding: 0 10px;
12 | line-height: 40px;
13 | vertical-align: middle;
14 | color: white;
15 | display: flex;
16 | justify-content: space-between;
17 | }
18 |
19 | .tab-name {
20 | white-space: nowrap;
21 | text-overflow: ellipsis;
22 | overflow: hidden;
23 | }
24 |
25 | .close-tab:hover {
26 | color: red;
27 | padding-left: 5px;
28 | }
29 |
30 |
31 |
--------------------------------------------------------------------------------
/ui/search.css:
--------------------------------------------------------------------------------
1 | #search-bar-wrap {
2 | border-radius: 10px;
3 | background-color: white;
4 | height: 30;
5 | margin: 5px 10px;
6 | padding-right: 10px;
7 | /* Grow as large as possible */
8 | display: flex;
9 | flex-grow: 9999999;
10 | align-items: center;
11 | }
12 |
13 | #search-bar {
14 | font-size: 20px;
15 | padding: 2px 10px;
16 | width: 400px;
17 | border: none;
18 | height: 30;
19 | /* Grow as large as possible */
20 | flex-grow: 9999999;
21 | }
22 |
23 | #search-bar:focus {
24 | border: none;
25 | outline: none;
26 | }
27 |
28 | #search-bar-wrap .logo {
29 | padding-left: 10px;
30 | display: flex;
31 | }
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "electron-debug": "^1.0.0",
4 | "fs-chunk-store": "1.3.5",
5 | "parse-torrent": "^5.8.0",
6 | "prettier-bytes": "^1.0.3",
7 | "request": "^2.74.0",
8 | "temp": "^0.8.3",
9 | "webtorrent": "0.81.2"
10 | },
11 | "devDependencies": {
12 | "async": "^1.5.2",
13 | "devtron": "^1.1.0",
14 | "electron-packager": "7.0.1",
15 | "electron-prebuilt": "1.0.1",
16 | "eslint": "2.9.0",
17 | "eslint-config-airbnb": "9.0.1",
18 | "eslint-config-standard": "5.3.1",
19 | "eslint-plugin-import": "1.8.0",
20 | "eslint-plugin-jsx-a11y": "1.2.0",
21 | "eslint-plugin-promise": "1.1.0",
22 | "eslint-plugin-react": "5.1.1",
23 | "eslint-plugin-standard": "1.3.2",
24 | "mkdirp": "0.5.1",
25 | "rimraf": "2.5.2",
26 | "spectron": "3.0.0",
27 | "tape": "4.5.1"
28 | },
29 | "scripts": {
30 | "start": "electron .",
31 | "pretest": "eslint --ignore-pattern 'node_modules' --ignore-pattern 'build' --ignore-pattern 'downloads' .",
32 | "test": "tape tests/*/**/*.js"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ui/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0px;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | #content {
8 | display: flex;
9 | flex-grow: 999999999;
10 | }
11 |
12 | #nav-bar {
13 | display: flex;
14 | flex-direction: column;
15 | background-color: #222831;
16 | }
17 |
18 | .search-row {
19 | height: 40px;
20 | display: flex;
21 | }
22 |
23 | #tab-row {
24 | height: 40px;
25 | display: flex;
26 | }
27 |
28 | #search-back {
29 | transform: scaleX(-1);
30 | }
31 |
32 | .button.inactive {
33 | color: #889;
34 | }
35 |
36 | #search-refresh {
37 | font-weight: bold;
38 | transform: rotate(.25turn);
39 | }
40 |
41 | .button {
42 | border-right: solid transparent 2px;
43 | width: 38px;
44 | height: 40px;
45 | font-size: 35px;
46 | text-align: center;
47 | color: white;
48 | }
49 |
50 | .button:hover, .tab.selected {
51 | background-color: #2D4059;
52 | }
53 |
54 | webview {
55 | display: none;
56 | }
57 |
58 | webview.selected {
59 | display: flex;
60 | flex-grow: 99999999;
61 | }
62 |
63 | #webtorrent-stats {
64 | background-color: #bbc;
65 | padding: 5px;
66 | }
67 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 William Blankenship
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 |
4 | This document will walk you through getting up and running with PeerWeb during it's early alpha release. Things will be changing fast, but an effort will be made to keep this document up-to-date.
5 |
6 | # Requirements
7 |
8 | We don't yet have a build of PeerWeb available for download. Luckily, there is no build step for running this locally, but you still will need `git` to clone the PeerWeb repository and the `nodejs` platform.
9 |
10 | Installation requirements:
11 |
12 | 1. [nodejs and npm](https://nodejs.org/en/download)
13 | 2. [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
14 |
15 | # Installation
16 |
17 | Installation currently involves cloning the PeerWeb repository with `git`, using `npm` to install PeerWeb's dependencies, and then running `npm start`.
18 |
19 | ```text
20 | $ git clone https://github.com/retrohacker/peerweb.git
21 | $ cd peerweb
22 | $ NODE_ENV=DEV npm install
23 | $ npm start
24 | ```
25 |
26 | From now on, from the `peerweb` directory, you can simply run `npm start` to start the browser.
27 |
28 | Moving forward, we will provide pre-built binaries for Windows, OSX, and various Linux distributions.
29 |
30 | # Unboxing Experience.
31 |
32 | When you first run the browser, you should be presented with the following UI:
33 |
34 | 
35 |
36 | This UI has many shortcomings compared to other web browsers, I encourage you to open up [issues](https://github.com/retrohacker/peerweb/issues) with suggestions on how we can make the best first impression with this browser.
37 |
38 | # Sharing your own website
39 |
40 | While the browser currently can handle loading content from a torrent, there is not yet a way to share your own content through the UI. This is on the roadmap and is being tracked in [issue #5](https://github.com/retrohacker/issues/5).
41 |
42 | ## Ports
43 |
44 | Every router and network is a special little snowflake. The main thing here is NAT, and making sure that incomming connections make it to the bittorrent client they were destined for. NAT traversal is on the roadmap, but is no easy path to walk.
45 |
46 | For now, in order to use Bittorrent's DHT protocol, you will need to use a computer that receives all incomming traffic on the network. This is easiest on something like Digital Ocean.
47 |
48 | ## Webtorrent
49 |
50 | To share your website, follow these steps.
51 |
52 | ```
53 | $ npm install -g webtorrent-cli
54 | $ webtorrent seed [website_folder]
55 | ```
56 |
57 | This will show your your torrent hash. It will look something like: `000260db64bc4d57afb5458639f2364d628b6e1b`
58 |
59 | You can then take this hash and use it as a url in peerweb: [peer://000260db64bc4d57afb5458639f2364d628b6e1b](peer://000260db64bc4d57afb5458639f2364d628b6e1b)
60 |
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
38 |
50 |
51 |
52 |
53 |
54 |
55 | +
56 |
57 |
58 |
59 |
60 | ➤
61 |
62 |
63 | ➤
64 |
65 |
66 | ↻
67 |
68 |
69 |
⚛
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ui/tabs.js:
--------------------------------------------------------------------------------
1 | /*
2 | * tabs.js
3 | *
4 | * This file contains all of the code immediately relevant to the tabs in the
5 | * browser UI.
6 | *
7 | * All logic associated with updating the UI in response to an event triggered
8 | * by a tab is delegated to ui.js
9 | */
10 |
11 | // Create a global singleton that scopes the globally exposed functions from
12 | // this application
13 | // eslint-disable-next-line prefer-const, no-use-before-define, no-var
14 | var peerWeb = peerWeb || {}
15 |
16 | // Scope all functions exposed by this file to tabs
17 | peerWeb.tabs = peerWeb.tabs || {}
18 |
19 | ;(function scope () {
20 | // The closeTab function is added as an event listener to a newly created tab
21 | // in newTab. It listens for a click event on the close-tab button of a tab,
22 | // and removes the tab and it's dependencies from the browser.
23 | peerWeb.tabs.closeTab = function closeTab (e) {
24 | // Get the id of the tab we are closing. The tab id will be set as the
25 | // CSS id of the parent element of the close icon on the tab.
26 | const id = peerWeb.utils.getId(e.target.parentElement)
27 | peerWeb.ui.remove(id)
28 | }
29 |
30 | // tabClick is registered as an event listener on every tab. Its purpose
31 | // is to determine if a click event represents a user trying to click on a
32 | // tab. If the click was meant to go to the tab, then the work is passed off
33 | // to the claimOwnership function from ui.js to handle updating the
34 | // browser accordingly.
35 | peerWeb.tabs.tabClick = function tabClick (e) {
36 | // Get the target DOM element of the event
37 | let target = e.target
38 |
39 | // Make sure the user wasn't trying to close the tab
40 | if (target.className === 'close-tab') {
41 | return null
42 | }
43 |
44 | // If the user clicked on 'tab-name' then they were clicking on the tab
45 | // itself, so select 'tab-name's parent, which is the tab container, and
46 | // use that as the target
47 | if (target.className === 'tab-name') {
48 | target = target.parentNode
49 | }
50 |
51 | // Get the id of the clicked tab
52 | const id = peerWeb.utils.getId(target)
53 | if (id == null) {
54 | // If we were unable to parse an id, there is nothing left for us to do
55 | // TODO: This would probably be a good place to report this error back to
56 | // the developers since this is an edge case that should never be reached
57 | return null
58 | }
59 |
60 | // Update the UI to reflect the new tab being active
61 | peerWeb.ui.claimOwnership(id)
62 |
63 | // All done!
64 | return null
65 | }
66 |
67 | // We use tabCount as a generator of unique IDs for the newTab function
68 | let tabCount = 0
69 |
70 | // newTab is an event listener that is wired up to the add tab button. When
71 | // clicked, it delegates a majority of its work to peerWeb.ui.add, and simply
72 | // keeps track of the tabCount UID generator.
73 | peerWeb.tabs.newTab = function newTab () {
74 | peerWeb.ui.add(tabCount++)
75 | }
76 | })()
77 |
--------------------------------------------------------------------------------
/ui/utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | * utils.js
3 | *
4 | * This file contains some handy dandy functions that handle some rather
5 | * tedious tasks such as DOM manipulation and adding
6 | */
7 |
8 | // Create a global singleton that scopes the globally exposed functions from
9 | // this application
10 | // eslint-disable-next-line prefer-const, no-use-before-define, no-var
11 | var peerWeb = peerWeb || {}
12 |
13 | // Scope all functions exposed by this file to utils
14 | peerWeb.utils = peerWeb.utils || {}
15 |
16 | ;(function scope () {
17 | // addClass adds a CSS class to a DOM element and returns the new
18 | // string-separated list of class names as a result
19 | peerWeb.utils.addClass = function addClass (element, c) {
20 | // Ensure we don't add an empty space at the beginning if there are no
21 | // currently existing classes
22 | if (element.className == null || element.className.length === 0) {
23 | return c
24 | }
25 |
26 | return `${element.className} ${c}`
27 | }
28 |
29 | // removeClass removes a CSS class element from a DOM element and returns
30 | // the new string-separated list of class names as a result
31 | peerWeb.utils.removeClass = function removeClass (element, c) {
32 | // If there are no class names, we have no work to do
33 | if (element.className == null || element.className.length === 0) {
34 | return ''
35 | }
36 |
37 | // Grab the current list of space-separated class names
38 | return element.className
39 | // Split around spaces to get an array of class names
40 | .split(' ')
41 | // Filter the list, removing any name that matches the class
42 | .filter((v) => v !== c)
43 | // Turn it back into a space-separated list
44 | .join(' ')
45 | }
46 |
47 | // hasClass takes a DOM element and returns true if the DOM element has the
48 | // CSS class name provided
49 | peerWeb.utils.hasClass = function hasClass (element, c) {
50 | // If the element has no classNames, just return false
51 | if (element.className == null) {
52 | return false
53 | }
54 |
55 | // return true if the element has the class name, false otherwise
56 | return element.className
57 | .split(' ')
58 | // This is gross. To understand what is happening here, first read
59 | // the documentation on the reduce function for arrays in
60 | // JavaScript. We are starting the reduce function with the initial
61 | // value of `false`. We then or the previous value with the strict
62 | // equality of the class name we are looking for. If at any point,
63 | // a value in the array is equal to the class name provided to this
64 | // function, the `or` operation will evaluate to true, meaning the
65 | // rest of the checks will evaluate to true
66 | .reduce((p, v) => p || v === c, false)
67 | }
68 |
69 | // getId takes an element and returns the Unique ID of that element. This
70 | // assumes that the element conforms to the standard of `TYPE-ID` for the
71 | // CSS id name that has been established by this project
72 | peerWeb.utils.getId = function getId (element) {
73 | // We make sure that the element has an id, and that the id has the
74 | // function `split`. If so, we return null, since we cant determine an
75 | // id
76 | if (element.id == null ||
77 | typeof element.id.split !== 'function') {
78 | return null
79 | }
80 |
81 | // Split the element around `-` and return the id which should be the
82 | // second component
83 | return element.id.split('-')[1]
84 | }
85 | })()
86 |
--------------------------------------------------------------------------------
/tests/build.js:
--------------------------------------------------------------------------------
1 | /*
2 | * build.js
3 | *
4 | * This is a singleton responsible for generating an electron binary for the
5 | * tests to drive. The binary is built once, and then the path is cached for
6 | * subsequent requests.
7 | */
8 | 'use strict'
9 |
10 | /* Require in deps */
11 | const electronPackager = require('electron-packager')
12 | const path = require('path')
13 | const rimraf = require('rimraf')
14 | const mkdirp = require('mkdirp')
15 | const packageJson = require('../package.json')
16 |
17 | /* Define global variables for module */
18 |
19 | // We will cache the path to the compiled electron app for future invocations
20 | let cachedBuild = null
21 |
22 | // This is the directory we will drop built artifacts into
23 | const outDir = path.join(__dirname, '..', 'build')
24 |
25 | // This is the name of the binary we will build
26 | const buildName = 'test-artifact'
27 |
28 | // Configuration settings for electron-packager
29 | const opts = {
30 | // We are only building the architecture for the current platform
31 | arch: process.arch,
32 | // We want to build the entire project directory
33 | dir: path.join(__dirname, '..'),
34 | // And we want to build this for the OS we are currently running
35 | platform: process.platform,
36 | // The directory to drop the builds in
37 | out: outDir,
38 | // Give it a meaningful name in the event we fail to cleanup
39 | name: buildName,
40 | // Give it a version to skip looking up the package.json, this avoids an
41 | // error condition we were reaching in the electron-packager module
42 | version: packageJson.devDependencies['electron-prebuilt']
43 | }
44 |
45 | /* Done defining globals */
46 |
47 | // Once all of the tests have been run or the process aborts, we want to delete
48 | // the built binary since it was only for these tests
49 | process.on('exit', function cleanupBuild () {
50 | // Disable globbing ensures that outDir doesn't delete more than one folder
51 | // NOTE: We are in the `exit` event handler, yielding the event loop here
52 | // causes the process to exit, so we use the sync version of rimraf
53 | // return rimraf.sync(outDir, { glob: false })
54 | })
55 |
56 | module.exports.getPath = function getPath (cb) {
57 | // If we have already built a binary, simply return the cached path
58 | if (cachedBuild) return cb(null, cachedBuild)
59 |
60 | // If a binary has not yet been built for this set of tests, delete the
61 | // current output directory if it exists, recreate it, and build a fresh
62 | // binary.
63 | return rimraf(outDir, function removeDirectory (rimrafError) {
64 | // If we fail to delete the directory, we can't recreate and build it so
65 | // there is nothing left for our tests to do. Report the error and crash.
66 | // Yes, throwing is an anti-pattern, but these are tests so it shouldn't
67 | // matter. Note: The callback is error first, even though we never return
68 | // an error. This is only for convention, since it doesn't make sense for
69 | // the process to continue when an error condition is encountered in this
70 | // method
71 | if (rimrafError) throw rimrafError
72 | mkdirp(outDir, function directoryExists (mkdirpError) {
73 | // If we fail to create the output directory, we can't build the binary and
74 | // there is nothing for our tests to do. Yes throwing is an anti-pattern,
75 | // but these are tests so it shouldn't matter.
76 | if (mkdirpError) throw mkdirpError
77 |
78 | // We now invoke electron-packager and let it build us a binary to test
79 | electronPackager(opts, function binaryBuilt (buildError, paths) {
80 | // If the package failed to build, there is nothing left for us to do
81 | if (buildError) throw buildError
82 |
83 | // If we have more than one binary built, our assumptions about the
84 | // options object are wrong and this module needs to be redesigned
85 | if (paths.length !== 1) {
86 | throw new Error(`electron-packager built ${paths.length} binaries`)
87 | }
88 |
89 | // Cache the path to our new binary
90 | cachedBuild = path.join(paths[0], buildName)
91 | return cb(null, cachedBuild)
92 | })
93 | })
94 | })
95 | }
96 |
--------------------------------------------------------------------------------
/ui/search.js:
--------------------------------------------------------------------------------
1 | /*
2 | * search.js
3 | *
4 | * This file contains all of the code immediately relevant to the search bar
5 | * at the top of the page. The search bar is an overloaded term, its really
6 | * just the bar you type URLs into for navigation. It currently offers no
7 | * search.
8 | */
9 |
10 | // Create a global singleton that scopes the globally exposed functions from
11 | // this application
12 | // eslint-disable-next-line prefer-const, no-use-before-define, no-var
13 | var peerWeb = peerWeb || {}
14 | // eslint-disable-next-line prefer-const, no-use-before-define, no-var
15 | var url = require('url')
16 |
17 | // Scope all functions exposed by this file to search
18 | peerWeb.search = peerWeb.search || {}
19 |
20 | ;(function scope () {
21 | // Scrub the url to make it electron friendly
22 | /*eslint-disable no-param-reassign*/
23 | peerWeb.search.scrubUrl = function scrubUrl (addr) {
24 | // Check if any valid protocol present
25 | // TODO:Improve Scrubber Logic
26 | if (!/\w+:\/\//.test(addr)) {
27 | addr = `https://${addr}`
28 | }
29 | if (url.parse(addr).path == null) {
30 | return `${addr}/`
31 | }
32 | return addr
33 | }
34 |
35 | // checkSubmit is an event listener for the search bar in the UI. It waits
36 | // until a user presses enter and then sends the user to the URL typed into
37 | // the search bar
38 | peerWeb.search.checkSubmit = function checkSubmit (e) {
39 | // If event is defined and the keyCode of the event is the enter key, go
40 | // ahead and try to send the user to the url they typed in
41 | if (e != null && e.keyCode === 13) {
42 | const addr = peerWeb.search.scrubUrl(e.target.value)
43 | document.getElementById('search-bar').value = addr
44 | peerWeb.ui.navigate(addr)
45 | }
46 | }
47 |
48 | // navigateBack will send the currently selected webview to it's previous
49 | // page if possible
50 | peerWeb.search.back = function navigateBack (e) {
51 | // Iterate through all webviews looking for the selected one
52 | const webviews = document.getElementsByTagName('webview')
53 | let webview = webviews[0]
54 |
55 | for (let i = 0;
56 | i < webviews.length && !peerWeb.utils.hasClass(webview, 'selected');
57 | i++, webview = webviews[i]) {} // eslint-disable-line no-empty
58 |
59 | // If we didn't find a selected webview, then we have nothing to do
60 | if (webview == null || !peerWeb.utils.hasClass(webview, 'selected')) {
61 | return null
62 | }
63 |
64 | // If we can't navigate backwards, there is nothing to do
65 | if (!webview.canGoBack()) {
66 | return null
67 | }
68 |
69 | // Navigate back a page
70 | webview.goBack()
71 |
72 | // If we can't go back any further, reflect that in the UI
73 | if (!webview.canGoBack()) {
74 | const navBack = document.getElementById('search-back')
75 | navBack.className = peerWeb.utils.removeClass(navBack, 'inactive')
76 | navBack.className = peerWeb.utils.addClass(navBack, 'inactive')
77 | }
78 |
79 | return null
80 | }
81 |
82 | // navigateForward will send the currently selected webview to it's next
83 | // page if possible
84 | peerWeb.search.forward = function navigateForward (e) {
85 | // Iterate through all webviews looking for the selected one
86 | const webviews = document.getElementsByTagName('webview')
87 | let webview = webviews[0]
88 |
89 | for (let i = 0;
90 | i < webviews.length && !peerWeb.utils.hasClass(webview, 'selected');
91 | i++, webview = webviews[i]) {} // eslint-disable-line no-empty
92 |
93 | // If we didn't find a selected webview, then we have nothing to do
94 | if (webview == null || !peerWeb.utils.hasClass(webview, 'selected')) {
95 | return null
96 | }
97 |
98 | // If we can't navigate forwards, there is nothing to do
99 | if (!webview.canGoForward()) {
100 | return null
101 | }
102 |
103 | // Navigate forward a page
104 | webview.goForward()
105 |
106 | // If we can't go forward any further, reflect that in the UI
107 | if (!webview.canGoForward()) {
108 | const navForward = document.getElementById('search-forward')
109 | navForward.className = peerWeb.utils.removeClass(navForward, 'inactive')
110 | navForward.className = peerWeb.utils.addClass(navForward, 'inactive')
111 | }
112 |
113 | return null
114 | }
115 |
116 | // refresh will refresh the webview's current page
117 | peerWeb.search.refresh = function refresh (e) {
118 | // Iterate through all webviews looking for the selected one
119 | const webviews = document.getElementsByTagName('webview')
120 | let webview = webviews[0]
121 |
122 | for (let i = 0;
123 | i < webviews.length && !peerWeb.utils.hasClass(webview, 'selected');
124 | i++, webview = webviews[i]) {} // eslint-disable-line no-empty
125 |
126 | // If we didn't find a selected webview, then we have nothing to do
127 | if (webview == null || !peerWeb.utils.hasClass(webview, 'selected')) {
128 | return null
129 | }
130 |
131 | // refresh a page
132 | webview.reload()
133 |
134 | return null
135 | }
136 | })()
137 |
--------------------------------------------------------------------------------
/tests/unit/tabs.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* eslint-disable func-names */
4 | const test = require('tape')
5 | const buildBinary = require('../build.js')
6 | const spectron = require('spectron')
7 | const asyncLib = require('async')
8 |
9 | /* Define globals for tests */
10 | let app = null
11 | /* Done defining globals */
12 |
13 | // This is a hack. Since tape runs tests serially, we can have this test run
14 | // before everything else in this file. This lets us get a path to the binary
15 | // (the module will build it for us if necessary) and then the rest of the
16 | // tests can use the global variable defined above.
17 | test('Get path to binary', function (t) {
18 | buildBinary.getPath(function (e, path) {
19 | app = new spectron.Application({
20 | path
21 | })
22 | t.end()
23 | })
24 | })
25 |
26 | // We want to make sure the nav-bar loads and is visible, so we start the
27 | // browser and query the DOM
28 | test('UI Loads', function (t) {
29 | app.start()
30 | .then(function () {
31 | return app.client.isVisibleWithinViewport('#nav-bar')
32 | })
33 | .then(function (isVisible) {
34 | t.equal(isVisible, true, 'Nav Bar should be visible')
35 | })
36 | .catch(function (e) {
37 | t.fail(e)
38 | return
39 | })
40 | .then(function () {
41 | return app.stop()
42 | })
43 | .catch(function (e) {
44 | t.fail(e)
45 | return
46 | })
47 | .then(function (e) {
48 | t.end()
49 | return
50 | })
51 | })
52 |
53 | // Lets make sure that clicking the "add tab" button actually adds a tab
54 | test('Tab functionality', function (t) {
55 | app.start()
56 | // Click the "add tab" button 3 times, resulting in 4 tabs on the page
57 | .then(function () {
58 | return app.client.click('#add-tab')
59 | })
60 | .then(function () {
61 | return app.client.click('#add-tab')
62 | })
63 | .then(function () {
64 | return app.client.click('#add-tab')
65 | })
66 | .then(function (isVisible) {
67 | return app.client.elements('.tab')
68 | })
69 | .then(function (tabs) {
70 | t.equal(tabs.value.length, 4, 'clicking add-tab 3 times added 3 tabs')
71 | return
72 | })
73 | .then(function () {
74 | return app.client.click('#tab-2')
75 | })
76 | .then(function () {
77 | return app.client.isVisibleWithinViewport('#webview-2')
78 | })
79 | .then(function (isVisible) {
80 | t.equal(isVisible,
81 | true,
82 | 'webview becomes visible when corresponding tab is clicked')
83 | })
84 | .then(function () {
85 | return app.client.click('.close-tab')
86 | })
87 | .then(function () {
88 | return app.client.elements('.tab')
89 | })
90 | .then(function (tabs) {
91 | t.equal(tabs.value.length, 3, 'clicking close-tab removes a tab')
92 | return
93 | })
94 | .catch(function (e) {
95 | t.fail(e)
96 | return
97 | })
98 | .then(function () {
99 | return app.stop()
100 | })
101 | .catch(function (e) {
102 | t.fail(e)
103 | return
104 | })
105 | .then(function (e) {
106 | t.end()
107 | return
108 | })
109 | })
110 |
111 | // Let's make sure the nav bar updates the tab after page load
112 | test('Navbar updates tab value', function (t) {
113 | app.start()
114 | // Set the URL to google and then press enter
115 | .then(function () {
116 | return app.client.setValue('#search-bar', 'http://www.google.com')
117 | })
118 | .then(function () {
119 | return app.client.click('#search-bar')
120 | })
121 | .then(function (bar) {
122 | return app.client.keys('Enter')
123 | })
124 | // Keep checking to see if the page has loaded
125 | .then(function () {
126 | return new Promise(function (resolve, reject) {
127 | asyncLib.retry(
128 | { times: 10, interval: 2000 },
129 | function (cb) {
130 | app.client.selectorExecute('webview', function (webview) {
131 | return `${webview[0].isLoading()}`
132 | })
133 | .then(function (isLoading) {
134 | if (isLoading === 'false') {
135 | return cb()
136 | }
137 | return cb(new Error('WebView still loading...'))
138 | })
139 | .catch(cb)
140 | },
141 | function (e) {
142 | if (e) return reject(e)
143 | return resolve()
144 | }
145 | )
146 | })
147 | })
148 | // Make sure tab value was updated
149 | .then(function () {
150 | return app.client.selectorExecute('.tab-name', function (tabs) {
151 | return tabs[0].innerHTML
152 | })
153 | })
154 | .then(function (text) {
155 | if (text !== 'Google') throw new Error(`Invalid tab title: ${text}`)
156 | return null
157 | })
158 | .catch(function (e) {
159 | t.fail(e)
160 | return
161 | })
162 | .then(function () {
163 | return app.stop()
164 | })
165 | .catch(function (e) {
166 | t.fail(e)
167 | return
168 | })
169 | .then(function (e) {
170 | t.end()
171 | return
172 | })
173 | })
174 |
175 | /* eslint-enable func-names */
176 |
--------------------------------------------------------------------------------
/docs/imgs/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
197 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* Configuration:
2 | *
3 | * config.protocol: defines the name of the protocol that will serve static
4 | * websites over torrent. For example, if you set this to 'peer', then any
5 | * url prefixed with 'peer://' will be handled by our torrent protocol
6 | *
7 | */
8 | const config = require('./config.json')
9 | require('electron-debug')({ showDevTools: true })
10 |
11 | /* Begin Dependencies */
12 |
13 | // We use path to locate files on the local filesystem
14 | const path = require('path')
15 |
16 | // We use url to parse incomming requests for their file path
17 | const url = require('url')
18 |
19 | // Electron is used to render a browser for the user
20 | const electron = require('electron') // eslint-disable-line import/no-unresolved
21 |
22 | // IPC is used to convey torrent information to rendering process
23 | const ipcMain = electron.ipcMain
24 |
25 | // WebTorrent gives our application the ability to download/seed torrents
26 | const WebTorrent = require('webtorrent')
27 |
28 | // prettier-bytes takes download/upload rates and makes them human friendly
29 | const pbytes = require('prettier-bytes')
30 |
31 | // parse-torrent lets us verify a torrent hash before passing it onto the
32 | // webtorrent handler
33 | const parseTorrent = require('parse-torrent')
34 |
35 | // request will be used for the http fallback when parse-torrent says a request
36 | // was not in the form of a torrent info
37 | const request = require('request')
38 |
39 | // temp will allow us to save html files fetched over the http fallback and
40 | // they will be purged on application shutdown
41 | const temp = require('temp')
42 | temp.track()
43 |
44 | // fs will allow us to write to our temporary file and load torrents from a
45 | // previous session
46 | const fs = require('fs')
47 |
48 | /* End Dependencies */
49 |
50 | // Create a new client responsible for seeding/downloading torrents
51 | const client = new WebTorrent()
52 |
53 | // ipc Global Status channel , only for recieving Stat requests
54 | // Reply sent on : global-status-reply
55 | ipcMain.on('global-status', function updateStatus (event, arg) {
56 | // status contains WebTorrent Client Stats
57 | const status = {
58 | download: pbytes(client.downloadSpeed),
59 | upload: pbytes(client.uploadSpeed),
60 | torrents: client.torrents
61 | }
62 | // Send Asynchronous reply. Note that status is serialized internally
63 | event.sender.send('global-status-reply', status)
64 | })
65 |
66 | // peerProtocolHandler resolves a peer:// request against a torrent, returning
67 | // the requested file as the result
68 | function peerProtocolHandler (req, callback) {
69 | // Take the incomming request and parse out the url components
70 | let requestedUrl = url.parse(req.url)
71 |
72 | // If the requested file has a trailing `/`, assume it is a folder and add
73 | // index.html to the end
74 | let requestedFile = requestedUrl.pathname
75 |
76 | if (requestedFile.substring(requestedFile.length - 1) === '/') {
77 | requestedFile += 'index.html'
78 | }
79 |
80 | // Make sure the path isn't prefixed with a slash
81 | if (requestedFile.substring(0, 1) === '/') {
82 | requestedFile = requestedFile.substring(1)
83 | }
84 |
85 | // Log the requested file
86 | console.log(requestedFile) // eslint-disable-line no-console
87 |
88 | // The hash is the hostname of the requested url
89 | const hash = requestedUrl.host
90 |
91 | // We create a directory using the torrent's hash and have webtorrent
92 | // download the website's contents there
93 | const opts = {
94 | path: path.join(__dirname, config.downloadDir, hash)
95 | }
96 |
97 | // Ensure the torrent hash is valid before we pass it on to webtorrent, if it
98 | // is not, try fetching it over http
99 | try {
100 | parseTorrent(hash)
101 | } catch (exception) {
102 | // eslint-disable-next-line no-console
103 | console.log('Not a torent identifier, falling back to http')
104 | requestedUrl.protocol = 'http'
105 | requestedUrl = url.format(requestedUrl)
106 | return request(requestedUrl, function httpFallback (e, resp, body) {
107 | // If we didn't get a 200 response, return an error
108 | if (e) {
109 | // eslint-disable-next-line no-console
110 | console.log(`Failed to fetch ${requestedUrl}: ${e}`)
111 | return callback(e)
112 | } else if (resp.statusCode !== 200) {
113 | // eslint-disable-next-line no-console
114 | console.log(`Failed to fetch ${requestedUrl}: ${resp.statusCode}`)
115 | return callback(resp.statusCode)
116 | }
117 |
118 | // Create a temporary file for our downloaded resource
119 | return temp.open('peerwebhttp', function tempFileOpened (e2, info) {
120 | if (e2) {
121 | // eslint-disable-next-line no-console
122 | console.log(`Failed to open temp file: ${e2}`)
123 | return callback(e2)
124 | }
125 |
126 | // We then write our response to the temporary file and return the path
127 | // back to electron to serve as the response
128 | return fs.write(info.fd, body, function tempFileWritten () {
129 | fs.close(info.fd, function tempFileClosed () {
130 | return callback(info.path)
131 | })
132 | })
133 | })
134 | })
135 | }
136 |
137 | // Lets kick off the download through webtorrent
138 | return client.add(hash, opts, function loaded (torrent) {
139 | // Search the torrent for the requestedFile
140 | let returnFile = null
141 | for (let i = 0; i < torrent.files.length; i++) {
142 | const file = torrent.files[i]
143 | // Webtorrent prepends the torrent name to the beginning of the file,
144 | // we want to remove that when searching for the requested file
145 | const name = file.path.substring((`${torrent.name}/`).length)
146 | if (name === requestedFile) {
147 | // found it!
148 | returnFile = file
149 | }
150 | }
151 |
152 | // If the requested file was not found, try assuming it is a directory and
153 | // look for index.html in that directory
154 | if (returnFile == null) {
155 | // eslint-disable-next-line no-console
156 | console.log(`Trying ${requestedFile}/index.html`)
157 | for (let i = 0; i < torrent.files.length; i++) {
158 | const file = torrent.files[i]
159 | // Webtorrent prepends the torrent name to the beginning of the file,
160 | // we want to remove that when searching for the requested file
161 | const name = file.path.substring((`${torrent.name}/`).length)
162 | if (name === `${requestedFile}/index.html`) {
163 | // found it!
164 | returnFile = file
165 | }
166 | }
167 | }
168 |
169 | // If the requested file wasn't a folder, try checking if there is a file
170 | // by the name of requestedFile.html in the torrent
171 | if (returnFile == null) {
172 | // eslint-disable-next-line no-console
173 | console.log(`Trying ${requestedFile}.html`)
174 | for (let i = 0; i < torrent.files.length; i++) {
175 | const file = torrent.files[i]
176 | // Webtorrent prepends the torrent name to the beginning of the file,
177 | // we want to remove that when searching for the requested file
178 | const name = file.path.substring((`${torrent.name}/`).length)
179 | if (name === `${requestedFile}.html`) {
180 | // found it!
181 | returnFile = file
182 | }
183 | }
184 | }
185 |
186 | // If it is still not found, tell electron we didn't find the file
187 | if (returnFile == null) {
188 | // eslint-disable-next-line no-console
189 | console.log(`${requestedFile} not found, returning null`)
190 | return callback(404)
191 | }
192 |
193 | // Wait for the file to become available, downloading from the network at
194 | // highest priority. This ensures we don't return a path to a file that
195 | // hasn't finished downloading yet.
196 | return returnFile.getBuffer(function getBuffer (e) {
197 | // We don't actually care about the buffer, we only care if the file
198 | // was downloaded
199 | if (e) return callback(e)
200 | // Generate the path to the file on the local fs
201 | const file = path.join(__dirname,
202 | config.downloadDir,
203 | hash,
204 | returnFile.path)
205 |
206 | // Give the file back to electron
207 | console.log(`Returning: ${file}`)// eslint-disable-line no-console
208 | return callback({ path: file })
209 | })
210 | })
211 | }
212 |
213 | // registerTorrentProtocol takes an instance of electron and registers a
214 | // handler for our new protocol, allowing the instance of electron to resolve
215 | // requests against a torrent
216 | function registerTorrentProtocol (localElectron, cb) {
217 | localElectron
218 | .protocol
219 | .registerFileProtocol(config.protocol, peerProtocolHandler,
220 | function registeredProtocol (e) {
221 | if (e) {
222 | return cb(e)
223 | }
224 | // Don't treat our new protocol like http
225 | electron
226 | .protocol
227 | .registerStandardSchemes([config.protocol])
228 | // Done setting up our new protocol
229 | return cb()
230 | })
231 | }
232 |
233 | // Begin seeding torrents from previous sessions
234 | function restartTorrents (cb) {
235 | const dir = path.join(__dirname, config.downloadDir)
236 | fs.readdir(dir, function reloadTorrents (e, torrents) {
237 | if (e) return cb(e)
238 | // Each folder in that directory is a torrent hash, re-add them
239 | return torrents.forEach(function reloadTorrent (infoHash) {
240 | try {
241 | // Try parsing the torrent first to keep webtorrent from throwing in
242 | // async land
243 | parseTorrent(infoHash)
244 | client.add(infoHash,
245 | { path: path.join(__dirname, config.downloadDir, infoHash) })
246 | } catch (exception) {
247 | // eslint-disable-next-line no-console
248 | console.error(`Unable to re-add torrent: ${infoHash}`)
249 | // eslint-disable-next-line no-console
250 | console.error(exception)
251 | }
252 | })
253 | })
254 | }
255 |
256 | // configureElectron registers the custom protocol with or electron app
257 | function configureElectron () {
258 | // electron is now ready to be configured
259 | registerTorrentProtocol(electron, function init2 (e) {
260 | if (e) {
261 | throw e
262 | }
263 |
264 | // peer:// protocol has been registered
265 | const opts = {}
266 |
267 | // Start the application
268 | const mainWindow = new electron.BrowserWindow(opts)
269 |
270 | // Hide the menubar
271 | mainWindow.setMenu(null)
272 |
273 | // Load the UI
274 | mainWindow.loadURL(`file://${path.join(__dirname, 'ui', 'index.html')}`)
275 | // Let's resume all the torrents from a previous session. No need for a
276 | // callback here since we do this after the UI has loaded and we don't take
277 | // any action when this completes. Just log the error if there is one.
278 | // eslint-disable-next-line no-console
279 | restartTorrents(console.error)
280 | })
281 | }
282 |
283 | // The main logic of our application. This is what runs when called directly
284 | // from the command line
285 | function applicationLogic () {
286 | electron.app.on('ready', configureElectron)
287 | }
288 |
289 | // Kick off the application
290 | applicationLogic()
291 |
--------------------------------------------------------------------------------
/ui/ui.js:
--------------------------------------------------------------------------------
1 | /*
2 | * ui.js
3 | *
4 | * At the time of writting, this is the most meta-thinking file in this
5 | * directory. I suspect it may not be immediately obvious why this file exists
6 | * and why it is necessary.
7 | *
8 | * Essentially, in our browser, we have a state that drives the entire user
9 | * experience. That state is the currently selected tab. When we change tabs,
10 | * a majority of the UI needs to be updated to reflect the newly selected tab.
11 | * This includes everything from the navigation buttons to the visible webview.
12 | *
13 | * ui.js has a collection of functions which together localize all of the logic
14 | * behind tab operations to this file.
15 | */
16 |
17 | // Create a global singleton that scopes the globally exposed functions from
18 | // this application
19 | // eslint-disable-next-line prefer-const, no-use-before-define, no-var
20 | var peerWeb = peerWeb || {}
21 |
22 | // Scope all functions exposed by this file to ui
23 | peerWeb.ui = peerWeb.ui || {}
24 |
25 | // Scope all functions in this file to prevent polluting the global space
26 | ;(function scope () {
27 | // claimOwnership will take the ID of a tab, and handle all the logic
28 | // necessary to update the UI showing that the tab is selected
29 | peerWeb.ui.claimOwnership = function claimOwnership (id) {
30 | // We need to fetch all of the webviews on the page, remove the CSS class
31 | // `selected` from any that have it set, and add the CSS class name
32 | // `selected` to the webview that belongs to this tab.
33 | const webviews = window.document.getElementsByTagName('webview')
34 | for (let i = 0; i < webviews.length; i++) {
35 | const webview = webviews[i]
36 | // Remove `selected` from all of the webviews, even the one that is
37 | // being selected. This prevents us from adding `selected` to the same
38 | // element multiple times.
39 | webview.className = peerWeb.utils.removeClass(webview, 'selected')
40 | // If this webview matches the id of the selected tab, add the CSS class
41 | // `selected` to it. We force id to be a string for this comparison so
42 | // that we can use strict equality.
43 | if (peerWeb.utils.getId(webview) === id.toString()) {
44 | webview.className = peerWeb.utils.addClass(webview, 'selected')
45 |
46 | // Update the searchbar to match the webview's content
47 | const searchBar = window.document.getElementById('search-bar')
48 | searchBar.value = webview.src
49 |
50 | // Make the navigation buttons inactive if there is no history
51 | const navBack = window.document.getElementById('search-back')
52 | const navForward = window.document.getElementById('search-forward')
53 | navBack.className = peerWeb.utils.removeClass(navBack, 'inactive')
54 | navForward
55 | .className = peerWeb.utils.removeClass(navForward, 'inactive')
56 | navBack.className = peerWeb.utils.addClass(navBack, 'inactive')
57 | navForward.className = peerWeb.utils.addClass(navForward, 'inactive')
58 |
59 | try {
60 | if (webview.canGoBack()) {
61 | navBack.className = peerWeb.utils.removeClass(navBack, 'inactive')
62 | }
63 |
64 | if (webview.canGoForward()) {
65 | navForward
66 | .className = peerWeb.utils.removeClass(navForward, 'inactive')
67 | }
68 | } catch (e) {
69 | webview.addEventListener('dom-ready', function webviewReady () {
70 | if (webview.canGoBack()) {
71 | navBack.className = peerWeb.utils.removeClass(navBack, 'inactive')
72 | }
73 |
74 | if (webview.canGoForward()) {
75 | navForward
76 | .className = peerWeb.utils.removeClass(navForward, 'inactive')
77 | }
78 | })
79 | }
80 | }
81 | }
82 |
83 | // Do the same thing for tabs that we did for webviews above
84 | const tabs = window.document.getElementsByClassName('tab')
85 | for (let i = 0; i < tabs.length; i++) {
86 | const tab = tabs[i]
87 | tab.className = peerWeb.utils.removeClass(tab, 'selected')
88 | if (peerWeb.utils.getId(tab) === id.toString()) {
89 | tab.className = peerWeb.utils.addClass(tab, 'selected')
90 | }
91 | }
92 |
93 | // Do the same thing for the progressBars
94 | const progressBars = window.document.getElementsByClassName('progress')
95 | for (let i = 0; i < progressBars.length; i++) {
96 | const progressBar = progressBars[i]
97 | progressBar.className = peerWeb.utils.removeClass(progressBar, 'selected')
98 | if (peerWeb.utils.getId(progressBar) === id.toString()) {
99 | progressBar.className = peerWeb.utils.addClass(progressBar, 'selected')
100 | }
101 | }
102 | }
103 |
104 | // remove will take the ID of a tab, and handle all the logic necessary to
105 | // remove the tab and it's dependencies from the UI.
106 | peerWeb.ui.remove = function remove (id) {
107 | // Lets get all of the webviews so we can find the one we need to remove
108 | const webviews = window.document.getElementsByTagName('webview')
109 | // Search through all the webviews until we find the one with the proper id
110 | for (let i = 0; i < webviews.length; i++) {
111 | const webview = webviews[i]
112 | // Check this webview for the id
113 | if (peerWeb.utils.getId(webview) === id) {
114 | // If this webview is the one we are looking for, simply remove it from
115 | // the document
116 | webview.remove()
117 | // Since we have found the webview, there is nothing left to do
118 | break
119 | }
120 | }
121 |
122 | // Do the same thing for the progressBars
123 | const progressBars = window.document.getElementsByClassName('progress')
124 | for (let i = 0; i < progressBars.length; i++) {
125 | const progressBar = progressBars[i]
126 | if (peerWeb.utils.getId(progressBar) === id.toString()) {
127 | progressBar.remove()
128 | break
129 | }
130 | }
131 |
132 | // Now we will do the same exact thing for tabs that we did for the
133 | // webviews above
134 | const tabs = window.document.getElementsByClassName('tab')
135 |
136 | for (let i = 0; i < tabs.length; i++) {
137 | const tab = tabs[i]
138 | if (peerWeb.utils.getId(tab) === id) {
139 | // We will also need to check if this tab is the currently selected
140 | // one. If so, we need to switch the user to another tab before
141 | // deleting the one they are currently using.
142 | if (peerWeb.utils.hasClass(tab, 'selected')) {
143 | if (tabs[i + 1] != null) {
144 | peerWeb.ui.claimOwnership(peerWeb.utils.getId(tabs[i + 1]))
145 | } else if (tabs[i - 1] != null) {
146 | peerWeb.ui.claimOwnership(peerWeb.utils.getId(tabs[i - 1]))
147 | }
148 | }
149 |
150 | tab.remove()
151 |
152 | break
153 | }
154 | }
155 | }
156 |
157 | // add will take the ID of a tab, and handle all the logic necessary to
158 | // create a new tab with that ID and add it to the browser.
159 | peerWeb.ui.add = function add (id) {
160 | // First, we create a new tab element
161 | const newTabElement = window.document.createElement('span')
162 | const tabName = window.document.createElement('span')
163 | let tabNameText = window.document.createTextNode('New Tab')
164 | tabName.appendChild(tabNameText)
165 | tabName.className = 'tab-name'
166 | newTabElement.appendChild(tabName)
167 | newTabElement.id = `tab-${id}`
168 | newTabElement.className = 'tab'
169 | const closeTabElement = window.document.createElement('span')
170 | closeTabElement.appendChild(window.document.createTextNode('x'))
171 | closeTabElement.className = 'close-tab'
172 | newTabElement.appendChild(closeTabElement)
173 |
174 | // Next, we wire up all of the listeners for the new tab
175 | closeTabElement.addEventListener('click', peerWeb.tabs.closeTab)
176 | newTabElement.addEventListener('click', peerWeb.tabs.tabClick)
177 |
178 | // Next, we will create the webview that the tab will own
179 | const newWebViewElement = window.document.createElement('webview')
180 | newWebViewElement.id = `webview-${id}`
181 |
182 | // Register event listeners for the webview to wire up its state to the UI
183 | newWebViewElement.addEventListener('did-stop-loading',
184 | peerWeb.ui.endLoading)
185 | newWebViewElement.addEventListener('did-start-loading',
186 | peerWeb.ui.beginLoading)
187 |
188 | // Next, we create the progress bar that will belong to the webview
189 | const newProgressBar = window.document.createElement('div')
190 | newProgressBar.id = `progress-${id}`
191 | newProgressBar.className = 'progress'
192 | const newProgressComplete = window.document.createElement('div')
193 | newProgressComplete.className = 'complete'
194 | newProgressBar.appendChild(newProgressComplete)
195 |
196 | // Finally, we add everything to the document so the user can see it
197 | window.document.getElementById('tab-row').appendChild(newTabElement)
198 | window.document.getElementById('content').appendChild(newProgressBar)
199 | window.document.getElementById('content').appendChild(newWebViewElement)
200 |
201 | // Give the user something to look at for their new tab
202 | newWebViewElement.src = 'http://will.blankenship.io/peerweb'
203 |
204 | // Wire up the tab's title to the webview
205 | newWebViewElement
206 | .addEventListener('page-title-updated', function updateTabTitle (event) {
207 | tabNameText.remove()
208 | tabNameText = window.document.createTextNode(event.title)
209 | tabName.appendChild(tabNameText)
210 | })
211 |
212 | // If the webview element is currently selected, update the navbar to
213 | // reflect its src when this function is called
214 | function updateNavBar (url) {
215 | // If the webview isn't selected, there is nothing to do
216 | if (!peerWeb.utils.hasClass(newWebViewElement, 'selected')) {
217 | return null
218 | }
219 |
220 | // Update the UI to reflect the new src
221 | return peerWeb.ui.claimOwnership(id)
222 | }
223 |
224 | // Whenever the webview changes its src, update the navbar
225 | newWebViewElement.addEventListener('did-navigate', updateNavBar)
226 | newWebViewElement.addEventListener('did-navigate-in-page', updateNavBar)
227 |
228 | peerWeb.ui.claimOwnership(id)
229 | }
230 |
231 | // beginLoading handles updating the browser state to reflect that a tab has
232 | // started loading new content. It is an event listener that should be
233 | // registered in response to an event emitted from a webview
234 | peerWeb.ui.beginLoading = function beginLoading () {
235 | // `this` refers to the webview that triggered the event
236 | const id = peerWeb.utils.getId(this)
237 | const progressBar = window.document.getElementById(`progress-${id}`)
238 | // We will use the `progress` attribute to calculate how much time has
239 | // passed since the download began. The width of the `completed` bar inside
240 | // the progress bar will be a function of the amount of time that has
241 | // passed
242 | progressBar.setAttribute('progress', Date.now())
243 | progressBar.className = peerWeb.utils.addClass(progressBar, 'loading')
244 | }
245 |
246 | // endLoading handles updating the browser state to reflect that a tab has
247 | // finished loading new content. It is an event listener that should be
248 | // registered in response to an event emitted from a webview
249 | peerWeb.ui.endLoading = function endLoading () {
250 | // `this` refers to the webview that triggered the event
251 | const id = peerWeb.utils.getId(this)
252 | const progressBar = window.document.getElementById(`progress-${id}`)
253 | // We no longer need to track the progress of this download
254 | progressBar.removeAttribute('progress')
255 | progressBar.className = peerWeb.utils.removeClass(progressBar, 'loading')
256 | }
257 |
258 | // navigate will take a url and will update the browser state to prepare for
259 | // the user to be sent to a new webpage. This makes the assumption that the
260 | // navigation is happening on the _currently selected_ tab.
261 | peerWeb.ui.navigate = function navigate (url) {
262 | // collect all the webviews and find which one is currently selected
263 | const webviews = window.document.getElementsByTagName('webview')
264 | // We create the webview variable outside of the for loop, and terminate
265 | // the loop immediately when we find the webview we are looking for. This
266 | // ensures webview will either be the currently selected webview or that
267 | // we will navigate the last tab in the event there is no selected webview
268 | // (which would be a bug)
269 | let webview = null
270 | for (let i = 0; i < webviews.length; i++) {
271 | webview = webviews[i]
272 | if (peerWeb.utils.hasClass(webview, 'selected')) {
273 | break
274 | }
275 | }
276 |
277 | // We have found the webview we are looking for as defined above, now lets
278 | // send it to the page we are looking for
279 | webview.loadURL(url)
280 | }
281 |
282 | // updateProgress will go through the UI and update all progress bars that
283 | // belong to a webview that is loading content and is visible
284 | peerWeb.ui.updateProgress = function updateProgress () {
285 | // Get the currently loading and visible progress bar if one exists.
286 | // There will only ever be one of these at a time, since two progressBars
287 | // can not be visible, and it is possible that the visible webview will not
288 | // be loading so there may not be work to do here
289 | const progressBar = window.document
290 | .querySelector('.progress.selected.loading .complete')
291 |
292 | // If we didn't find a visible progress bar that belonged to a loading
293 | // webview, we don't have any work left to do
294 | if (progressBar == null) {
295 | return null
296 | }
297 |
298 | // We store the initial start time of the webview download as the progress
299 | // attribute on the DOM element of the progress bar, so lets grab that
300 | // and turn it into a JavaScript date.
301 | const started = progressBar.parentElement.getAttribute('progress')
302 |
303 | // Lets get how much time has passed since the download started
304 | const progress = Date.now() - started
305 |
306 | // Lets calculate the width of the progress bar using a logaritmic function
307 | // that starts out having the progress bar be "really" fast, and then slows
308 | // down quickly to inch its way towards 100%. This function will take quite
309 | // a while to reach 100%. You may wonder why the width is not a function of
310 | // the actual download progress and instead a function of time. Well, we
311 | // would be hard pressed to know how much data any one website needs to
312 | // pull over the wire, since a successfully downloaded file could trigger
313 | // the download of even more files. Instead of implementing the extremely
314 | // complex logic of tracking the files pending download, and updating that
315 | // when new files are added and reflecting all of that in the progress bar,
316 | // we opted in for a simpler feedback mechanism.
317 | let width = Math.log(progress) * 10
318 |
319 | // Make sure progress never exceeds 100%
320 | if (width > 99.9) width = 99.9
321 |
322 | // Finally, lets update the width of the progress bar
323 | progressBar.setAttribute('style', `width:${width}`)
324 |
325 | return null
326 | }
327 |
328 | // Create an interval that updates all progress bars that are visible and
329 | // loading. I suspect this will be hot code, and I'm a little concerned about
330 | // querying the DOM in this. We will gather metrics in the future to see how
331 | // this is impacting performance
332 | setInterval(peerWeb.ui.updateProgress, 100)
333 | })()
334 |
--------------------------------------------------------------------------------