From 4566528179a73e4295f3a8fa4351311b20f8f2ed Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 1 Mar 2023 10:04:37 +0100 Subject: [PATCH] - restructure UI to use flexbox everywhere - structure peers on desktop responsively - make peer box scrollable when peers are overflowing + shadow - add highlight badge to differentiate local peers into paired and not paired - change websocket fallback warning and move to the bottom --- public/index.html | 32 +- public/scripts/ui.js | 112 ++++--- public/styles.css | 351 ++++++++++++++------ public_included_ws_fallback/index.html | 38 ++- public_included_ws_fallback/scripts/ui.js | 111 ++++--- public_included_ws_fallback/styles.css | 382 ++++++++++++++++------ 6 files changed, 724 insertions(+), 302 deletions(-) diff --git a/public/index.html b/public/index.html index 14d50cf..2c88850 100644 --- a/public/index.html +++ b/public/index.html @@ -69,17 +69,21 @@ + - - - - -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
-
- -

-
+ +
+ +
+ + +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
+
+ +

+
+

PairDrop - Send a Message

-
+

PairDrop

+
+ Send a Message to + +
+
diff --git a/public/scripts/ui.js b/public/scripts/ui.js index c4373ae..cd1aedd 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -28,7 +28,7 @@ class PeersUI { Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); this.peers = {}; - this.$cancelPasteModeBtn = $('cancelPasteModeBtn'); + this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); Events.on('dragover', e => this._onDragOver(e)); @@ -38,8 +38,12 @@ class PeersUI { Events.on('drop', e => this._onDrop(e)); Events.on('keydown', e => this._onKeyDown(e)); + this.$xPeers = $$('x-peers'); this.$xNoPeers = $$('x-no-peers'); this.$xInstructions = $$('x-instructions'); + + Events.on('peer-added', _ => this.evaluateOverflowing()); + Events.on('bg-resize', _ => this.evaluateOverflowing()); } _onKeyDown(e) { @@ -53,11 +57,11 @@ class PeersUI { } _joinPeer(peer, roomType, roomSecret) { - peer.roomType = roomType; + peer.roomTypes = [roomType]; peer.roomSecret = roomSecret; if (this.peers[peer.id]) { - this.peers[peer.id].roomType = peer.roomType; - this._redrawPeer(peer); + if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType); + this._redrawPeer(this.peers[peer.id]); return; // peer already exists } this.peers[peer.id] = peer; @@ -72,7 +76,15 @@ class PeersUI { const peerNode = $(peer.id); if (!peerNode) return; peerNode.classList.remove('type-ip', 'type-secret'); - peerNode.classList.add(`type-${peer.roomType}`) + peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`)); + } + + evaluateOverflowing() { + if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) { + this.$xPeers.classList.add('overflowing'); + } else { + this.$xPeers.classList.remove('overflowing'); + } } _onPeers(msg) { @@ -83,6 +95,7 @@ class PeersUI { const $peer = $(peerId); if (!$peer) return; $peer.remove(); + this.evaluateOverflowing(); if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again } @@ -213,6 +226,18 @@ class PeersUI { class PeerUI { + constructor(peer, connectionHash) { + this._peer = peer; + this._connectionHash = connectionHash; + this._initDom(); + this._bindListeners(); + + $$('x-peers').appendChild(this.$el) + Events.fire('peer-added'); + this.$xInstructions = $$('x-instructions'); + setTimeout(_ => window.animateBackground(false), 1750); // Stop animation + } + html() { let title; let input = ''; @@ -225,17 +250,24 @@ class PeerUI { this.$el.innerHTML = ` `; this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); @@ -245,23 +277,12 @@ class PeerUI { this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16); } - constructor(peer, connectionHash) { - this._peer = peer; - this._roomType = peer.roomType; - this._roomSecret = peer.roomSecret; - this._connectionHash = connectionHash; - this._initDom(); - this._bindListeners(); - $$('x-peers').appendChild(this.$el); - this.$xInstructions = $$('x-instructions'); - setTimeout(_ => window.animateBackground(false), 1750); // Stop animation - } - _initDom() { this.$el = document.createElement('x-peer'); this.$el.id = this._peer.id; this.$el.ui = this; - this.$el.classList.add(`type-${this._roomType}`); + this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`)); + this.$el.classList.add('center'); this.html(); this._callbackInput = e => this._onFilesSelected(e) @@ -272,7 +293,7 @@ class PeerUI { this._callbackDragLeave = e => this._onDragEnd(e) this._callbackDragOver = e => this._onDragOver(e) this._callbackContextMenu = e => this._onRightClick(e) - this._callbackTouchStart = _ => this._onTouchStart() + this._callbackTouchStart = e => this._onTouchStart(e) this._callbackTouchEnd = e => this._onTouchEnd(e) this._callbackPointerDown = e => this._onPointerDown(e) // PasteMode @@ -393,21 +414,28 @@ class PeerUI { _onRightClick(e) { e.preventDefault(); - Events.fire('text-recipient', this._peer.id); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); } - _onTouchStart() { + _onTouchStart(e) { this._touchStart = Date.now(); - this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610); + this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610); } _onTouchEnd(e) { if (Date.now() - this._touchStart < 500) { clearTimeout(this._touchTimer); - } else { // this was a long tap - if (e) e.preventDefault(); - Events.fire('text-recipient', this._peer.id); + } else if (this._touchTimer) { // this was a long tap + e.preventDefault(); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); } + this._touchTimer = null; } } @@ -843,7 +871,7 @@ class PairDeviceDialog extends Dialog { height: 150, padding: 0, background: "transparent", - color: getComputedStyle(document.body).getPropertyValue('--text-color'), + color: `rgb(var(--text-color))`, ecl: "L", join: true }); @@ -935,6 +963,7 @@ class PairDeviceDialog extends Dialog { this.$clearSecretsBtn.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); } + Events.fire('bg-resize'); }).catch(_ => PersistentStorage.logBrowserNotCapable()); } } @@ -960,8 +989,9 @@ class ClearDevicesDialog extends Dialog { class SendTextDialog extends Dialog { constructor() { super('sendTextDialog'); - Events.on('text-recipient', e => this._onRecipient(e.detail)); + Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); this.$text = this.$el.querySelector('#textInput'); + this.$peerDisplayName = this.$el.querySelector('#textSendPeerDisplayName'); this.$form = this.$el.querySelector('form'); this.$submit = this.$el.querySelector('button[type="submit"]'); this.$form.addEventListener('submit', _ => this._send()); @@ -992,8 +1022,9 @@ class SendTextDialog extends Dialog { } } - _onRecipient(peerId) { + _onRecipient(peerId, deviceName) { this.correspondingPeerId = peerId; + this.$peerDisplayName.innerText = deviceName; this.show(); const range = document.createRange(); @@ -1246,6 +1277,7 @@ class Notifications { this.$button.removeAttribute('hidden'); this.$button.addEventListener('click', _ => this._requestPermission()); } + // Todo: fix Notifications Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('files-received', e => this._downloadNotification(e.detail.files)); } @@ -1714,19 +1746,15 @@ Events.on('load', () => { h = window.innerHeight; c.width = w; c.height = h; - offset = h > 800 - ? 116 - : h > 380 - ? 100 - : 65; - - if (w < 420) offset += 20; + offset = $$('footer').offsetHeight - 32; + if (h > 800) offset += 16; x0 = w / 2; y0 = h - offset; dw = Math.max(w, h, 1000) / 13; drawCircles(); } - window.onresize = init; + Events.on('bg-resize', _ => init()); + window.onresize = _ => Events.fire('bg-resize'); function drawCircle(radius) { ctx.beginPath(); diff --git a/public/styles.css b/public/styles.css index aa08cbc..25be4a1 100644 --- a/public/styles.css +++ b/public/styles.css @@ -10,28 +10,25 @@ /* Layout */ -html { - min-height: 100%; - height: -webkit-fill-available; -} - html, body { margin: 0; display: flex; flex-direction: column; - width: 100%; + width: 100vw; overflow-x: hidden; - overscroll-behavior-y: none; + overscroll-behavior: none; + overflow-y: hidden; } body { - min-height: 100%; + min-height: 100vh; + /* mobile viewport bug fix */ min-height: -webkit-fill-available; - flex-grow: 1; - align-items: center; - justify-content: center; - overflow-y: hidden; +} + +html { + height: -webkit-fill-available; } .row-reverse { @@ -73,10 +70,7 @@ body { } header { - position: absolute; - top: 0; - left: 0; - right: 0; + position: relative; height: 56px; align-items: center; padding: 16px; @@ -119,9 +113,9 @@ h3 { } .font-subheading { - font-size: 16px; + font-size: 14px; font-weight: 400; - line-height: 24px; + line-height: 18px; word-break: normal; } @@ -199,20 +193,151 @@ body>header a { margin-left: 8px; } +#center { + position: relative; + display: flex; + flex-direction: column-reverse; + flex-grow: 1; + --footer-height: 132px; + max-height: calc(100vh - 56px - var(--footer-height)); + justify-content: space-around; + align-items: center; + overflow-x: hidden; + overflow-y: scroll; + overscroll-behavior-x: none; +} + +@media screen and (max-width: 425px) { + header:has(#clear-pair-devices:not([hidden]))~#center { + --footer-height: 150px; + } +} + /* Peers List */ +#x-peers-filler { + display: flex; + flex-grow: 1; +} + x-peers { - width: 100%; - overflow: hidden; + position: relative; + display: flex; flex-flow: row wrap; + flex-grow: 1; + align-items: start !important; + justify-content: center; + z-index: 2; - transition: color 300ms; + 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; +} + +@media screen and (min-height: 505px) and (max-height: 649px) and (max-width: 426px), +screen and (min-height: 486px) and (max-height: 631px) 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; + } +} + +@media screen and (min-height: 649px) and (max-width: 425px), +screen and (min-height: 631px) 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 { - height: 114px; + display: flex; + flex-direction: column; padding: 8px; text-align: center; /* prevent flickering on load */ @@ -254,25 +379,19 @@ x-no-peers[drop-bg] * { x-peer { -webkit-user-select: none; user-select: none; + padding: 8px; + align-content: start; + flex-wrap: wrap; } x-peer label { width: var(--peer-width); - padding: 8px; cursor: pointer; touch-action: manipulation; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); position: relative; } -x-peer .name { - width: var(--peer-width); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: center; -} - input[type="file"] { visibility: hidden; position: absolute; @@ -280,21 +399,45 @@ input[type="file"] { 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; - margin-bottom: 8px; - transition: transform 150ms; - will-change: transform; } -x-peer:not(.type-ip) x-icon { +x-peer:not(.type-ip).type-secret .icon-wrapper { background: var(--paired-device-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: 6px; + height: 6px; + border-radius: 50%; + display: none; +} + +x-peer.type-secret x-icon > .highlight-wrapper > .highlight { + background-color: var(--paired-device-color); + display: inline; +} + x-peer:not([status]):hover x-icon, x-peer:not([status]):focus x-icon { transform: scale(1.05); @@ -306,6 +449,18 @@ x-peer[status] x-icon { transform: scale(1); } +.device-descriptor { + text-align: center; +} + +.name { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} + .status, .device-name, .connection-hash { @@ -371,10 +526,9 @@ x-peer[drop] x-icon { /* Footer */ footer { - position: absolute; - bottom: 0; - left: 0; - right: 0; + position: relative; + margin-top: auto; + z-index: 2; align-items: center; padding: 0 0 16px 0; text-align: center; @@ -385,6 +539,7 @@ footer .logo { --icon-size: 80px; margin-bottom: 8px; color: var(--primary-color); + margin-top: -10px; } footer .font-body2 { @@ -430,6 +585,9 @@ x-dialog x-paper { top: max(50%, 350px); height: 650px; margin-top: -325px; + display: flex; + flex-direction: column; + justify-content: space-between; } x-dialog:not([show]) { @@ -509,6 +667,7 @@ x-dialog .font-subheading { #pairDeviceDialog hr { margin-top: 40px; margin-bottom: 40px; + width: 100%; } #pairDeviceDialog x-background { @@ -532,7 +691,7 @@ x-dialog h2 { } x-dialog .row-reverse { - margin: 40px -24px auto; + margin: 40px -24px 0; border-top: solid 2.5px var(--border-color); } @@ -689,16 +848,18 @@ x-dialog .row-reverse { opacity: 0.1; } -#cancelPasteModeBtn { +#cancel-paste-mode-btn { z-index: 2; - margin-top: 0; + margin: 0; + padding: 0; position: absolute; top: 0; right: 0; left: 0; - width: 100%; + width: 100vw; height: 56px; - border-bottom: solid 2.5px var(--border-color); + background-color: var(--primary-color); + color: rgb(238, 238, 238); } .button:focus:before, @@ -809,7 +970,7 @@ button::-moz-focus-inner { width: 80px; height: 80px; position: absolute; - top: 0; + top: -8px; clip: rect(0px, 80px, 80px, 40px); --progress: rotate(0deg); transition: transform 200ms; @@ -876,13 +1037,16 @@ x-toast:not([show]):not(:hover) { /* Instructions */ x-instructions { - position: absolute; - top: 120px; + position: relative; opacity: 0.5; transition: opacity 300ms; - z-index: -1; text-align: center; - width: 80%; + margin-left: 10px; + margin-right: 10px; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; } x-instructions:not([drop-peer]):not([drop-bg]):before { @@ -899,88 +1063,84 @@ x-instructions[drop-bg]:not([drop-peer]):before { x-instructions p { display: none; - margin: 0 auto auto; - max-width: 80%; } x-peers:empty~x-instructions { opacity: 0; } +@media (hover: none) and (pointer: coarse) { + x-peer { + transform: scale(0.95); + padding: 4px 0; + } +} + +#websocket-fallback { + margin-left: 5px; + margin-right: 5px; + padding: 5px; + text-align: center; + opacity: 0.5; + transition: opacity 300ms; +} + +#websocket-fallback>span { + margin: 2px; +} + +#websocket-fallback > span > span { + border-bottom: solid 4px var(--ws-peer-color); +} /* Responsive Styles */ -@media (min-height: 800px) { +@media screen and (min-height: 800px) { footer { margin-bottom: 16px; } } -@media screen and (min-height: 800px), -screen and (min-width: 1100px) { +@media (hover: hover) and (pointer: fine) { x-instructions:not([drop-peer]):not([drop-bg]):before { content: attr(desktop); } } -@media (max-height: 420px) { - x-instructions { - top: 24px; - } - - footer .logo { - --icon-size: 40px; - } -} - -/* - iOS specific styles -*/ -@supports (-webkit-overflow-scrolling: touch) { - - - html { - position: fixed; - } - - x-instructions:not([drop-peer]):not([drop-bg]):before { - content: attr(mobile); - } -} - /* Color Themes */ /* Default colors */ body { - --text-color: #333; - --bg-color: #fff; + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ + --bg-color-test: 18,18,18; --bg-color-secondary: #f1f3f4; --border-color: #e7e8e8; } /* Dark theme colors */ body.dark-theme { - --text-color: #eee; - --bg-color: #121212; + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ --bg-color-secondary: #333; --border-color: #252525; } /* Colored Elements */ body { - color: var(--text-color); - background-color: var(--bg-color); + color: rgb(var(--text-color)); + background-color: rgb(var(--bg-color)); transition: background-color 0.5s ease; } x-dialog x-paper { - background-color: var(--bg-color); + background-color: rgb(var(--bg-color)); } .textarea { - color: var(--text-color) !important; + color: rgb(var(--text-color)) !important; background-color: var(--bg-color-secondary) !important; } @@ -1018,16 +1178,16 @@ x-dialog x-paper { /* defaults to dark theme */ body { - --text-color: #eee; - --bg-color: #121212; + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ --bg-color-secondary: #333; --border-color: #252525; } /* Override dark mode with light mode styles if the user decides to swap */ body.light-theme { - --text-color: #333; - --bg-color: #fafafa; + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ --bg-color-secondary: #f1f3f4; --border-color: #e7e8e8; } @@ -1045,6 +1205,15 @@ x-dialog x-paper { } } +/* + iOS specific styles +*/ +@supports (-webkit-overflow-scrolling: touch) { + html { + min-height: -webkit-fill-available; + } +} + /* webkit scrollbar style*/ ::-webkit-scrollbar{ diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index bc7bc22..b5042a8 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -69,20 +69,21 @@ + - - - - -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
-
-
A websocket fallback is implemented on this instance. Use only if you trust the server!
-
- -
A websocket fallback is implemented on this instance. Use only if you trust the server!
-

-
+ +
+ +
+ + +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
+
+ +

+
+
on this network
+
+ Traffic is routed through the server if WebRTC is not available. +
@@ -186,8 +190,12 @@
-

PairDrop - Send a Message

-
+

PairDrop

+
+ Send a Message to + +
+
diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index e33dddb..d368173 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -28,7 +28,7 @@ class PeersUI { Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); this.peers = {}; - this.$cancelPasteModeBtn = $('cancelPasteModeBtn'); + this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); Events.on('dragover', e => this._onDragOver(e)); @@ -38,8 +38,12 @@ class PeersUI { Events.on('drop', e => this._onDrop(e)); Events.on('keydown', e => this._onKeyDown(e)); + this.$xPeers = $$('x-peers'); this.$xNoPeers = $$('x-no-peers'); this.$xInstructions = $$('x-instructions'); + + Events.on('peer-added', _ => this.evaluateOverflowing()); + Events.on('bg-resize', _ => this.evaluateOverflowing()); } _onKeyDown(e) { @@ -53,11 +57,11 @@ class PeersUI { } _joinPeer(peer, roomType, roomSecret) { - peer.roomType = roomType; + peer.roomTypes = [roomType]; peer.roomSecret = roomSecret; if (this.peers[peer.id]) { - this.peers[peer.id].roomType = peer.roomType; - this._redrawPeer(peer); + if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType); + this._redrawPeer(this.peers[peer.id]); return; // peer already exists } this.peers[peer.id] = peer; @@ -72,7 +76,15 @@ class PeersUI { const peerNode = $(peer.id); if (!peerNode) return; peerNode.classList.remove('type-ip', 'type-secret'); - peerNode.classList.add(`type-${peer.roomType}`) + peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`)); + } + + evaluateOverflowing() { + if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) { + this.$xPeers.classList.add('overflowing'); + } else { + this.$xPeers.classList.remove('overflowing'); + } } _onPeers(msg) { @@ -83,6 +95,7 @@ class PeersUI { const $peer = $(peerId); if (!$peer) return; $peer.remove(); + this.evaluateOverflowing(); if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again } @@ -213,6 +226,18 @@ class PeersUI { class PeerUI { + constructor(peer, connectionHash) { + this._peer = peer; + this._connectionHash = connectionHash; + this._initDom(); + this._bindListeners(); + + $$('x-peers').appendChild(this.$el) + Events.fire('peer-added'); + this.$xInstructions = $$('x-instructions'); + setTimeout(_ => window.animateBackground(false), 1750); // Stop animation + } + html() { let title; let input = ''; @@ -225,17 +250,24 @@ class PeerUI { this.$el.innerHTML = ` `; this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); @@ -245,23 +277,12 @@ class PeerUI { this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16); } - constructor(peer, connectionHash) { - this._peer = peer; - this._roomType = peer.roomType; - this._roomSecret = peer.roomSecret; - this._connectionHash = connectionHash; - this._initDom(); - this._bindListeners(); - $$('x-peers').appendChild(this.$el); - this.$xInstructions = $$('x-instructions'); - setTimeout(_ => window.animateBackground(false), 1750); // Stop animation - } - _initDom() { this.$el = document.createElement('x-peer'); this.$el.id = this._peer.id; this.$el.ui = this; - this.$el.classList.add(`type-${this._roomType}`); + this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`)); + this.$el.classList.add('center'); if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer') this.html(); @@ -273,7 +294,7 @@ class PeerUI { this._callbackDragLeave = e => this._onDragEnd(e) this._callbackDragOver = e => this._onDragOver(e) this._callbackContextMenu = e => this._onRightClick(e) - this._callbackTouchStart = _ => this._onTouchStart() + this._callbackTouchStart = e => this._onTouchStart(e) this._callbackTouchEnd = e => this._onTouchEnd(e) this._callbackPointerDown = e => this._onPointerDown(e) // PasteMode @@ -394,21 +415,28 @@ class PeerUI { _onRightClick(e) { e.preventDefault(); - Events.fire('text-recipient', this._peer.id); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); } - _onTouchStart() { + _onTouchStart(e) { this._touchStart = Date.now(); - this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610); + this._touchTimer = setTimeout(_ => this._onTouchEnd(e), 610); } _onTouchEnd(e) { if (Date.now() - this._touchStart < 500) { clearTimeout(this._touchTimer); - } else { // this was a long tap - if (e) e.preventDefault(); - Events.fire('text-recipient', this._peer.id); + } else if (this._touchTimer) { // this was a long tap + e.preventDefault(); + Events.fire('text-recipient', { + peerId: this._peer.id, + deviceName: e.target.closest('x-peer').querySelector('.name').innerText + }); } + this._touchTimer = null; } } @@ -844,7 +872,7 @@ class PairDeviceDialog extends Dialog { height: 150, padding: 0, background: "transparent", - color: getComputedStyle(document.body).getPropertyValue('--text-color'), + color: `rgb(var(--text-color))`, ecl: "L", join: true }); @@ -936,6 +964,7 @@ class PairDeviceDialog extends Dialog { this.$clearSecretsBtn.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); } + Events.fire('bg-resize'); }).catch(_ => PersistentStorage.logBrowserNotCapable()); } } @@ -961,8 +990,9 @@ class ClearDevicesDialog extends Dialog { class SendTextDialog extends Dialog { constructor() { super('sendTextDialog'); - Events.on('text-recipient', e => this._onRecipient(e.detail)); + Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); this.$text = this.$el.querySelector('#textInput'); + this.$peerDisplayName = this.$el.querySelector('#textSendPeerDisplayName'); this.$form = this.$el.querySelector('form'); this.$submit = this.$el.querySelector('button[type="submit"]'); this.$form.addEventListener('submit', _ => this._send()); @@ -993,8 +1023,9 @@ class SendTextDialog extends Dialog { } } - _onRecipient(peerId) { + _onRecipient(peerId, deviceName) { this.correspondingPeerId = peerId; + this.$peerDisplayName.innerText = deviceName; this.show(); const range = document.createRange(); @@ -1247,6 +1278,7 @@ class Notifications { this.$button.removeAttribute('hidden'); this.$button.addEventListener('click', _ => this._requestPermission()); } + // Todo: fix Notifications Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('files-received', e => this._downloadNotification(e.detail.files)); } @@ -1715,19 +1747,14 @@ Events.on('load', () => { h = window.innerHeight; c.width = w; c.height = h; - offset = h > 800 - ? 116 - : h > 380 - ? 100 - : 65; - - if (w < 420) offset += 20; + offset = $$('footer').offsetHeight - 32; x0 = w / 2; y0 = h - offset; dw = Math.max(w, h, 1000) / 13; drawCircles(); } - window.onresize = init; + Events.on('bg-resize', _ => init()); + window.onresize = _ => Events.fire('bg-resize'); function drawCircle(radius) { ctx.beginPath(); diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index ab61629..60bb6a6 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -11,28 +11,25 @@ /* Layout */ -html { - min-height: 100%; - height: -webkit-fill-available; -} - html, body { margin: 0; display: flex; flex-direction: column; - width: 100%; + width: 100vw; overflow-x: hidden; - overscroll-behavior-y: none; + overscroll-behavior: none; + overflow-y: hidden; } body { - min-height: 100%; + min-height: 100vh; + /* mobile viewport bug fix */ min-height: -webkit-fill-available; - flex-grow: 1; - align-items: center; - justify-content: center; - overflow-y: hidden; +} + +html { + height: -webkit-fill-available; } .row-reverse { @@ -74,10 +71,7 @@ body { } header { - position: absolute; - top: 0; - left: 0; - right: 0; + position: relative; height: 56px; align-items: center; padding: 16px; @@ -120,9 +114,9 @@ h3 { } .font-subheading { - font-size: 16px; + font-size: 14px; font-weight: 400; - line-height: 24px; + line-height: 18px; word-break: normal; } @@ -200,20 +194,160 @@ body>header a { margin-left: 8px; } +#center { + position: relative; + display: flex; + flex-direction: column-reverse; + flex-grow: 1; + --footer-height: 146px; + max-height: calc(100vh - 56px - var(--footer-height)); + justify-content: space-around; + align-items: center; + overflow-x: hidden; + overflow-y: scroll; + overscroll-behavior-x: none; +} + +@media screen and (min-width: 402px) and (max-width: 425px) { + header:has(#clear-pair-devices:not([hidden]))~#center { + --footer-height: 164px; + } +} + +@media screen and (max-width: 402px) { + #center { + --footer-height: 184px; + } +} /* Peers List */ +#x-peers-filler { + display: flex; + flex-grow: 1; +} + x-peers { - width: 100%; - overflow: hidden; + position: relative; + display: flex; flex-flow: row wrap; + flex-grow: 1; + align-items: start !important; + justify-content: center; + z-index: 2; - transition: color 300ms; + 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 (max-width: 426px), +screen and (min-height: 501px) and (max-height: 647px) 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 (max-width: 426px), +screen and (min-height: 647px) 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 { - height: 114px; + display: flex; + flex-direction: column; padding: 8px; text-align: center; /* prevent flickering on load */ @@ -255,25 +389,19 @@ x-no-peers[drop-bg] * { x-peer { -webkit-user-select: none; user-select: none; + padding: 8px; + align-content: start; + flex-wrap: wrap; } x-peer label { width: var(--peer-width); - padding: 8px; cursor: pointer; touch-action: manipulation; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); position: relative; } -x-peer .name { - width: var(--peer-width); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: center; -} - input[type="file"] { visibility: hidden; position: absolute; @@ -281,27 +409,43 @@ input[type="file"] { 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; - margin-bottom: 8px; - transition: transform 150ms; - will-change: transform; } -x-peer:not(.type-ip) x-icon { +x-peer:not(.type-ip).type-secret .icon-wrapper { background: var(--paired-device-color); } -x-peer.ws-peer x-icon { - border: solid 4px var(--ws-peer-color); +x-peer x-icon > .highlight-wrapper { + align-self: center; + align-items: center; + margin: 7px auto 0; + height: 6px; } -x-peer.ws-peer .progress { - margin-top: 4px; +x-peer x-icon > .highlight-wrapper > .highlight { + width: 6px; + height: 6px; + border-radius: 50%; + display: none; +} + +x-peer.type-secret x-icon > .highlight-wrapper > .highlight { + background-color: var(--paired-device-color); + display: inline; } x-peer:not([status]):hover x-icon, @@ -315,6 +459,35 @@ x-peer[status] x-icon { 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; +} + +.device-descriptor { + text-align: center; +} + +.name { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} + .status, .device-name, .connection-hash { @@ -380,12 +553,10 @@ x-peer[drop] x-icon { /* Footer */ footer { - position: absolute; - bottom: 0; - left: 0; - right: 0; + position: relative; + margin-top: auto; + z-index: 2; align-items: center; - padding: 0 0 16px 0; text-align: center; transition: color 300ms; } @@ -394,6 +565,7 @@ footer .logo { --icon-size: 80px; margin-bottom: 8px; color: var(--primary-color); + margin-top: -10px; } footer .font-body2 { @@ -439,6 +611,9 @@ x-dialog x-paper { top: max(50%, 350px); height: 650px; margin-top: -325px; + display: flex; + flex-direction: column; + justify-content: space-between; } x-dialog:not([show]) { @@ -518,6 +693,7 @@ x-dialog .font-subheading { #pairDeviceDialog hr { margin-top: 40px; margin-bottom: 40px; + width: 100%; } #pairDeviceDialog x-background { @@ -541,7 +717,7 @@ x-dialog h2 { } x-dialog .row-reverse { - margin: 40px -24px auto; + margin: 40px -24px 0; border-top: solid 2.5px var(--border-color); } @@ -698,16 +874,18 @@ x-dialog .row-reverse { opacity: 0.1; } -#cancelPasteModeBtn { +#cancel-paste-mode-btn { z-index: 2; - margin-top: 0; + margin: 0; + padding: 0; position: absolute; top: 0; right: 0; left: 0; - width: 100%; + width: 100vw; height: 56px; - border-bottom: solid 2.5px var(--border-color); + background-color: var(--primary-color); + color: rgb(238, 238, 238); } .button:focus:before, @@ -818,7 +996,7 @@ button::-moz-focus-inner { width: 80px; height: 80px; position: absolute; - top: 0; + top: -8px; clip: rect(0px, 80px, 80px, 40px); --progress: rotate(0deg); transition: transform 200ms; @@ -885,13 +1063,16 @@ x-toast:not([show]):not(:hover) { /* Instructions */ x-instructions { - position: absolute; - top: 120px; + position: relative; opacity: 0.5; transition: opacity 300ms; - z-index: -1; text-align: center; - width: 80%; + margin-left: 10px; + margin-right: 10px; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; } x-instructions:not([drop-peer]):not([drop-bg]):before { @@ -908,92 +1089,84 @@ x-instructions[drop-bg]:not([drop-peer]):before { x-instructions p { display: none; - margin: 0 auto auto; - max-width: 80%; } x-peers:empty~x-instructions { opacity: 0; } -.websocket-fallback { +@media (hover: none) and (pointer: coarse) { + x-peer { + transform: scale(0.95); + padding: 4px 0; + } +} + +#websocket-fallback { + margin-left: 5px; + margin-right: 5px; + padding: 5px; + text-align: center; + opacity: 0.5; + transition: opacity 300ms; +} + +#websocket-fallback>span { + margin: 2px; +} + +#websocket-fallback > span > span { border-bottom: solid 4px var(--ws-peer-color); - padding-bottom: 1px; } /* Responsive Styles */ -@media (min-height: 800px) { - footer { - margin-bottom: 16px; +@media screen and (min-height: 800px) { + #websocket-fallback { + padding-bottom: 15px; } } -@media screen and (min-height: 800px), -screen and (min-width: 1100px) { +@media (hover: hover) and (pointer: fine) { x-instructions:not([drop-peer]):not([drop-bg]):before { content: attr(desktop); } } -@media (max-height: 420px) { - x-instructions { - top: 24px; - } - - footer .logo { - --icon-size: 40px; - } -} - -/* - iOS specific styles -*/ -@supports (-webkit-overflow-scrolling: touch) { - - - html { - position: fixed; - } - - x-instructions:not([drop-peer]):not([drop-bg]):before { - content: attr(mobile); - } -} - /* Color Themes */ /* Default colors */ body { - --text-color: #333; - --bg-color: #fff; + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ + --bg-color-test: 18,18,18; --bg-color-secondary: #f1f3f4; --border-color: #e7e8e8; } /* Dark theme colors */ body.dark-theme { - --text-color: #eee; - --bg-color: #121212; + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ --bg-color-secondary: #333; --border-color: #252525; } /* Colored Elements */ body { - color: var(--text-color); - background-color: var(--bg-color); + color: rgb(var(--text-color)); + background-color: rgb(var(--bg-color)); transition: background-color 0.5s ease; } x-dialog x-paper { - background-color: var(--bg-color); + background-color: rgb(var(--bg-color)); } .textarea { - color: var(--text-color) !important; + color: rgb(var(--text-color)) !important; background-color: var(--bg-color-secondary) !important; } @@ -1031,16 +1204,16 @@ x-dialog x-paper { /* defaults to dark theme */ body { - --text-color: #eee; - --bg-color: #121212; + --text-color: 238,238,238; + --bg-color: 18,18,18; /*rgb code*/ --bg-color-secondary: #333; --border-color: #252525; } /* Override dark mode with light mode styles if the user decides to swap */ body.light-theme { - --text-color: #333; - --bg-color: #fafafa; + --text-color: 51,51,51; + --bg-color: 250,250,250; /*rgb code*/ --bg-color-secondary: #f1f3f4; --border-color: #e7e8e8; } @@ -1058,6 +1231,15 @@ x-dialog x-paper { } } +/* + iOS specific styles +*/ +@supports (-webkit-overflow-scrolling: touch) { + html { + min-height: -webkit-fill-available; + } +} + /* webkit scrollbar style*/ ::-webkit-scrollbar{