diff --git a/public/index.html b/public/index.html index cf1fdde..b71028d 100644 --- a/public/index.html +++ b/public/index.html @@ -35,7 +35,7 @@ - + @@ -595,14 +595,17 @@ - - - - - - - + + + + + + + + + + diff --git a/public/scripts/main.js b/public/scripts/main.js new file mode 100644 index 0000000..4e412eb --- /dev/null +++ b/public/scripts/main.js @@ -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(); +}); \ No newline at end of file diff --git a/public/scripts/network.js b/public/scripts/network.js index 50bf4b8..ebf04b5 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -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); - } -} diff --git a/public/scripts/NoSleep.min.js b/public/scripts/no-sleep.min.js similarity index 100% rename from public/scripts/NoSleep.min.js rename to public/scripts/no-sleep.min.js diff --git a/public/scripts/persistent-storage.js b/public/scripts/persistent-storage.js new file mode 100644 index 0000000..1e98e33 --- /dev/null +++ b/public/scripts/persistent-storage.js @@ -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); + }) + } +} \ No newline at end of file diff --git a/public/scripts/QRCode.min.js b/public/scripts/qr-code.min.js similarity index 100% rename from public/scripts/QRCode.min.js rename to public/scripts/qr-code.min.js diff --git a/public/scripts/ui.js b/public/scripts/ui.js index d331298..3f4fc97 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -5,22 +5,14 @@ class PeersUI { this.$xPeers = $$('x-peers'); this.$xNoPeers = $$('x-no-peers'); 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.evaluateHeader = ["notification", "edit-paired-devices"]; - this.fadedIn = false; this.peers = {}; this.pasteMode = {}; this.pasteMode.activated = false; 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-added', _ => this._evaluateOverflowing()); 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('dragleave', _ => 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('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); @@ -42,24 +34,7 @@ class PeersUI { 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('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)) } @@ -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) { this.peers[peerId].name.displayName = displayName; const peerIdNode = $(peerId); @@ -1292,8 +1149,6 @@ class PairDeviceDialog extends Dialog { this.evaluateUrlAttributes(); this.pairPeer = {}; - - this._evaluateNumberRoomSecrets(); } _onKeyDown(e) { @@ -1493,7 +1348,6 @@ class PairDeviceDialog extends Dialog { this.$footerInstructionsPairedDevices.setAttribute('hidden', true); } Events.fire('evaluate-footer-badges'); - Events.fire('header-evaluated', 'edit-paired-devices'); }); } } @@ -1544,7 +1398,8 @@ class EditPairedDevicesDialog extends Dialog { $pairedDevice .querySelector('input[type="checkbox"]') .addEventListener('click', e => { - PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked) + PersistentStorage + .updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked) .then(roomSecretsEntry => { Events.fire('auto-accept-updated', { 'roomSecret': roomSecretsEntry.entry.secret, @@ -1556,7 +1411,8 @@ class EditPairedDevicesDialog extends Dialog { $pairedDevice .querySelector('button') .addEventListener('click', e => { - PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret) + PersistentStorage + .deleteRoomSecret(roomSecretsEntry.secret) .then(roomSecret => { Events.fire('room-secrets-deleted', [roomSecret]); Events.fire('evaluate-number-room-secrets'); @@ -2197,14 +2053,10 @@ class Notifications { // Check if the browser supports notifications if (!('Notification' in window)) return; - // Check whether notification permissions have already been granted - if (Notification.permission !== 'granted') { - this.$headerNotificationButton = $('notification'); - this.$headerNotificationButton.removeAttribute('hidden'); - this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission()); - } + this.$headerNotificationButton = $('notification'); + + this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission()); - Events.fire('header-evaluated', 'notification'); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); 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 { constructor() { this.bc = new BroadcastChannel('pairdrop'); @@ -2834,115 +2386,4 @@ class BrowserTabsConnector { localStorage.setItem("peer_ids_browser", JSON.stringify(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(); -}); \ No newline at end of file +} \ No newline at end of file diff --git a/public/scripts/util-main.js b/public/scripts/util-main.js new file mode 100644 index 0000000..9143962 --- /dev/null +++ b/public/scripts/util-main.js @@ -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); + } +} \ No newline at end of file diff --git a/public/scripts/util.js b/public/scripts/util.js index 9ccab8f..6dba206 100644 --- a/public/scripts/util.js +++ b/public/scripts/util.js @@ -60,9 +60,6 @@ window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; window.android = /android/i.test(navigator.userAgent); window.isMobile = window.iOS || window.android; -// Selector shortcuts -const $ = query => document.getElementById(query); -const $$ = query => document.querySelector(query); // Helper functions const zipper = (() => { diff --git a/public/service-worker.js b/public/service-worker.js index f87e879..16e1e94 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -5,16 +5,21 @@ const relativePathsToCache = [ './', 'index.html', 'manifest.json', - 'styles.css', + 'styles/main-styles.css', + 'styles/deferred-styles.css', 'scripts/localization.js', + 'scripts/main.js', 'scripts/network.js', - 'scripts/NoSleep.min.js', - 'scripts/QRCode.min.js', + 'scripts/no-sleep.min.js', + 'scripts/persistent-storage.js', + 'scripts/qr-code.min.js', 'scripts/theme.js', 'scripts/ui.js', 'scripts/util.js', + 'scripts/util-main.js', 'scripts/zip.min.js', 'sounds/blop.mp3', + 'sounds/blop.ogg', 'images/favicon-96x96.png', 'images/favicon-96x96-notification.png', 'images/android-chrome-192x192.png', @@ -32,6 +37,7 @@ const relativePathsToCache = [ 'lang/ja.json', 'lang/nb.json', 'lang/nl.json', + 'lang/tr.json', 'lang/ro.json', 'lang/ru.json', 'lang/zh-CN.json' diff --git a/public/styles/deferred-styles.css b/public/styles/deferred-styles.css new file mode 100644 index 0000000..8fe43eb --- /dev/null +++ b/public/styles/deferred-styles.css @@ -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; +} \ No newline at end of file diff --git a/public/styles.css b/public/styles/main-styles.css similarity index 51% rename from public/styles.css rename to public/styles/main-styles.css index 26ca6eb..ac28b04 100644 --- a/public/styles.css +++ b/public/styles/main-styles.css @@ -1,3 +1,5 @@ +/* All styles in this sheet are needed on page load */ + /* Constants */ :root { @@ -31,17 +33,10 @@ body { body { 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 { 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 { @@ -293,128 +288,13 @@ x-noscript { } -/* Peers List */ +/* Peers */ #x-peers-filler { display: flex; 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 */ 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"] { visibility: hidden; position: absolute; } -x-peer x-icon { - --icon-size: 40px; - margin-bottom: 4px; - transition: transform 150ms; - will-change: transform; +x-peers { + position: relative; 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 { @@ -730,403 +465,12 @@ html[dir="rtl"] #edit-pen { transform: rotateY(180deg); } -/* 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); -} - +/* Dialogs needed on page load */ x-dialog:not([show]) x-background { 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 { @@ -1338,76 +682,11 @@ canvas.circles { 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 */ [placeholder]:empty:before { 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 */ 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 */ @@ -1508,46 +819,6 @@ body { 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 */ @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 { - min-height: -webkit-fill-available; - } + +body { + /* 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*/