Defer loading of all render-blocking resources until the UI has loaded

This commit is contained in:
schlagmichdoch 2023-11-09 03:48:17 +01:00
parent 778d49e84b
commit 99332037bf
12 changed files with 1447 additions and 1385 deletions

View file

@ -35,7 +35,7 @@
<meta property="og:image" content="images/logo_transparent_512x512.png"> <meta property="og:image" content="images/logo_transparent_512x512.png">
<!-- Resources --> <!-- Resources -->
<link rel="preload" href="lang/en.json" as="fetch"> <link rel="preload" href="lang/en.json" as="fetch">
<link rel="stylesheet" type="text/css" href="styles.css"> <link rel="stylesheet" type="text/css" href="styles/main-styles.css">
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
@ -595,14 +595,17 @@
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/util.js"></script>
<script src="scripts/localization.js"></script>
<script src="scripts/theme.js"></script> <script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script> <script src="scripts/util-main.js"></script>
<script src="scripts/ui.js"></script> <script src="scripts/localization.js"></script>
<script src="scripts/QRCode.min.js" async></script> <script src="scripts/persistent-storage.js"></script>
<script src="scripts/zip.min.js" async></script> <script src="scripts/main.js"></script>
<script src="scripts/NoSleep.min.js" async></script> <script defer src="scripts/util.js"></script>
<script defer src="scripts/network.js"></script>
<script defer src="scripts/ui.js"></script>
<script defer src="scripts/qr-code.min.js"></script>
<script defer src="scripts/zip.min.js"></script>
<script defer src="scripts/no-sleep.min.js"></script>
<!-- Sounds --> <!-- Sounds -->
<audio id="blop" autobuffer="true"> <audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg"> <source src="sounds/blop.mp3" type="audio/mpeg">

307
public/scripts/main.js Normal file
View file

@ -0,0 +1,307 @@
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){
console.debug(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 * radius / Math.max(this.w, this.h)));
ctx.strokeStyle = `rgba(128, 128, 128, ${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);
}
}
}
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');
Events.on('initial-translation-loaded', _ => {
const backgroundCanvas = new BackgroundCanvas();
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 delayed assets
this.loadDeferredAssets();
});
}
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.debug("Load deferred assets");
if (document.readyState === "loading") {
// Loading hasn't finished yet
Events.on('DOMContentLoaded', _ => this.hydrate());
} else {
// `DOMContentLoaded` has already fired
this.hydrate();
}
}
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);
}
hydrate() {
this.loadStyleSheet('styles/deferred-styles.css', _ => {
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);
});
}
}
const persistentStorage = new PersistentStorage();
const pairDrop = new PairDrop();
const localization = new Localization();
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(serviceWorker => {
console.log('Service Worker registered');
window.serviceWorker = serviceWorker
});
}
window.addEventListener('beforeinstallprompt', installEvent => {
if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
// only display install btn when not installed
const installBtn = $('install')
installBtn.removeAttribute('hidden');
installBtn.addEventListener('click', () => {
installBtn.setAttribute('hidden', true);
installEvent.prompt();
});
}
return installEvent.preventDefault();
});

View file

@ -1360,17 +1360,3 @@ class FileDigester {
} }
} }
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);
}
}

View 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);
})
}
}

View file

@ -5,22 +5,14 @@ class PeersUI {
this.$xPeers = $$('x-peers'); this.$xPeers = $$('x-peers');
this.$xNoPeers = $$('x-no-peers'); this.$xNoPeers = $$('x-no-peers');
this.$xInstructions = $$('x-instructions'); this.$xInstructions = $$('x-instructions');
this.$center = $$('#center');
this.$footer = $$('footer');
this.$discoveryWrapper = $$('footer .discovery-wrapper');
this.$displayName = $('display-name');
this.$header = $$('header.opacity-0');
this.$wsFallbackWarning = $('websocket-fallback'); this.$wsFallbackWarning = $('websocket-fallback');
this.evaluateHeader = ["notification", "edit-paired-devices"];
this.fadedIn = false;
this.peers = {}; this.peers = {};
this.pasteMode = {}; this.pasteMode = {};
this.pasteMode.activated = false; this.pasteMode.activated = false;
this.pasteMode.descriptor = ""; this.pasteMode.descriptor = "";
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
Events.on('peer-joined', e => this._onPeerJoined(e.detail)); Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-added', _ => this._evaluateOverflowing()); Events.on('peer-added', _ => this._evaluateOverflowing());
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash)); Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash));
@ -33,7 +25,7 @@ class PeersUI {
Events.on('dragover', e => this._onDragOver(e)); Events.on('dragover', e => this._onDragOver(e));
Events.on('dragleave', _ => this._onDragEnd()); Events.on('dragleave', _ => this._onDragEnd());
Events.on('dragend', _ => this._onDragEnd()); Events.on('dragend', _ => this._onDragEnd());
Events.on('bg-resize', _ => this._evaluateOverflowing()); Events.on('resize', _ => this._evaluateOverflowing());
Events.on('paste', e => this._onPaste(e)); Events.on('paste', e => this._onPaste(e));
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
@ -42,24 +34,7 @@ class PeersUI {
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
// 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('self-display-name-changed', e => this._insertDisplayName(e.detail));
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e)); Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
Events.on('evaluate-footer-badges', _ => this._evaluateFooterBadges())
if (!('Notification' in window)) this.evaluateHeader.splice(this.evaluateHeader.indexOf("notification"), 1);
// wait for evaluation of notification and edit-paired-devices buttons
Events.on('header-evaluated', e => this._fadeInHeader(e.detail));
// Load saved display name on page load
Events.on('ws-connected', _ => this._loadSavedDisplayName());
Events.on('ws-config', e => this._evaluateRtcSupport(e.detail)) Events.on('ws-config', e => this._evaluateRtcSupport(e.detail))
} }
@ -76,124 +51,6 @@ class PeersUI {
} }
} }
_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);
}
_fadeInHeader(id) {
this.evaluateHeader.splice(this.evaluateHeader.indexOf(id), 1);
console.log(`Header btn ${id} evaluated. ${this.evaluateHeader.length} to go.`);
if (this.evaluateHeader.length !== 0) return;
this.$header.classList.remove('opacity-0');
}
_fadeInUI() {
if (this.fadedIn) return;
this.fadedIn = true;
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);
Events.fire('ui-faded-in');
}
_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');
this._fadeInUI();
}
_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);
})
});
}
_changePeerDisplayName(peerId, displayName) { _changePeerDisplayName(peerId, displayName) {
this.peers[peerId].name.displayName = displayName; this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId); const peerIdNode = $(peerId);
@ -1292,8 +1149,6 @@ class PairDeviceDialog extends Dialog {
this.evaluateUrlAttributes(); this.evaluateUrlAttributes();
this.pairPeer = {}; this.pairPeer = {};
this._evaluateNumberRoomSecrets();
} }
_onKeyDown(e) { _onKeyDown(e) {
@ -1493,7 +1348,6 @@ class PairDeviceDialog extends Dialog {
this.$footerInstructionsPairedDevices.setAttribute('hidden', true); this.$footerInstructionsPairedDevices.setAttribute('hidden', true);
} }
Events.fire('evaluate-footer-badges'); Events.fire('evaluate-footer-badges');
Events.fire('header-evaluated', 'edit-paired-devices');
}); });
} }
} }
@ -1544,7 +1398,8 @@ class EditPairedDevicesDialog extends Dialog {
$pairedDevice $pairedDevice
.querySelector('input[type="checkbox"]') .querySelector('input[type="checkbox"]')
.addEventListener('click', e => { .addEventListener('click', e => {
PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked) PersistentStorage
.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked)
.then(roomSecretsEntry => { .then(roomSecretsEntry => {
Events.fire('auto-accept-updated', { Events.fire('auto-accept-updated', {
'roomSecret': roomSecretsEntry.entry.secret, 'roomSecret': roomSecretsEntry.entry.secret,
@ -1556,7 +1411,8 @@ class EditPairedDevicesDialog extends Dialog {
$pairedDevice $pairedDevice
.querySelector('button') .querySelector('button')
.addEventListener('click', e => { .addEventListener('click', e => {
PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret) PersistentStorage
.deleteRoomSecret(roomSecretsEntry.secret)
.then(roomSecret => { .then(roomSecret => {
Events.fire('room-secrets-deleted', [roomSecret]); Events.fire('room-secrets-deleted', [roomSecret]);
Events.fire('evaluate-number-room-secrets'); Events.fire('evaluate-number-room-secrets');
@ -2197,14 +2053,10 @@ class Notifications {
// Check if the browser supports notifications // Check if the browser supports notifications
if (!('Notification' in window)) return; if (!('Notification' in window)) return;
// Check whether notification permissions have already been granted
if (Notification.permission !== 'granted') {
this.$headerNotificationButton = $('notification'); this.$headerNotificationButton = $('notification');
this.$headerNotificationButton.removeAttribute('hidden');
this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission());
}
Events.fire('header-evaluated', 'notification'); this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission());
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files)); Events.on('files-received', e => this._downloadNotification(e.detail.files));
@ -2475,306 +2327,6 @@ class NoSleepUI {
} }
} }
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);
})
}
}
class BrowserTabsConnector { class BrowserTabsConnector {
constructor() { constructor() {
this.bc = new BroadcastChannel('pairdrop'); this.bc = new BroadcastChannel('pairdrop');
@ -2835,114 +2387,3 @@ class BrowserTabsConnector {
return peerIdsBrowser; return peerIdsBrowser;
} }
} }
class BackgroundCanvas {
constructor() {
this.c = $$('canvas');
this.cCtx = this.c.getContext('2d');
this.$footer = $$('footer');
Events.on('bg-resize', _ => this.init());
Events.on('redraw-canvas', _ => this.init());
Events.on('translation-loaded', _ => this.init());
//fade-in on load
Events.on('ui-faded-in', _ => this._fadeIn());
window.onresize = _ => Events.fire('bg-resize');
}
_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 * radius / Math.max(this.w, this.h)));
ctx.strokeStyle = `rgba(128, 128, 128, ${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);
}
}
}
class PairDrop {
constructor() {
Events.on('initial-translation-loaded', _ => {
const peersUI = new PeersUI();
const backgroundCanvas = new BackgroundCanvas();
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);
});
}
}
const persistentStorage = new PersistentStorage();
const pairDrop = new PairDrop();
const localization = new Localization();
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(serviceWorker => {
console.log('Service Worker registered');
window.serviceWorker = serviceWorker
});
}
window.addEventListener('beforeinstallprompt', installEvent => {
if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
// only display install btn when not installed
const installBtn = document.querySelector('#install')
installBtn.removeAttribute('hidden');
installBtn.addEventListener('click', () => {
installBtn.setAttribute('hidden', true);
installEvent.prompt();
});
}
return installEvent.preventDefault();
});

View file

@ -0,0 +1,17 @@
// Selector shortcuts
const $ = query => document.getElementById(query);
const $$ = query => document.querySelector(query);
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);
}
}

View file

@ -60,9 +60,6 @@ window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent); window.android = /android/i.test(navigator.userAgent);
window.isMobile = window.iOS || window.android; window.isMobile = window.iOS || window.android;
// Selector shortcuts
const $ = query => document.getElementById(query);
const $$ = query => document.querySelector(query);
// Helper functions // Helper functions
const zipper = (() => { const zipper = (() => {

View file

@ -5,16 +5,21 @@ const relativePathsToCache = [
'./', './',
'index.html', 'index.html',
'manifest.json', 'manifest.json',
'styles.css', 'styles/main-styles.css',
'styles/deferred-styles.css',
'scripts/localization.js', 'scripts/localization.js',
'scripts/main.js',
'scripts/network.js', 'scripts/network.js',
'scripts/NoSleep.min.js', 'scripts/no-sleep.min.js',
'scripts/QRCode.min.js', 'scripts/persistent-storage.js',
'scripts/qr-code.min.js',
'scripts/theme.js', 'scripts/theme.js',
'scripts/ui.js', 'scripts/ui.js',
'scripts/util.js', 'scripts/util.js',
'scripts/util-main.js',
'scripts/zip.min.js', 'scripts/zip.min.js',
'sounds/blop.mp3', 'sounds/blop.mp3',
'sounds/blop.ogg',
'images/favicon-96x96.png', 'images/favicon-96x96.png',
'images/favicon-96x96-notification.png', 'images/favicon-96x96-notification.png',
'images/android-chrome-192x192.png', 'images/android-chrome-192x192.png',
@ -32,6 +37,7 @@ const relativePathsToCache = [
'lang/ja.json', 'lang/ja.json',
'lang/nb.json', 'lang/nb.json',
'lang/nl.json', 'lang/nl.json',
'lang/tr.json',
'lang/ro.json', 'lang/ro.json',
'lang/ru.json', 'lang/ru.json',
'lang/zh-CN.json' 'lang/zh-CN.json'

View file

@ -0,0 +1,727 @@
/* All styles in this sheet are not needed on page load and deferred */
/* Peers */
x-peers.overflowing {
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
x-peers:has(> x-peer) {
--peers-per-row: 10;
}
/* peers-per-row if height is too small for 2 rows */
@media screen and (min-height: 538px) and (max-height: 683px) and (max-width: 402px),
screen and (min-height: 517px) and (max-height: 664px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(7)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 10;
}
}
/* peers-per-row if height is too small for 3 rows */
@media screen and (min-height: 683px) and (max-width: 402px),
screen and (min-height: 664px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(28)) {
--peers-per-row: 10;
}
}
/* Peer */
x-peer {
padding: 8px;
align-content: start;
flex-wrap: wrap;
}
x-peer label {
width: var(--peer-width);
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative;
}
x-peer x-icon {
--icon-size: 40px;
margin-bottom: 4px;
transition: transform 150ms;
will-change: transform;
display: flex;
flex-direction: column;
}
x-peer .icon-wrapper {
width: var(--icon-size);
padding: 12px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
}
x-peer.type-secret .icon-wrapper {
background: var(--paired-device-color);
}
x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
background: var(--public-room-color);
}
x-peer x-icon > .highlight-wrapper {
align-self: center;
align-items: center;
margin: 7px auto 0;
height: 6px;
}
x-peer x-icon > .highlight-wrapper > .highlight {
width: 15px;
height: 6px;
border-radius: 4px;
margin-left: 1px;
margin-right: 1px;
display: none;
}
x-peer.type-ip x-icon > .highlight-wrapper > .highlight.highlight-room-ip {
background-color: var(--primary-color);
display: inline;
}
x-peer.type-secret x-icon > .highlight-wrapper > .highlight.highlight-room-secret {
background-color: var(--paired-device-color);
display: inline;
}
x-peer.type-public-id x-icon > .highlight-wrapper > .highlight.highlight-room-public-id {
background-color: var(--public-room-color);
display: inline;
}
x-peer:not([status]):hover x-icon,
x-peer:not([status]):focus x-icon {
transform: scale(1.05);
}
x-peer[status] x-icon {
box-shadow: none;
opacity: 0.8;
transform: scale(1);
}
x-peer.ws-peer {
margin-top: -1.5px;
}
x-peer.ws-peer .progress {
margin-top: 3px;
}
x-peer.ws-peer .icon-wrapper{
border: solid 3px var(--ws-peer-color);
}
x-peer.ws-peer .highlight-wrapper {
margin-top: 3px;
}
#websocket-fallback {
opacity: 0.5;
}
#websocket-fallback > span:nth-of-type(2) {
border-bottom: solid 2px var(--ws-peer-color);
}
.device-descriptor {
width: 100%;
text-align: center;
}
.device-descriptor > div {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.status,
.device-name,
.connection-hash {
opacity: 0.7;
}
.device-name {
font-size: 14px;
white-space: nowrap;
}
.connection-hash {
font-size: 12px;
white-space: nowrap;
}
x-peer:not([status]) .status,
x-peer[status] .device-name {
display: none;
}
x-peer[status] {
pointer-events: none;
}
x-peer x-icon {
animation: pop 600ms ease-out 1;
}
@keyframes pop {
0% {
transform: scale(0.7);
}
40% {
transform: scale(1.2);
}
}
x-peer[drop] x-icon {
transform: scale(1.1);
}
Dialog
x-dialog x-background {
background: rgba(0, 0, 0, 0.61);
z-index: 10;
transition: opacity 300ms;
will-change: opacity;
padding: 15px;
overflow: overlay;
}
x-dialog x-paper {
display: flex;
flex-direction: column;
width: calc(100vw - 10px);
z-index: 3;
background: white;
border-radius: 8px;
max-width: 400px;
overflow: hidden;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
#pair-device-dialog x-paper,
#edit-paired-devices-dialog x-paper,
#public-room-dialog x-paper,
#language-select-dialog x-paper {
position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
}
x-paper > .row:first-of-type {
background-color: var(--accent-color);
border-bottom: solid 4px var(--border-color);
margin-bottom: 10px;
}
x-paper > .row:first-of-type h2 {
color: white;
}
#pair-device-dialog,
#edit-paired-devices-dialog {
--accent-color: var(--paired-device-color);
}
#public-room-dialog {
--accent-color: var(--public-room-color);
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
#public-room-dialog ::-moz-selection,
#public-room-dialog ::selection {
color: black;
background: var(--public-room-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
x-dialog:not([show]) x-paper {
transform: scale(0.1);
}
x-dialog a {
color: var(--primary-color);
}
/* Pair Devices Dialog & Public Room Dialog */
.input-key-container {
width: 100%;
display: flex;
justify-content: center;
margin-top: 10px;
}
.input-key-container > input {
width: 45px;
height: 45px;
font-size: 30px;
padding: 0;
text-align: center;
text-transform: uppercase;
display: -webkit-box !important;
display: -webkit-flex !important;
display: -moz-flex !important;
display: -ms-flexbox !important;
display: flex !important;
-webkit-justify-content: center;
-ms-justify-content: center;
justify-content: center;
}
.input-key-container > input {
margin: 0 3px;
}
.input-key-container.six-chars > input:nth-of-type(4) {
margin-left: 5%;
}
.key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
display: inline-block;
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 20px);
text-indent: calc(0.5 * (11px + min(calc((100vw - 80px - 99px) / 100 * 6), 28px)));
margin: 25px 0;
}
.key-qr-code {
margin: 16px;
width: fit-content;
align-self: center;
}
.key-instructions {
flex-direction: column;
}
x-dialog h2 {
margin-top: 5px;
margin-bottom: 0;
}
x-dialog hr {
height: 3px;
border: none;
width: 100%;
background-color: var(--border-color);
}
.hr-note {
margin-top: 10px;
margin-bottom: 20px;
}
.hr-note hr {
margin-bottom: -2px;
}
.hr-note > div {
height: 0;
transform: translateY(-10px);
}
.hr-note > div > span {
padding: 3px 10px;
border-radius: 10px;
color: rgb(var(--text-color));
background-color: rgb(var(--bg-color));
border: var(--border-color) solid 3px;
text-transform: uppercase;
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: attr(data-empty);
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
/* Receive Dialog */
x-paper > .row {
padding: 10px;
}
/* button row*/
x-paper > .button-row {
border-top: solid 3px var(--border-color);
height: 50px;
margin-top: 10px;
}
x-paper > .button-row > .button {
height: 100%;
width: 100%;
}
html:not([dir="rtl"]) x-paper > .button-row > .button:not(:first-child) {
border-right: solid 1.5px var(--border-color);
}
html:not([dir="rtl"]) x-paper > .button-row > .button:not(:last-child) {
border-left: solid 1.5px var(--border-color);
}
html[dir="rtl"] x-paper > .button-row > .button:not(:first-child) {
border-left: solid 1.5px var(--border-color);
}
html[dir="rtl"] x-paper > .button-row > .button:not(:last-child) {
border-right: solid 1.5px var(--border-color);
}
.language-buttons > button > span {
margin: 0 0.3em;
}
.file-description {
max-width: 100%;
}
.file-description span {
display: inline;
word-break: normal;
}
.file-name {
font-style: italic;
max-width: 100%;
margin-top: 5px;
}
.file-stem {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 1px;
}
/* Send Text Dialog */
x-dialog .dialog-subheader {
padding-top: 16px;
padding-bottom: 16px;
}
#send-text-dialog .display-name-wrapper {
padding-bottom: 0;
}
#text-input {
min-height: 200px;
width: 100%;
}
/* Receive Text Dialog */
#receive-text-dialog #text {
width: 100%;
word-break: break-all;
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
white-space: pre-wrap;
}
#receive-text-dialog #text a {
cursor: pointer;
}
#receive-text-dialog #text a:hover {
text-decoration: underline;
}
#receive-text-dialog h3 {
/* Select the received text when double-clicking the dialog */
user-select: none;
pointer-events: none;
}
.row-separator {
border-bottom: solid 2.5px var(--border-color);
margin: auto -24px;
}
#base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
border-radius: 8px;
}
#base64-paste-dialog .textarea {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
#base64-paste-dialog .textarea::before {
font-size: 15px;
letter-spacing: 0.12em;
color: var(--primary-color);
font-weight: 700;
text-transform: uppercase;
white-space: pre-wrap;
}
/* Peer loading Indicator */
.progress {
width: 80px;
height: 80px;
position: absolute;
top: -8px;
clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg);
transition: transform 200ms;
}
.circle {
width: 72px;
height: 72px;
border: 4px solid var(--primary-color);
border-radius: 40px;
position: absolute;
clip: rect(0px, 40px, 80px, 0px);
will-change: transform;
transform: var(--progress);
}
.over50 {
clip: rect(auto, auto, auto, auto);
}
.over50 .circle.right {
transform: rotate(180deg);
}
/*
Color Themes
*/
/* Colored Elements */
x-dialog x-paper {
background-color: rgb(var(--bg-color));
}
.textarea {
color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important;
}
.textarea * {
margin: 0 !important;
padding: 0 !important;
color: unset !important;
background: unset !important;
border: unset !important;
opacity: unset !important;
font-family: inherit !important;
font-size: inherit !important;
font-style: unset !important;
font-weight: unset !important;
}
/* Image/Video/Audio Preview */
.file-preview {
margin-bottom: 15px;
}
.file-preview:empty {
display: none;
}
.file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%;
max-height: 40vh;
margin: auto;
display: block;
}

