mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-21 23:36:17 -04:00
Merge branch 'next' into translate
This commit is contained in:
commit
d78c138dad
136 changed files with 5042 additions and 13894 deletions
60
public/scripts/browser-tabs-connector.js
Normal file
60
public/scripts/browser-tabs-connector.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
class BrowserTabsConnector {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
|
||||
}
|
||||
|
||||
_broadcastSend(message) {
|
||||
this.bc.postMessage(message);
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast:', e.data)
|
||||
switch (e.data.type) {
|
||||
case 'self-display-name-changed':
|
||||
Events.fire('self-display-name-changed', e.data.detail);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static peerIsSameBrowser(peerId) {
|
||||
let peerIdsBrowser = JSON.parse(localStorage.getItem("peer_ids_browser"));
|
||||
return peerIdsBrowser
|
||||
? peerIdsBrowser.indexOf(peerId) !== -1
|
||||
: false;
|
||||
}
|
||||
|
||||
static async addPeerIdToLocalStorage() {
|
||||
const peerId = sessionStorage.getItem("peer_id");
|
||||
if (!peerId) return false;
|
||||
|
||||
let peerIdsBrowser = [];
|
||||
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peer_ids_browser"));
|
||||
|
||||
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
|
||||
peerIdsBrowser.push(peerId);
|
||||
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
|
||||
localStorage.setItem("peer_ids_browser", JSON.stringify(peerIdsBrowser));
|
||||
|
||||
return peerIdsBrowser;
|
||||
}
|
||||
|
||||
static async removePeerIdFromLocalStorage(peerId) {
|
||||
let peerIdsBrowser = JSON.parse(localStorage.getItem("peer_ids_browser"));
|
||||
const index = peerIdsBrowser.indexOf(peerId);
|
||||
peerIdsBrowser.splice(index, 1);
|
||||
localStorage.setItem("peer_ids_browser", JSON.stringify(peerIdsBrowser));
|
||||
return peerId;
|
||||
}
|
||||
|
||||
|
||||
static async removeOtherPeerIdsFromLocalStorage() {
|
||||
const peerId = sessionStorage.getItem("peer_id");
|
||||
if (!peerId) return false;
|
||||
|
||||
let peerIdsBrowser = [peerId];
|
||||
localStorage.setItem("peer_ids_browser", JSON.stringify(peerIdsBrowser));
|
||||
return peerIdsBrowser;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,8 @@ class Localization {
|
|||
? storedLanguageCode
|
||||
: Localization.systemLocale;
|
||||
|
||||
Localization.setTranslation(Localization.initialLocale)
|
||||
Localization
|
||||
.setTranslation(Localization.initialLocale)
|
||||
.then(_ => {
|
||||
console.log("Initial translation successful.");
|
||||
Events.fire("initial-translation-loaded");
|
||||
|
@ -50,7 +51,8 @@ class Localization {
|
|||
|
||||
if (Localization.isRTLLanguage(locale)) {
|
||||
htmlRootNode.setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
htmlRootNode.removeAttribute('dir');
|
||||
}
|
||||
|
||||
|
@ -112,13 +114,9 @@ class Localization {
|
|||
let attr = attrs[i];
|
||||
if (attr === "text") {
|
||||
element.innerText = Localization.getTranslation(key);
|
||||
} else {
|
||||
if (attr.startsWith("data-")) {
|
||||
let dataAttr = attr.substring(5);
|
||||
element.dataset.dataAttr = Localization.getTranslation(key, attr);
|
||||
} {
|
||||
element.setAttribute(attr, Localization.getTranslation(key, attr));
|
||||
}
|
||||
}
|
||||
else {
|
||||
element.setAttribute(attr, Localization.getTranslation(key, attr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +154,8 @@ class Localization {
|
|||
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
|
||||
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`)
|
||||
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
console.warn("Missing translation in default language:", key, attr);
|
||||
}
|
||||
}
|
||||
|
|
180
public/scripts/main.js
Normal file
180
public/scripts/main.js
Normal file
|
@ -0,0 +1,180 @@
|
|||
class PairDrop {
|
||||
constructor() {
|
||||
this.$header = $$('header.opacity-0');
|
||||
this.$center = $$('#center');
|
||||
this.$footer = $$('footer');
|
||||
this.$xNoPeers = $$('x-no-peers');
|
||||
this.$headerNotificationButton = $('notification');
|
||||
this.$editPairedDevicesHeaderBtn = $('edit-paired-devices');
|
||||
this.$footerInstructionsPairedDevices = $$('.discovery-wrapper .badge-room-secret');
|
||||
this.$head = $$('head');
|
||||
this.$installBtn = $('install');
|
||||
|
||||
this.registerServiceWorker();
|
||||
|
||||
Events.on('beforeinstallprompt', e => this.onPwaInstallable(e));
|
||||
|
||||
const persistentStorage = new PersistentStorage();
|
||||
const themeUI = new ThemeUI();
|
||||
const backgroundCanvas = new BackgroundCanvas();
|
||||
|
||||
Events.on('initial-translation-loaded', _ => {
|
||||
// FooterUI needs translations
|
||||
const footerUI = new FooterUI();
|
||||
|
||||
Events.on('fade-in-ui', _ => this.fadeInUI())
|
||||
Events.on('fade-in-header', _ => this.fadeInHeader())
|
||||
|
||||
// Evaluate UI elements and fade in UI
|
||||
this.evaluateUI();
|
||||
|
||||
// Load deferred assets
|
||||
this.loadDeferredAssets();
|
||||
});
|
||||
|
||||
// Translate page -> fires 'initial-translation-loaded' on finish
|
||||
const localization = new Localization();
|
||||
}
|
||||
|
||||
registerServiceWorker() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register('service-worker.js')
|
||||
.then(serviceWorker => {
|
||||
console.log('Service Worker registered');
|
||||
window.serviceWorker = serviceWorker
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onPwaInstallable(e) {
|
||||
if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
|
||||
// only display install btn when not installed
|
||||
this.$installBtn.removeAttribute('hidden');
|
||||
this.$installBtn.addEventListener('click', () => {
|
||||
this.$installBtn.setAttribute('hidden', true);
|
||||
e.prompt();
|
||||
});
|
||||
}
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
evaluateUI() {
|
||||
// Check whether notification permissions have already been granted
|
||||
if ('Notification' in window && Notification.permission !== 'granted') {
|
||||
this.$headerNotificationButton.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
PersistentStorage
|
||||
.getAllRoomSecrets()
|
||||
.then(roomSecrets => {
|
||||
if (roomSecrets.length > 0) {
|
||||
this.$editPairedDevicesHeaderBtn.removeAttribute('hidden');
|
||||
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
Events.fire('evaluate-footer-badges');
|
||||
Events.fire('fade-in-header');
|
||||
});
|
||||
}
|
||||
|
||||
fadeInUI() {
|
||||
this.$center.classList.remove('opacity-0');
|
||||
this.$footer.classList.remove('opacity-0');
|
||||
|
||||
// Prevent flickering on load
|
||||
setTimeout(() => {
|
||||
this.$xNoPeers.classList.remove('no-animation-on-load');
|
||||
}, 600);
|
||||
}
|
||||
|
||||
fadeInHeader() {
|
||||
this.$header.classList.remove('opacity-0');
|
||||
}
|
||||
|
||||
loadDeferredAssets() {
|
||||
console.log("Load deferred assets");
|
||||
this.deferredStyles = [
|
||||
"styles/deferred-styles.css"
|
||||
];
|
||||
this.deferredScripts = [
|
||||
"scripts/browser-tabs-connector.js",
|
||||
"scripts/util.js",
|
||||
"scripts/network.js",
|
||||
"scripts/ui.js",
|
||||
"scripts/qr-code.min.js",
|
||||
"scripts/zip.min.js",
|
||||
"scripts/no-sleep.min.js"
|
||||
];
|
||||
this.deferredStyles.forEach(url => this.loadStyleSheet(url, _ => this.onStyleLoaded(url)))
|
||||
this.deferredScripts.forEach(url => this.loadScript(url, _ => this.onScriptLoaded(url)))
|
||||
}
|
||||
|
||||
loadStyleSheet(url, callback) {
|
||||
let stylesheet = document.createElement('link');
|
||||
stylesheet.rel = 'stylesheet';
|
||||
stylesheet.href = url;
|
||||
stylesheet.type = 'text/css';
|
||||
stylesheet.onload = callback;
|
||||
this.$head.appendChild(stylesheet);
|
||||
}
|
||||
|
||||
loadScript(url, callback) {
|
||||
let script = document.createElement("script");
|
||||
script.src = url;
|
||||
script.onload = callback;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
onStyleLoaded(url) {
|
||||
// remove entry from array
|
||||
const index = this.deferredStyles.indexOf(url);
|
||||
if (index !== -1) {
|
||||
this.deferredStyles.splice(index, 1);
|
||||
}
|
||||
this.onAssetLoaded();
|
||||
}
|
||||
|
||||
onScriptLoaded(url) {
|
||||
// remove entry from array
|
||||
const index = this.deferredScripts.indexOf(url);
|
||||
if (index !== -1) {
|
||||
this.deferredScripts.splice(index, 1);
|
||||
}
|
||||
this.onAssetLoaded();
|
||||
}
|
||||
|
||||
onAssetLoaded() {
|
||||
if (this.deferredScripts.length || this.deferredStyles.length) return;
|
||||
|
||||
console.log("Loading of deferred assets completed. Start UI hydration.");
|
||||
|
||||
this.hydrate();
|
||||
}
|
||||
|
||||
hydrate() {
|
||||
const peersUI = new PeersUI();
|
||||
const languageSelectDialog = new LanguageSelectDialog();
|
||||
const receiveFileDialog = new ReceiveFileDialog();
|
||||
const receiveRequestDialog = new ReceiveRequestDialog();
|
||||
const sendTextDialog = new SendTextDialog();
|
||||
const receiveTextDialog = new ReceiveTextDialog();
|
||||
const pairDeviceDialog = new PairDeviceDialog();
|
||||
const clearDevicesDialog = new EditPairedDevicesDialog();
|
||||
const publicRoomDialog = new PublicRoomDialog();
|
||||
const base64ZipDialog = new Base64ZipDialog();
|
||||
const toast = new Toast();
|
||||
const notifications = new Notifications();
|
||||
const networkStatusUI = new NetworkStatusUI();
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
const webFileHandlersUI = new WebFileHandlersUI();
|
||||
const noSleepUI = new NoSleepUI();
|
||||
const broadCast = new BrowserTabsConnector();
|
||||
const server = new ServerConnection();
|
||||
const peers = new PeersManager(server);
|
||||
console.log("UI hydrated.")
|
||||
}
|
||||
}
|
||||
|
||||
const pairDrop = new PairDrop();
|
|
@ -1,26 +1,15 @@
|
|||
window.URL = window.URL || window.webkitURL;
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
|
||||
|
||||
window.hiddenProperty = 'hidden' in document ? 'hidden' :
|
||||
'webkitHidden' in document ? 'webkitHidden' :
|
||||
'mozHidden' in document ? 'mozHidden' :
|
||||
null;
|
||||
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
|
||||
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
|
||||
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
|
||||
null;
|
||||
|
||||
class ServerConnection {
|
||||
|
||||
constructor() {
|
||||
this._connect();
|
||||
Events.on('pagehide', _ => this._disconnect());
|
||||
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
|
||||
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
|
||||
Events.on(window.visibilityChangeEvent, _ => this._onVisibilityChange());
|
||||
|
||||
if (navigator.connection) {
|
||||
navigator.connection.addEventListener('change', _ => this._reconnect());
|
||||
}
|
||||
|
||||
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
|
||||
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
||||
Events.on('join-ip-room', _ => this.send({ type: 'join-ip-room'}));
|
||||
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
||||
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||
|
@ -33,6 +22,49 @@ class ServerConnection {
|
|||
|
||||
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
||||
Events.on('online', _ => this._connect());
|
||||
|
||||
this._getConfig().then(() => this._connect());
|
||||
}
|
||||
|
||||
_getConfig() {
|
||||
console.log("Loading config...")
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener("load", () => {
|
||||
if (xhr.status === 200) {
|
||||
// Config received
|
||||
let config = JSON.parse(xhr.responseText);
|
||||
console.log("Config loaded:", config)
|
||||
this._config = config;
|
||||
Events.fire('config', config);
|
||||
resolve()
|
||||
} else if (xhr.status < 200 || xhr.status >= 300) {
|
||||
retry(xhr);
|
||||
}
|
||||
})
|
||||
|
||||
xhr.addEventListener("error", _ => {
|
||||
retry(xhr);
|
||||
});
|
||||
|
||||
function retry(request) {
|
||||
setTimeout(function () {
|
||||
openAndSend(request)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function openAndSend() {
|
||||
xhr.open('GET', 'config');
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
openAndSend(xhr);
|
||||
})
|
||||
}
|
||||
|
||||
_setWsConfig(wsConfig) {
|
||||
this._wsConfig = wsConfig;
|
||||
Events.fire('ws-config', wsConfig);
|
||||
}
|
||||
|
||||
_connect() {
|
||||
|
@ -69,7 +101,7 @@ class ServerConnection {
|
|||
|
||||
_onPairDeviceJoin(pairKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
|
||||
setTimeout(() => this._onPairDeviceJoin(pairKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', pairKey: pairKey });
|
||||
|
@ -85,7 +117,7 @@ class ServerConnection {
|
|||
|
||||
_onJoinPublicRoom(roomId, createIfInvalid) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
|
||||
setTimeout(() => this._onJoinPublicRoom(roomId), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
|
||||
|
@ -93,22 +125,18 @@ class ServerConnection {
|
|||
|
||||
_onLeavePublicRoom() {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onLeavePublicRoom(), 1000);
|
||||
setTimeout(() => this._onLeavePublicRoom(), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'leave-public-room' });
|
||||
}
|
||||
|
||||
_setRtcConfig(config) {
|
||||
window.rtcConfig = config;
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS receive:', msg);
|
||||
switch (msg.type) {
|
||||
case 'rtc-config':
|
||||
this._setRtcConfig(msg.config);
|
||||
case 'ws-config':
|
||||
this._setWsConfig(msg.wsConfig);
|
||||
break;
|
||||
case 'peers':
|
||||
this._onPeers(msg);
|
||||
|
@ -158,6 +186,25 @@ class ServerConnection {
|
|||
case 'public-room-left':
|
||||
Events.fire('public-room-left');
|
||||
break;
|
||||
case 'request':
|
||||
case 'header':
|
||||
case 'partition':
|
||||
case 'partition-received':
|
||||
case 'progress':
|
||||
case 'files-transfer-response':
|
||||
case 'file-transfer-complete':
|
||||
case 'message-transfer-complete':
|
||||
case 'text':
|
||||
case 'display-name-changed':
|
||||
case 'ws-chunk':
|
||||
// ws-fallback
|
||||
if (this._wsConfig.wsFallback) {
|
||||
Events.fire('ws-relay', JSON.stringify(msg));
|
||||
}
|
||||
else {
|
||||
console.log("WS receive: message type is for websocket fallback only but websocket fallback is not activated on this instance.")
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error('WS receive: unknown message type', msg);
|
||||
}
|
||||
|
@ -175,45 +222,57 @@ class ServerConnection {
|
|||
|
||||
_onDisplayName(msg) {
|
||||
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
||||
sessionStorage.setItem('peer_id', msg.message.peerId);
|
||||
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
|
||||
sessionStorage.setItem('peer_id', msg.peerId);
|
||||
sessionStorage.setItem('peer_id_hash', msg.peerIdHash);
|
||||
|
||||
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
||||
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
||||
if (!peerId) return;
|
||||
console.log("successfully added peerId to localStorage");
|
||||
BrowserTabsConnector
|
||||
.addPeerIdToLocalStorage()
|
||||
.then(peerId => {
|
||||
if (!peerId) return;
|
||||
console.log("successfully added peerId to localStorage");
|
||||
|
||||
// Only now join rooms
|
||||
Events.fire('join-ip-room');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
// Only now join rooms
|
||||
Events.fire('join-ip-room');
|
||||
PersistentStorage.getAllRoomSecrets()
|
||||
.then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
// Check whether the instance specifies another signaling server otherwise use the current instance for signaling
|
||||
let wsServerDomain = this._config.signalingServer
|
||||
? this._config.signalingServer
|
||||
: location.host + location.pathname;
|
||||
|
||||
let wsUrl = new URL(protocol + '://' + wsServerDomain + 'server');
|
||||
|
||||
wsUrl.searchParams.append('webrtc_supported', window.isRtcSupported ? 'true' : 'false');
|
||||
|
||||
const peerId = sessionStorage.getItem('peer_id');
|
||||
const peerIdHash = sessionStorage.getItem('peer_id_hash');
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
wsUrl.searchParams.append('peer_id', peerId);
|
||||
wsUrl.searchParams.append('peer_id_hash', peerIdHash);
|
||||
}
|
||||
return ws_url.toString();
|
||||
|
||||
return wsUrl.toString();
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
|
||||
const peerId = sessionStorage.getItem('peer_id');
|
||||
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
||||
console.log("successfully removed peerId from localStorage");
|
||||
});
|
||||
BrowserTabsConnector
|
||||
.removePeerIdFromLocalStorage(peerId)
|
||||
.then(_ => {
|
||||
console.log("successfully removed peerId from localStorage");
|
||||
});
|
||||
|
||||
if (!this._socket) return;
|
||||
|
||||
|
@ -229,7 +288,7 @@ class ServerConnection {
|
|||
setTimeout(() => {
|
||||
this._isReconnect = true;
|
||||
Events.fire('ws-disconnected');
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
this._reconnectTimer = setTimeout(() => this._connect(), 1000);
|
||||
}, 100); //delay for 100ms to prevent flickering on page reload
|
||||
}
|
||||
|
||||
|
@ -281,6 +340,9 @@ class Peer {
|
|||
this._send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
// Is overwritten in expanding classes
|
||||
_send(message) {}
|
||||
|
||||
sendDisplayName(displayName) {
|
||||
this.sendJSON({type: 'display-name-changed', displayName: displayName});
|
||||
}
|
||||
|
@ -297,16 +359,27 @@ class Peer {
|
|||
return this._roomIds['secret'];
|
||||
}
|
||||
|
||||
_regenerationOfPairSecretNeeded() {
|
||||
return this._getPairSecret() && this._getPairSecret().length !== 256
|
||||
}
|
||||
|
||||
_getRoomTypes() {
|
||||
return Object.keys(this._roomIds);
|
||||
}
|
||||
|
||||
_updateRoomIds(roomType, roomId) {
|
||||
const roomTypeIsSecret = roomType === "secret";
|
||||
const roomIdIsNotPairSecret = this._getPairSecret() !== roomId;
|
||||
|
||||
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
|
||||
// -> do not delete duplicates and do not regenerate room secrets
|
||||
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
|
||||
if (!this._isSameBrowser()
|
||||
&& roomTypeIsSecret
|
||||
&& this._isPaired()
|
||||
&& roomIdIsNotPairSecret) {
|
||||
// multiple roomSecrets with same peer -> delete old roomSecret
|
||||
PersistentStorage.deleteRoomSecret(this._getPairSecret())
|
||||
PersistentStorage
|
||||
.deleteRoomSecret(this._getPairSecret())
|
||||
.then(deletedRoomSecret => {
|
||||
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
||||
});
|
||||
|
@ -314,8 +387,13 @@ class Peer {
|
|||
|
||||
this._roomIds[roomType] = roomId;
|
||||
|
||||
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
|
||||
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
|
||||
if (!this._isSameBrowser()
|
||||
&& roomTypeIsSecret
|
||||
&& this._isPaired()
|
||||
&& this._regenerationOfPairSecretNeeded()
|
||||
&& this._isCaller) {
|
||||
// increase security by initiating the increase of the roomSecret length
|
||||
// from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
|
||||
console.log('RoomSecret is regenerated to increase security')
|
||||
Events.fire('regenerate-room-secret', this._getPairSecret());
|
||||
}
|
||||
|
@ -336,7 +414,8 @@ class Peer {
|
|||
return;
|
||||
}
|
||||
|
||||
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
|
||||
PersistentStorage
|
||||
.getRoomSecretEntry(this._getPairSecret())
|
||||
.then(roomSecretEntry => {
|
||||
const autoAccept = roomSecretEntry
|
||||
? roomSecretEntry.entry.auto_accept
|
||||
|
@ -367,13 +446,16 @@ class Peer {
|
|||
if (width && height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else if (width) {
|
||||
}
|
||||
else if (width) {
|
||||
canvas.width = width;
|
||||
canvas.height = Math.floor(imageHeight * width / imageWidth)
|
||||
} else if (height) {
|
||||
}
|
||||
else if (height) {
|
||||
canvas.width = Math.floor(imageWidth * height / imageHeight);
|
||||
canvas.height = height;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
canvas.width = imageWidth;
|
||||
canvas.height = imageHeight
|
||||
}
|
||||
|
@ -385,9 +467,11 @@ class Peer {
|
|||
resolve(dataUrl);
|
||||
}
|
||||
image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`);
|
||||
}).then(dataUrl => {
|
||||
})
|
||||
.then(dataUrl => {
|
||||
return dataUrl;
|
||||
}).catch(e => console.error(e));
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
}
|
||||
|
||||
async requestFileTransfer(files) {
|
||||
|
@ -622,7 +706,8 @@ class Peer {
|
|||
this._busy = false;
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
|
||||
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this._dequeueFile();
|
||||
}
|
||||
}
|
||||
|
@ -673,9 +758,12 @@ class Peer {
|
|||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomId, rtcConfig) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomId);
|
||||
|
||||
this.rtcSupported = true;
|
||||
this.rtcConfig = rtcConfig
|
||||
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._connect();
|
||||
}
|
||||
|
@ -685,13 +773,14 @@ class RTCPeer extends Peer {
|
|||
|
||||
if (this._isCaller) {
|
||||
this._openChannel();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this._conn.ondatachannel = e => this._onChannelOpened(e);
|
||||
}
|
||||
}
|
||||
|
||||
_openConnection() {
|
||||
this._conn = new RTCPeerConnection(window.rtcConfig);
|
||||
this._conn = new RTCPeerConnection(this.rtcConfig);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onicecandidateerror = e => this._onError(e);
|
||||
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
|
||||
|
@ -708,14 +797,16 @@ class RTCPeer extends Peer {
|
|||
channel.onopen = e => this._onChannelOpened(e);
|
||||
channel.onerror = e => this._onError(e);
|
||||
|
||||
this._conn.createOffer()
|
||||
this._conn
|
||||
.createOffer()
|
||||
.then(d => this._onDescription(d))
|
||||
.catch(e => this._onError(e));
|
||||
}
|
||||
|
||||
_onDescription(description) {
|
||||
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
|
||||
this._conn.setLocalDescription(description)
|
||||
this._conn
|
||||
.setLocalDescription(description)
|
||||
.then(_ => this._sendSignal({ sdp: description }))
|
||||
.catch(e => this._onError(e));
|
||||
}
|
||||
|
@ -729,16 +820,20 @@ class RTCPeer extends Peer {
|
|||
if (!this._conn) this._connect();
|
||||
|
||||
if (message.sdp) {
|
||||
this._conn.setRemoteDescription(message.sdp)
|
||||
.then( _ => {
|
||||
this._conn
|
||||
.setRemoteDescription(message.sdp)
|
||||
.then(_ => {
|
||||
if (message.sdp.type === 'offer') {
|
||||
return this._conn.createAnswer()
|
||||
return this._conn
|
||||
.createAnswer()
|
||||
.then(d => this._onDescription(d));
|
||||
}
|
||||
})
|
||||
.catch(e => this._onError(e));
|
||||
} else if (message.ice) {
|
||||
this._conn.addIceCandidate(new RTCIceCandidate(message.ice))
|
||||
}
|
||||
else if (message.ice) {
|
||||
this._conn
|
||||
.addIceCandidate(new RTCIceCandidate(message.ice))
|
||||
.catch(e => this._onError(e));
|
||||
}
|
||||
}
|
||||
|
@ -879,6 +974,48 @@ class RTCPeer extends Peer {
|
|||
}
|
||||
}
|
||||
|
||||
class WSPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||
super(serverConnection, isCaller, peerId, roomType, roomId);
|
||||
|
||||
this.rtcSupported = false;
|
||||
|
||||
if (!this._isCaller) return; // we will listen for a caller
|
||||
this._sendSignal();
|
||||
}
|
||||
|
||||
_send(chunk) {
|
||||
this.sendJSON({
|
||||
type: 'ws-chunk',
|
||||
chunk: arrayBufferToBase64(chunk)
|
||||
});
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
message.to = this._peerId;
|
||||
message.roomType = this._getRoomTypes()[0];
|
||||
message.roomId = this._roomIds[this._getRoomTypes()[0]];
|
||||
this._server.send(message);
|
||||
}
|
||||
|
||||
_sendSignal(connected = false) {
|
||||
this.sendJSON({type: 'signal', connected: connected});
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
this._peerId = message.sender.id;
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (message.connected) return;
|
||||
this._sendSignal(true);
|
||||
}
|
||||
|
||||
getConnectionHash() {
|
||||
// Todo: implement SubtleCrypto asymmetric encryption and create connectionHash from public keys
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
class PeersManager {
|
||||
|
||||
constructor(serverConnection) {
|
||||
|
@ -902,10 +1039,17 @@ class PeersManager {
|
|||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
|
||||
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
|
||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
|
||||
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||
Events.on('ws-config', e => this._onWsConfig(e.detail));
|
||||
}
|
||||
|
||||
_onWsConfig(wsConfig) {
|
||||
this._wsConfig = wsConfig;
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
|
@ -913,9 +1057,10 @@ class PeersManager {
|
|||
this.peers[peerId].onServerMessage(message);
|
||||
}
|
||||
|
||||
_refreshPeer(peer, roomType, roomId) {
|
||||
if (!peer) return false;
|
||||
_refreshPeer(peerId, roomType, roomId) {
|
||||
if (!this._peerExists(peerId)) return false;
|
||||
|
||||
const peer = this.peers[peerId];
|
||||
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
|
||||
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
|
||||
|
||||
|
@ -933,26 +1078,42 @@ class PeersManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer) {
|
||||
this._refreshPeer(peer, roomType, roomId);
|
||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
|
||||
if (this._peerExists(peerId)) {
|
||||
this._refreshPeer(peerId, roomType, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
|
||||
if (window.isRtcSupported && rtcSupported) {
|
||||
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId, this._wsConfig.rtcConfig);
|
||||
}
|
||||
else if (this._wsConfig.wsFallback) {
|
||||
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
|
||||
}
|
||||
else {
|
||||
console.warn("Websocket fallback is not activated on this instance.\n" +
|
||||
"Activate WebRTC in this browser or ask the admin of this instance to activate the websocket fallback.")
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerJoined(message) {
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId);
|
||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported);
|
||||
}
|
||||
|
||||
_onPeers(message) {
|
||||
message.peers.forEach(peer => {
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId);
|
||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported);
|
||||
})
|
||||
}
|
||||
|
||||
_onWsRelay(message) {
|
||||
if (!this._wsConfig.wsFallback) return;
|
||||
|
||||
const messageJSON = JSON.parse(message);
|
||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
||||
this.peers[messageJSON.sender.id]._onMessage(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
|
@ -978,6 +1139,9 @@ class PeersManager {
|
|||
}
|
||||
|
||||
_onPeerLeft(message) {
|
||||
if (this._peerExists(message.peerId) && this._webRtcSupported(message.peerId)) {
|
||||
console.log('WSPeer left:', message.peerId);
|
||||
}
|
||||
if (message.disconnect === true) {
|
||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
|
||||
|
@ -985,10 +1149,12 @@ class PeersManager {
|
|||
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
|
||||
// Tidy up peerIds in localStorage
|
||||
if (Object.keys(this.peers).length === 0) {
|
||||
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
|
||||
if (!peerIds) return;
|
||||
console.log("successfully removed other peerIds from localStorage");
|
||||
});
|
||||
BrowserTabsConnector
|
||||
.removeOtherPeerIdsFromLocalStorage()
|
||||
.then(peerIds => {
|
||||
if (!peerIds) return;
|
||||
console.log("successfully removed other peerIds from localStorage");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -997,6 +1163,24 @@ class PeersManager {
|
|||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
|
||||
_peerExists(peerId) {
|
||||
return !!this.peers[peerId];
|
||||
}
|
||||
|
||||
_webRtcSupported(peerId) {
|
||||
return this.peers[peerId].rtcSupported
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
if (!this._wsConfig || !this._wsConfig.wsFallback) return;
|
||||
|
||||
for (const peerId in this.peers) {
|
||||
if (!this._webRtcSupported(peerId)) {
|
||||
Events.fire('peer-disconnected', peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
|
@ -1038,16 +1222,19 @@ class PeersManager {
|
|||
|
||||
if (peer._getRoomTypes().length > 1) {
|
||||
peer._removeRoomType(roomType);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
Events.fire('peer-disconnected', peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomSecretRegenerated(message) {
|
||||
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
|
||||
console.log("successfully regenerated room secret");
|
||||
Events.fire("room-secrets", [message.newRoomSecret]);
|
||||
})
|
||||
PersistentStorage
|
||||
.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret)
|
||||
.then(_ => {
|
||||
console.log("successfully regenerated room secret");
|
||||
Events.fire("room-secrets", [message.newRoomSecret]);
|
||||
})
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
|
@ -1173,17 +1360,3 @@ class FileDigester {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
class Events {
|
||||
static fire(type, detail = {}) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback, options = false) {
|
||||
return window.addEventListener(type, callback, options);
|
||||
}
|
||||
|
||||
static off(type, callback, options = false) {
|
||||
return window.removeEventListener(type, callback, options);
|
||||
}
|
||||
}
|
||||
|
|
299
public/scripts/persistent-storage.js
Normal file
299
public/scripts/persistent-storage.js
Normal file
|
@ -0,0 +1,299 @@
|
|||
class PersistentStorage {
|
||||
constructor() {
|
||||
if (!('indexedDB' in window)) {
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
return;
|
||||
}
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
|
||||
DBOpenRequest.onerror = e => {
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
console.log('Error initializing database: ');
|
||||
console.log(e)
|
||||
};
|
||||
DBOpenRequest.onsuccess = _ => {
|
||||
console.log('Database initialised.');
|
||||
};
|
||||
DBOpenRequest.onupgradeneeded = e => {
|
||||
const db = e.target.result;
|
||||
const txn = e.target.transaction;
|
||||
|
||||
db.onerror = e => console.log('Error loading database: ' + e);
|
||||
|
||||
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
|
||||
|
||||
if (e.oldVersion === 0) {
|
||||
// initiate v1
|
||||
db.createObjectStore('keyval');
|
||||
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
|
||||
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
|
||||
}
|
||||
if (e.oldVersion <= 1) {
|
||||
// migrate to v2
|
||||
db.createObjectStore('share_target_files');
|
||||
}
|
||||
if (e.oldVersion <= 2) {
|
||||
// migrate to v3
|
||||
db.deleteObjectStore('share_target_files');
|
||||
db.createObjectStore('share_target_files', {autoIncrement: true});
|
||||
}
|
||||
if (e.oldVersion <= 3) {
|
||||
// migrate to v4
|
||||
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
|
||||
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
|
||||
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static logBrowserNotCapable() {
|
||||
console.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed.");
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.put(value, key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Added key-pair: ${key} - ${value}`);
|
||||
resolve(value);
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = e => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static get(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readonly');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.get(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
|
||||
resolve(objectStoreRequest.result);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = e => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static delete(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.delete(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted key: ${key}`);
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = e => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static addRoomSecret(roomSecret, displayName, deviceName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.add({
|
||||
'secret': roomSecret,
|
||||
'display_name': displayName,
|
||||
'device_name': deviceName,
|
||||
'auto_accept': false
|
||||
});
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = e => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async getAllRoomSecrets() {
|
||||
try {
|
||||
const roomSecrets = await this.getAllRoomSecretEntries();
|
||||
let secrets = [];
|
||||
for (let i = 0; i < roomSecrets.length; i++) {
|
||||
secrets.push(roomSecrets[i].secret);
|
||||
}
|
||||
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
return(secrets);
|
||||
} catch (e) {
|
||||
this.logBrowserNotCapable();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static getAllRoomSecretEntries() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readonly');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.getAll();
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
resolve(e.target.result);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static getRoomSecretEntry(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readonly');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
const key = e.target.result;
|
||||
if (!key) {
|
||||
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestRetrieval = objectStore.get(key);
|
||||
objectStoreRequestRetrieval.onsuccess = e => {
|
||||
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
|
||||
resolve({
|
||||
"entry": e.target.result,
|
||||
"key": key
|
||||
});
|
||||
}
|
||||
objectStoreRequestRetrieval.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static deleteRoomSecret(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
if (!e.target.result) {
|
||||
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const key = e.target.result;
|
||||
const objectStoreRequestDeletion = objectStore.delete(key);
|
||||
objectStoreRequestDeletion.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted room_secret: ${key}`);
|
||||
resolve(roomSecret);
|
||||
}
|
||||
objectStoreRequestDeletion.onerror = e => {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = e => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static clearRoomSecrets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.clear();
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log('Request successful. All room_secrets cleared');
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = e => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
|
||||
}
|
||||
|
||||
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
|
||||
}
|
||||
|
||||
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
const db = e.target.result;
|
||||
this.getRoomSecretEntry(roomSecret)
|
||||
.then(roomSecretEntry => {
|
||||
if (!roomSecretEntry) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const transaction = db.transaction('room_secrets', 'readwrite');
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
|
||||
const updatedRoomSecretEntry = {
|
||||
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
|
||||
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
|
||||
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
|
||||
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
|
||||
};
|
||||
|
||||
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
|
||||
|
||||
objectStoreRequestUpdate.onsuccess = e => {
|
||||
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
|
||||
resolve({
|
||||
"entry": updatedRoomSecretEntry,
|
||||
"key": roomSecretEntry.key
|
||||
});
|
||||
}
|
||||
|
||||
objectStoreRequestUpdate.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
.catch(e => reject(e));
|
||||
};
|
||||
|
||||
DBOpenRequest.onerror = e => reject(e);
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
(function(){
|
||||
|
||||
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
|
||||
const $themeAuto = document.getElementById('theme-auto');
|
||||
const $themeLight = document.getElementById('theme-light');
|
||||
const $themeDark = document.getElementById('theme-dark');
|
||||
|
||||
let currentTheme = localStorage.getItem('theme');
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
setModeToDark();
|
||||
} else if (currentTheme === 'light') {
|
||||
setModeToLight();
|
||||
}
|
||||
|
||||
$themeAuto.addEventListener('click', _ => {
|
||||
if (currentTheme) {
|
||||
setModeToAuto();
|
||||
} else {
|
||||
setModeToDark();
|
||||
}
|
||||
});
|
||||
$themeLight.addEventListener('click', _ => {
|
||||
if (currentTheme !== 'light') {
|
||||
setModeToLight();
|
||||
} else {
|
||||
setModeToAuto();
|
||||
}
|
||||
});
|
||||
$themeDark.addEventListener('click', _ => {
|
||||
if (currentTheme !== 'dark') {
|
||||
setModeToDark();
|
||||
} else {
|
||||
setModeToLight();
|
||||
}
|
||||
});
|
||||
|
||||
function setModeToDark() {
|
||||
document.body.classList.remove('light-theme');
|
||||
document.body.classList.add('dark-theme');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
currentTheme = 'dark';
|
||||
|
||||
$themeAuto.classList.remove("selected");
|
||||
$themeLight.classList.remove("selected");
|
||||
$themeDark.classList.add("selected");
|
||||
}
|
||||
|
||||
function setModeToLight() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.add('light-theme');
|
||||
localStorage.setItem('theme', 'light');
|
||||
currentTheme = 'light';
|
||||
|
||||
$themeAuto.classList.remove("selected");
|
||||
$themeLight.classList.add("selected");
|
||||
$themeDark.classList.remove("selected");
|
||||
}
|
||||
|
||||
function setModeToAuto() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.remove('light-theme');
|
||||
if (prefersDarkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
} else if (prefersLightTheme) {
|
||||
document.body.classList.add('light-theme');
|
||||
}
|
||||
localStorage.removeItem('theme');
|
||||
currentTheme = undefined;
|
||||
|
||||
$themeAuto.classList.add("selected");
|
||||
$themeLight.classList.remove("selected");
|
||||
$themeDark.classList.remove("selected");
|
||||
}
|
||||
|
||||
})();
|
286
public/scripts/ui-main.js
Normal file
286
public/scripts/ui-main.js
Normal file
|
@ -0,0 +1,286 @@
|
|||
// Selector shortcuts
|
||||
const $ = query => document.getElementById(query);
|
||||
const $$ = query => document.querySelector(query);
|
||||
|
||||
// Event listener shortcuts
|
||||
class Events {
|
||||
static fire(type, detail = {}) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback, options) {
|
||||
return window.addEventListener(type, callback, options);
|
||||
}
|
||||
|
||||
static off(type, callback, options) {
|
||||
return window.removeEventListener(type, callback, options);
|
||||
}
|
||||
}
|
||||
|
||||
// UIs needed on start
|
||||
class ThemeUI {
|
||||
|
||||
constructor() {
|
||||
this.prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
this.prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
|
||||
this.$themeAutoBtn = document.getElementById('theme-auto');
|
||||
this.$themeLightBtn = document.getElementById('theme-light');
|
||||
this.$themeDarkBtn = document.getElementById('theme-dark');
|
||||
|
||||
let currentTheme = this.getCurrentTheme();
|
||||
if (currentTheme === 'dark') {
|
||||
this.setModeToDark();
|
||||
} else if (currentTheme === 'light') {
|
||||
this.setModeToLight();
|
||||
}
|
||||
|
||||
this.$themeAutoBtn.addEventListener('click', _ => this.onClickAuto());
|
||||
this.$themeLightBtn.addEventListener('click', _ => this.onClickLight());
|
||||
this.$themeDarkBtn.addEventListener('click', _ => this.onClickDark());
|
||||
}
|
||||
|
||||
getCurrentTheme() {
|
||||
return localStorage.getItem('theme');
|
||||
}
|
||||
|
||||
setCurrentTheme(theme) {
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
onClickAuto() {
|
||||
if (this.getCurrentTheme()) {
|
||||
this.setModeToAuto();
|
||||
} else {
|
||||
this.setModeToDark();
|
||||
}
|
||||
}
|
||||
|
||||
onClickLight() {
|
||||
if (this.getCurrentTheme() !== 'light') {
|
||||
this.setModeToLight();
|
||||
} else {
|
||||
this.setModeToAuto();
|
||||
}
|
||||
}
|
||||
|
||||
onClickDark() {
|
||||
if (this.getCurrentTheme() !== 'dark') {
|
||||
this.setModeToDark();
|
||||
} else {
|
||||
this.setModeToLight();
|
||||
}
|
||||
}
|
||||
|
||||
setModeToDark() {
|
||||
document.body.classList.remove('light-theme');
|
||||
document.body.classList.add('dark-theme');
|
||||
|
||||
this.setCurrentTheme('dark');
|
||||
|
||||
this.$themeAutoBtn.classList.remove("selected");
|
||||
this.$themeLightBtn.classList.remove("selected");
|
||||
this.$themeDarkBtn.classList.add("selected");
|
||||
}
|
||||
|
||||
setModeToLight() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.add('light-theme');
|
||||
|
||||
this.setCurrentTheme('light');
|
||||
|
||||
this.$themeAutoBtn.classList.remove("selected");
|
||||
this.$themeLightBtn.classList.add("selected");
|
||||
this.$themeDarkBtn.classList.remove("selected");
|
||||
}
|
||||
|
||||
setModeToAuto() {
|
||||
document.body.classList.remove('dark-theme');
|
||||
document.body.classList.remove('light-theme');
|
||||
if (this.prefersDarkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
}
|
||||
else if (this.prefersLightTheme) {
|
||||
document.body.classList.add('light-theme');
|
||||
}
|
||||
localStorage.removeItem('theme');
|
||||
|
||||
this.$themeAutoBtn.classList.add("selected");
|
||||
this.$themeLightBtn.classList.remove("selected");
|
||||
this.$themeDarkBtn.classList.remove("selected");
|
||||
}
|
||||
}
|
||||
|
||||
class FooterUI {
|
||||
|
||||
constructor() {
|
||||
this.$displayName = $('display-name');
|
||||
this.$discoveryWrapper = $$('footer .discovery-wrapper');
|
||||
|
||||
// Show "Loading…"
|
||||
this.$displayName.setAttribute('placeholder', this.$displayName.dataset.placeholder);
|
||||
|
||||
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
|
||||
// Load saved display name on page load
|
||||
Events.on('ws-connected', _ => this._loadSavedDisplayName());
|
||||
|
||||
Events.on('evaluate-footer-badges', _ => this._evaluateFooterBadges());
|
||||
}
|
||||
|
||||
_evaluateFooterBadges() {
|
||||
if (this.$discoveryWrapper.querySelectorAll('div:last-of-type > span[hidden]').length < 2) {
|
||||
this.$discoveryWrapper.classList.remove('row');
|
||||
this.$discoveryWrapper.classList.add('column');
|
||||
}
|
||||
else {
|
||||
this.$discoveryWrapper.classList.remove('column');
|
||||
this.$discoveryWrapper.classList.add('row');
|
||||
}
|
||||
Events.fire('redraw-canvas');
|
||||
Events.fire('fade-in-ui');
|
||||
}
|
||||
|
||||
_loadSavedDisplayName() {
|
||||
this._getSavedDisplayName()
|
||||
.then(displayName => {
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
if (displayName) {
|
||||
Events.fire('self-display-name-changed', displayName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onDisplayName(displayName){
|
||||
// set display name
|
||||
this.$displayName.setAttribute('placeholder', displayName);
|
||||
}
|
||||
|
||||
|
||||
_insertDisplayName(displayName) {
|
||||
this.$displayName.textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDownDisplayName(e) {
|
||||
if (e.key === "Enter" || e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUpDisplayName(e) {
|
||||
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||
}
|
||||
|
||||
async _saveDisplayName(newDisplayName) {
|
||||
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
|
||||
const savedDisplayName = await this._getSavedDisplayName();
|
||||
if (newDisplayName === savedDisplayName) return;
|
||||
|
||||
if (newDisplayName) {
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName)
|
||||
.then(_ => {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-permanently"));
|
||||
})
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily"));
|
||||
})
|
||||
.finally(() => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
}
|
||||
else {
|
||||
PersistentStorage.delete('editedDisplayName')
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
})
|
||||
.finally(() => {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-random-again"));
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getSavedDisplayName() {
|
||||
return new Promise((resolve) => {
|
||||
PersistentStorage.get('editedDisplayName')
|
||||
.then(displayName => {
|
||||
if (!displayName) displayName = "";
|
||||
resolve(displayName);
|
||||
})
|
||||
.catch(_ => {
|
||||
let displayName = localStorage.getItem('editedDisplayName');
|
||||
if (!displayName) displayName = "";
|
||||
resolve(displayName);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BackgroundCanvas {
|
||||
constructor() {
|
||||
this.c = $$('canvas');
|
||||
this.cCtx = this.c.getContext('2d');
|
||||
this.$footer = $$('footer');
|
||||
|
||||
// fade-in on load
|
||||
Events.on('fade-in-ui', _ => this._fadeIn());
|
||||
|
||||
// redraw canvas
|
||||
Events.on('resize', _ => this.init());
|
||||
Events.on('redraw-canvas', _ => this.init());
|
||||
Events.on('translation-loaded', _ => this.init());
|
||||
}
|
||||
|
||||
_fadeIn() {
|
||||
this.c.classList.remove('opacity-0');
|
||||
}
|
||||
|
||||
init() {
|
||||
let oldW = this.w;
|
||||
let oldH = this.h;
|
||||
let oldOffset = this.offset
|
||||
this.w = document.documentElement.clientWidth;
|
||||
this.h = document.documentElement.clientHeight;
|
||||
this.offset = this.$footer.offsetHeight - 27;
|
||||
if (this.h >= 800) this.offset += 10;
|
||||
|
||||
if (oldW === this.w && oldH === this.h && oldOffset === this.offset) return; // nothing has changed
|
||||
|
||||
this.c.width = this.w;
|
||||
this.c.height = this.h;
|
||||
this.x0 = this.w / 2;
|
||||
this.y0 = this.h - this.offset;
|
||||
this.dw = Math.round(Math.max(this.w, this.h, 1000) / 13);
|
||||
|
||||
this.drawCircles(this.cCtx);
|
||||
}
|
||||
|
||||
|
||||
drawCircle(ctx, radius) {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
let opacity = Math.max(0, 0.3 * (1 - 1.2 * radius / Math.max(this.w, this.h)));
|
||||
ctx.strokeStyle = `rgba(165, 165, 165, ${opacity})`;
|
||||
ctx.arc(this.x0, this.y0, radius, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawCircles(ctx) {
|
||||
ctx.clearRect(0, 0, this.w, this.h);
|
||||
for (let i = 0; i < 13; i++) {
|
||||
this.drawCircle(ctx, this.dw * i + 33 + 66);
|
||||
}
|
||||
}
|
||||
}
|
1156
public/scripts/ui.js
1156
public/scripts/ui.js
File diff suppressed because it is too large
Load diff
|
@ -37,6 +37,31 @@ if (!navigator.clipboard) {
|
|||
}
|
||||
}
|
||||
|
||||
// Polyfills
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
window.hiddenProperty = 'hidden' in document
|
||||
? 'hidden'
|
||||
: 'webkitHidden' in document
|
||||
? 'webkitHidden'
|
||||
: 'mozHidden' in document
|
||||
? 'mozHidden'
|
||||
: null;
|
||||
|
||||
window.visibilityChangeEvent = 'visibilitychange' in document
|
||||
? 'visibilitychange'
|
||||
: 'webkitvisibilitychange' in document
|
||||
? 'webkitvisibilitychange'
|
||||
: 'mozvisibilitychange' in document
|
||||
? 'mozvisibilitychange'
|
||||
: null;
|
||||
|
||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
window.android = /android/i.test(navigator.userAgent);
|
||||
window.isMobile = window.iOS || window.android;
|
||||
|
||||
|
||||
// Helper functions
|
||||
const zipper = (() => {
|
||||
|
||||
let zipWriter;
|
||||
|
@ -52,7 +77,8 @@ const zipper = (() => {
|
|||
const blobURL = URL.createObjectURL(await zipWriter.close());
|
||||
zipWriter = null;
|
||||
return blobURL;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
throw new Error("Zip file closed");
|
||||
}
|
||||
},
|
||||
|
@ -61,7 +87,8 @@ const zipper = (() => {
|
|||
const file = new File([await zipWriter.close()], filename, {type: "application/zip"});
|
||||
zipWriter = null;
|
||||
return file;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
throw new Error("Zip file closed");
|
||||
}
|
||||
},
|
||||
|
@ -411,3 +438,23 @@ function changeFavicon(src) {
|
|||
document.querySelector('[rel="icon"]').href = src;
|
||||
document.querySelector('[rel="shortcut icon"]').href = src;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(buffer);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa( binary );
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(base64) {
|
||||
let binary_string = window.atob(base64);
|
||||
let len = binary_string.length;
|
||||
let bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue