diff --git a/.github/workflows/github-image.yml b/.github/workflows/github-image.yml index 5e04e6f..c240bf8 100644 --- a/.github/workflows/github-image.yml +++ b/.github/workflows/github-image.yml @@ -16,7 +16,7 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository | downcase }} + IMAGE_NAME: ${{ github.repository }} jobs: build-and-push-image: @@ -48,4 +48,4 @@ jobs: context: . push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 2448a5a..31d3c5b 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -1,32 +1,35 @@ # Deployment Notes The easiest way to get PairDrop up and running is by using Docker. -## Deployment with Docker from Docker Hub +## Deployment with Docker + +### Docker Image from Docker Hub ```bash docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop ``` + > You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. -### Options / Flags +#### Options / Flags Set options by using the following flags in the `docker run` command: -#### Port +##### Port ```bash -p 127.0.0.1:8080:3000 ``` > Specify the port used by the docker image > - 3000 -> `-p 127.0.0.1:3000:3000` > - 8080 -> `-p 127.0.0.1:8080:3000` -#### Rate limiting requests +##### Rate limiting requests ``` -e RATE_LIMIT=true ``` -> Limits clients to 100 requests per 5 min +> Limits clients to 1000 requests per 5 min -#### Websocket Fallback (for VPN) +##### Websocket Fallback (for VPN) ```bash -e WS_FALLBACK=true ``` @@ -69,8 +72,18 @@ Set options by using the following flags in the `docker run` command:
-## Deployment with Docker with self-built image -### Build the image +### Docker Image from GHCR +```bash +docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod +``` +> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> +> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) + +### Docker Image self-built +#### Build the image ```bash docker build --pull . -f Dockerfile -t pairdrop ``` @@ -78,15 +91,45 @@ docker build --pull . -f Dockerfile -t pairdrop > > `--pull` ensures always the latest node image is used. -### Run the image +#### Run the image ```bash docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod ``` > You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy and reach the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. > -> To specify options replace `npm run start:prod` according to [the documentation above.](#options--flags) +> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) + +
+ +## Deployment with Docker Compose +Here's an example docker-compose file: + +```yaml +version: "2" +services: + pairdrop: + image: lscr.io/linuxserver/pairdrop:latest + container_name: pairdrop + restart: unless-stopped + environment: + - PUID=1000 # UID to run the application as + - PGID=1000 # GID to run the application as + - WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client. + - RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min. + - TZ=Etc/UTC # Time Zone + ports: + - 127.0.0.1:3000:3000 # Web UI +``` + +Run the compose file with `docker compose up -d`. + +> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> +> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. + +
## Deployment with node @@ -169,7 +212,7 @@ npm start -- --localhost-only > > You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> Use this when deploying PairDrop with node to prevent bypassing the proxy and reach the docker container directly. +> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly. #### Automatic restart on error ```bash @@ -183,7 +226,7 @@ npm start -- --auto-restart ```bash npm start -- --rate-limit ``` -> Limits clients to 100 requests per 5 min +> Limits clients to 1000 requests per 5 min
@@ -218,7 +261,7 @@ When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Ot ### Using nginx #### Allow http and https requests -```nginx configuration +``` server { listen 80; @@ -251,7 +294,7 @@ server { ``` #### Automatic http to https redirect: -```nginx configuration +``` server { listen 80; diff --git a/package-lock.json b/package-lock.json index f1dbbca..c9ddb2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.1.1", + "version": "1.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.1.1", + "version": "1.1.3", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index ff66482..30444eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.1.1", + "version": "1.1.3", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 14d50cf..59a257a 100644 --- a/public/index.html +++ b/public/index.html @@ -69,45 +69,49 @@ + - - - - -

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
+
+ +

+
+
- +

Pair Devices

-
-

000 000

-
Input this key on another device
or scan the QR-Code.
+
+

000 000

+
Input this key on another device
or scan the QR-Code.