View file

@ -1,3 +1,5 @@
/* All styles in this sheet are needed on page load */
/* Constants */ /* Constants */
:root { :root {
@ -31,17 +33,10 @@ body {
body { body {
height: 100%; height: 100%;
/* mobile viewport bug fix */
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
html { html {
height: 100%; height: 100%;
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
.fw { .fw {
@ -293,128 +288,13 @@ x-noscript {
} }
/* Peers List */ /* Peers */
#x-peers-filler { #x-peers-filler {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
} }
x-peers {
position: relative;
display: flex;
flex-flow: row wrap;
flex-grow: 1;
align-items: start !important;
justify-content: center;
z-index: 2;
transition: --bg-color 0.5s ease;
overflow-y: scroll;
overflow-x: hidden;
overscroll-behavior-x: none;
scrollbar-width: none;
--peers-per-row: 6; /* default if browser does not support :has selector */
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
width: var(--x-peers-width);
margin-right: 20px;
margin-left: 20px;
}
x-peers.overflowing {
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .2), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .2), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
x-peers:has(> x-peer) {
--peers-per-row: 10;
}
/* peers-per-row if height is too small for 2 rows */
@media screen and (min-height: 538px) and (max-height: 683px) and (max-width: 402px),
screen and (min-height: 517px) and (max-height: 664px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(7)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 10;
}
}
/* peers-per-row if height is too small for 3 rows */
@media screen and (min-height: 683px) and (max-width: 402px),
screen and (min-height: 664px) and (min-width: 426px) {
x-peers:has(> x-peer) {
--peers-per-row: 3;
}
x-peers:has(> x-peer:nth-of-type(10)) {
--peers-per-row: 4;
}
x-peers:has(> x-peer:nth-of-type(13)) {
--peers-per-row: 5;
}
x-peers:has(> x-peer:nth-of-type(16)) {
--peers-per-row: 6;
}
x-peers:has(> x-peer:nth-of-type(19)) {
--peers-per-row: 7;
}
x-peers:has(> x-peer:nth-of-type(22)) {
--peers-per-row: 8;
}
x-peers:has(> x-peer:nth-of-type(25)) {
--peers-per-row: 9;
}
x-peers:has(> x-peer:nth-of-type(28)) {
--peers-per-row: 10;
}
}
::-webkit-scrollbar {
display: none;
}
/* Empty Peers List */ /* Empty Peers List */
x-no-peers { x-no-peers {
@ -452,178 +332,33 @@ x-no-peers[drop-bg] * {
} }
/* Peer */
x-peer {
padding: 8px;
align-content: start;
flex-wrap: wrap;
}
x-peer label {
width: var(--peer-width);
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative;
}
input[type="file"] { input[type="file"] {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
} }
x-peer x-icon { x-peers {
--icon-size: 40px; position: relative;
margin-bottom: 4px;
transition: transform 150ms;
will-change: transform;
display: flex; display: flex;
flex-direction: column; flex-flow: row wrap;
flex-grow: 1;
align-items: start !important;
justify-content: center;
z-index: 2;
transition: --bg-color 0.5s ease;
overflow-y: scroll;
overflow-x: hidden;
overscroll-behavior-x: none;
scrollbar-width: none;
--peers-per-row: 6; /* default if browser does not support :has selector */
--x-peers-width: min(100vw, calc(var(--peers-per-row) * (var(--peer-width) + 25px) - 16px));
width: var(--x-peers-width);
margin-right: 20px;
margin-left: 20px;
} }
x-peer .icon-wrapper {
width: var(--icon-size);
padding: 12px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
}
x-peer.type-secret .icon-wrapper {
background: var(--paired-device-color);
}
x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
background: var(--public-room-color);
}
x-peer x-icon > .highlight-wrapper {
align-self: center;
align-items: center;
margin: 7px auto 0;
height: 6px;
}
x-peer x-icon > .highlight-wrapper > .highlight {
width: 15px;
height: 6px;
border-radius: 4px;
margin-left: 1px;
margin-right: 1px;
display: none;
}
x-peer.type-ip x-icon > .highlight-wrapper > .highlight.highlight-room-ip {
background-color: var(--primary-color);
display: inline;
}
x-peer.type-secret x-icon > .highlight-wrapper > .highlight.highlight-room-secret {
background-color: var(--paired-device-color);
display: inline;
}
x-peer.type-public-id x-icon > .highlight-wrapper > .highlight.highlight-room-public-id {
background-color: var(--public-room-color);
display: inline;
}
x-peer:not([status]):hover x-icon,
x-peer:not([status]):focus x-icon {
transform: scale(1.05);
}
x-peer[status] x-icon {
box-shadow: none;
opacity: 0.8;
transform: scale(1);
}
x-peer.ws-peer {
margin-top: -1.5px;
}
x-peer.ws-peer .progress {
margin-top: 3px;
}
x-peer.ws-peer .icon-wrapper{
border: solid 3px var(--ws-peer-color);
}
x-peer.ws-peer .highlight-wrapper {
margin-top: 3px;
}
#websocket-fallback {
opacity: 0.5;
}
#websocket-fallback > span:nth-of-type(2) {
border-bottom: solid 2px var(--ws-peer-color);
}
.device-descriptor {
width: 100%;
text-align: center;
}
.device-descriptor > div {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.status,
.device-name,
.connection-hash {
opacity: 0.7;
}
.device-name {
font-size: 14px;
white-space: nowrap;
}
.connection-hash {
font-size: 12px;
white-space: nowrap;
}
x-peer:not([status]) .status,
x-peer[status] .device-name {
display: none;
}
x-peer[status] {
pointer-events: none;
}
x-peer x-icon {
animation: pop 600ms ease-out 1;
}
@keyframes pop {
0% {
transform: scale(0.7);
}
40% {
transform: scale(1.2);
}
}
x-peer[drop] x-icon {
transform: scale(1.1);
}
/* Footer */ /* Footer */
footer { footer {
@ -730,403 +465,12 @@ html[dir="rtl"] #edit-pen {
transform: rotateY(180deg); transform: rotateY(180deg);
} }
/* Dialog */ /* Dialogs needed on page load */
x-dialog x-background {
background: rgba(0, 0, 0, 0.61);
z-index: 10;
transition: opacity 300ms;
will-change: opacity;
padding: 15px;
overflow: overlay;
}
x-dialog x-paper {
display: flex;
flex-direction: column;
width: calc(100vw - 10px);
z-index: 3;
background: white;
border-radius: 8px;
max-width: 400px;
overflow: hidden;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
#pair-device-dialog x-paper,
#edit-paired-devices-dialog x-paper,
#public-room-dialog x-paper,
#language-select-dialog x-paper {
position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
}
x-paper > .row:first-of-type {
background-color: var(--accent-color);
border-bottom: solid 4px var(--border-color);
margin-bottom: 10px;
}
x-paper > .row:first-of-type h2 {
color: white;
}
#pair-device-dialog,
#edit-paired-devices-dialog {
--accent-color: var(--paired-device-color);
}
#public-room-dialog {
--accent-color: var(--public-room-color);
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
#public-room-dialog ::-moz-selection,
#public-room-dialog ::selection {
color: black;
background: var(--public-room-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
x-dialog:not([show]) x-paper {
transform: scale(0.1);
}
x-dialog:not([show]) x-background { x-dialog:not([show]) x-background {
opacity: 0; opacity: 0;
} }
x-dialog a {
color: var(--primary-color);
}
/* Pair Devices Dialog & Public Room Dialog */
.input-key-container {
width: 100%;
display: flex;
justify-content: center;
margin-top: 10px;
}
.input-key-container > input {
width: 45px;
height: 45px;
font-size: 30px;
padding: 0;
text-align: center;
text-transform: uppercase;
display: -webkit-box !important;
display: -webkit-flex !important;
display: -moz-flex !important;
display: -ms-flexbox !important;
display: flex !important;
-webkit-justify-content: center;
-ms-justify-content: center;
justify-content: center;
}
.input-key-container > input {
margin: 0 3px;
}
.input-key-container.six-chars > input:nth-of-type(4) {
margin-left: 5%;
}
.key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
display: inline-block;
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 20px);
text-indent: calc(0.5 * (11px + min(calc((100vw - 80px - 99px) / 100 * 6), 28px)));
margin: 25px 0;
}
.key-qr-code {
margin: 16px;
width: fit-content;
align-self: center;
}
.key-instructions {
flex-direction: column;
}
x-dialog h2 {
margin-top: 5px;
margin-bottom: 0;
}
x-dialog hr {
height: 3px;
border: none;
width: 100%;
background-color: var(--border-color);
}
.hr-note {
margin-top: 10px;
margin-bottom: 20px;
}
.hr-note hr {
margin-bottom: -2px;
}
.hr-note > div {
height: 0;
transform: translateY(-10px);
}
.hr-note > div > span {
padding: 3px 10px;
border-radius: 10px;
color: rgb(var(--text-color));
background-color: rgb(var(--bg-color));
border: var(--border-color) solid 3px;
text-transform: uppercase;
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: attr(data-empty);
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
/* Receive Dialog */
x-paper > .row {
padding: 10px;
}
/* button row*/
x-paper > .button-row {
border-top: solid 3px var(--border-color);
height: 50px;
margin-top: 10px;
}
x-paper > .button-row > .button {
height: 100%;
width: 100%;
}
html:not([dir="rtl"]) x-paper > .button-row > .button:not(:first-child) {
border-right: solid 1.5px var(--border-color);
}
html:not([dir="rtl"]) x-paper > .button-row > .button:not(:last-child) {
border-left: solid 1.5px var(--border-color);
}
html[dir="rtl"] x-paper > .button-row > .button:not(:first-child) {
border-left: solid 1.5px var(--border-color);
}
html[dir="rtl"] x-paper > .button-row > .button:not(:last-child) {
border-right: solid 1.5px var(--border-color);
}
.language-buttons > button > span {
margin: 0 0.3em;
}
.file-description {
max-width: 100%;
}
.file-description span {
display: inline;
word-break: normal;
}
.file-name {
font-style: italic;
max-width: 100%;
margin-top: 5px;
}
.file-stem {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 1px;
}
/* Send Text Dialog */
x-dialog .dialog-subheader {
padding-top: 16px;
padding-bottom: 16px;
}
#send-text-dialog .display-name-wrapper {
padding-bottom: 0;
}
#text-input {
min-height: 200px;
width: 100%;
}
/* Receive Text Dialog */
#receive-text-dialog #text {
width: 100%;
word-break: break-all;
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
white-space: pre-wrap;
}
#receive-text-dialog #text a {
cursor: pointer;
}
#receive-text-dialog #text a:hover {
text-decoration: underline;
}
#receive-text-dialog h3 {
/* Select the received text when double-clicking the dialog */
user-select: none;
pointer-events: none;
}
.row-separator {
border-bottom: solid 2.5px var(--border-color);
margin: auto -24px;
}
#base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
border-radius: 8px;
}
#base64-paste-dialog .textarea {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
#base64-paste-dialog .textarea::before {
font-size: 15px;
letter-spacing: 0.12em;
color: var(--primary-color);
font-weight: 700;
text-transform: uppercase;
white-space: pre-wrap;
}
/* Button */ /* Button */
.button { .button {
@ -1338,76 +682,11 @@ canvas.circles {
left: 0; left: 0;
} }
/* Loading Indicator */
.progress {
width: 80px;
height: 80px;
position: absolute;
top: -8px;
clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg);
transition: transform 200ms;
}
.circle {
width: 72px;
height: 72px;
border: 4px solid var(--primary-color);
border-radius: 40px;
position: absolute;
clip: rect(0px, 40px, 80px, 0px);
will-change: transform;
transform: var(--progress);
}
.over50 {
clip: rect(auto, auto, auto, auto);
}
.over50 .circle.right {
transform: rotate(180deg);
}
/* Generic placeholder */ /* Generic placeholder */
[placeholder]:empty:before { [placeholder]:empty:before {
content: attr(placeholder); content: attr(placeholder);
} }
/* Toast */
.toast-container {
padding: 0 8px 24px;
overflow: hidden;
pointer-events: none;
}
x-toast {
position: absolute;
min-height: 48px;
top: 50px;
width: 100%;
max-width: 344px;
background-color: rgb(var(--text-color));
color: rgb(var(--bg-color));
align-items: center;
box-sizing: border-box;
padding: 8px 24px;
z-index: 20;
transition: opacity 200ms, transform 300ms ease-out;
cursor: default;
line-height: 24px;
border-radius: 8px;
pointer-events: all;
}
x-toast:not([show]):not(:hover) {
opacity: 0;
transform: translateY(-100px);
}
/* Instructions */ /* Instructions */
x-instructions { x-instructions {
@ -1478,6 +757,38 @@ x-peers:empty~x-instructions {
} }
} }
/* Toast */
.toast-container {
padding: 0 8px 24px;
overflow: hidden;
pointer-events: none;
}
x-toast {
position: absolute;
min-height: 48px;
top: 50px;
width: 100%;
max-width: 344px;
background-color: rgb(var(--text-color));
color: rgb(var(--bg-color));
align-items: center;
box-sizing: border-box;
padding: 8px 24px;
z-index: 20;
transition: opacity 200ms, transform 300ms ease-out;
cursor: default;
line-height: 24px;
border-radius: 8px;
pointer-events: all;
}
x-toast:not([show]):not(:hover) {
opacity: 0;
transform: translateY(-100px);
}
/* /*
Color Themes Color Themes
*/ */
@ -1508,46 +819,6 @@ body {
transition: background-color 0.5s ease; transition: background-color 0.5s ease;
} }
x-dialog x-paper {
background-color: rgb(var(--bg-color));
}
.textarea {
color: rgb(var(--text-color)) !important;
background-color: var(--bg-color-secondary) !important;
}
.textarea * {
margin: 0 !important;
padding: 0 !important;
color: unset !important;
background: unset !important;
border: unset !important;
opacity: unset !important;
font-family: inherit !important;
font-size: inherit !important;
font-style: unset !important;
font-weight: unset !important;
}
/* Image/Video/Audio Preview */
.file-preview {
margin-bottom: 15px;
}
.file-preview:empty {
display: none;
}
.file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%;
max-height: 40vh;
margin: auto;
display: block;
}
/* Styles for users who prefer dark mode at the OS level */ /* Styles for users who prefer dark mode at the OS level */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -1583,12 +854,20 @@ x-dialog x-paper {
} }
/* /*
iOS specific styles Browser specific styles
*/ */
@supports (-webkit-overflow-scrolling: touch) {
html { body {
min-height: -webkit-fill-available; /* mobile viewport bug fix */
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
html {
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
/* webkit scrollbar style*/ /* webkit scrollbar style*/