-
- - - - - - +
+ + + + + +
Enter key from another device to continue.
@@ -120,7 +124,7 @@ - +
@@ -135,43 +139,43 @@
- +

PairDrop

- + would like to share
-
- - +
+ +
- +
- +
- +
- + -

+

- +
@@ -179,12 +183,16 @@
- +
-

PairDrop - Send a Message

-
+

PairDrop

+
+ Send a Message to + +
+
@@ -195,36 +203,36 @@ - +

PairDrop - Message Received

-
- +
+ sent the following message:
- +
- +
- - + + - +
- +
diff --git a/public/scripts/ui.js b/public/scripts/ui.js index e864781..25733b8 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -10,7 +10,7 @@ window.pasteMode.activated = false; // set display name Events.on('display-name', e => { const me = e.detail.message; - const $displayName = $('displayName') + const $displayName = $('display-name') $displayName.textContent = 'You are known as ' + me.displayName; $displayName.title = me.deviceName; }); @@ -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; } } @@ -469,10 +497,10 @@ class ReceiveDialog extends Dialog { class ReceiveFileDialog extends ReceiveDialog { constructor() { - super('receiveFileDialog'); + super('receive-file-dialog'); - this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload'); - this.$receiveTitleNode = this.$el.querySelector('#receiveTitle') + this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); + this.$receiveTitleNode = this.$el.querySelector('#receive-title') Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request)); this._filesQueue = []; @@ -631,15 +659,15 @@ class ReceiveFileDialog extends ReceiveDialog { class ReceiveRequestDialog extends ReceiveDialog { constructor() { - super('receiveRequestDialog'); + super('receive-request-dialog'); - this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName'); - this.$fileStemNode = this.$el.querySelector('#fileStem'); - this.$fileExtensionNode = this.$el.querySelector('#fileExtension'); - this.$fileOtherNode = this.$el.querySelector('#fileOther'); + this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name'); + this.$fileStemNode = this.$el.querySelector('#file-stem'); + this.$fileExtensionNode = this.$el.querySelector('#file-extension'); + this.$fileOtherNode = this.$el.querySelector('#file-other'); - this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest'); - this.$declineRequestBtn = this.$el.querySelector('#declineRequest'); + this.$acceptRequestBtn = this.$el.querySelector('#accept-request'); + this.$declineRequestBtn = this.$el.querySelector('#decline-request'); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false)); @@ -720,12 +748,12 @@ class ReceiveRequestDialog extends ReceiveDialog { class PairDeviceDialog extends Dialog { constructor() { - super('pairDeviceDialog'); + super('pair-device-dialog'); $('pair-device').addEventListener('click', _ => this._pairDeviceInitiate()); - this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input'); + this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input'); this.$submitBtn = this.$el.querySelector('button[type="submit"]'); - this.$roomKey = this.$el.querySelector('#roomKey'); - this.$qrCode = this.$el.querySelector('#roomKeyQrCode'); + this.$roomKey = this.$el.querySelector('#room-key'); + this.$qrCode = this.$el.querySelector('#room-key-qr-code'); this.$clearSecretsBtn = $('clear-pair-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); let createJoinForm = this.$el.querySelector('form'); @@ -799,7 +827,7 @@ class PairDeviceDialog extends Dialog { } evaluateRoomKeyChars() { - if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) { + if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) { this.$submitBtn.setAttribute("disabled", ""); } else { this.inputRoomKey = ""; @@ -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,13 +963,14 @@ class PairDeviceDialog extends Dialog { this.$clearSecretsBtn.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); } + Events.fire('bg-resize'); }).catch(_ => PersistentStorage.logBrowserNotCapable()); } } class ClearDevicesDialog extends Dialog { constructor() { - super('clearDevicesDialog'); + super('clear-devices-dialog'); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); let clearDevicesForm = this.$el.querySelector('form'); clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); @@ -959,9 +988,10 @@ class ClearDevicesDialog extends Dialog { class SendTextDialog extends Dialog { constructor() { - super('sendTextDialog'); - Events.on('text-recipient', e => this._onRecipient(e.detail)); - this.$text = this.$el.querySelector('#textInput'); + super('send-text-dialog'); + Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); + this.$text = this.$el.querySelector('#text-input'); + this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name'); 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(); @@ -1017,7 +1048,7 @@ class SendTextDialog extends Dialog { class ReceiveTextDialog extends Dialog { constructor() { - super('receiveTextDialog'); + super('receive-text-dialog'); Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId)); this.$text = this.$el.querySelector('#text'); this.$copy = this.$el.querySelector('#copy'); @@ -1028,7 +1059,7 @@ class ReceiveTextDialog extends Dialog { Events.on("keydown", e => this._onKeyDown(e)); - this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName'); + this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name'); this._receiveTextQueue = []; } @@ -1089,13 +1120,13 @@ class ReceiveTextDialog extends Dialog { class Base64ZipDialog extends Dialog { constructor() { - super('base64PasteDialog'); + super('base64-paste-dialog'); const urlParams = new URL(window.location).searchParams; const base64Text = urlParams.get('base64text'); const base64Zip = urlParams.get('base64zip'); const base64Hash = window.location.hash.substring(1); - this.$pasteBtn = this.$el.querySelector('#base64PasteBtn'); + this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); if (base64Text) { this.show(); @@ -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)); } @@ -1321,7 +1353,7 @@ class Notifications { } _download(notification) { - $('shareOrDownload').click(); + $('share-or-download').click(); notification.close(); } @@ -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(); @@ -1791,9 +1819,3 @@ Notifications permission has been blocked as the user has dismissed the permission prompt several times. This can be reset in Page Info which can be accessed by clicking the lock icon next to the URL.`; - -document.body.onclick = _ => { // safari hack to fix audio - document.body.onclick = null; - if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return; - blop.play(); -} diff --git a/public/service-worker.js b/public/service-worker.js index 8230bfe..1f1adc6 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.1.1'; +const cacheVersion = 'v1.1.3'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public/styles.css b/public/styles.css index aa08cbc..d3c05ac 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 { @@ -425,11 +580,14 @@ x-dialog x-paper { will-change: transform; } -#pairDeviceDialog x-paper { +#pair-device-dialog x-paper { position: absolute; top: max(50%, 350px); height: 650px; margin-top: -325px; + display: flex; + flex-direction: column; + justify-content: space-between; } x-dialog:not([show]) { @@ -461,13 +619,13 @@ x-dialog .font-subheading { /* PairDevicesDialog */ -#keyInputContainer { +#key-input-container { width: 100%; display: flex; justify-content: center; } -#keyInputContainer>input { +#key-input-container>input { width: 45px; height: 45px; font-size: 30px; @@ -483,15 +641,15 @@ x-dialog .font-subheading { justify-content: center; } -#keyInputContainer>input + * { +#key-input-container>input + * { margin-left: 6px; } -#keyInputContainer>input:nth-of-type(4) { +#key-input-container>input:nth-of-type(4) { margin-left: 18px; } -#roomKey { +#room-key { font-size: 50px; letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); display: inline-block; @@ -499,19 +657,20 @@ x-dialog .font-subheading { margin: 15px -15px; } -#roomKeyQrCode { +#room-key-qr-code { padding: inherit; margin: auto; width: 150px; height: 150px; } -#pairDeviceDialog hr { +#pair-device-dialog hr { margin-top: 40px; margin-bottom: 40px; + width: 100%; } -#pairDeviceDialog x-background { +#pair-device-dialog x-background { padding: 16px!important; } @@ -526,13 +685,13 @@ x-dialog h2 { margin-top: 1rem; } -#receiveRequestDialog h2, -#receiveFileDialog h2 { +#receive-request-dialog h2, +#receive-file-dialog h2 { margin-bottom: 0.5rem; } x-dialog .row-reverse { - margin: 40px -24px auto; + margin: 40px -24px 0; border-top: solid 2.5px var(--border-color); } @@ -556,11 +715,11 @@ x-dialog .row-reverse { word-break: normal; } -#fileName { +#file-name { font-style: italic; } -#fileStem { +#file-stem { max-width: 80%; overflow: hidden; text-overflow: ellipsis; @@ -574,13 +733,13 @@ x-dialog .row-reverse { /* Send Text Dialog */ -#textInput { +#text-input { min-height: 120px; } /* Receive Text Dialog */ -#receiveTextDialog #text { +#receive-text-dialog #text { width: 100%; word-break: break-all; max-height: 300px; @@ -593,15 +752,15 @@ x-dialog .row-reverse { margin-top:36px; } -#receiveTextDialog #text a { +#receive-text-dialog #text a { cursor: pointer; } -#receiveTextDialog #text a:hover { +#receive-text-dialog #text a:hover { text-decoration: underline; } -#receiveTextDialog h3 { +#receive-text-dialog h3 { /* Select the received text when double-clicking the dialog */ user-select: none; pointer-events: none; @@ -612,26 +771,26 @@ x-dialog .row-reverse { margin: auto -25px; } -#receiveTextDescriptionContainer { +#receive-text-description-container { margin-bottom: 25px; } -#base64PasteBtn { +#base64-paste-btn { width: 100%; height: 40vh; border: solid 12px #438cff; } -#base64PasteDialog button { +#base64-paste-dialog button { margin: auto; border-radius: 8px; } -#base64PasteDialog button[close] { +#base64-paste-dialog button[close] { margin-top: 20px; } -#base64PasteDialog button[close]:before { +#base64-paste-dialog button[close]:before { border-radius: 8px; } @@ -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..8227434 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -69,48 +69,52 @@ + - - - - -

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
+
+ +

+
+
-
+
You can be discovered by everyone on this network
+
+ Traffic is routed through the server if WebRTC is not available. +
- +

Pair Devices

-
-

000 000

-
Input this key on another device
or scan the QR-Code.
+
+

000 000

+
Input this key on another device
or scan the QR-Code.

-
- - - - - - +
+ + + + + +
Enter key from another device to continue.
@@ -123,7 +127,7 @@ - +
@@ -138,43 +142,43 @@
- +

PairDrop

- + would like to share
-
- - +
+ +
- +
- +
- +
- + -

+

- +
@@ -182,12 +186,16 @@
- +
-

PairDrop - Send a Message

-
+

PairDrop

+
+ Send a Message to + +
+
@@ -198,36 +206,36 @@ - +

PairDrop - Message Received

-
- +
+ sent the following message:
- +
- +
- - + + - +
- +
diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index da103ed..dadfb02 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -10,7 +10,7 @@ window.pasteMode.activated = false; // set display name Events.on('display-name', e => { const me = e.detail.message; - const $displayName = $('displayName') + const $displayName = $('display-name') $displayName.textContent = 'You are known as ' + me.displayName; $displayName.title = me.deviceName; }); @@ -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; } } @@ -470,10 +498,10 @@ class ReceiveDialog extends Dialog { class ReceiveFileDialog extends ReceiveDialog { constructor() { - super('receiveFileDialog'); + super('receive-file-dialog'); - this.$shareOrDownloadBtn = this.$el.querySelector('#shareOrDownload'); - this.$receiveTitleNode = this.$el.querySelector('#receiveTitle') + this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); + this.$receiveTitleNode = this.$el.querySelector('#receive-title') Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request)); this._filesQueue = []; @@ -632,15 +660,15 @@ class ReceiveFileDialog extends ReceiveDialog { class ReceiveRequestDialog extends ReceiveDialog { constructor() { - super('receiveRequestDialog'); + super('receive-request-dialog'); - this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requestingPeerDisplayName'); - this.$fileStemNode = this.$el.querySelector('#fileStem'); - this.$fileExtensionNode = this.$el.querySelector('#fileExtension'); - this.$fileOtherNode = this.$el.querySelector('#fileOther'); + this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name'); + this.$fileStemNode = this.$el.querySelector('#file-stem'); + this.$fileExtensionNode = this.$el.querySelector('#file-extension'); + this.$fileOtherNode = this.$el.querySelector('#file-other'); - this.$acceptRequestBtn = this.$el.querySelector('#acceptRequest'); - this.$declineRequestBtn = this.$el.querySelector('#declineRequest'); + this.$acceptRequestBtn = this.$el.querySelector('#accept-request'); + this.$declineRequestBtn = this.$el.querySelector('#decline-request'); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false)); @@ -721,12 +749,12 @@ class ReceiveRequestDialog extends ReceiveDialog { class PairDeviceDialog extends Dialog { constructor() { - super('pairDeviceDialog'); + super('pair-device-dialog'); $('pair-device').addEventListener('click', _ => this._pairDeviceInitiate()); - this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input'); + this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input'); this.$submitBtn = this.$el.querySelector('button[type="submit"]'); - this.$roomKey = this.$el.querySelector('#roomKey'); - this.$qrCode = this.$el.querySelector('#roomKeyQrCode'); + this.$roomKey = this.$el.querySelector('#room-key'); + this.$qrCode = this.$el.querySelector('#room-key-qr-code'); this.$clearSecretsBtn = $('clear-pair-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); let createJoinForm = this.$el.querySelector('form'); @@ -800,7 +828,7 @@ class PairDeviceDialog extends Dialog { } evaluateRoomKeyChars() { - if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) { + if (this.$el.querySelectorAll('#key-input-container>input:placeholder-shown').length > 0) { this.$submitBtn.setAttribute("disabled", ""); } else { this.inputRoomKey = ""; @@ -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,13 +964,14 @@ class PairDeviceDialog extends Dialog { this.$clearSecretsBtn.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); } + Events.fire('bg-resize'); }).catch(_ => PersistentStorage.logBrowserNotCapable()); } } class ClearDevicesDialog extends Dialog { constructor() { - super('clearDevicesDialog'); + super('clear-devices-dialog'); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); let clearDevicesForm = this.$el.querySelector('form'); clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); @@ -960,9 +989,10 @@ class ClearDevicesDialog extends Dialog { class SendTextDialog extends Dialog { constructor() { - super('sendTextDialog'); - Events.on('text-recipient', e => this._onRecipient(e.detail)); - this.$text = this.$el.querySelector('#textInput'); + super('send-text-dialog'); + Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); + this.$text = this.$el.querySelector('#text-input'); + this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name'); 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(); @@ -1018,7 +1049,7 @@ class SendTextDialog extends Dialog { class ReceiveTextDialog extends Dialog { constructor() { - super('receiveTextDialog'); + super('receive-text-dialog'); Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId)); this.$text = this.$el.querySelector('#text'); this.$copy = this.$el.querySelector('#copy'); @@ -1029,7 +1060,7 @@ class ReceiveTextDialog extends Dialog { Events.on("keydown", e => this._onKeyDown(e)); - this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receiveTextPeerDisplayName'); + this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name'); this._receiveTextQueue = []; } @@ -1090,13 +1121,13 @@ class ReceiveTextDialog extends Dialog { class Base64ZipDialog extends Dialog { constructor() { - super('base64PasteDialog'); + super('base64-paste-dialog'); const urlParams = new URL(window.location).searchParams; const base64Text = urlParams.get('base64text'); const base64Zip = urlParams.get('base64zip'); const base64Hash = window.location.hash.substring(1); - this.$pasteBtn = this.$el.querySelector('#base64PasteBtn'); + this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); if (base64Text) { this.show(); @@ -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)); } @@ -1322,7 +1354,7 @@ class Notifications { } _download(notification) { - $('shareOrDownload').click(); + $('share-or-download').click(); notification.close(); } @@ -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(); @@ -1792,9 +1819,3 @@ Notifications permission has been blocked as the user has dismissed the permission prompt several times. This can be reset in Page Info which can be accessed by clicking the lock icon next to the URL.`; - -document.body.onclick = _ => { // safari hack to fix audio - document.body.onclick = null; - if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return; - blop.play(); -} diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index fe52427..9e968ec 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.1.1'; +const cacheVersion = 'v1.1.3'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index ab61629..f153398 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 { @@ -434,11 +606,14 @@ x-dialog x-paper { will-change: transform; } -#pairDeviceDialog x-paper { +#pair-device-dialog x-paper { position: absolute; top: max(50%, 350px); height: 650px; margin-top: -325px; + display: flex; + flex-direction: column; + justify-content: space-between; } x-dialog:not([show]) { @@ -470,13 +645,13 @@ x-dialog .font-subheading { /* PairDevicesDialog */ -#keyInputContainer { +#key-input-container { width: 100%; display: flex; justify-content: center; } -#keyInputContainer>input { +#key-input-container>input { width: 45px; height: 45px; font-size: 30px; @@ -492,15 +667,15 @@ x-dialog .font-subheading { justify-content: center; } -#keyInputContainer>input + * { +#key-input-container>input + * { margin-left: 6px; } -#keyInputContainer>input:nth-of-type(4) { +#key-input-container>input:nth-of-type(4) { margin-left: 18px; } -#roomKey { +#room-key { font-size: 50px; letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px); display: inline-block; @@ -508,19 +683,20 @@ x-dialog .font-subheading { margin: 15px -15px; } -#roomKeyQrCode { +#room-key-qr-code { padding: inherit; margin: auto; width: 150px; height: 150px; } -#pairDeviceDialog hr { +#pair-device-dialog hr { margin-top: 40px; margin-bottom: 40px; + width: 100%; } -#pairDeviceDialog x-background { +#pair-device-dialog x-background { padding: 16px!important; } @@ -535,13 +711,13 @@ x-dialog h2 { margin-top: 1rem; } -#receiveRequestDialog h2, -#receiveFileDialog h2 { +#receive-request-dialog h2, +#receive-file-dialog h2 { margin-bottom: 0.5rem; } x-dialog .row-reverse { - margin: 40px -24px auto; + margin: 40px -24px 0; border-top: solid 2.5px var(--border-color); } @@ -565,11 +741,11 @@ x-dialog .row-reverse { word-break: normal; } -#fileName { +#file-name { font-style: italic; } -#fileStem { +#file-stem { max-width: 80%; overflow: hidden; text-overflow: ellipsis; @@ -583,13 +759,13 @@ x-dialog .row-reverse { /* Send Text Dialog */ -#textInput { +#text-input { min-height: 120px; } /* Receive Text Dialog */ -#receiveTextDialog #text { +#receive-text-dialog #text { width: 100%; word-break: break-all; max-height: 300px; @@ -602,15 +778,15 @@ x-dialog .row-reverse { margin-top:36px; } -#receiveTextDialog #text a { +#receive-text-dialog #text a { cursor: pointer; } -#receiveTextDialog #text a:hover { +#receive-text-dialog #text a:hover { text-decoration: underline; } -#receiveTextDialog h3 { +#receive-text-dialog h3 { /* Select the received text when double-clicking the dialog */ user-select: none; pointer-events: none; @@ -621,26 +797,26 @@ x-dialog .row-reverse { margin: auto -25px; } -#receiveTextDescriptionContainer { +#receive-text-description-container { margin-bottom: 25px; } -#base64PasteBtn { +#base64-paste-btn { width: 100%; height: 40vh; border: solid 12px #438cff; } -#base64PasteDialog button { +#base64-paste-dialog button { margin: auto; border-radius: 8px; } -#base64PasteDialog button[close] { +#base64-paste-dialog button[close] { margin-top: 20px; } -#base64PasteDialog button[close]:before { +#base64-paste-dialog button[close]:before { border-radius: 8px; } @@ -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{