mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-20 23:16:13 -04:00
Merge pull request #151 from schlagmichdoch/translate
Add two major features: Translation #35 & Temporary Public Room #55
This commit is contained in:
commit
c305996799
24 changed files with 4829 additions and 1282 deletions
60
README.md
60
README.md
|
@ -20,8 +20,12 @@
|
||||||
## Features
|
## Features
|
||||||
[PairDrop](https://pairdrop.net) is a sublime alternative to AirDrop that works on all platforms.
|
[PairDrop](https://pairdrop.net) is a sublime alternative to AirDrop that works on all platforms.
|
||||||
|
|
||||||
Send images, documents or text via peer to peer connection to devices in the same local network/Wi-Fi or to paired devices.
|
- File Sharing on your local network
|
||||||
As it is web based, it runs on all devices.
|
- Send images, documents or text via peer to peer connection to devices on the same local network.
|
||||||
|
- Internet Transfers
|
||||||
|
- Join temporary public rooms to transfer files easily over the internet!
|
||||||
|
- Web-Application
|
||||||
|
- As it is web based, it runs on all devices.
|
||||||
|
|
||||||
You want to quickly send a file from your phone to your laptop?
|
You want to quickly send a file from your phone to your laptop?
|
||||||
<br>You want to share photos in original quality with friends that use a mixture of Android and iOS?
|
<br>You want to share photos in original quality with friends that use a mixture of Android and iOS?
|
||||||
|
@ -32,14 +36,29 @@ You want to quickly send a file from your phone to your laptop?
|
||||||
Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||||
|
|
||||||
## Differences to Snapdrop
|
## Differences to Snapdrop
|
||||||
|
<details><summary>Click to expand</summary>
|
||||||
|
|
||||||
### Device Pairing / Internet Transfer
|
### Paired Devices and Public Rooms - Internet Transfer
|
||||||
* Pair devices via 6-digit code or QR-Code
|
* Transfer files over the internet between paired devices or by entering temporary public rooms.
|
||||||
* Pair devices outside your local network or in complex network environment (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
|
* Connect to devices in complex network environments (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
|
||||||
* Connect to devices on your mobile hotspot.
|
* Connect to devices on your mobile hotspot.
|
||||||
* Paired devices will always find each other via shared secrets even after reopening the browser or the Progressive Web App
|
* Devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
|
||||||
* You will always discover devices on your local network. Paired devices are shown additionally.
|
* Connect to devices on your mobile hotspot.
|
||||||
* Paired devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
|
* You will always discover devices on your local network. Paired devices and devices in the same public room are shown additionally.
|
||||||
|
|
||||||
|
#### Persistent Device Pairing
|
||||||
|
* Pair your devices via a 6-digit code or a QR-Code.
|
||||||
|
* Paired devices will always find each other via shared secrets independently of their local network.
|
||||||
|
* Paired devices are persistent. You find your devices even after reopening PairDrop.
|
||||||
|
* You can edit and unpair devices easily
|
||||||
|
* Ideal to always connect easily to your own devices
|
||||||
|
|
||||||
|
#### Temporary Public Rooms
|
||||||
|
* Enter a public room via a 5-letter code or a QR-Code.
|
||||||
|
* Enter a public room to temporarily connect to devices outside your local network.
|
||||||
|
* All devices in the same public room see each other mutually.
|
||||||
|
* Public rooms are temporary. Public rooms are left as soon as PairDrop is closed.
|
||||||
|
* Ideal to connect easily to others in complex network situations or over the internet.
|
||||||
|
|
||||||
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
|
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
|
||||||
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
||||||
|
@ -66,13 +85,12 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||||
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
|
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
|
||||||
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
|
||||||
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
|
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
|
||||||
|
* Built-in translations
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
<div align="center">
|
<img src="https://raw.githubusercontent.com/schlagmichdoch/PairDrop/master/docs/pairdrop_screenshot_mobile.gif" style="max-height: 50vh">
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## PairDrop is built with the following awesome technologies:
|
## PairDrop is built with the following awesome technologies:
|
||||||
* Vanilla HTML5 / ES6 / CSS3 frontend
|
* Vanilla HTML5 / ES6 / CSS3 frontend
|
||||||
|
@ -82,23 +100,29 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||||
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
||||||
* [cyrb53](https://github.com/bryc) super fast hash function
|
* [cyrb53](https://github.com/bryc) super fast hash function
|
||||||
|
* [Weblate](https://weblate.org/) Web based localization tool
|
||||||
|
|
||||||
Have any questions? Read our [FAQ](/docs/faq.md).
|
Have any questions? Read our [FAQ](/docs/faq.md).
|
||||||
|
|
||||||
You can [host your own instance with Docker](/docs/host-your-own.md).
|
You can [host your own instance with Docker](/docs/host-your-own.md).
|
||||||
|
|
||||||
|
|
||||||
## Support the Community
|
## Support PairDrop
|
||||||
PairDrop is free and always will be. Still, we have to pay for the domain and the server.
|
|
||||||
|
|
||||||
To contribute and support:<br>
|
|
||||||
<a href="https://www.buymeacoffee.com/pairdrop" target="_blank">
|
<a href="https://www.buymeacoffee.com/pairdrop" target="_blank">
|
||||||
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
|
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
PairDrop is free and always will be.
|
||||||
|
Still, we have to pay for the domain and the server.
|
||||||
|
|
||||||
|
To contribute and support, please use BuyMeACoffee via the button above.
|
||||||
|
|
||||||
Thanks a lot for supporting free and open software!
|
Thanks a lot for supporting free and open software!
|
||||||
|
|
||||||
To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop).
|
## Translate PairDrop
|
||||||
|
<a href="https://hosted.weblate.org/engage/pairdrop/">
|
||||||
|
<img src="https://hosted.weblate.org/widget/pairdrop/pairdrop-spa/open-graph.png" alt="Translation status" style="max-height: 30vh" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## How to contribute
|
## How to contribute
|
||||||
|
|
||||||
|
|
318
index.js
318
index.js
|
@ -130,8 +130,10 @@ class PairDropServer {
|
||||||
this._wss = new WebSocket.Server({ server });
|
this._wss = new WebSocket.Server({ server });
|
||||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||||
|
|
||||||
this._rooms = {};
|
this._rooms = {}; // { roomId: peers[] }
|
||||||
this._roomSecrets = {};
|
this._roomSecrets = {}; // { pairKey: roomSecret }
|
||||||
|
|
||||||
|
this._keepAliveTimers = {};
|
||||||
|
|
||||||
console.log('PairDrop is running on port', port);
|
console.log('PairDrop is running on port', port);
|
||||||
}
|
}
|
||||||
|
@ -139,7 +141,9 @@ class PairDropServer {
|
||||||
_onConnection(peer) {
|
_onConnection(peer) {
|
||||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||||
peer.socket.onerror = e => console.error(e);
|
peer.socket.onerror = e => console.error(e);
|
||||||
|
|
||||||
this._keepAlive(peer);
|
this._keepAlive(peer);
|
||||||
|
|
||||||
this._send(peer, {
|
this._send(peer, {
|
||||||
type: 'rtc-config',
|
type: 'rtc-config',
|
||||||
config: rtcConfig
|
config: rtcConfig
|
||||||
|
@ -170,10 +174,10 @@ class PairDropServer {
|
||||||
this._onDisconnect(sender);
|
this._onDisconnect(sender);
|
||||||
break;
|
break;
|
||||||
case 'pong':
|
case 'pong':
|
||||||
sender.lastBeat = Date.now();
|
this._keepAliveTimers[sender.id].lastBeat = Date.now();
|
||||||
break;
|
break;
|
||||||
case 'join-ip-room':
|
case 'join-ip-room':
|
||||||
this._joinRoom(sender);
|
this._joinIpRoom(sender);
|
||||||
break;
|
break;
|
||||||
case 'room-secrets':
|
case 'room-secrets':
|
||||||
this._onRoomSecrets(sender, message);
|
this._onRoomSecrets(sender, message);
|
||||||
|
@ -192,9 +196,15 @@ class PairDropServer {
|
||||||
break;
|
break;
|
||||||
case 'regenerate-room-secret':
|
case 'regenerate-room-secret':
|
||||||
this._onRegenerateRoomSecret(sender, message);
|
this._onRegenerateRoomSecret(sender, message);
|
||||||
break
|
break;
|
||||||
case 'resend-peers':
|
case 'create-public-room':
|
||||||
this._notifyPeers(sender);
|
this._onCreatePublicRoom(sender);
|
||||||
|
break;
|
||||||
|
case 'join-public-room':
|
||||||
|
this._onJoinPublicRoom(sender, message);
|
||||||
|
break;
|
||||||
|
case 'leave-public-room':
|
||||||
|
this._onLeavePublicRoom(sender);
|
||||||
break;
|
break;
|
||||||
case 'signal':
|
case 'signal':
|
||||||
default:
|
default:
|
||||||
|
@ -203,7 +213,9 @@ class PairDropServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_signalAndRelay(sender, message) {
|
_signalAndRelay(sender, message) {
|
||||||
const room = message.roomType === 'ip' ? sender.ip : message.roomSecret;
|
const room = message.roomType === 'ip'
|
||||||
|
? sender.ip
|
||||||
|
: message.roomId;
|
||||||
|
|
||||||
// relay message to recipient
|
// relay message to recipient
|
||||||
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
|
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
|
||||||
|
@ -223,10 +235,16 @@ class PairDropServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_disconnect(sender) {
|
_disconnect(sender) {
|
||||||
this._leaveRoom(sender, 'ip', '', true);
|
this._removePairKey(sender.pairKey);
|
||||||
|
sender.pairKey = null;
|
||||||
|
|
||||||
|
this._cancelKeepAlive(sender);
|
||||||
|
delete this._keepAliveTimers[sender.id];
|
||||||
|
|
||||||
|
this._leaveIpRoom(sender, true);
|
||||||
this._leaveAllSecretRooms(sender, true);
|
this._leaveAllSecretRooms(sender, true);
|
||||||
this._removeRoomKey(sender.roomKey);
|
this._leavePublicRoom(sender, true);
|
||||||
sender.roomKey = null;
|
|
||||||
sender.socket.terminate();
|
sender.socket.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +273,7 @@ class PairDropServer {
|
||||||
for (const peerId in room) {
|
for (const peerId in room) {
|
||||||
const peer = room[peerId];
|
const peer = room[peerId];
|
||||||
|
|
||||||
this._leaveRoom(peer, 'secret', roomSecret);
|
this._leaveSecretRoom(peer, roomSecret, true);
|
||||||
|
|
||||||
this._send(peer, {
|
this._send(peer, {
|
||||||
type: 'secret-room-deleted',
|
type: 'secret-room-deleted',
|
||||||
|
@ -266,34 +284,35 @@ class PairDropServer {
|
||||||
|
|
||||||
_onPairDeviceInitiate(sender) {
|
_onPairDeviceInitiate(sender) {
|
||||||
let roomSecret = randomizer.getRandomString(256);
|
let roomSecret = randomizer.getRandomString(256);
|
||||||
let roomKey = this._createRoomKey(sender, roomSecret);
|
let pairKey = this._createPairKey(sender, roomSecret);
|
||||||
if (sender.roomKey) this._removeRoomKey(sender.roomKey);
|
|
||||||
sender.roomKey = roomKey;
|
if (sender.pairKey) {
|
||||||
|
this._removePairKey(sender.pairKey);
|
||||||
|
}
|
||||||
|
sender.pairKey = pairKey;
|
||||||
|
|
||||||
this._send(sender, {
|
this._send(sender, {
|
||||||
type: 'pair-device-initiated',
|
type: 'pair-device-initiated',
|
||||||
roomSecret: roomSecret,
|
roomSecret: roomSecret,
|
||||||
roomKey: roomKey
|
pairKey: pairKey
|
||||||
});
|
});
|
||||||
this._joinRoom(sender, 'secret', roomSecret);
|
this._joinSecretRoom(sender, roomSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPairDeviceJoin(sender, message) {
|
_onPairDeviceJoin(sender, message) {
|
||||||
// rate limit implementation: max 10 attempts every 10s
|
if (sender.rateLimitReached()) {
|
||||||
if (sender.roomKeyRate >= 10) {
|
this._send(sender, { type: 'join-key-rate-limit' });
|
||||||
this._send(sender, { type: 'pair-device-join-key-rate-limit' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sender.roomKeyRate += 1;
|
|
||||||
setTimeout(_ => sender.roomKeyRate -= 1, 10000);
|
|
||||||
|
|
||||||
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
|
if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].creator.id) {
|
||||||
this._send(sender, { type: 'pair-device-join-key-invalid' });
|
this._send(sender, { type: 'pair-device-join-key-invalid' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
|
const roomSecret = this._roomSecrets[message.pairKey].roomSecret;
|
||||||
const creator = this._roomSecrets[message.roomKey].creator;
|
const creator = this._roomSecrets[message.pairKey].creator;
|
||||||
this._removeRoomKey(message.roomKey);
|
this._removePairKey(message.pairKey);
|
||||||
this._send(sender, {
|
this._send(sender, {
|
||||||
type: 'pair-device-joined',
|
type: 'pair-device-joined',
|
||||||
roomSecret: roomSecret,
|
roomSecret: roomSecret,
|
||||||
|
@ -304,22 +323,53 @@ class PairDropServer {
|
||||||
roomSecret: roomSecret,
|
roomSecret: roomSecret,
|
||||||
peerId: sender.id
|
peerId: sender.id
|
||||||
});
|
});
|
||||||
this._joinRoom(sender, 'secret', roomSecret);
|
this._joinSecretRoom(sender, roomSecret);
|
||||||
this._removeRoomKey(sender.roomKey);
|
this._removePairKey(sender.pairKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPairDeviceCancel(sender) {
|
_onPairDeviceCancel(sender) {
|
||||||
const roomKey = sender.roomKey
|
const pairKey = sender.pairKey
|
||||||
|
|
||||||
if (!roomKey) return;
|
if (!pairKey) return;
|
||||||
|
|
||||||
this._removeRoomKey(roomKey);
|
this._removePairKey(pairKey);
|
||||||
this._send(sender, {
|
this._send(sender, {
|
||||||
type: 'pair-device-canceled',
|
type: 'pair-device-canceled',
|
||||||
roomKey: roomKey,
|
pairKey: pairKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onCreatePublicRoom(sender) {
|
||||||
|
let publicRoomId = randomizer.getRandomString(5, true).toLowerCase();
|
||||||
|
|
||||||
|
this._send(sender, {
|
||||||
|
type: 'public-room-created',
|
||||||
|
roomId: publicRoomId
|
||||||
|
});
|
||||||
|
|
||||||
|
this._joinPublicRoom(sender, publicRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onJoinPublicRoom(sender, message) {
|
||||||
|
if (sender.rateLimitReached()) {
|
||||||
|
this._send(sender, { type: 'join-key-rate-limit' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) {
|
||||||
|
this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._leavePublicRoom(sender);
|
||||||
|
this._joinPublicRoom(sender, message.publicRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLeavePublicRoom(sender) {
|
||||||
|
this._leavePublicRoom(sender, true);
|
||||||
|
this._send(sender, { type: 'public-room-left' });
|
||||||
|
}
|
||||||
|
|
||||||
_onRegenerateRoomSecret(sender, message) {
|
_onRegenerateRoomSecret(sender, message) {
|
||||||
const oldRoomSecret = message.roomSecret;
|
const oldRoomSecret = message.roomSecret;
|
||||||
const newRoomSecret = randomizer.getRandomString(256);
|
const newRoomSecret = randomizer.getRandomString(256);
|
||||||
|
@ -337,122 +387,158 @@ class PairDropServer {
|
||||||
delete this._rooms[oldRoomSecret];
|
delete this._rooms[oldRoomSecret];
|
||||||
}
|
}
|
||||||
|
|
||||||
_createRoomKey(creator, roomSecret) {
|
_createPairKey(creator, roomSecret) {
|
||||||
let roomKey;
|
let pairKey;
|
||||||
do {
|
do {
|
||||||
// get randomInt until keyRoom not occupied
|
// get randomInt until keyRoom not occupied
|
||||||
roomKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
|
pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
|
||||||
} while (roomKey in this._roomSecrets)
|
} while (pairKey in this._roomSecrets)
|
||||||
|
|
||||||
this._roomSecrets[roomKey] = {
|
this._roomSecrets[pairKey] = {
|
||||||
roomSecret: roomSecret,
|
roomSecret: roomSecret,
|
||||||
creator: creator
|
creator: creator
|
||||||
}
|
}
|
||||||
|
|
||||||
return roomKey;
|
return pairKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeRoomKey(roomKey) {
|
_removePairKey(roomKey) {
|
||||||
if (roomKey in this._roomSecrets) {
|
if (roomKey in this._roomSecrets) {
|
||||||
this._roomSecrets[roomKey].creator.roomKey = null
|
this._roomSecrets[roomKey].creator.roomKey = null
|
||||||
delete this._roomSecrets[roomKey];
|
delete this._roomSecrets[roomKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
|
_joinIpRoom(peer) {
|
||||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
this._joinRoom(peer, 'ip', peer.ip);
|
||||||
|
}
|
||||||
|
|
||||||
if (this._rooms[room] && this._rooms[room][peer.id]) {
|
_joinSecretRoom(peer, roomSecret) {
|
||||||
|
this._joinRoom(peer, 'secret', roomSecret);
|
||||||
|
|
||||||
|
// add secret to peer
|
||||||
|
peer.addRoomSecret(roomSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
_joinPublicRoom(peer, publicRoomId) {
|
||||||
|
// prevent joining of 2 public rooms simultaneously
|
||||||
|
this._leavePublicRoom(peer);
|
||||||
|
|
||||||
|
this._joinRoom(peer, 'public-id', publicRoomId);
|
||||||
|
|
||||||
|
peer.publicRoomId = publicRoomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_joinRoom(peer, roomType, roomId) {
|
||||||
|
// roomType: 'ip', 'secret' or 'public-id'
|
||||||
|
if (this._rooms[roomId] && this._rooms[roomId][peer.id]) {
|
||||||
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
|
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
|
||||||
this._leaveRoom(peer, roomType, roomSecret);
|
this._leaveRoom(peer, roomType, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if room doesn't exist, create it
|
// if room doesn't exist, create it
|
||||||
if (!this._rooms[room]) {
|
if (!this._rooms[roomId]) {
|
||||||
this._rooms[room] = {};
|
this._rooms[roomId] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this._notifyPeers(peer, roomType, roomSecret);
|
this._notifyPeers(peer, roomType, roomId);
|
||||||
|
|
||||||
// add peer to room
|
// add peer to room
|
||||||
this._rooms[room][peer.id] = peer;
|
this._rooms[roomId][peer.id] = peer;
|
||||||
// add secret to peer
|
|
||||||
if (roomType === 'secret') {
|
|
||||||
peer.addRoomSecret(roomSecret);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_leaveRoom(peer, roomType = 'ip', roomSecret = '', disconnect = false) {
|
|
||||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
|
||||||
|
|
||||||
if (!this._rooms[room] || !this._rooms[room][peer.id]) return;
|
_leaveIpRoom(peer, disconnect = false) {
|
||||||
this._cancelKeepAlive(this._rooms[room][peer.id]);
|
this._leaveRoom(peer, 'ip', peer.ip, disconnect);
|
||||||
|
}
|
||||||
|
|
||||||
// delete the peer
|
_leaveSecretRoom(peer, roomSecret, disconnect = false) {
|
||||||
delete this._rooms[room][peer.id];
|
this._leaveRoom(peer, 'secret', roomSecret, disconnect)
|
||||||
|
|
||||||
//if room is empty, delete the room
|
|
||||||
if (!Object.keys(this._rooms[room]).length) {
|
|
||||||
delete this._rooms[room];
|
|
||||||
} else {
|
|
||||||
// notify all other peers
|
|
||||||
for (const otherPeerId in this._rooms[room]) {
|
|
||||||
const otherPeer = this._rooms[room][otherPeerId];
|
|
||||||
this._send(otherPeer, {
|
|
||||||
type: 'peer-left',
|
|
||||||
peerId: peer.id,
|
|
||||||
roomType: roomType,
|
|
||||||
roomSecret: roomSecret,
|
|
||||||
disconnect: disconnect
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//remove secret from peer
|
//remove secret from peer
|
||||||
if (roomType === 'secret') {
|
peer.removeRoomSecret(roomSecret);
|
||||||
peer.removeRoomSecret(roomSecret);
|
}
|
||||||
|
|
||||||
|
_leavePublicRoom(peer, disconnect = false) {
|
||||||
|
if (!peer.publicRoomId) return;
|
||||||
|
|
||||||
|
this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect);
|
||||||
|
|
||||||
|
peer.publicRoomId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_leaveRoom(peer, roomType, roomId, disconnect = false) {
|
||||||
|
if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return;
|
||||||
|
|
||||||
|
// remove peer from room
|
||||||
|
delete this._rooms[roomId][peer.id];
|
||||||
|
|
||||||
|
// delete room if empty and abort
|
||||||
|
if (!Object.keys(this._rooms[roomId]).length) {
|
||||||
|
delete this._rooms[roomId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify all other peers that remain in room that peer left
|
||||||
|
for (const otherPeerId in this._rooms[roomId]) {
|
||||||
|
const otherPeer = this._rooms[roomId][otherPeerId];
|
||||||
|
|
||||||
|
let msg = {
|
||||||
|
type: 'peer-left',
|
||||||
|
peerId: peer.id,
|
||||||
|
roomType: roomType,
|
||||||
|
roomId: roomId,
|
||||||
|
disconnect: disconnect
|
||||||
|
};
|
||||||
|
|
||||||
|
this._send(otherPeer, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_notifyPeers(peer, roomType = 'ip', roomSecret = '') {
|
_notifyPeers(peer, roomType, roomId) {
|
||||||
const room = roomType === 'ip' ? peer.ip : roomSecret;
|
if (!this._rooms[roomId]) return;
|
||||||
if (!this._rooms[room]) return;
|
|
||||||
|
|
||||||
// notify all other peers
|
// notify all other peers that peer joined
|
||||||
for (const otherPeerId in this._rooms[room]) {
|
for (const otherPeerId in this._rooms[roomId]) {
|
||||||
if (otherPeerId === peer.id) continue;
|
if (otherPeerId === peer.id) continue;
|
||||||
const otherPeer = this._rooms[room][otherPeerId];
|
const otherPeer = this._rooms[roomId][otherPeerId];
|
||||||
this._send(otherPeer, {
|
|
||||||
|
let msg = {
|
||||||
type: 'peer-joined',
|
type: 'peer-joined',
|
||||||
peer: peer.getInfo(),
|
peer: peer.getInfo(),
|
||||||
roomType: roomType,
|
roomType: roomType,
|
||||||
roomSecret: roomSecret
|
roomId: roomId
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this._send(otherPeer, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify peer about the other peers
|
// notify peer about peers already in the room
|
||||||
const otherPeers = [];
|
const otherPeers = [];
|
||||||
for (const otherPeerId in this._rooms[room]) {
|
for (const otherPeerId in this._rooms[roomId]) {
|
||||||
if (otherPeerId === peer.id) continue;
|
if (otherPeerId === peer.id) continue;
|
||||||
otherPeers.push(this._rooms[room][otherPeerId].getInfo());
|
otherPeers.push(this._rooms[roomId][otherPeerId].getInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
this._send(peer, {
|
let msg = {
|
||||||
type: 'peers',
|
type: 'peers',
|
||||||
peers: otherPeers,
|
peers: otherPeers,
|
||||||
roomType: roomType,
|
roomType: roomType,
|
||||||
roomSecret: roomSecret
|
roomId: roomId
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this._send(peer, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
_joinSecretRooms(peer, roomSecrets) {
|
_joinSecretRooms(peer, roomSecrets) {
|
||||||
for (let i=0; i<roomSecrets.length; i++) {
|
for (let i=0; i<roomSecrets.length; i++) {
|
||||||
this._joinRoom(peer, 'secret', roomSecrets[i])
|
this._joinSecretRoom(peer, roomSecrets[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_leaveAllSecretRooms(peer, disconnect = false) {
|
_leaveAllSecretRooms(peer, disconnect = false) {
|
||||||
for (let i=0; i<peer.roomSecrets.length; i++) {
|
for (let i=0; i<peer.roomSecrets.length; i++) {
|
||||||
this._leaveRoom(peer, 'secret', peer.roomSecrets[i], disconnect);
|
this._leaveSecretRoom(peer, peer.roomSecrets[i], disconnect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,23 +551,29 @@ class PairDropServer {
|
||||||
|
|
||||||
_keepAlive(peer) {
|
_keepAlive(peer) {
|
||||||
this._cancelKeepAlive(peer);
|
this._cancelKeepAlive(peer);
|
||||||
let timeout = 500;
|
let timeout = 1000;
|
||||||
if (!peer.lastBeat) {
|
|
||||||
peer.lastBeat = Date.now();
|
if (!this._keepAliveTimers[peer.id]) {
|
||||||
|
this._keepAliveTimers[peer.id] = {
|
||||||
|
timer: 0,
|
||||||
|
lastBeat: Date.now()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (Date.now() - peer.lastBeat > 2 * timeout) {
|
|
||||||
|
if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) {
|
||||||
|
// Disconnect peer if unresponsive for 10s
|
||||||
this._disconnect(peer);
|
this._disconnect(peer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._send(peer, { type: 'ping' });
|
this._send(peer, { type: 'ping' });
|
||||||
|
|
||||||
peer.timerId = setTimeout(() => this._keepAlive(peer), timeout);
|
this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cancelKeepAlive(peer) {
|
_cancelKeepAlive(peer) {
|
||||||
if (peer && peer.timerId) {
|
if (this._keepAliveTimers[peer.id]?.timer) {
|
||||||
clearTimeout(peer.timerId);
|
clearTimeout(this._keepAliveTimers[peer.id].timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -506,13 +598,22 @@ class Peer {
|
||||||
// set name
|
// set name
|
||||||
this._setName(request);
|
this._setName(request);
|
||||||
|
|
||||||
// for keepalive
|
this.requestRate = 0;
|
||||||
this.timerId = 0;
|
|
||||||
this.lastBeat = Date.now();
|
|
||||||
|
|
||||||
this.roomSecrets = [];
|
this.roomSecrets = [];
|
||||||
this.roomKey = null;
|
this.roomKey = null;
|
||||||
this.roomKeyRate = 0;
|
|
||||||
|
this.publicRoomId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimitReached() {
|
||||||
|
// rate limit implementation: max 10 attempts every 10s
|
||||||
|
if (this.requestRate >= 10) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.requestRate += 1;
|
||||||
|
setTimeout(_ => this.requestRate -= 1, 10000);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setIP(request) {
|
_setIP(request) {
|
||||||
|
@ -688,8 +789,15 @@ const hasher = (() => {
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const randomizer = (() => {
|
const randomizer = (() => {
|
||||||
|
let charCodeLettersOnly = r => 65 <= r && r <= 90;
|
||||||
|
let charCodeAllPrintableChars = r => r === 45 || 47 <= r && r <= 57 || 64 <= r && r <= 90 || 97 <= r && r <= 122;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getRandomString(length) {
|
getRandomString(length, lettersOnly = false) {
|
||||||
|
const charCodeCondition = lettersOnly
|
||||||
|
? charCodeLettersOnly
|
||||||
|
: charCodeAllPrintableChars;
|
||||||
|
|
||||||
let string = "";
|
let string = "";
|
||||||
while (string.length < length) {
|
while (string.length < length) {
|
||||||
let arr = new Uint16Array(length);
|
let arr = new Uint16Array(length);
|
||||||
|
@ -700,7 +808,7 @@ const randomizer = (() => {
|
||||||
})
|
})
|
||||||
arr = arr.filter(function (r) {
|
arr = arr.filter(function (r) {
|
||||||
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
|
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
|
||||||
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
|
return charCodeCondition(r);
|
||||||
});
|
});
|
||||||
string += String.fromCharCode.apply(String, arr);
|
string += String.fromCharCode.apply(String, arr);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,62 +39,76 @@
|
||||||
|
|
||||||
<body translate="no">
|
<body translate="no">
|
||||||
<header class="row-reverse">
|
<header class="row-reverse">
|
||||||
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
|
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label" title="About PairDrop" aria-label="Open About PairDrop">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#info-outline" />
|
<use xlink:href="#info-outline" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title" title="Select Language">
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlink:href="#icon-language-selector" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div id="theme-wrapper">
|
<div id="theme-wrapper">
|
||||||
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
|
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#icon-theme-auto" />
|
<use xlink:href="#icon-theme-auto" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
|
<div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title" title="Always Use Light-Theme" >
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#icon-theme-light" />
|
<use xlink:href="#icon-theme-light" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" >
|
<div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title" title="Always Use Dark-Theme" >
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#icon-theme-dark" />
|
<use xlink:href="#icon-theme-dark" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="notification" class="icon-button" title="Enable Notifications" hidden>
|
<div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" title="Enable Notifications" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#notifications" />
|
<use xlink:href="#notifications" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="install" class="icon-button" title="Install PairDrop" hidden>
|
<div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" title="Install PairDrop" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#homescreen" />
|
<use xlink:href="#homescreen" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="pair-device" class="icon-button" title="Pair Device" hidden>
|
<div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title" title="Pair Your Devices Permanently">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#pair-device-icon" />
|
<use xlink:href="#pair-device-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
|
<div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" title="Edit Paired Devices" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#edit-pair-devices-icon" />
|
<use xlink:href="#edit-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
<div id="join-public-room" class="icon-button" data-i18n-key="header.join-public-room" data-i18n-attrs="title" title="Join Public Room Temporarily">
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlink:href="#public-room-icon" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden>Done</div>
|
||||||
</header>
|
</header>
|
||||||
<!-- Center -->
|
<!-- Center -->
|
||||||
<div id="center">
|
<div id="center">
|
||||||
<!-- Peers -->
|
<!-- Peers -->
|
||||||
<div class="x-peers-filler"></div>
|
<div class="x-peers-filler"></div>
|
||||||
<x-peers class="center"></x-peers>
|
<x-peers class="center"></x-peers>
|
||||||
<x-no-peers>
|
<x-no-peers data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg" data-drop-bg="Release to select recipient">
|
||||||
<h2>Open PairDrop on other devices to send files</h2>
|
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text">Open PairDrop on other devices to send files</h2>
|
||||||
<div>Pair devices to be discoverable on other networks</div>
|
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text">Pair devices or enter a public room to be discoverable on other networks</div>
|
||||||
</x-no-peers>
|
</x-no-peers>
|
||||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
<x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"
|
||||||
|
desktop="Click to send files or right click to send a message"
|
||||||
|
mobile="Tap to send files or long tap to send a message"
|
||||||
|
data-drop-peer="Release to send to peer"
|
||||||
|
data-drop-bg="Release to select recipient">
|
||||||
<p id="paste-filename"></p>
|
<p id="paste-filename"></p>
|
||||||
</x-instructions>
|
</x-instructions>
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,40 +117,89 @@
|
||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div class="column">
|
||||||
<span>You are known as:</span>
|
<div class="known-as-wrapper">
|
||||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
<span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
|
||||||
<svg id="edit-pen" class="icon">
|
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title"
|
||||||
<use xlink:href="#edit-pen-icon" />
|
placeholder="Loading..." data-placeholder="Loading..." title="Edit your device name permanently"
|
||||||
</svg>
|
autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||||
</div>
|
<svg id="edit-pen" class="icon">
|
||||||
<div class="font-body2">
|
<use xlink:href="#edit-pen-icon" />
|
||||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
</svg>
|
||||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
</div>
|
||||||
|
<div class="discovery-wrapper row">
|
||||||
|
<div class="row center">
|
||||||
|
<span data-i18n-key="footer.discovery" data-i18n-attrs="text">You can be discovered:</span>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title">on this network</span>
|
||||||
|
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden>paired devices</span>
|
||||||
|
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<!-- Language Select Dialog -->
|
||||||
|
<x-dialog id="language-select-dialog">
|
||||||
|
<x-background class="full center">
|
||||||
|
<x-paper shadow="2">
|
||||||
|
<div class="row center">
|
||||||
|
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text">Select Language</h2>
|
||||||
|
</div>
|
||||||
|
<div class="language-buttons">
|
||||||
|
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text">System Language</button>
|
||||||
|
<button class="button fw" value="en">English</button>
|
||||||
|
<button class="button fw" value="de">Deutsch (German)</button>
|
||||||
|
<button class="button fw" value="nb">Norsk (Norwegian)</button>
|
||||||
|
<button class="button fw" value="ru">Русский язык (Russian)</button>
|
||||||
|
<button class="button fw" value="zh-CN">中文 (Chinese)</button>
|
||||||
|
</div>
|
||||||
|
<div class="center row-reverse button-row">
|
||||||
|
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
|
</div>
|
||||||
|
</x-paper>
|
||||||
|
</x-background>
|
||||||
|
</x-dialog>
|
||||||
<!-- Pair Device Dialog -->
|
<!-- Pair Device Dialog -->
|
||||||
<x-dialog id="pair-device-dialog">
|
<x-dialog id="pair-device-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Pair Devices</h2>
|
<div class="row center">
|
||||||
<div id="room-key-qr-code" class="center"></div>
|
<h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text">Pair Devices</h2>
|
||||||
<h1 id="room-key" class="center">000 000</h1>
|
|
||||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
|
||||||
<hr>
|
|
||||||
<div id="key-input-container">
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="row center">
|
||||||
<div class="center row-reverse">
|
<div class="column">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<div class="center key-qr-code"></div>
|
||||||
<button class="button" type="button" close>Cancel</button>
|
<h1 class="center key">000 000</h1>
|
||||||
|
<p class="center text-center key-instructions">
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text">Input this key on another device</span>
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hr-note">
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<div class="input-key-container six-chars">
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
</div>
|
||||||
|
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device here.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-row row-reverse">
|
||||||
|
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
|
||||||
|
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -147,13 +210,70 @@
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Edit Paired Devices</h2>
|
<div class="row center">
|
||||||
<div class="paired-devices-wrapper"></div>
|
<h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text">Edit Paired Devices</h2>
|
||||||
<div class="font-subheading center">
|
|
||||||
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="center row-reverse">
|
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty" data-empty="No paired devices."></div>
|
||||||
|
<div class="font-subheading center">
|
||||||
|
<p>
|
||||||
|
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text">
|
||||||
|
Activate
|
||||||
|
</span>
|
||||||
|
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text">auto-accept</u>
|
||||||
|
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text">
|
||||||
|
to automatically accept all files sent from that device.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="center row-reverse button-row">
|
||||||
|
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
|
</div>
|
||||||
|
</x-paper>
|
||||||
|
</x-background>
|
||||||
|
</form>
|
||||||
|
</x-dialog>
|
||||||
|
<!-- Public Room Dialog -->
|
||||||
|
<x-dialog id="public-room-dialog">
|
||||||
|
<form action="#">
|
||||||
|
<x-background class="full center text-center">
|
||||||
|
<x-paper shadow="2">
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<h2 class="center">Temporary Public Room</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<div class="center key-qr-code"></div>
|
||||||
|
<h1 class="center key">IOX9P</h1>
|
||||||
|
<p class="center text-center key-instructions">
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text">Input this room id on another device</span>
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hr-note">
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<div class="input-key-container">
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
</div>
|
||||||
|
<p class="font-subheading center text-center">Enter room id from another device to join.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center row-reverse button-row">
|
||||||
|
<button class="button" type="submit" disabled>Join</button>
|
||||||
<button class="button" type="button" close>Close</button>
|
<button class="button" type="button" close>Close</button>
|
||||||
|
<button class="button leave-room" type="button">Leave</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -163,24 +283,30 @@
|
||||||
<x-dialog id="receive-request-dialog">
|
<x-dialog id="receive-request-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center"></h2>
|
<div class="row center">
|
||||||
<div class="center column file-description">
|
<div class="column">
|
||||||
<div>
|
<h2 class="center"></h2>
|
||||||
<span class="display-name"></span>
|
|
||||||
<span>would like to share</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-name" >
|
</div>
|
||||||
<span class="file-stem"></span>
|
<div class="row center">
|
||||||
<span class="file-extension"></span>
|
<div class="column center file-description">
|
||||||
|
<div>
|
||||||
|
<span class="display-name badge"></span>
|
||||||
|
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text">would like to share</span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-name" >
|
||||||
|
<span class="file-stem"></span>
|
||||||
|
<span class="file-extension"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-other">
|
||||||
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-other">
|
|
||||||
</div>
|
|
||||||
<div class="row font-body2 file-size"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="center row-reverse">
|
<div class="row-reverse center button-row">
|
||||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
|
||||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -189,24 +315,31 @@
|
||||||
<x-dialog id="receive-file-dialog">
|
<x-dialog id="receive-file-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center"></h2>
|
<div class="row center">
|
||||||
<div class="center column file-description">
|
<div class="column">
|
||||||
<div>
|
<h2 class="center"></h2>
|
||||||
<span class="display-name"></span>
|
|
||||||
<span>has sent</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-name" >
|
</div>
|
||||||
<span class="file-stem"></span>
|
<div class="row center">
|
||||||
<span class="file-extension"></span>
|
<div class="column center file-description">
|
||||||
|
<div>
|
||||||
|
<span class="display-name badge"></span>
|
||||||
|
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent</span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-name" >
|
||||||
|
<span class="file-stem"></span>
|
||||||
|
<span class="file-extension"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-other">
|
||||||
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-other"></div>
|
|
||||||
<div class="row font-body2 file-size"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="center row-reverse">
|
<div class="row-reverse center button-row">
|
||||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden>Share</button>
|
||||||
<button id="download-btn" class="button" autofocus>Download</button>
|
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
|
||||||
<button class="button" close>Close</button>
|
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -216,16 +349,27 @@
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="text-center">Send Message</h2>
|
<div class="row center">
|
||||||
<div class="dialog-subheader text-center">
|
<div class="column">
|
||||||
<span>Send a Message to</span>
|
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text">Send Message</h2>
|
||||||
<span class="display-name"></span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row center display-name-wrapper">
|
||||||
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div class="column">
|
||||||
<div class="center row-reverse">
|
<div class="text-center">
|
||||||
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
|
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text">Send a Message to</span>
|
||||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
<span class="display-name badge"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="column fw">
|
||||||
|
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-row row-reverse">
|
||||||
|
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
|
||||||
|
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -235,16 +379,23 @@
|
||||||
<x-dialog id="receive-text-dialog">
|
<x-dialog id="receive-text-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="text-center">Message Received</h2>
|
<div class="row center">
|
||||||
<div class="text-center dialog-subheader">
|
<h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text">Message Received</h2>
|
||||||
<span class="display-name"></span>
|
|
||||||
<span>has sent:</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row center">
|
||||||
<div id="text"></div>
|
<div class="text-center">
|
||||||
<div class="center row-reverse">
|
<span class="display-name badge"></span>
|
||||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent:</span>
|
||||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column fw">
|
||||||
|
<div id="text" class="textarea fw"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-reverse center button-row">
|
||||||
|
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
|
||||||
|
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -253,20 +404,22 @@
|
||||||
<x-dialog id="base64-paste-dialog">
|
<x-dialog id="base64-paste-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
<button class="button center" id="base64-paste-btn" title="Paste"></button>
|
||||||
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||||
<button class="button center" close>Close</button>
|
<div class="row-reverse center button-row">
|
||||||
|
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Toast -->
|
<!-- Toast -->
|
||||||
<div class="toast-container full center">
|
<div class="toast-container full center">
|
||||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
<x-toast id="toast" class="row center" shadow="1"></x-toast>
|
||||||
</div>
|
</div>
|
||||||
<!-- About Page -->
|
<!-- About Page -->
|
||||||
<x-about id="about" class="full center column">
|
<x-about id="about" class="full center column">
|
||||||
<header class="row-reverse fade-in">
|
<header class="row-reverse fade-in">
|
||||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
<a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="aria-label" aria-label="Close About PairDrop">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#close-icon" />
|
<use xlink:href="#close-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -280,24 +433,24 @@
|
||||||
<h1>PairDrop</h1>
|
<h1>PairDrop</h1>
|
||||||
<div class="font-subheading">v1.7.7</div>
|
<div class="font-subheading">v1.7.7</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading">The easiest way to transfer files across devices</div>
|
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text">The easiest way to transfer files across devices</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer" data-i18n-key="about.github" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#github" />
|
<use xlink:href="#github" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#monetarization" />
|
<use xlink:href="#monetarization" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#twitter" />
|
<use xlink:href="#twitter" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#help-outline" />
|
<use xlink:href="#help-outline" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -371,8 +524,18 @@
|
||||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="public-room-icon" viewBox="0 0 640 512">
|
||||||
|
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path d="M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-language-selector" viewBox="0 0 640 512">
|
||||||
|
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<script src="scripts/localization.js"></script>
|
||||||
<script src="scripts/theme.js"></script>
|
<script src="scripts/theme.js"></script>
|
||||||
<script src="scripts/network.js"></script>
|
<script src="scripts/network.js"></script>
|
||||||
<script src="scripts/ui.js"></script>
|
<script src="scripts/ui.js"></script>
|
||||||
|
|
156
public/lang/de.json
Normal file
156
public/lang/de.json
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "Über PairDrop",
|
||||||
|
"notification_title": "Benachrichtigungen aktivieren",
|
||||||
|
"about_aria-label": "Über PairDrop öffnen",
|
||||||
|
"install_title": "PairDrop installieren",
|
||||||
|
"pair-device_title": "Deine Geräte dauerhaft koppeln",
|
||||||
|
"edit-paired-devices_title": "Gekoppelte Geräte bearbeiten",
|
||||||
|
"theme-auto_title": "Systemstil verwenden",
|
||||||
|
"theme-dark_title": "Dunklen Stil verwenden",
|
||||||
|
"theme-light_title": "Hellen Stil verwenden",
|
||||||
|
"cancel-paste-mode": "Fertig",
|
||||||
|
"language-selector_title": "Sprache auswählen",
|
||||||
|
"join-public-room_title": "Öffentlichen Raum temporär betreten"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"share": "Teilen",
|
||||||
|
"download": "Herunterladen",
|
||||||
|
"pair-devices-title": "Geräte dauerhaft koppeln",
|
||||||
|
"input-key-on-this-device": "Gebe diesen Schlüssel auf einem anderen Gerät ein",
|
||||||
|
"enter-key-from-another-device": "Gebe den Schlüssel von einem anderen Gerät hier ein.",
|
||||||
|
"pair": "Koppeln",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"edit-paired-devices-title": "Gekoppelte Geräte bearbeiten",
|
||||||
|
"paired-devices-wrapper_data-empty": "Keine gekoppelten Geräte.",
|
||||||
|
"close": "Schließen",
|
||||||
|
"accept": "Akzeptieren",
|
||||||
|
"decline": "Ablehnen",
|
||||||
|
"title-image": "Bild",
|
||||||
|
"title-file": "Datei",
|
||||||
|
"title-image-plural": "Bilder",
|
||||||
|
"title-file-plural": "Dateien",
|
||||||
|
"scan-qr-code": "oder scanne den QR-Code.",
|
||||||
|
"would-like-to-share": "möchte Folgendes teilen",
|
||||||
|
"send": "Senden",
|
||||||
|
"copy": "Kopieren",
|
||||||
|
"receive-text-title": "Textnachricht erhalten",
|
||||||
|
"file-other-description-image-plural": "und {{count}} andere Bilder",
|
||||||
|
"file-other-description-file-plural": "und {{count}} andere Dateien",
|
||||||
|
"auto-accept-instructions-1": "Aktiviere",
|
||||||
|
"auto-accept": "auto-accept",
|
||||||
|
"auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.",
|
||||||
|
"has-sent": "hat Folgendes gesendet:",
|
||||||
|
"send-message-title": "Textnachricht senden",
|
||||||
|
"send-message-to": "Sende eine Textnachricht an",
|
||||||
|
"base64-tap-to-paste": "Hier tippen, um {{type}} einzufügen",
|
||||||
|
"base64-paste-to-send": "Hier einfügen, um {{type}} zu versenden",
|
||||||
|
"base64-text": "Text",
|
||||||
|
"base64-files": "Dateien",
|
||||||
|
"base64-processing": "Bearbeitung läuft…",
|
||||||
|
"file-other-description-image": "und ein anderes Bild",
|
||||||
|
"file-other-description-file": "und eine andere Datei",
|
||||||
|
"receive-title": "{{descriptor}} erhalten",
|
||||||
|
"download-again": "Erneut herunterladen",
|
||||||
|
"system-language": "Systemsprache",
|
||||||
|
"language-selector-title": "Sprache auswählen",
|
||||||
|
"hr-or": "ODER",
|
||||||
|
"input-room-id-on-another-device": "Gebe diese Raum ID auf einem anderen Gerät ein",
|
||||||
|
"unpair": "Entkoppeln"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"tweet_title": "Über PairDrop twittern",
|
||||||
|
"faq_title": "Häufig gestellte Fragen",
|
||||||
|
"close-about_aria-label": "Schließe Über PairDrop",
|
||||||
|
"github_title": "PairDrop auf GitHub",
|
||||||
|
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
|
||||||
|
"claim": "Der einfachste Weg Dateien zwischen Geräten zu teilen"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"known-as": "Du wirst angezeigt als:",
|
||||||
|
"display-name_title": "Setze einen permanenten Gerätenamen",
|
||||||
|
"on-this-network": "in diesem Netzwerk",
|
||||||
|
"paired-devices": "für gekoppelte Geräte",
|
||||||
|
"traffic": "Datenverkehr wird",
|
||||||
|
"display-name_placeholder": "Lade…",
|
||||||
|
"routed": "durch den Server geleitet",
|
||||||
|
"webrtc": "wenn WebRTC nicht verfügbar ist.",
|
||||||
|
"display-name_data-placeholder": "Lade…",
|
||||||
|
"public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum unabhängig von deinem Netzwerk gefunden werden.",
|
||||||
|
"paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.",
|
||||||
|
"public-room-devices": "in Raum {{roomId}}",
|
||||||
|
"discovery": "Du bist sichtbar:",
|
||||||
|
"on-this-network_title": "Du kannst von jedem in diesem Netzwerk gefunden werden."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"link-received": "Link von {{name}} empfangen - Klicke um ihn zu öffnen",
|
||||||
|
"message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren",
|
||||||
|
"click-to-download": "Klicken zum Download",
|
||||||
|
"copied-text": "Text in die Zwischenablage kopiert",
|
||||||
|
"connected": "Verbunden.",
|
||||||
|
"pairing-success": "Geräte gekoppelt.",
|
||||||
|
"display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert.",
|
||||||
|
"pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden.",
|
||||||
|
"pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent.",
|
||||||
|
"pairing-key-invalid": "Ungültiger Schlüssel",
|
||||||
|
"pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht.",
|
||||||
|
"copied-to-clipboard": "In die Zwischenablage kopiert",
|
||||||
|
"text-content-incorrect": "Textinhalt ist fehlerhaft.",
|
||||||
|
"clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft.",
|
||||||
|
"copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!",
|
||||||
|
"file-content-incorrect": "Dateiinhalt ist fehlerhaft.",
|
||||||
|
"notifications-enabled": "Benachrichtigungen aktiviert.",
|
||||||
|
"offline": "Du bist offline",
|
||||||
|
"online": "Du bist wieder Online",
|
||||||
|
"unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?",
|
||||||
|
"display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert.",
|
||||||
|
"download-successful": "{{descriptor}} heruntergeladen",
|
||||||
|
"pairing-cleared": "Alle Geräte entkoppelt.",
|
||||||
|
"click-to-show": "Klicken zum Anzeigen",
|
||||||
|
"online-requirement": "Du musst online sein um Geräte zu koppeln.",
|
||||||
|
"display-name-changed-temporarily": "Anzeigename wurde nur für diese Sitzung geändert.",
|
||||||
|
"request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen",
|
||||||
|
"connecting": "Verbindung wird aufgebaut…",
|
||||||
|
"files-incorrect": "Dateien sind fehlerhaft.",
|
||||||
|
"file-transfer-completed": "Dateiübertragung fertiggestellt.",
|
||||||
|
"message-transfer-completed": "Nachrichtenübertragung fertiggestellt.",
|
||||||
|
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
|
||||||
|
"selected-peer-left": "Ausgewählter Peer ist gegangen.",
|
||||||
|
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
|
||||||
|
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
|
||||||
|
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
|
||||||
|
"public-room-id-invalid": "Ungültige Raum ID",
|
||||||
|
"online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln.",
|
||||||
|
"online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können."
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_desktop": "Klicke, um Dateien zu Senden oder klicke mit der rechten Maustaste, um Textnachrichten zu senden",
|
||||||
|
"no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden",
|
||||||
|
"no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen",
|
||||||
|
"no-peers-subtitle": "Kopple Geräte oder besuche einen öffentlichen Raum, damit du in anderen Netzwerken sichtbar bist",
|
||||||
|
"click-to-send": "Klicke zum Senden von",
|
||||||
|
"tap-to-send": "Tippe zum Senden von",
|
||||||
|
"x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden",
|
||||||
|
"x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen",
|
||||||
|
"x-instructions_mobile": "Tippe zum Senden von Dateien oder tippe lange zum Senden von Nachrichten",
|
||||||
|
"activate-paste-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von",
|
||||||
|
"activate-paste-mode-and-other-files": "und {{count}} anderen Dateien",
|
||||||
|
"activate-paste-mode-shared-text": "freigegebenem Text"
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-transfer-requested": "Datenübertagung angefordert",
|
||||||
|
"file-received": "Datei erhalten",
|
||||||
|
"file-received-plural": "{{count}} Dateien erhalten",
|
||||||
|
"message-received": "Nachricht erhalten",
|
||||||
|
"message-received-plural": "{{count}} Nachrichten erhalten"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send": "Klicke um Dateien zu senden oder nutze einen Rechtsklick um eine Textnachricht zu senden",
|
||||||
|
"connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten",
|
||||||
|
"waiting": "Warte…",
|
||||||
|
"click-to-send-paste-mode": "Klicken um {{descriptor}} zu senden",
|
||||||
|
"transferring": "Übertragung läuft…",
|
||||||
|
"processing": "Bearbeitung läuft…",
|
||||||
|
"preparing": "Vorbereitung läuft…"
|
||||||
|
}
|
||||||
|
}
|
154
public/lang/en.json
Normal file
154
public/lang/en.json
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "About PairDrop",
|
||||||
|
"language-selector_title": "Select Language",
|
||||||
|
"about_aria-label": "Open About PairDrop",
|
||||||
|
"theme-auto_title": "Adapt Theme to System",
|
||||||
|
"theme-light_title": "Always Use Light-Theme",
|
||||||
|
"theme-dark_title": "Always Use Dark-Theme",
|
||||||
|
"notification_title": "Enable Notifications",
|
||||||
|
"install_title": "Install PairDrop",
|
||||||
|
"pair-device_title": "Pair Your Devices Permanently",
|
||||||
|
"edit-paired-devices_title": "Edit Paired Devices",
|
||||||
|
"join-public-room_title": "Join Public Room Temporarily",
|
||||||
|
"cancel-paste-mode": "Done"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"no-peers_data-drop-bg": "Release to select recipient",
|
||||||
|
"no-peers-title": "Open PairDrop on other devices to send files",
|
||||||
|
"no-peers-subtitle": "Pair devices or enter a public room to be discoverable on other networks",
|
||||||
|
"x-instructions_desktop": "Click to send files or right click to send a message",
|
||||||
|
"x-instructions_mobile": "Tap to send files or long tap to send a message",
|
||||||
|
"x-instructions_data-drop-peer": "Release to send to peer",
|
||||||
|
"x-instructions_data-drop-bg": "Release to select recipient",
|
||||||
|
"click-to-send": "Click to send",
|
||||||
|
"tap-to-send": "Tap to send",
|
||||||
|
"activate-paste-mode-base": "Open PairDrop on other devices to send",
|
||||||
|
"activate-paste-mode-and-other-files": "and {{count}} other files",
|
||||||
|
"activate-paste-mode-shared-text": "shared text"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"known-as": "You are known as:",
|
||||||
|
"display-name_data-placeholder": "Loading…",
|
||||||
|
"display-name_title": "Edit your device name permanently",
|
||||||
|
"discovery": "You can be discovered:",
|
||||||
|
"on-this-network": "on this network",
|
||||||
|
"on-this-network_title": "You can be discovered by everyone on this network.",
|
||||||
|
"paired-devices": "by paired devices",
|
||||||
|
"paired-devices_title": "You can be discovered by paired devices at all times independent of the network.",
|
||||||
|
"public-room-devices": "in room {{roomId}}",
|
||||||
|
"public-room-devices_title": "You can be discovered by devices in this public room independent of the network.",
|
||||||
|
"traffic": "Traffic is",
|
||||||
|
"routed": "routed through the server",
|
||||||
|
"webrtc": "if WebRTC is not available."
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"pair-devices-title": "Pair Devices Permanently",
|
||||||
|
"input-key-on-this-device": "Input this key on another device",
|
||||||
|
"scan-qr-code": "or scan the QR-code.",
|
||||||
|
"enter-key-from-another-device": "Enter key from another device here.",
|
||||||
|
"input-room-id-on-another-device": "Input this room id on another device",
|
||||||
|
"hr-or": "OR",
|
||||||
|
"pair": "Pair",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"edit-paired-devices-title": "Edit Paired Devices",
|
||||||
|
"unpair": "Unpair",
|
||||||
|
"paired-devices-wrapper_data-empty": "No paired devices.",
|
||||||
|
"auto-accept-instructions-1": "Activate",
|
||||||
|
"auto-accept": "auto-accept",
|
||||||
|
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
|
||||||
|
"close": "Close",
|
||||||
|
"would-like-to-share": "would like to share",
|
||||||
|
"accept": "Accept",
|
||||||
|
"decline": "Decline",
|
||||||
|
"has-sent": "has sent:",
|
||||||
|
"share": "Share",
|
||||||
|
"download": "Download",
|
||||||
|
"send-message-title": "Send Message",
|
||||||
|
"send-message-to": "Send a Message to",
|
||||||
|
"send": "Send",
|
||||||
|
"receive-text-title": "Message Received",
|
||||||
|
"copy": "Copy",
|
||||||
|
"base64-processing": "Processing…",
|
||||||
|
"base64-tap-to-paste": "Tap here to paste {{type}}",
|
||||||
|
"base64-paste-to-send": "Paste here to send {{type}}",
|
||||||
|
"base64-text": "text",
|
||||||
|
"base64-files": "files",
|
||||||
|
"file-other-description-image": "and 1 other image",
|
||||||
|
"file-other-description-file": "and 1 other file",
|
||||||
|
"file-other-description-image-plural": "and {{count}} other images",
|
||||||
|
"file-other-description-file-plural": "and {{count}} other files",
|
||||||
|
"title-image": "Image",
|
||||||
|
"title-file": "File",
|
||||||
|
"title-image-plural": "Images",
|
||||||
|
"title-file-plural": "Files",
|
||||||
|
"receive-title": "{{descriptor}} Received",
|
||||||
|
"download-again": "Download again",
|
||||||
|
"language-selector-title": "Select Language",
|
||||||
|
"system-language": "System Language"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"close-about_aria-label": "Close About PairDrop",
|
||||||
|
"claim": "The easiest way to transfer files across devices",
|
||||||
|
"github_title": "PairDrop on GitHub",
|
||||||
|
"buy-me-a-coffee_title": "Buy me a coffee!",
|
||||||
|
"tweet_title": "Tweet about PairDrop",
|
||||||
|
"faq_title": "Frequently asked questions"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"display-name-changed-permanently": "Display name is changed permanently.",
|
||||||
|
"display-name-changed-temporarily": "Display name is changed only for this session.",
|
||||||
|
"display-name-random-again": "Display name is randomly generated again.",
|
||||||
|
"download-successful": "{{descriptor}} downloaded",
|
||||||
|
"pairing-tabs-error": "Pairing two web browser tabs is impossible.",
|
||||||
|
"pairing-success": "Devices paired.",
|
||||||
|
"pairing-not-persistent": "Paired devices are not persistent.",
|
||||||
|
"pairing-key-invalid": "Invalid key",
|
||||||
|
"pairing-key-invalidated": "Key {{key}} invalidated.",
|
||||||
|
"pairing-cleared": "All Devices unpaired.",
|
||||||
|
"public-room-id-invalid": "Invalid room id",
|
||||||
|
"public-room-left": "Left public room {{publicRoomId}}",
|
||||||
|
"copied-to-clipboard": "Copied to clipboard",
|
||||||
|
"copied-to-clipboard-error": "Copying not possible. Copy manually.",
|
||||||
|
"text-content-incorrect": "Text content is incorrect.",
|
||||||
|
"file-content-incorrect": "File content is incorrect.",
|
||||||
|
"clipboard-content-incorrect": "Clipboard content is incorrect.",
|
||||||
|
"notifications-enabled": "Notifications enabled.",
|
||||||
|
"link-received": "Link received by {{name}} - Click to open",
|
||||||
|
"message-received": "Message received by {{name}} - Click to copy",
|
||||||
|
"click-to-download": "Click to download",
|
||||||
|
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
|
||||||
|
"click-to-show": "Click to show",
|
||||||
|
"copied-text": "Copied text to clipboard",
|
||||||
|
"copied-text-error": "Writing to clipboard failed. Copy manually!",
|
||||||
|
"offline": "You are offline",
|
||||||
|
"online": "You are back online",
|
||||||
|
"connected": "Connected.",
|
||||||
|
"online-requirement-pairing": "You need to be online to pair devices.",
|
||||||
|
"online-requirement-public-room": "You need to be online to create a public room.",
|
||||||
|
"connecting": "Connecting…",
|
||||||
|
"files-incorrect": "Files are incorrect.",
|
||||||
|
"file-transfer-completed": "File transfer completed.",
|
||||||
|
"ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once",
|
||||||
|
"message-transfer-completed": "Message transfer completed.",
|
||||||
|
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
|
||||||
|
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
|
||||||
|
"selected-peer-left": "Selected peer left."
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-received": "File Received",
|
||||||
|
"file-received-plural": "{{count}} Files Received",
|
||||||
|
"file-transfer-requested": "File Transfer Requested",
|
||||||
|
"message-received": "Message Received",
|
||||||
|
"message-received-plural": "{{count}} Messages Received"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send-paste-mode": "Click to send {{descriptor}}",
|
||||||
|
"click-to-send": "Click to send files or right click to send a message",
|
||||||
|
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
|
||||||
|
"preparing": "Preparing…",
|
||||||
|
"waiting": "Waiting…",
|
||||||
|
"processing": "Processing…",
|
||||||
|
"transferring": "Transferring…"
|
||||||
|
}
|
||||||
|
}
|
138
public/lang/nb.json
Normal file
138
public/lang/nb.json
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"edit-paired-devices_title": "Rediger sammenkoblede enheter",
|
||||||
|
"about_title": "Om PairDrop",
|
||||||
|
"about_aria-label": "Åpne «Om PairDrop»",
|
||||||
|
"theme-auto_title": "Juster drakt til system",
|
||||||
|
"theme-light_title": "Alltid bruk lys drakt",
|
||||||
|
"theme-dark_title": "Alltid bruk mørk drakt",
|
||||||
|
"notification_title": "Skru på merknader",
|
||||||
|
"cancel-paste-mode": "Ferdig",
|
||||||
|
"install_title": "Installer PairDrop",
|
||||||
|
"pair-device_title": "Sammenkoble enhet"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
|
||||||
|
"display-name_data-placeholder": "Laster inn…",
|
||||||
|
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
|
||||||
|
"traffic": "Trafikken",
|
||||||
|
"on-this-network": "på dette nettverket",
|
||||||
|
"known-as": "Du er kjent som:",
|
||||||
|
"paired-devices": "sammenkoblede enheter",
|
||||||
|
"routed": "Sendes gjennom tjeneren"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||||
|
"x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding",
|
||||||
|
"x-instructions_data-drop-bg": "Slipp for å velge mottager",
|
||||||
|
"click-to-send": "Klikk for å sende",
|
||||||
|
"no-peers_data-drop-bg": "Slipp for å velge mottager",
|
||||||
|
"no-peers-title": "Åpne PairDrop på andre enheter for å sende filer",
|
||||||
|
"no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk",
|
||||||
|
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
|
||||||
|
"tap-to-send": "Trykk for å sende",
|
||||||
|
"activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende",
|
||||||
|
"activate-paste-mode-and-other-files": "og {{count}} andre filer",
|
||||||
|
"activate-paste-mode-shared-text": "delt tekst"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet",
|
||||||
|
"pair-devices-title": "Sammenkoble enheter",
|
||||||
|
"would-like-to-share": "ønsker å dele",
|
||||||
|
"auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.",
|
||||||
|
"paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter",
|
||||||
|
"enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.",
|
||||||
|
"edit-paired-devices-title": "Rediger sammenkoblede enheter",
|
||||||
|
"accept": "Godta",
|
||||||
|
"has-sent": "har sendt:",
|
||||||
|
"base64-paste-to-send": "Trykk her for å sende {{type}}",
|
||||||
|
"base64-text": "tekst",
|
||||||
|
"base64-files": "filer",
|
||||||
|
"file-other-description-image-plural": "og {{count}} andre bilder",
|
||||||
|
"receive-title": "{{descriptor}} mottatt",
|
||||||
|
"send-message-title": "Send melding",
|
||||||
|
"base64-processing": "Behandler…",
|
||||||
|
"close": "Lukk",
|
||||||
|
"decline": "Avslå",
|
||||||
|
"download": "Last ned",
|
||||||
|
"copy": "Kopier",
|
||||||
|
"pair": "Sammenkoble",
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"scan-qr-code": "eller skann QR-koden.",
|
||||||
|
"auto-accept-instructions-1": "Aktiver",
|
||||||
|
"receive-text-title": "Melding mottatt",
|
||||||
|
"auto-accept": "auto-godkjenn",
|
||||||
|
"share": "Del",
|
||||||
|
"send-message-to": "Send en melding til",
|
||||||
|
"send": "Send",
|
||||||
|
"base64-tap-to-paste": "Trykk her for å lime inn {{type}}",
|
||||||
|
"file-other-description-image": "og ett annet bilde",
|
||||||
|
"file-other-description-file-plural": "og {{count}} andre filer",
|
||||||
|
"title-file-plural": "Filer",
|
||||||
|
"download-again": "Last ned igjen",
|
||||||
|
"file-other-description-file": "og én annen fil",
|
||||||
|
"title-image": "Bilde",
|
||||||
|
"title-file": "Fil",
|
||||||
|
"title-image-plural": "Bilder"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"close-about_aria-label": "Lukk «Om PairDrop»",
|
||||||
|
"faq_title": "Ofte stilte spørsmål",
|
||||||
|
"claim": "Den enkleste måten å overføre filer mellom enheter",
|
||||||
|
"buy-me-a-coffee_title": "Spander drikke.",
|
||||||
|
"tweet_title": "Tvitre om PairDrop",
|
||||||
|
"github_title": "PairDrop på GitHub"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"copied-to-clipboard": "Kopiert til utklippstavlen",
|
||||||
|
"pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.",
|
||||||
|
"notifications-enabled": "Merknader påskrudd.",
|
||||||
|
"click-to-show": "Klikk for å vise",
|
||||||
|
"copied-text": "Tekst kopiert til utklippstavlen",
|
||||||
|
"connected": "Tilkoblet.",
|
||||||
|
"online": "Du er tilbake på nett",
|
||||||
|
"file-transfer-completed": "Filoverføring utført.",
|
||||||
|
"selected-peer-left": "Valgt likemann dro.",
|
||||||
|
"pairing-key-invalid": "Ugyldig nøkkel",
|
||||||
|
"connecting": "Kobler til …",
|
||||||
|
"pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.",
|
||||||
|
"offline": "Du er frakoblet",
|
||||||
|
"online-requirement": "Du må være på nett for å koble sammen enheter.",
|
||||||
|
"display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.",
|
||||||
|
"display-name-changed-permanently": "Visningsnavnet er endret for godt.",
|
||||||
|
"display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.",
|
||||||
|
"text-content-incorrect": "Tekstinnholdet er uriktig.",
|
||||||
|
"file-content-incorrect": "Filinnholdet er uriktig.",
|
||||||
|
"click-to-download": "Klikk for å laste ned",
|
||||||
|
"message-transfer-completed": "Meldingsoverføring utført.",
|
||||||
|
"download-successful": "{{descriptor}} nedlastet",
|
||||||
|
"pairing-success": "Enheter sammenkoblet.",
|
||||||
|
"pairing-cleared": "Sammenkobling av alle enheter opphevet.",
|
||||||
|
"pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.",
|
||||||
|
"copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt!",
|
||||||
|
"clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.",
|
||||||
|
"link-received": "Lenke mottatt av {{name}} - Klikk for å åpne.",
|
||||||
|
"request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}",
|
||||||
|
"message-received": "Melding mottatt av {{name}} - Klikk for å åpne.",
|
||||||
|
"files-incorrect": "Filene er uriktige.",
|
||||||
|
"ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.",
|
||||||
|
"unfinished-transfers-warning": "Lukk med ufullførte overføringer?",
|
||||||
|
"rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen."
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-received": "Fil mottatt",
|
||||||
|
"file-received-plural": "{{count}} filer mottatt",
|
||||||
|
"message-received": "Melding mottatt",
|
||||||
|
"file-transfer-requested": "Filoverføring forespurt",
|
||||||
|
"message-received-plural": "{{count}} meldinger mottatt"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"preparing": "Forbereder …",
|
||||||
|
"waiting": "Venter…",
|
||||||
|
"processing": "Behandler …",
|
||||||
|
"transferring": "Overfører …",
|
||||||
|
"click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||||
|
"click-to-send-paste-mode": "Klikk for å sende {{descriptor}}",
|
||||||
|
"connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen."
|
||||||
|
}
|
||||||
|
}
|
156
public/lang/ru.json
Normal file
156
public/lang/ru.json
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_aria-label": "Открыть страницу \"О сервисе\"",
|
||||||
|
"pair-device_title": "Связать ваши устройства навсегда",
|
||||||
|
"install_title": "Установить PairDrop",
|
||||||
|
"cancel-paste-mode": "Выполнено",
|
||||||
|
"edit-paired-devices_title": "Редактировать сопряженные устройства",
|
||||||
|
"notification_title": "Включить уведомления",
|
||||||
|
"about_title": "О сервисе",
|
||||||
|
"theme-auto_title": "Адаптировать тему к системной",
|
||||||
|
"theme-dark_title": "Всегда использовать темную тему",
|
||||||
|
"theme-light_title": "Всегда использовать светлую тему",
|
||||||
|
"join-public-room_title": "Войти на время в публичную комнату",
|
||||||
|
"language-selector_title": "Выбрать язык"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение",
|
||||||
|
"no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||||
|
"click-to-send": "Нажмите, чтобы отправить",
|
||||||
|
"x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||||
|
"tap-to-send": "Прикоснитесь, чтобы отправить",
|
||||||
|
"x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу",
|
||||||
|
"x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение",
|
||||||
|
"no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы",
|
||||||
|
"no-peers-subtitle": "Сопрягите устройства или войдите в публичную комнату, чтобы вас могли обнаружить из других сетей",
|
||||||
|
"activate-paste-mode-and-other-files": "и {{count}} других файлов",
|
||||||
|
"activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить",
|
||||||
|
"activate-paste-mode-shared-text": "общий текст"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"display-name_data-placeholder": "Загрузка…",
|
||||||
|
"routed": "направляется через сервер",
|
||||||
|
"webrtc": ", если WebRTC недоступен.",
|
||||||
|
"traffic": "Трафик",
|
||||||
|
"paired-devices": "сопряженными устройствами",
|
||||||
|
"known-as": "Вы известны под именем:",
|
||||||
|
"on-this-network": "в этой сети",
|
||||||
|
"display-name_title": "Изменить имя вашего устройства навсегда",
|
||||||
|
"public-room-devices_title": "Вы можете быть обнаружены устройствами в этой публичной комнате вне зависимости от сети.",
|
||||||
|
"paired-devices_title": "Вы можете быть обнаружены сопряженными устройствами в любое время вне зависимости от сети.",
|
||||||
|
"public-room-devices": "в комнате {{roomId}}",
|
||||||
|
"discovery": "Вы можете быть обнаружены:",
|
||||||
|
"on-this-network_title": "Вы можете быть обнаружены кем угодно в этой сети."
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"edit-paired-devices-title": "Редактировать сопряженные устройства",
|
||||||
|
"auto-accept": "автоприем",
|
||||||
|
"close": "Закрыть",
|
||||||
|
"decline": "Отклонить",
|
||||||
|
"share": "Поделиться",
|
||||||
|
"would-like-to-share": "хотел бы поделиться",
|
||||||
|
"has-sent": "отправил:",
|
||||||
|
"paired-devices-wrapper_data-empty": "Нет сопряженных устройств.",
|
||||||
|
"download": "Скачать",
|
||||||
|
"receive-text-title": "Сообщение получено",
|
||||||
|
"send": "Отправить",
|
||||||
|
"send-message-to": "Отправить сообщение",
|
||||||
|
"send-message-title": "Отправить сообщение",
|
||||||
|
"copy": "Копировать",
|
||||||
|
"base64-files": "файлы",
|
||||||
|
"base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}",
|
||||||
|
"base64-processing": "Обработка…",
|
||||||
|
"base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}",
|
||||||
|
"base64-text": "текст",
|
||||||
|
"title-file": "Файл",
|
||||||
|
"title-file-plural": "Файлы",
|
||||||
|
"title-image": "Изображение",
|
||||||
|
"title-image-plural": "Изображения",
|
||||||
|
"download-again": "Скачать еще раз",
|
||||||
|
"auto-accept-instructions-2": ", чтобы автоматически принимать все файлы, отправленные с того устройства.",
|
||||||
|
"enter-key-from-another-device": "Введите сюда ключ с другого устройства.",
|
||||||
|
"pair-devices-title": "Сопрягите устройства навсегда",
|
||||||
|
"input-key-on-this-device": "Введите этот ключ на другом устройстве",
|
||||||
|
"scan-qr-code": "или отсканируйте QR-код.",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"pair": "Подключить",
|
||||||
|
"accept": "Принять",
|
||||||
|
"auto-accept-instructions-1": "Активировать",
|
||||||
|
"file-other-description-file": "и 1 другой файл",
|
||||||
|
"file-other-description-image-plural": "и {{count}} других изображений",
|
||||||
|
"file-other-description-image": "и 1 другое изображение",
|
||||||
|
"file-other-description-file-plural": "и {{count}} других файлов",
|
||||||
|
"receive-title": "{{descriptor}} получен",
|
||||||
|
"system-language": "Язык системы",
|
||||||
|
"unpair": "Разорвать сопряжение",
|
||||||
|
"language-selector-title": "Выберите язык",
|
||||||
|
"hr-or": "ИЛИ",
|
||||||
|
"input-room-id-on-another-device": "Введите этот ID комнаты на другом устройстве"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"close-about-aria-label": "Закрыть страницу \"О сервисе\"",
|
||||||
|
"claim": "Самый простой способ передачи файлов между устройствами",
|
||||||
|
"close-about_aria-label": "Закрыть страницу \"О сервисе\"",
|
||||||
|
"buy-me-a-coffee_title": "Купить мне кофе!",
|
||||||
|
"github_title": "PairDrop на GitHub",
|
||||||
|
"tweet_title": "Твит о PairDrop",
|
||||||
|
"faq_title": "Часто задаваемые вопросы"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"display-name-changed-permanently": "Отображаемое имя было изменено навсегда.",
|
||||||
|
"display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.",
|
||||||
|
"pairing-success": "Устройства сопряжены.",
|
||||||
|
"pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.",
|
||||||
|
"copied-to-clipboard": "Скопировано в буфер обмена",
|
||||||
|
"pairing-not-persistent": "Сопряженные устройства непостоянны.",
|
||||||
|
"link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть",
|
||||||
|
"notifications-enabled": "Уведомления включены.",
|
||||||
|
"text-content-incorrect": "Содержание текста неверно.",
|
||||||
|
"message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать",
|
||||||
|
"connected": "Подключено.",
|
||||||
|
"copied-text": "Текст скопирован в буфер обмена",
|
||||||
|
"online": "Вы снова в сети",
|
||||||
|
"offline": "Вы находитесь вне сети",
|
||||||
|
"online-requirement": "Для сопряжения устройств вам нужно быть в сети.",
|
||||||
|
"files-incorrect": "Файлы неверны.",
|
||||||
|
"message-transfer-completed": "Передача сообщения завершена.",
|
||||||
|
"ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз",
|
||||||
|
"selected-peer-left": "Выбранный узел вышел.",
|
||||||
|
"request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}",
|
||||||
|
"rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.",
|
||||||
|
"unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть PairDrop?",
|
||||||
|
"copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!",
|
||||||
|
"pairing-cleared": "Все устройства не сопряжены.",
|
||||||
|
"pairing-key-invalid": "Неверный ключ",
|
||||||
|
"pairing-key-invalidated": "Ключ {{key}} признан недействительным.",
|
||||||
|
"click-to-download": "Нажмите, чтобы скачать",
|
||||||
|
"clipboard-content-incorrect": "Содержание буфера обмена неверно.",
|
||||||
|
"click-to-show": "Нажмите, чтобы показать",
|
||||||
|
"connecting": "Подключение…",
|
||||||
|
"download-successful": "{{descriptor}} загружен",
|
||||||
|
"display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.",
|
||||||
|
"file-content-incorrect": "Содержимое файла неверно.",
|
||||||
|
"file-transfer-completed": "Передача файла завершена.",
|
||||||
|
"public-room-left": "Покинуть публичную комнату {{publicRoomId}}",
|
||||||
|
"copied-to-clipboard-error": "Копирование невозможно. Скопируйте вручную.",
|
||||||
|
"public-room-id-invalid": "Неверный ID комнаты",
|
||||||
|
"online-requirement-pairing": "Для сопряжения устройств необходимо находиться быть онлайн.",
|
||||||
|
"online-requirement-public-room": "Для создания публичной комнаты необходимо быть онлайн."
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}",
|
||||||
|
"preparing": "Подготовка…",
|
||||||
|
"transferring": "Передача…",
|
||||||
|
"processing": "Обработка…",
|
||||||
|
"waiting": "Ожидание…",
|
||||||
|
"connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах",
|
||||||
|
"click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение"
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-received-plural": "{{count}} файлов получено",
|
||||||
|
"message-received-plural": "{{count}} сообщений получено",
|
||||||
|
"file-received": "Файл получен",
|
||||||
|
"file-transfer-requested": "Запрошена передача файлов",
|
||||||
|
"message-received": "Сообщение получено"
|
||||||
|
}
|
||||||
|
}
|
25
public/lang/tr.json
Normal file
25
public/lang/tr.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "PairDrop Hakkında",
|
||||||
|
"about_aria-label": "PairDrop Hakkında Aç",
|
||||||
|
"theme-auto_title": "Temayı Sisteme Uyarla",
|
||||||
|
"theme-light_title": "Daima Açık Tema Kullan",
|
||||||
|
"theme-dark_title": "Daima Koyu Tema Kullan",
|
||||||
|
"notification_title": "Bildirimleri Etkinleştir",
|
||||||
|
"install_title": "PairDrop'u Yükle",
|
||||||
|
"pair-device_title": "Cihaz Eşleştir",
|
||||||
|
"edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle",
|
||||||
|
"cancel-paste-mode": "Bitti"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"display-name_data-placeholder": "Yükleniyor…",
|
||||||
|
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"cancel": "İptal",
|
||||||
|
"edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle"
|
||||||
|
}
|
||||||
|
}
|
155
public/lang/zh-CN.json
Normal file
155
public/lang/zh-CN.json
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "关于 PairDrop",
|
||||||
|
"about_aria-label": "打开 关于 PairDrop",
|
||||||
|
"theme-light_title": "总是使用明亮主题",
|
||||||
|
"install_title": "安装 PairDrop",
|
||||||
|
"pair-device_title": "长久配对您的设备",
|
||||||
|
"theme-auto_title": "主题适应系统",
|
||||||
|
"theme-dark_title": "总是使用暗黑主题",
|
||||||
|
"notification_title": "开启通知",
|
||||||
|
"edit-paired-devices_title": "管理已配对设备",
|
||||||
|
"cancel-paste-mode": "完成",
|
||||||
|
"join-public-room_title": "暂时加入公共房间",
|
||||||
|
"language-selector_title": "选择语言"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_data-drop-peer": "释放以发送到此设备",
|
||||||
|
"no-peers_data-drop-bg": "释放来选择接收者",
|
||||||
|
"no-peers-subtitle": "配对新设备 或 加入一个公共房间 使在其他网络上可见",
|
||||||
|
"no-peers-title": "在其他设备上打开 PairDrop 来发送文件",
|
||||||
|
"x-instructions_desktop": "点击以发送文件 或 右键来发送信息",
|
||||||
|
"x-instructions_mobile": "轻触以发送文件 或 长按来发送信息",
|
||||||
|
"x-instructions_data-drop-bg": "释放来选择接收者",
|
||||||
|
"click-to-send": "点击发送",
|
||||||
|
"tap-to-send": "轻触发送",
|
||||||
|
"activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送",
|
||||||
|
"activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件",
|
||||||
|
"activate-paste-mode-shared-text": "分享文本"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"routed": "途径服务器",
|
||||||
|
"webrtc": "如果 WebRTC 不可用。",
|
||||||
|
"known-as": "你的名字是:",
|
||||||
|
"display-name_data-placeholder": "加载中…",
|
||||||
|
"display-name_title": "修改你的默认设备名",
|
||||||
|
"on-this-network": "在此网络上",
|
||||||
|
"paired-devices": "已配对的设备",
|
||||||
|
"traffic": "流量将",
|
||||||
|
"public-room-devices_title": "您可以被这个独立于网络的公共房间中的设备发现。",
|
||||||
|
"paired-devices_title": "您可以在任何时候被已配对的设备发现,而不依赖于网络。",
|
||||||
|
"public-room-devices": "在房间 {{roomId}} 中",
|
||||||
|
"discovery": "您可以被发现:",
|
||||||
|
"on-this-network_title": "您可以被这个网络上的每个人发现。"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"pair-devices-title": "配对新设备(常驻)",
|
||||||
|
"input-key-on-this-device": "在另一个设备上输入这串数字",
|
||||||
|
"base64-text": "信息",
|
||||||
|
"enter-key-from-another-device": "在此处输入从另一个设备上获得的数字。",
|
||||||
|
"edit-paired-devices-title": "管理已配对的设备",
|
||||||
|
"pair": "配对",
|
||||||
|
"cancel": "取消",
|
||||||
|
"scan-qr-code": "或者 扫描二维码。",
|
||||||
|
"paired-devices-wrapper_data-empty": "无已配对设备。",
|
||||||
|
"auto-accept-instructions-1": "启用",
|
||||||
|
"auto-accept": "自动接收",
|
||||||
|
"decline": "拒绝",
|
||||||
|
"base64-processing": "处理中…",
|
||||||
|
"base64-tap-to-paste": "轻触此处粘贴{{type}}",
|
||||||
|
"base64-paste-to-send": "粘贴到此处以发送 {{type}}",
|
||||||
|
"auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。",
|
||||||
|
"would-like-to-share": "想要分享",
|
||||||
|
"accept": "接收",
|
||||||
|
"close": "关闭",
|
||||||
|
"share": "分享",
|
||||||
|
"download": "保存",
|
||||||
|
"send": "发送",
|
||||||
|
"receive-text-title": "收到信息",
|
||||||
|
"copy": "复制",
|
||||||
|
"send-message-title": "发送信息",
|
||||||
|
"send-message-to": "发了一条信息给",
|
||||||
|
"has-sent": "发送了:",
|
||||||
|
"base64-files": "文件",
|
||||||
|
"file-other-description-file": "和 1 个其他的文件",
|
||||||
|
"file-other-description-image": "和 1 个其他的图片",
|
||||||
|
"file-other-description-image-plural": "和 {{count}} 个其他的图片",
|
||||||
|
"file-other-description-file-plural": "和 {{count}} 个其他的文件",
|
||||||
|
"title-image-plural": "图片",
|
||||||
|
"receive-title": "收到 {{descriptor}}",
|
||||||
|
"title-image": "图片",
|
||||||
|
"title-file": "文件",
|
||||||
|
"title-file-plural": "文件",
|
||||||
|
"download-again": "再次保存",
|
||||||
|
"system-language": "跟随系统语言",
|
||||||
|
"unpair": "取消配对",
|
||||||
|
"language-selector-title": "选择语言",
|
||||||
|
"hr-or": "或者",
|
||||||
|
"input-room-id-on-another-device": "在另一个设备上输入这串房间号"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"faq_title": "常见问题",
|
||||||
|
"close-about_aria-label": "关闭 关于 PairDrop",
|
||||||
|
"github_title": "PairDrop 在 GitHub 上开源",
|
||||||
|
"claim": "最简单的跨设备传输方案",
|
||||||
|
"buy-me-a-coffee_title": "帮我买杯咖啡!",
|
||||||
|
"tweet_title": "关于 PairDrop 的推特"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"display-name-changed-permanently": "展示的名字已经长久变更。",
|
||||||
|
"display-name-changed-temporarily": "展示的名字已经变更 仅在此会话中。",
|
||||||
|
"display-name-random-again": "展示的名字再次随机生成。",
|
||||||
|
"download-successful": "{{descriptor}} 已下载",
|
||||||
|
"pairing-tabs-error": "无法配对两个浏览器标签页。",
|
||||||
|
"pairing-success": "新设备已配对。",
|
||||||
|
"pairing-not-persistent": "配对的设备不是持久的。",
|
||||||
|
"pairing-key-invalid": "无效配对码",
|
||||||
|
"pairing-key-invalidated": "配对码 {{key}} 已失效。",
|
||||||
|
"text-content-incorrect": "文本内容不合法。",
|
||||||
|
"file-content-incorrect": "文件内容不合法。",
|
||||||
|
"clipboard-content-incorrect": "剪贴板内容不合法。",
|
||||||
|
"link-received": "收到来自 {{name}} 的链接 - 点击打开",
|
||||||
|
"message-received": "收到来自 {{name}} 的信息 - 点击复制",
|
||||||
|
"request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}",
|
||||||
|
"click-to-show": "点击展示",
|
||||||
|
"copied-text": "复制到剪贴板",
|
||||||
|
"selected-peer-left": "选择的设备已离开。",
|
||||||
|
"pairing-cleared": "所有设备已解除配对。",
|
||||||
|
"copied-to-clipboard": "已复制到剪贴板",
|
||||||
|
"notifications-enabled": "通知已启用。",
|
||||||
|
"copied-text-error": "写入剪贴板失败。请手动复制!",
|
||||||
|
"click-to-download": "点击以保存",
|
||||||
|
"unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭 PairDrop 吗?",
|
||||||
|
"message-transfer-completed": "信息传输已完成。",
|
||||||
|
"offline": "你未连接到网络",
|
||||||
|
"online": "你已重新连接到网络",
|
||||||
|
"connected": "已连接。",
|
||||||
|
"online-requirement": "你需要连接网络来配对新设备。",
|
||||||
|
"files-incorrect": "文件不合法。",
|
||||||
|
"file-transfer-completed": "文件传输已完成。",
|
||||||
|
"connecting": "连接中…",
|
||||||
|
"ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB",
|
||||||
|
"rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。",
|
||||||
|
"public-room-left": "已退出公共房间 {{publicRoomId}}",
|
||||||
|
"copied-to-clipboard-error": "无法复制。请手动复制。",
|
||||||
|
"public-room-id-invalid": "无效的房间号",
|
||||||
|
"online-requirement-pairing": "您需要连接到互联网来配对新设备。",
|
||||||
|
"online-requirement-public-room": "您需要连接到互联网来创建一个公共房间。"
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"message-received": "收到信息",
|
||||||
|
"message-received-plural": "收到 {{count}} 条信息",
|
||||||
|
"file-transfer-requested": "文件传输请求",
|
||||||
|
"file-received-plural": "收到 {{count}} 个文件",
|
||||||
|
"file-received": "收到文件"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send-paste-mode": "点击发送 {{descriptor}}",
|
||||||
|
"click-to-send": "点击以发送文件 或 右键来发送信息",
|
||||||
|
"connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号",
|
||||||
|
"preparing": "准备中…",
|
||||||
|
"waiting": "请等待…",
|
||||||
|
"transferring": "传输中…",
|
||||||
|
"processing": "处理中…"
|
||||||
|
}
|
||||||
|
}
|
144
public/scripts/localization.js
Normal file
144
public/scripts/localization.js
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
class Localization {
|
||||||
|
constructor() {
|
||||||
|
Localization.defaultLocale = "en";
|
||||||
|
Localization.supportedLocales = ["en", "nb", "ru", "zh-CN", "de"];
|
||||||
|
Localization.translations = {};
|
||||||
|
Localization.defaultTranslations = {};
|
||||||
|
|
||||||
|
Localization.systemLocale = Localization.supportedOrDefault(navigator.languages);
|
||||||
|
|
||||||
|
let storedLanguageCode = localStorage.getItem("language-code");
|
||||||
|
|
||||||
|
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
|
||||||
|
? storedLanguageCode
|
||||||
|
: Localization.systemLocale;
|
||||||
|
|
||||||
|
Localization.setTranslation(Localization.initialLocale)
|
||||||
|
.then(_ => {
|
||||||
|
console.log("Initial translation successful.");
|
||||||
|
Events.fire("initial-translation-loaded");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static isSupported(locale) {
|
||||||
|
return Localization.supportedLocales.indexOf(locale) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static supportedOrDefault(locales) {
|
||||||
|
return locales.find(Localization.isSupported) || Localization.defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setTranslation(locale) {
|
||||||
|
if (!locale) locale = Localization.systemLocale;
|
||||||
|
|
||||||
|
await Localization.setLocale(locale)
|
||||||
|
await Localization.translatePage();
|
||||||
|
|
||||||
|
console.log("Page successfully translated",
|
||||||
|
`System language: ${Localization.systemLocale}`,
|
||||||
|
`Selected language: ${locale}`
|
||||||
|
);
|
||||||
|
|
||||||
|
Events.fire("translation-loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setLocale(newLocale) {
|
||||||
|
if (newLocale === Localization.locale) return false;
|
||||||
|
|
||||||
|
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
|
||||||
|
|
||||||
|
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
|
||||||
|
|
||||||
|
if(!newTranslations) return false;
|
||||||
|
|
||||||
|
Localization.locale = newLocale;
|
||||||
|
Localization.translations = newTranslations;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getLocale() {
|
||||||
|
return Localization.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isSystemLocale() {
|
||||||
|
return !localStorage.getItem('language-code');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fetchTranslationsFor(newLocale) {
|
||||||
|
const response = await fetch(`lang/${newLocale}.json`)
|
||||||
|
|
||||||
|
if (response.redirected === true || response.status !== 200) return false;
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async translatePage() {
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-i18n-key]")
|
||||||
|
.forEach(element => Localization.translateElement(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async translateElement(element) {
|
||||||
|
const key = element.getAttribute("data-i18n-key");
|
||||||
|
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
|
||||||
|
|
||||||
|
for (let i in attrs) {
|
||||||
|
let attr = attrs[i];
|
||||||
|
if (attr === "text") {
|
||||||
|
element.innerText = Localization.getTranslation(key);
|
||||||
|
} else {
|
||||||
|
if (attr.startsWith("data-")) {
|
||||||
|
let dataAttr = attr.substring(5);
|
||||||
|
element.dataset.dataAttr = Localization.getTranslation(key, attr);
|
||||||
|
} {
|
||||||
|
element.setAttribute(attr, Localization.getTranslation(key, attr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTranslation(key, attr=null, data={}, useDefault=false) {
|
||||||
|
const keys = key.split(".");
|
||||||
|
|
||||||
|
let translationCandidates = useDefault
|
||||||
|
? Localization.defaultTranslations
|
||||||
|
: Localization.translations;
|
||||||
|
|
||||||
|
let translation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
translationCandidates = translationCandidates[keys[i]]
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastKey = keys[keys.length - 1];
|
||||||
|
|
||||||
|
if (attr) lastKey += "_" + attr;
|
||||||
|
|
||||||
|
translation = translationCandidates[lastKey];
|
||||||
|
|
||||||
|
for (let j in data) {
|
||||||
|
translation = translation.replace(`{{${j}}}`, data[j]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
translation = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!translation) {
|
||||||
|
if (!useDefault) {
|
||||||
|
translation = this.getTranslation(key, attr, data, true);
|
||||||
|
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
|
||||||
|
console.warn("Help translating PairDrop: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/");
|
||||||
|
} else {
|
||||||
|
console.warn("Missing translation in default language:", key, attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Localization.escapeHTML(translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
static escapeHTML(unsafeText) {
|
||||||
|
let div = document.createElement('div');
|
||||||
|
div.innerText = unsafeText;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,10 +23,14 @@ class ServerConnection {
|
||||||
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
||||||
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
||||||
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
||||||
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
|
||||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||||
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
||||||
|
|
||||||
|
Events.on('create-public-room', _ => this._onCreatePublicRoom());
|
||||||
|
Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid));
|
||||||
|
Events.on('leave-public-room', _ => this._onLeavePublicRoom());
|
||||||
|
|
||||||
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
||||||
Events.on('online', _ => this._connect());
|
Events.on('online', _ => this._connect());
|
||||||
}
|
}
|
||||||
|
@ -46,23 +50,47 @@ class ServerConnection {
|
||||||
_onOpen() {
|
_onOpen() {
|
||||||
console.log('WS: server connected');
|
console.log('WS: server connected');
|
||||||
Events.fire('ws-connected');
|
Events.fire('ws-connected');
|
||||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPairDeviceInitiate() {
|
_onPairDeviceInitiate() {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-pairing"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-initiate' })
|
this.send({ type: 'pair-device-initiate' });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPairDeviceJoin(roomKey) {
|
_onPairDeviceJoin(pairKey) {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', pairKey: pairKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCreatePublicRoom() {
|
||||||
|
if (!this._isConnected()) {
|
||||||
|
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-public-room"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send({ type: 'create-public-room' });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onJoinPublicRoom(roomId, createIfInvalid) {
|
||||||
|
if (!this._isConnected()) {
|
||||||
|
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLeavePublicRoom() {
|
||||||
|
if (!this._isConnected()) {
|
||||||
|
setTimeout(_ => this._onLeavePublicRoom(), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send({ type: 'leave-public-room' });
|
||||||
}
|
}
|
||||||
|
|
||||||
_setRtcConfig(config) {
|
_setRtcConfig(config) {
|
||||||
|
@ -104,10 +132,10 @@ class ServerConnection {
|
||||||
Events.fire('pair-device-join-key-invalid');
|
Events.fire('pair-device-join-key-invalid');
|
||||||
break;
|
break;
|
||||||
case 'pair-device-canceled':
|
case 'pair-device-canceled':
|
||||||
Events.fire('pair-device-canceled', msg.roomKey);
|
Events.fire('pair-device-canceled', msg.pairKey);
|
||||||
break;
|
break;
|
||||||
case 'pair-device-join-key-rate-limit':
|
case 'join-key-rate-limit':
|
||||||
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
|
||||||
break;
|
break;
|
||||||
case 'secret-room-deleted':
|
case 'secret-room-deleted':
|
||||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||||
|
@ -115,6 +143,15 @@ class ServerConnection {
|
||||||
case 'room-secret-regenerated':
|
case 'room-secret-regenerated':
|
||||||
Events.fire('room-secret-regenerated', msg);
|
Events.fire('room-secret-regenerated', msg);
|
||||||
break;
|
break;
|
||||||
|
case 'public-room-id-invalid':
|
||||||
|
Events.fire('public-room-id-invalid', msg.publicRoomId);
|
||||||
|
break;
|
||||||
|
case 'public-room-created':
|
||||||
|
Events.fire('public-room-created', msg.roomId);
|
||||||
|
break;
|
||||||
|
case 'public-room-left':
|
||||||
|
Events.fire('public-room-left');
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('WS receive: unknown message type', msg);
|
console.error('WS receive: unknown message type', msg);
|
||||||
}
|
}
|
||||||
|
@ -132,8 +169,8 @@ class ServerConnection {
|
||||||
|
|
||||||
_onDisplayName(msg) {
|
_onDisplayName(msg) {
|
||||||
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
||||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
sessionStorage.setItem('peer_id', msg.message.peerId);
|
||||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
|
||||||
|
|
||||||
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
||||||
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
||||||
|
@ -155,8 +192,8 @@ class ServerConnection {
|
||||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||||
const peerId = sessionStorage.getItem("peerId");
|
const peerId = sessionStorage.getItem('peer_id');
|
||||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
const peerIdHash = sessionStorage.getItem('peer_id_hash');
|
||||||
if (peerId && peerIdHash) {
|
if (peerId && peerIdHash) {
|
||||||
ws_url.searchParams.append('peer_id', peerId);
|
ws_url.searchParams.append('peer_id', peerId);
|
||||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||||
|
@ -167,7 +204,7 @@ class ServerConnection {
|
||||||
_disconnect() {
|
_disconnect() {
|
||||||
this.send({ type: 'disconnect' });
|
this.send({ type: 'disconnect' });
|
||||||
|
|
||||||
const peerId = sessionStorage.getItem("peerId");
|
const peerId = sessionStorage.getItem('peer_id');
|
||||||
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
||||||
console.log("successfully removed peerId from localStorage");
|
console.log("successfully removed peerId from localStorage");
|
||||||
});
|
});
|
||||||
|
@ -183,7 +220,7 @@ class ServerConnection {
|
||||||
|
|
||||||
_onDisconnect() {
|
_onDisconnect() {
|
||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'Connecting..');
|
Events.fire('notify-user', Localization.getTranslation("notifications.connecting"));
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
@ -215,12 +252,13 @@ class ServerConnection {
|
||||||
|
|
||||||
class Peer {
|
class Peer {
|
||||||
|
|
||||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||||
this._server = serverConnection;
|
this._server = serverConnection;
|
||||||
this._isCaller = isCaller;
|
this._isCaller = isCaller;
|
||||||
this._peerId = peerId;
|
this._peerId = peerId;
|
||||||
this._roomType = roomType;
|
|
||||||
this._updateRoomSecret(roomSecret);
|
this._roomIds = {};
|
||||||
|
this._updateRoomIds(roomType, roomId);
|
||||||
|
|
||||||
this._filesQueue = [];
|
this._filesQueue = [];
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
|
@ -241,34 +279,58 @@ class Peer {
|
||||||
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateRoomSecret(roomSecret) {
|
_isPaired() {
|
||||||
|
return !!this._roomIds['secret'];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPairSecret() {
|
||||||
|
return this._roomIds['secret'];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getRoomTypes() {
|
||||||
|
return Object.keys(this._roomIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateRoomIds(roomType, roomId) {
|
||||||
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
|
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
|
||||||
// -> do not delete duplicates and do not regenerate room secrets
|
// -> do not delete duplicates and do not regenerate room secrets
|
||||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
|
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
|
||||||
// remove old roomSecrets to prevent multiple pairings with same peer
|
// multiple roomSecrets with same peer -> delete old roomSecret
|
||||||
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
|
PersistentStorage.deleteRoomSecret(this._getPairSecret())
|
||||||
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
.then(deletedRoomSecret => {
|
||||||
})
|
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._roomSecret = roomSecret;
|
this._roomIds[roomType] = roomId;
|
||||||
|
|
||||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
|
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
|
||||||
// increase security by increasing roomSecret length
|
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
|
||||||
console.log('RoomSecret is regenerated to increase security')
|
console.log('RoomSecret is regenerated to increase security')
|
||||||
Events.fire('regenerate-room-secret', this._roomSecret);
|
Events.fire('regenerate-room-secret', this._getPairSecret());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_removeRoomType(roomType) {
|
||||||
|
delete this._roomIds[roomType];
|
||||||
|
|
||||||
|
Events.fire('room-type-removed', {
|
||||||
|
peerId: this._peerId,
|
||||||
|
roomType: roomType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_evaluateAutoAccept() {
|
_evaluateAutoAccept() {
|
||||||
if (!this._roomSecret) {
|
if (!this._isPaired()) {
|
||||||
this._setAutoAccept(false);
|
this._setAutoAccept(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistentStorage.getRoomSecretEntry(this._roomSecret)
|
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
|
||||||
.then(roomSecretEntry => {
|
.then(roomSecretEntry => {
|
||||||
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
|
const autoAccept = roomSecretEntry
|
||||||
|
? roomSecretEntry.entry.auto_accept
|
||||||
|
: false;
|
||||||
this._setAutoAccept(autoAccept);
|
this._setAutoAccept(autoAccept);
|
||||||
})
|
})
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
|
@ -277,7 +339,9 @@ class Peer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_setAutoAccept(autoAccept) {
|
_setAutoAccept(autoAccept) {
|
||||||
this._autoAccept = autoAccept;
|
this._autoAccept = !this._isSameBrowser()
|
||||||
|
? autoAccept
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||||
|
@ -488,7 +552,7 @@ class Peer {
|
||||||
|
|
||||||
_abortTransfer() {
|
_abortTransfer() {
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||||
Events.fire('notify-user', 'Files are incorrect.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
|
||||||
this._filesReceived = [];
|
this._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
this._digester = null;
|
this._digester = null;
|
||||||
|
@ -536,7 +600,7 @@ class Peer {
|
||||||
if (!this._requestAccepted.header.length) {
|
if (!this._requestAccepted.header.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||||
this._filesReceived = [];
|
this._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
}
|
}
|
||||||
|
@ -546,7 +610,7 @@ class Peer {
|
||||||
this._chunker = null;
|
this._chunker = null;
|
||||||
if (!this._filesQueue.length) {
|
if (!this._filesQueue.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('notify-user', 'File transfer completed.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
|
||||||
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
|
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
|
||||||
} else {
|
} else {
|
||||||
this._dequeueFile();
|
this._dequeueFile();
|
||||||
|
@ -558,7 +622,7 @@ class Peer {
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||||
this._filesRequested = null;
|
this._filesRequested = null;
|
||||||
if (message.reason === 'ios-memory-limit') {
|
if (message.reason === 'ios-memory-limit') {
|
||||||
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once");
|
Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -568,7 +632,7 @@ class Peer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessageTransferCompleted() {
|
_onMessageTransferCompleted() {
|
||||||
Events.fire('notify-user', 'Message transfer completed.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
sendText(text) {
|
sendText(text) {
|
||||||
|
@ -599,8 +663,8 @@ class Peer {
|
||||||
|
|
||||||
class RTCPeer extends Peer {
|
class RTCPeer extends Peer {
|
||||||
|
|
||||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||||
super(serverConnection, isCaller, peerId, roomType, roomSecret);
|
super(serverConnection, isCaller, peerId, roomType, roomId);
|
||||||
this.rtcSupported = true;
|
this.rtcSupported = true;
|
||||||
if (!this._isCaller) return; // we will listen for a caller
|
if (!this._isCaller) return; // we will listen for a caller
|
||||||
this._connect();
|
this._connect();
|
||||||
|
@ -626,13 +690,17 @@ class RTCPeer extends Peer {
|
||||||
|
|
||||||
_openChannel() {
|
_openChannel() {
|
||||||
if (!this._conn) return;
|
if (!this._conn) return;
|
||||||
|
|
||||||
const channel = this._conn.createDataChannel('data-channel', {
|
const channel = this._conn.createDataChannel('data-channel', {
|
||||||
ordered: true,
|
ordered: true,
|
||||||
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
|
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
|
||||||
});
|
});
|
||||||
channel.onopen = e => this._onChannelOpened(e);
|
channel.onopen = e => this._onChannelOpened(e);
|
||||||
channel.onerror = e => this._onError(e);
|
channel.onerror = e => this._onError(e);
|
||||||
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
|
|
||||||
|
this._conn.createOffer()
|
||||||
|
.then(d => this._onDescription(d))
|
||||||
|
.catch(e => this._onError(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDescription(description) {
|
_onDescription(description) {
|
||||||
|
@ -713,7 +781,7 @@ class RTCPeer extends Peer {
|
||||||
_onBeforeUnload(e) {
|
_onBeforeUnload(e) {
|
||||||
if (this._busy) {
|
if (this._busy) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return "There are unfinished transfers. Are you sure you want to close?";
|
return Localization.getTranslation("notifications.unfinished-transfers-warning");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -772,8 +840,8 @@ class RTCPeer extends Peer {
|
||||||
_sendSignal(signal) {
|
_sendSignal(signal) {
|
||||||
signal.type = 'signal';
|
signal.type = 'signal';
|
||||||
signal.to = this._peerId;
|
signal.to = this._peerId;
|
||||||
signal.roomType = this._roomType;
|
signal.roomType = this._getRoomTypes()[0];
|
||||||
signal.roomSecret = this._roomSecret;
|
signal.roomId = this._roomIds[this._getRoomTypes()[0]];
|
||||||
this._server.send(signal);
|
this._server.send(signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -815,7 +883,14 @@ class PeersManager {
|
||||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
|
|
||||||
|
// this device closes connection
|
||||||
|
Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));
|
||||||
|
Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));
|
||||||
|
|
||||||
|
// peer closes connection
|
||||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||||
|
|
||||||
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
|
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
|
||||||
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||||
|
@ -828,47 +903,43 @@ class PeersManager {
|
||||||
this.peers[peerId].onServerMessage(message);
|
this.peers[peerId].onServerMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshPeer(peer, roomType, roomSecret) {
|
_refreshPeer(peer, roomType, roomId) {
|
||||||
if (!peer) return false;
|
if (!peer) return false;
|
||||||
|
|
||||||
const roomTypeIsSecret = roomType === "secret";
|
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
|
||||||
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
|
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
|
||||||
|
|
||||||
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
|
// if roomType or roomId for roomType differs peer is already connected
|
||||||
if (roomTypeIsSecret && roomSecretsDiffer) {
|
// -> only update roomSecret and reevaluate auto accept
|
||||||
peer._updateRoomSecret(roomSecret);
|
if (roomTypesDiffer || roomIdsDiffer) {
|
||||||
|
peer._updateRoomIds(roomType, roomId);
|
||||||
peer._evaluateAutoAccept();
|
peer._evaluateAutoAccept();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomTypesDiffer = peer._roomType !== roomType;
|
|
||||||
|
|
||||||
// if roomTypes differ peer is already connected -> abort
|
|
||||||
if (roomTypesDiffer) return true;
|
|
||||||
|
|
||||||
peer.refresh();
|
peer.refresh();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret) {
|
_createOrRefreshPeer(isCaller, peerId, roomType, roomId) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
if (peer) {
|
if (peer) {
|
||||||
this._refreshPeer(peer, roomType, roomSecret);
|
this._refreshPeer(peer, roomType, roomId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret);
|
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPeerJoined(message) {
|
_onPeerJoined(message) {
|
||||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret);
|
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPeers(message) {
|
_onPeers(message) {
|
||||||
message.peers.forEach(peer => {
|
message.peers.forEach(peer => {
|
||||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret);
|
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -899,7 +970,7 @@ class PeersManager {
|
||||||
_onPeerLeft(message) {
|
_onPeerLeft(message) {
|
||||||
if (message.disconnect === true) {
|
if (message.disconnect === true) {
|
||||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||||
Events.fire('peer-disconnected', message.peerId);
|
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
|
||||||
|
|
||||||
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
|
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
|
||||||
// Tidy up peerIds in localStorage
|
// Tidy up peerIds in localStorage
|
||||||
|
@ -923,14 +994,42 @@ class PeersManager {
|
||||||
if (peer._channel) peer._channel.onclose = null;
|
if (peer._channel) peer._channel.onclose = null;
|
||||||
peer._conn.close();
|
peer._conn.close();
|
||||||
peer._busy = false;
|
peer._busy = false;
|
||||||
|
peer._roomIds = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRoomSecretsDeleted(roomSecrets) {
|
||||||
|
for (let i=0; i<roomSecrets.length; i++) {
|
||||||
|
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecrets[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLeavePublicRoom(publicRoomId) {
|
||||||
|
this._disconnectOrRemoveRoomTypeByRoomId('public-id', publicRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSecretRoomDeleted(roomSecret) {
|
_onSecretRoomDeleted(roomSecret) {
|
||||||
for (const peerId in this.peers) {
|
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecret);
|
||||||
const peer = this.peers[peerId];
|
}
|
||||||
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
|
|
||||||
this._onPeerDisconnected(peerId);
|
_disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {
|
||||||
}
|
const peerIds = this._getPeerIdsFromRoomId(roomId);
|
||||||
|
|
||||||
|
if (!peerIds.length) return;
|
||||||
|
|
||||||
|
for (let i=0; i<peerIds.length; i++) {
|
||||||
|
this._disconnectOrRemoveRoomTypeByPeerId(peerIds[i], roomType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {
|
||||||
|
const peer = this.peers[peerId];
|
||||||
|
|
||||||
|
if (!peer) return;
|
||||||
|
|
||||||
|
if (peer._getRoomTypes().length > 1) {
|
||||||
|
peer._removeRoomType(roomType);
|
||||||
|
} else {
|
||||||
|
Events.fire('peer-disconnected', peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -961,20 +1060,26 @@ class PeersManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
||||||
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
|
const peerId = this._getPeerIdsFromRoomId(roomSecret)[0];
|
||||||
|
|
||||||
if (!peerId) return;
|
if (!peerId) return;
|
||||||
|
|
||||||
this.peers[peerId]._setAutoAccept(autoAccept);
|
this.peers[peerId]._setAutoAccept(autoAccept);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPeerIdFromRoomSecret(roomSecret) {
|
_getPeerIdsFromRoomId(roomId) {
|
||||||
|
if (!roomId) return [];
|
||||||
|
|
||||||
|
let peerIds = []
|
||||||
for (const peerId in this.peers) {
|
for (const peerId in this.peers) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
// peer must have same roomSecret and not be on the same browser.
|
|
||||||
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
|
// peer must have same roomId.
|
||||||
return peer._peerId;
|
if (Object.values(peer._roomIds).includes(roomId)) {
|
||||||
|
peerIds.push(peer._peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return peerIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1060,7 +1165,7 @@ class FileDigester {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Events {
|
class Events {
|
||||||
static fire(type, detail) {
|
static fire(type, detail = {}) {
|
||||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,8 @@
|
||||||
--icon-size: 24px;
|
--icon-size: 24px;
|
||||||
--primary-color: #4285f4;
|
--primary-color: #4285f4;
|
||||||
--paired-device-color: #00a69c;
|
--paired-device-color: #00a69c;
|
||||||
|
--public-room-color: #db8500;
|
||||||
|
--accent-color: var(--primary-color);
|
||||||
--peer-width: 120px;
|
--peer-width: 120px;
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +25,7 @@ body {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
transition: color 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -40,6 +43,10 @@ html {
|
||||||
min-height: fill-available;
|
min-height: fill-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fw {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.row-reverse {
|
.row-reverse {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
@ -51,7 +58,6 @@ html {
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +84,10 @@ html {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
@ -215,10 +225,6 @@ a,
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -275,8 +281,6 @@ x-noscript {
|
||||||
margin-top: 56px;
|
margin-top: 56px;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
--footer-height: 132px;
|
|
||||||
max-height: calc(100vh - 56px - var(--footer-height));
|
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
@ -284,11 +288,6 @@ x-noscript {
|
||||||
overscroll-behavior-x: none;
|
overscroll-behavior-x: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 425px) {
|
|
||||||
header:has(#edit-pair-devices:not([hidden]))~#center {
|
|
||||||
--footer-height: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Peers List */
|
/* Peers List */
|
||||||
|
|
||||||
|
@ -442,7 +441,7 @@ x-no-peers::before {
|
||||||
}
|
}
|
||||||
|
|
||||||
x-no-peers[drop-bg]::before {
|
x-no-peers[drop-bg]::before {
|
||||||
content: "Release to select recipient";
|
content: attr(data-drop-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-no-peers[drop-bg] * {
|
x-no-peers[drop-bg] * {
|
||||||
|
@ -461,7 +460,6 @@ x-peer {
|
||||||
|
|
||||||
x-peer label {
|
x-peer label {
|
||||||
width: var(--peer-width);
|
width: var(--peer-width);
|
||||||
cursor: pointer;
|
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -490,10 +488,14 @@ x-peer .icon-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
x-peer.type-secret .icon-wrapper {
|
||||||
background: var(--paired-device-color);
|
background: var(--paired-device-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
|
||||||
|
background: var(--public-room-color);
|
||||||
|
}
|
||||||
|
|
||||||
x-peer x-icon > .highlight-wrapper {
|
x-peer x-icon > .highlight-wrapper {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -502,17 +504,29 @@ x-peer x-icon > .highlight-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||||
width: 6px;
|
width: 15px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 4px;
|
||||||
|
margin-left: 1px;
|
||||||
|
margin-right: 1px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
x-peer.type-ip x-icon > .highlight-wrapper > .highlight.highlight-room-ip {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer.type-secret x-icon > .highlight-wrapper > .highlight.highlight-room-secret {
|
||||||
background-color: var(--paired-device-color);
|
background-color: var(--paired-device-color);
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x-peer.type-public-id x-icon > .highlight-wrapper > .highlight.highlight-room-public-id {
|
||||||
|
background-color: var(--public-room-color);
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
x-peer:not([status]):hover x-icon,
|
x-peer:not([status]):hover x-icon,
|
||||||
x-peer:not([status]):focus x-icon {
|
x-peer:not([status]):focus x-icon {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
|
@ -553,22 +567,6 @@ x-peer[status] x-icon {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer[status=transfer] .status:before {
|
|
||||||
content: 'Transferring...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer[status=prepare] .status:before {
|
|
||||||
content: 'Preparing...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer[status=wait] .status:before {
|
|
||||||
content: 'Waiting...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer[status=process] .status:before {
|
|
||||||
content: 'Processing...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer:not([status]) .status,
|
x-peer:not([status]) .status,
|
||||||
x-peer[status] .device-name {
|
x-peer[status] .device-name {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -602,13 +600,11 @@ x-peer[drop] x-icon {
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: auto;
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 0 16px 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: color 300ms;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
margin: auto 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .logo {
|
footer .logo {
|
||||||
|
@ -618,43 +614,71 @@ footer .logo {
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .font-body2 {
|
.discovery-wrapper {
|
||||||
color: var(--primary-color);
|
font-size: 12px;
|
||||||
margin: auto 18px;
|
margin: 10px auto auto;
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 2px;
|
||||||
|
background-color: rgb(var(--bg-color));
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#on-this-network {
|
/*You can be discovered wrapper*/
|
||||||
border-bottom: solid 4px var(--primary-color);
|
.discovery-wrapper > div:first-of-type {
|
||||||
padding-bottom: 1px;
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#paired-devices {
|
|
||||||
border-bottom: solid 4px var(--paired-device-color);
|
.discovery-wrapper .badge {
|
||||||
padding-bottom: 1px;
|
word-break: keep-all;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
border-radius: 0.3rem/0.3rem;
|
||||||
|
padding-right: 0.3rem;
|
||||||
|
padding-left: 0.3em;
|
||||||
|
background-color: var(--badge-color);
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-room-ip {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-room-secret {
|
||||||
|
background-color: var(--paired-device-color);
|
||||||
|
border-color: var(--paired-device-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-room-public-id {
|
||||||
|
background-color: var(--public-room-color);
|
||||||
|
border-color: var(--public-room-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#display-name {
|
#display-name {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
max-width: 15em;
|
max-width: 15em;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
margin-left: -1rem;
|
margin-left: -1rem;
|
||||||
margin-bottom: -6px;
|
margin-bottom: -6px;
|
||||||
padding-right: 0.3rem;
|
|
||||||
padding-left: 0.3em;
|
|
||||||
padding-bottom: 0.1rem;
|
padding-bottom: 0.1rem;
|
||||||
border-radius: 1.3rem/30%;
|
border-radius: 1.3rem/30%;
|
||||||
border-right: solid 1rem transparent;
|
border-right: solid 1rem transparent;
|
||||||
border-left: solid 1rem transparent;
|
border-left: solid 1rem transparent;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
background-color: rgba(var(--text-color), 43%);
|
|
||||||
color: white;
|
|
||||||
transition: background-color 0.5s ease;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-pen {
|
#edit-pen {
|
||||||
|
@ -663,7 +687,6 @@ footer .font-body2 {
|
||||||
margin-left: -1rem;
|
margin-left: -1rem;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dialog */
|
/* Dialog */
|
||||||
|
@ -681,7 +704,6 @@ x-dialog x-paper {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px 24px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -690,14 +712,33 @@ x-dialog x-paper {
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-paper {
|
x-paper > .row:first-of-type {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-bottom: solid 4px var(--border-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > .row:first-of-type h2 {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pair-device-dialog,
|
||||||
|
#edit-paired-devices-dialog {
|
||||||
|
--accent-color: var(--paired-device-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#public-room-dialog {
|
||||||
|
--accent-color: var(--public-room-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pair-device-dialog x-paper,
|
||||||
|
#public-room-dialog x-paper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: max(50%, 350px);
|
top: max(50%, 350px);
|
||||||
margin-top: -328.5px;
|
margin-top: -328.5px;
|
||||||
width: calc(100vw - 20px);
|
width: calc(100vw - 20px);
|
||||||
height: 625px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog ::-moz-selection,
|
#pair-device-dialog ::-moz-selection,
|
||||||
|
@ -706,6 +747,12 @@ x-dialog x-paper {
|
||||||
background: var(--paired-device-color);
|
background: var(--paired-device-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#public-room-dialog ::-moz-selection,
|
||||||
|
#public-room-dialog ::selection {
|
||||||
|
color: black;
|
||||||
|
background: var(--public-room-color);
|
||||||
|
}
|
||||||
|
|
||||||
x-dialog:not([show]) {
|
x-dialog:not([show]) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -723,24 +770,21 @@ x-dialog a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog .font-subheading {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pair Devices Dialog */
|
/* Pair Devices Dialog */
|
||||||
|
|
||||||
#key-input-container {
|
.input-key-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container > input {
|
.input-key-container > input {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
display: -webkit-box !important;
|
display: -webkit-box !important;
|
||||||
display: -webkit-flex !important;
|
display: -webkit-flex !important;
|
||||||
display: -moz-flex !important;
|
display: -moz-flex !important;
|
||||||
|
@ -751,15 +795,15 @@ x-dialog .font-subheading {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container > input + * {
|
.input-key-container > input + * {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container > input:nth-of-type(4) {
|
.input-key-container.six-chars > input:nth-of-type(4) {
|
||||||
margin-left: 5%;
|
margin-left: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#room-key {
|
.key {
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
-moz-user-select: text;
|
-moz-user-select: text;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
@ -770,13 +814,48 @@ x-dialog .font-subheading {
|
||||||
margin: 15px -15px;
|
margin: 15px -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#room-key-qr-code {
|
.key-qr-code {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-instructions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-dialog h2 {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
x-dialog hr {
|
x-dialog hr {
|
||||||
margin: 40px -24px 30px -24px;
|
height: 3px;
|
||||||
border: solid 1.25px var(--border-color);
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-note hr {
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-note > div {
|
||||||
|
height: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.hr-note > div > span {
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgb(var(--text-color));
|
||||||
|
background-color: rgb(var(--bg-color));
|
||||||
|
border: var(--border-color) solid 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-background {
|
#pair-device-dialog x-background {
|
||||||
|
@ -785,7 +864,7 @@ x-dialog hr {
|
||||||
|
|
||||||
/* Edit Paired Devices Dialog */
|
/* Edit Paired Devices Dialog */
|
||||||
.paired-devices-wrapper:empty:before {
|
.paired-devices-wrapper:empty:before {
|
||||||
content: "No paired devices.";
|
content: attr(data-empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
.paired-devices-wrapper:empty {
|
.paired-devices-wrapper:empty {
|
||||||
|
@ -870,39 +949,34 @@ x-dialog hr {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paired-device > .auto-accept {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Receive Dialog */
|
/* Receive Dialog */
|
||||||
|
|
||||||
x-dialog .row {
|
x-paper > .row {
|
||||||
margin-top: 24px;
|
padding: 10px;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* button row*/
|
/* button row*/
|
||||||
x-paper > div:last-child {
|
x-paper > .button-row {
|
||||||
margin: auto -24px -15px;
|
border-top: solid 3px var(--border-color);
|
||||||
border-top: solid 2.5px var(--border-color);
|
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-paper > div:last-child > .button {
|
x-paper > .button-row > .button {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-paper > div:last-child > .button:not(:last-child) {
|
x-paper > .button-row > .button:not(:first-child) {
|
||||||
border-left: solid 2.5px var(--border-color);
|
border-right: solid 1.5px var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > .button-row > .button:not(:last-child) {
|
||||||
|
border-left: solid 1.5px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description {
|
.file-description {
|
||||||
margin-bottom: 25px;
|
max-width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.file-description .row {
|
|
||||||
margin: 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description span {
|
.file-description span {
|
||||||
|
@ -913,23 +987,29 @@ x-paper > div:last-child > .button:not(:last-child) {
|
||||||
.file-name {
|
.file-name {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-stem {
|
.file-stem {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
padding-right: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send Text Dialog */
|
/* Send Text Dialog */
|
||||||
/* Todo: add pair underline to send / receive dialogs displayName */
|
|
||||||
x-dialog .dialog-subheader {
|
x-dialog .dialog-subheader {
|
||||||
margin-bottom: 25px;
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-text-dialog .display-name-wrapper {
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#text-input {
|
#text-input {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
margin: 14px auto;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receive Text Dialog */
|
/* Receive Text Dialog */
|
||||||
|
@ -944,7 +1024,6 @@ x-dialog .dialog-subheader {
|
||||||
-moz-user-select: text;
|
-moz-user-select: text;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
padding: 15px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#receive-text-dialog #text a {
|
#receive-text-dialog #text a {
|
||||||
|
@ -971,12 +1050,11 @@ x-dialog .dialog-subheader {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
border: solid 12px #438cff;
|
border: solid 12px #438cff;
|
||||||
text-align: center;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-dialog .textarea {
|
#base64-paste-dialog .textarea {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -988,21 +1066,9 @@ x-dialog .dialog-subheader {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
content: attr(placeholder);
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-dialog button {
|
|
||||||
margin: auto;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#base64-paste-dialog button[close] {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#base64-paste-dialog button[close]:before {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button */
|
/* Button */
|
||||||
|
|
||||||
|
@ -1019,12 +1085,13 @@ x-dialog .dialog-subheader {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: var(--primary-color);
|
color: var(--accent-color);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
color: #5B5B66;
|
color: #5B5B66;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1058,6 +1125,11 @@ x-dialog .dialog-subheader {
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button[selected],
|
||||||
|
.icon-button[selected] {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
#cancel-paste-mode {
|
#cancel-paste-mode {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -1100,8 +1172,7 @@ button::-moz-focus-inner {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
margin: 10px 0;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background: #f1f3f4;
|
background: #f1f3f4;
|
||||||
|
@ -1288,11 +1359,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions[drop-peer]:before {
|
x-instructions[drop-peer]:before {
|
||||||
content: "Release to send to peer";
|
content: attr(data-drop-peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions[drop-bg]:not([drop-peer]):before {
|
x-instructions[drop-bg]:not([drop-peer]):before {
|
||||||
content: "Release to select recipient";
|
content: attr(data-drop-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions p {
|
x-instructions p {
|
||||||
|
@ -1311,14 +1382,6 @@ x-peers:empty~x-instructions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Styles */
|
/* Responsive Styles */
|
||||||
@media screen and (max-width: 360px) {
|
|
||||||
x-dialog x-paper {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
x-paper > div:last-child {
|
|
||||||
margin: auto -15px -15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-height: 800px) {
|
@media screen and (min-height: 800px) {
|
||||||
footer {
|
footer {
|
||||||
|
@ -1341,8 +1404,9 @@ body {
|
||||||
--text-color: 51,51,51;
|
--text-color: 51,51,51;
|
||||||
--bg-color: 250,250,250; /*rgb code*/
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
--bg-color-test: 18,18,18;
|
--bg-color-test: 18,18,18;
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #e4e4e4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: rgb(169, 169, 169);
|
||||||
|
--badge-color: #a5a5a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme colors */
|
/* Dark theme colors */
|
||||||
|
@ -1350,7 +1414,8 @@ body.dark-theme {
|
||||||
--text-color: 238,238,238;
|
--text-color: 238,238,238;
|
||||||
--bg-color: 18,18,18; /*rgb code*/
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: rgb(238,238,238);
|
||||||
|
--badge-color: #717171;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colored Elements */
|
/* Colored Elements */
|
||||||
|
@ -1384,7 +1449,7 @@ x-dialog x-paper {
|
||||||
|
|
||||||
/* Image/Video/Audio Preview */
|
/* Image/Video/Audio Preview */
|
||||||
.file-preview {
|
.file-preview {
|
||||||
margin: 10px -24px 40px -24px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-preview:empty {
|
.file-preview:empty {
|
||||||
|
@ -1408,15 +1473,17 @@ x-dialog x-paper {
|
||||||
--text-color: 238,238,238;
|
--text-color: 238,238,238;
|
||||||
--bg-color: 18,18,18; /*rgb code*/
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: rgb(238,238,238);
|
||||||
|
--badge-color: #717171;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override dark mode with light mode styles if the user decides to swap */
|
/* Override dark mode with light mode styles if the user decides to swap */
|
||||||
body.light-theme {
|
body.light-theme {
|
||||||
--text-color: 51,51,51;
|
--text-color: 51,51,51;
|
||||||
--bg-color: 250,250,250; /*rgb code*/
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #e4e4e4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: rgb(169, 169, 169);
|
||||||
|
--badge-color: #a5a5a5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,107 +39,172 @@
|
||||||
|
|
||||||
<body translate="no">
|
<body translate="no">
|
||||||
<header class="row-reverse">
|
<header class="row-reverse">
|
||||||
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
|
<a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label" title="About PairDrop" aria-label="Open About PairDrop">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#info-outline" />
|
<use xlink:href="#info-outline" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title" title="Select Language">
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlink:href="#icon-language-selector" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div id="theme-wrapper">
|
<div id="theme-wrapper">
|
||||||
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
|
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#icon-theme-auto" />
|
<use xlink:href="#icon-theme-auto" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
|
<div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title" title="Always Use Light-Theme" >
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#icon-theme-light" />
|
<use xlink:href="#icon-theme-light" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" >
|
<div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title" title="Always Use Dark-Theme" >
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#icon-theme-dark" />
|
<use xlink:href="#icon-theme-dark" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="notification" class="icon-button" title="Enable Notifications" hidden>
|
<div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" title="Enable Notifications" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#notifications" />
|
<use xlink:href="#notifications" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="install" class="icon-button" title="Install PairDrop" hidden>
|
<div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" title="Install PairDrop" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#homescreen" />
|
<use xlink:href="#homescreen" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="pair-device" class="icon-button" title="Pair Device" hidden>
|
<div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title" title="Pair Your Devices Permanently">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#pair-device-icon" />
|
<use xlink:href="#pair-device-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
|
<div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" title="Edit Paired Devices" hidden>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#edit-pair-devices-icon" />
|
<use xlink:href="#edit-pair-devices-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="cancel-paste-mode" class="button" hidden>Done</div>
|
<div id="join-public-room" class="icon-button" data-i18n-key="header.join-public-room" data-i18n-attrs="title" title="Join Public Room Temporarily">
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlink:href="#public-room-icon" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="cancel-paste-mode" class="button" data-i18n-key="header.cancel-paste-mode" data-i18n-attrs="text" hidden>Done</div>
|
||||||
</header>
|
</header>
|
||||||
<!-- Center -->
|
<!-- Center -->
|
||||||
<div id="center">
|
<div id="center">
|
||||||
<!-- Peers -->
|
<!-- Peers -->
|
||||||
<div class="x-peers-filler"></div>
|
<div class="x-peers-filler"></div>
|
||||||
<x-peers class="center"></x-peers>
|
<x-peers class="center"></x-peers>
|
||||||
<x-no-peers>
|
<x-no-peers data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg" data-drop-bg="Release to select recipient">
|
||||||
<h2>Open PairDrop on other devices to send files</h2>
|
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text">Open PairDrop on other devices to send files</h2>
|
||||||
<div>Pair devices to be discoverable on other networks</div>
|
<div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text">Pair devices or enter a public room to be discoverable on other networks</div>
|
||||||
</x-no-peers>
|
</x-no-peers>
|
||||||
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message">
|
<x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"
|
||||||
|
desktop="Click to send files or right click to send a message"
|
||||||
|
mobile="Tap to send files or long tap to send a message"
|
||||||
|
data-drop-peer="Release to send to peer"
|
||||||
|
data-drop-bg="Release to select recipient">
|
||||||
<p id="paste-filename"></p>
|
<p id="paste-filename"></p>
|
||||||
</x-instructions>
|
</x-instructions>
|
||||||
|
<div id="websocket-fallback">
|
||||||
|
<span data-i18n-key="footer.traffic" data-i18n-attrs="text">Traffic is</span>
|
||||||
|
<span data-i18n-key="footer.routed" data-i18n-attrs="text">routed through the server</span>
|
||||||
|
<span data-i18n-key="footer.webrtc" data-i18n-attrs="text">if WebRTC is not available.</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="column">
|
<footer class="column">
|
||||||
<svg class="icon logo">
|
<svg class="icon logo">
|
||||||
<use xlink:href="#wifi-tethering" />
|
<use xlink:href="#wifi-tethering" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div class="column">
|
||||||
<span>You are known as:</span>
|
<div class="known-as-wrapper">
|
||||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
<span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
|
||||||
<svg id="edit-pen" class="icon">
|
<div id="display-name" class="badge" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title"
|
||||||
<use xlink:href="#edit-pen-icon" />
|
placeholder="Loading..." data-placeholder="Loading..." title="Edit your device name permanently"
|
||||||
</svg>
|
autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||||
</div>
|
<svg id="edit-pen" class="icon">
|
||||||
<div class="font-body2">
|
<use xlink:href="#edit-pen-icon" />
|
||||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
</svg>
|
||||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
</div>
|
||||||
</div>
|
<div class="discovery-wrapper row">
|
||||||
<div id="websocket-fallback">
|
<div class="row center">
|
||||||
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span>
|
<span data-i18n-key="footer.discovery" data-i18n-attrs="text">You can be discovered:</span>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<span class="badge badge-room-ip" data-i18n-key="footer.on-this-network" data-i18n-attrs="text title">on this network</span>
|
||||||
|
<span class="badge badge-room-secret pointer" data-i18n-key="footer.paired-devices" data-i18n-attrs="text title" hidden>paired devices</span>
|
||||||
|
<span class="badge badge-room-public-id pointer" data-i18n-key="footer.public-room-devices" data-i18n-attrs="title" hidden>in room IAIAI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<!-- Language Select Dialog -->
|
||||||
|
<x-dialog id="language-select-dialog">
|
||||||
|
<x-background class="full center">
|
||||||
|
<x-paper shadow="2">
|
||||||
|
<div class="row center">
|
||||||
|
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text">Select Language</h2>
|
||||||
|
</div>
|
||||||
|
<div class="language-buttons">
|
||||||
|
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text">System Language</button>
|
||||||
|
<button class="button fw" value="en">English</button>
|
||||||
|
<button class="button fw" value="de">Deutsch (German)</button>
|
||||||
|
<button class="button fw" value="nb">Norsk (Norwegian)</button>
|
||||||
|
<button class="button fw" value="ru">Русский язык (Russian)</button>
|
||||||
|
<button class="button fw" value="zh-CN">中文 (Chinese)</button>
|
||||||
|
</div>
|
||||||
|
<div class="center row-reverse button-row">
|
||||||
|
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
|
</div>
|
||||||
|
</x-paper>
|
||||||
|
</x-background>
|
||||||
|
</x-dialog>
|
||||||
<!-- Pair Device Dialog -->
|
<!-- Pair Device Dialog -->
|
||||||
<x-dialog id="pair-device-dialog">
|
<x-dialog id="pair-device-dialog">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Pair Devices</h2>
|
<div class="row center">
|
||||||
<div id="room-key-qr-code" class="center"></div>
|
<h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text">Pair Devices</h2>
|
||||||
<h1 id="room-key" class="center">000 000</h1>
|
|
||||||
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
|
|
||||||
<hr>
|
|
||||||
<div id="key-input-container">
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
|
<div class="row center">
|
||||||
<div class="center row-reverse">
|
<div class="column">
|
||||||
<button class="button" type="submit" disabled>Pair</button>
|
<div class="center key-qr-code"></div>
|
||||||
<button class="button" type="button" close>Cancel</button>
|
<h1 class="center key">000 000</h1>
|
||||||
|
<p class="center text-center key-instructions">
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text">Input this key on another device</span>
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hr-note">
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<div class="input-key-container six-chars">
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="tel" class="textarea center" aria-label="pair-key-char-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
</div>
|
||||||
|
<p class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device here.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-row row-reverse">
|
||||||
|
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
|
||||||
|
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -150,13 +215,70 @@
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center text-center">
|
<x-background class="full center text-center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center">Edit Paired Devices</h2>
|
<div class="row center">
|
||||||
<div class="paired-devices-wrapper"></div>
|
<h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text">Edit Paired Devices</h2>
|
||||||
<div class="font-subheading center">
|
|
||||||
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="center row-reverse">
|
<div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-wrapper" data-i18n-attrs="data-empty" data-empty="No paired devices."></div>
|
||||||
|
<div class="font-subheading center">
|
||||||
|
<p>
|
||||||
|
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text">
|
||||||
|
Activate
|
||||||
|
</span>
|
||||||
|
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text">auto-accept</u>
|
||||||
|
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text">
|
||||||
|
to automatically accept all files sent from that device.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="center row-reverse button-row">
|
||||||
|
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
|
</div>
|
||||||
|
</x-paper>
|
||||||
|
</x-background>
|
||||||
|
</form>
|
||||||
|
</x-dialog>
|
||||||
|
<!-- Public Room Dialog -->
|
||||||
|
<x-dialog id="public-room-dialog">
|
||||||
|
<form action="#">
|
||||||
|
<x-background class="full center text-center">
|
||||||
|
<x-paper shadow="2">
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<h2 class="center">Temporary Public Room</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<div class="center key-qr-code"></div>
|
||||||
|
<h1 class="center key">IOX9P</h1>
|
||||||
|
<p class="center text-center key-instructions">
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.input-room-id-on-another-device" data-i18n-attrs="text">Input this room id on another device</span>
|
||||||
|
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hr-note">
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<span data-i18n-key="dialogs.hr-or" data-i18n-attrs="text">OR</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column">
|
||||||
|
<div class="input-key-container">
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-2" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-3" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-4" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
<input type="text" class="textarea center" aria-label="room-id-char-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
|
||||||
|
</div>
|
||||||
|
<p class="font-subheading center text-center">Enter room id from another device to join.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center row-reverse button-row">
|
||||||
|
<button class="button" type="submit" disabled>Join</button>
|
||||||
<button class="button" type="button" close>Close</button>
|
<button class="button" type="button" close>Close</button>
|
||||||
|
<button class="button leave-room" type="button">Leave</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -166,24 +288,30 @@
|
||||||
<x-dialog id="receive-request-dialog">
|
<x-dialog id="receive-request-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center"></h2>
|
<div class="row center">
|
||||||
<div class="center column file-description">
|
<div class="column">
|
||||||
<div>
|
<h2 class="center"></h2>
|
||||||
<span class="display-name"></span>
|
|
||||||
<span>would like to share</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-name" >
|
</div>
|
||||||
<span class="file-stem"></span>
|
<div class="row center">
|
||||||
<span class="file-extension"></span>
|
<div class="column center file-description">
|
||||||
|
<div>
|
||||||
|
<span class="display-name badge"></span>
|
||||||
|
<span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text">would like to share</span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-name" >
|
||||||
|
<span class="file-stem"></span>
|
||||||
|
<span class="file-extension"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-other">
|
||||||
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-other">
|
|
||||||
</div>
|
|
||||||
<div class="row font-body2 file-size"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="center row-reverse">
|
<div class="row-reverse center button-row">
|
||||||
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
|
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
|
||||||
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
|
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -192,24 +320,31 @@
|
||||||
<x-dialog id="receive-file-dialog">
|
<x-dialog id="receive-file-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="center"></h2>
|
<div class="row center">
|
||||||
<div class="center column file-description">
|
<div class="column">
|
||||||
<div>
|
<h2 class="center"></h2>
|
||||||
<span class="display-name"></span>
|
|
||||||
<span>has sent</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-name" >
|
</div>
|
||||||
<span class="file-stem"></span>
|
<div class="row center">
|
||||||
<span class="file-extension"></span>
|
<div class="column center file-description">
|
||||||
|
<div>
|
||||||
|
<span class="display-name badge"></span>
|
||||||
|
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent</span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-name" >
|
||||||
|
<span class="file-stem"></span>
|
||||||
|
<span class="file-extension"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row file-other">
|
||||||
|
</div>
|
||||||
|
<div class="row font-body2 file-size"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row file-other"></div>
|
|
||||||
<div class="row font-body2 file-size"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="center file-preview"></div>
|
<div class="center file-preview"></div>
|
||||||
<div class="center row-reverse">
|
<div class="row-reverse center button-row">
|
||||||
<button id="share-btn" class="button" autofocus hidden>Share</button>
|
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden>Share</button>
|
||||||
<button id="download-btn" class="button" autofocus>Download</button>
|
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
|
||||||
<button class="button" close>Close</button>
|
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -219,16 +354,27 @@
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="text-center">Send Message</h2>
|
<div class="row center">
|
||||||
<div class="dialog-subheader text-center">
|
<div class="column">
|
||||||
<span>Send a Message to</span>
|
<h2 class="center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text">Send Message</h2>
|
||||||
<span class="display-name"></span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row center display-name-wrapper">
|
||||||
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
<div class="column">
|
||||||
<div class="center row-reverse">
|
<div class="text-center">
|
||||||
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
|
<span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text">Send a Message to</span>
|
||||||
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
|
<span class="display-name badge"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="column fw">
|
||||||
|
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-row row-reverse">
|
||||||
|
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
|
||||||
|
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -238,16 +384,23 @@
|
||||||
<x-dialog id="receive-text-dialog">
|
<x-dialog id="receive-text-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<h2 class="text-center">Message Received</h2>
|
<div class="row center">
|
||||||
<div class="text-center dialog-subheader">
|
<h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text">Message Received</h2>
|
||||||
<span class="display-name"></span>
|
|
||||||
<span>has sent:</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row-separator"></div>
|
<div class="row center">
|
||||||
<div id="text"></div>
|
<div class="text-center">
|
||||||
<div class="center row-reverse">
|
<span class="display-name badge"></span>
|
||||||
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
|
<span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent:</span>
|
||||||
<button id="close" class="button" title="ESCAPE">Close</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row center">
|
||||||
|
<div class="column fw">
|
||||||
|
<div id="text" class="textarea fw"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-reverse center button-row">
|
||||||
|
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
|
||||||
|
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
|
@ -256,20 +409,22 @@
|
||||||
<x-dialog id="base64-paste-dialog">
|
<x-dialog id="base64-paste-dialog">
|
||||||
<x-background class="full center">
|
<x-background class="full center">
|
||||||
<x-paper shadow="2">
|
<x-paper shadow="2">
|
||||||
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
|
<button class="button center" id="base64-paste-btn" title="Paste"></button>
|
||||||
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
|
||||||
<button class="button center" close>Close</button>
|
<div class="row-reverse center button-row">
|
||||||
|
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
|
||||||
|
</div>
|
||||||
</x-paper>
|
</x-paper>
|
||||||
</x-background>
|
</x-background>
|
||||||
</x-dialog>
|
</x-dialog>
|
||||||
<!-- Toast -->
|
<!-- Toast -->
|
||||||
<div class="toast-container full center">
|
<div class="toast-container full center">
|
||||||
<x-toast id="toast" class="row" shadow="1"></x-toast>
|
<x-toast id="toast" class="row center" shadow="1"></x-toast>
|
||||||
</div>
|
</div>
|
||||||
<!-- About Page -->
|
<!-- About Page -->
|
||||||
<x-about id="about" class="full center column">
|
<x-about id="about" class="full center column">
|
||||||
<header class="row-reverse fade-in">
|
<header class="row-reverse fade-in">
|
||||||
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
|
<a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="aria-label" aria-label="Close About PairDrop">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#close-icon" />
|
<use xlink:href="#close-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -283,24 +438,24 @@
|
||||||
<h1>PairDrop</h1>
|
<h1>PairDrop</h1>
|
||||||
<div class="font-subheading">v1.7.7</div>
|
<div class="font-subheading">v1.7.7</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-subheading">The easiest way to transfer files across devices</div>
|
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text">The easiest way to transfer files across devices</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer" data-i18n-key="about.github" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#github" />
|
<use xlink:href="#github" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" title="Buy me a coffee!" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#monetarization" />
|
<use xlink:href="#monetarization" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&" title="Tweet about PairDrop" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#twitter" />
|
<use xlink:href="#twitter" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">
|
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlink:href="#help-outline" />
|
<use xlink:href="#help-outline" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -374,8 +529,18 @@
|
||||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="public-room-icon" viewBox="0 0 640 512">
|
||||||
|
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path d="M0 24C0 10.7 10.7 0 24 0H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 48 0 37.3 0 24zM0 488c0-13.3 10.7-24 24-24H616c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24zM83.2 160a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM32 320c0-35.3 28.7-64 64-64h96c12.2 0 23.7 3.4 33.4 9.4c-37.2 15.1-65.6 47.2-75.8 86.6H64c-17.7 0-32-14.3-32-32zm461.6 32c-10.3-40.1-39.6-72.6-77.7-87.4c9.4-5.5 20.4-8.6 32.1-8.6h96c35.3 0 64 28.7 64 64c0 17.7-14.3 32-32 32H493.6zM391.2 290.4c32.1 7.4 58.1 30.9 68.9 61.6c3.5 10 5.5 20.8 5.5 32c0 17.7-14.3 32-32 32h-224c-17.7 0-32-14.3-32-32c0-11.2 1.9-22 5.5-32c10.5-29.7 35.3-52.8 66.1-60.9c7.8-2.1 16-3.1 24.5-3.1h96c7.4 0 14.7 .8 21.6 2.4zm44-130.4a64 64 0 1 1 128 0 64 64 0 1 1 -128 0zM321.6 96a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-language-selector" viewBox="0 0 640 512">
|
||||||
|
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<script src="scripts/localization.js"></script>
|
||||||
<script src="scripts/theme.js"></script>
|
<script src="scripts/theme.js"></script>
|
||||||
<script src="scripts/network.js"></script>
|
<script src="scripts/network.js"></script>
|
||||||
<script src="scripts/ui.js"></script>
|
<script src="scripts/ui.js"></script>
|
||||||
|
|
156
public_included_ws_fallback/lang/de.json
Normal file
156
public_included_ws_fallback/lang/de.json
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "Über PairDrop",
|
||||||
|
"notification_title": "Benachrichtigungen aktivieren",
|
||||||
|
"about_aria-label": "Über PairDrop öffnen",
|
||||||
|
"install_title": "PairDrop installieren",
|
||||||
|
"pair-device_title": "Deine Geräte dauerhaft koppeln",
|
||||||
|
"edit-paired-devices_title": "Gekoppelte Geräte bearbeiten",
|
||||||
|
"theme-auto_title": "Systemstil verwenden",
|
||||||
|
"theme-dark_title": "Dunklen Stil verwenden",
|
||||||
|
"theme-light_title": "Hellen Stil verwenden",
|
||||||
|
"cancel-paste-mode": "Fertig",
|
||||||
|
"language-selector_title": "Sprache auswählen",
|
||||||
|
"join-public-room_title": "Öffentlichen Raum temporär betreten"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"share": "Teilen",
|
||||||
|
"download": "Herunterladen",
|
||||||
|
"pair-devices-title": "Geräte dauerhaft koppeln",
|
||||||
|
"input-key-on-this-device": "Gebe diesen Schlüssel auf einem anderen Gerät ein",
|
||||||
|
"enter-key-from-another-device": "Gebe den Schlüssel von einem anderen Gerät hier ein.",
|
||||||
|
"pair": "Koppeln",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"edit-paired-devices-title": "Gekoppelte Geräte bearbeiten",
|
||||||
|
"paired-devices-wrapper_data-empty": "Keine gekoppelten Geräte.",
|
||||||
|
"close": "Schließen",
|
||||||
|
"accept": "Akzeptieren",
|
||||||
|
"decline": "Ablehnen",
|
||||||
|
"title-image": "Bild",
|
||||||
|
"title-file": "Datei",
|
||||||
|
"title-image-plural": "Bilder",
|
||||||
|
"title-file-plural": "Dateien",
|
||||||
|
"scan-qr-code": "oder scanne den QR-Code.",
|
||||||
|
"would-like-to-share": "möchte Folgendes teilen",
|
||||||
|
"send": "Senden",
|
||||||
|
"copy": "Kopieren",
|
||||||
|
"receive-text-title": "Textnachricht erhalten",
|
||||||
|
"file-other-description-image-plural": "und {{count}} andere Bilder",
|
||||||
|
"file-other-description-file-plural": "und {{count}} andere Dateien",
|
||||||
|
"auto-accept-instructions-1": "Aktiviere",
|
||||||
|
"auto-accept": "auto-accept",
|
||||||
|
"auto-accept-instructions-2": "um automatisch alle Dateien von diesem Gerät zu akzeptieren.",
|
||||||
|
"has-sent": "hat Folgendes gesendet:",
|
||||||
|
"send-message-title": "Textnachricht senden",
|
||||||
|
"send-message-to": "Sende eine Textnachricht an",
|
||||||
|
"base64-tap-to-paste": "Hier tippen, um {{type}} einzufügen",
|
||||||
|
"base64-paste-to-send": "Hier einfügen, um {{type}} zu versenden",
|
||||||
|
"base64-text": "Text",
|
||||||
|
"base64-files": "Dateien",
|
||||||
|
"base64-processing": "Bearbeitung läuft…",
|
||||||
|
"file-other-description-image": "und ein anderes Bild",
|
||||||
|
"file-other-description-file": "und eine andere Datei",
|
||||||
|
"receive-title": "{{descriptor}} erhalten",
|
||||||
|
"download-again": "Erneut herunterladen",
|
||||||
|
"system-language": "Systemsprache",
|
||||||
|
"language-selector-title": "Sprache auswählen",
|
||||||
|
"hr-or": "ODER",
|
||||||
|
"input-room-id-on-another-device": "Gebe diese Raum ID auf einem anderen Gerät ein",
|
||||||
|
"unpair": "Entkoppeln"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"tweet_title": "Über PairDrop twittern",
|
||||||
|
"faq_title": "Häufig gestellte Fragen",
|
||||||
|
"close-about_aria-label": "Schließe Über PairDrop",
|
||||||
|
"github_title": "PairDrop auf GitHub",
|
||||||
|
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
|
||||||
|
"claim": "Der einfachste Weg Dateien zwischen Geräten zu teilen"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"known-as": "Du wirst angezeigt als:",
|
||||||
|
"display-name_title": "Setze einen permanenten Gerätenamen",
|
||||||
|
"on-this-network": "in diesem Netzwerk",
|
||||||
|
"paired-devices": "für gekoppelte Geräte",
|
||||||
|
"traffic": "Datenverkehr wird",
|
||||||
|
"display-name_placeholder": "Lade…",
|
||||||
|
"routed": "durch den Server geleitet",
|
||||||
|
"webrtc": "wenn WebRTC nicht verfügbar ist.",
|
||||||
|
"display-name_data-placeholder": "Lade…",
|
||||||
|
"public-room-devices_title": "Du kannst von Geräten in diesem öffentlichen Raum unabhängig von deinem Netzwerk gefunden werden.",
|
||||||
|
"paired-devices_title": "Du kannst immer von gekoppelten Geräten gefunden werden, egal in welchem Netzwerk.",
|
||||||
|
"public-room-devices": "in Raum {{roomId}}",
|
||||||
|
"discovery": "Du bist sichtbar:",
|
||||||
|
"on-this-network_title": "Du kannst von jedem in diesem Netzwerk gefunden werden."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"link-received": "Link von {{name}} empfangen - Klicke um ihn zu öffnen",
|
||||||
|
"message-received": "Nachricht von {{name}} empfangen - Klicke um sie zu kopieren",
|
||||||
|
"click-to-download": "Klicken zum Download",
|
||||||
|
"copied-text": "Text in die Zwischenablage kopiert",
|
||||||
|
"connected": "Verbunden.",
|
||||||
|
"pairing-success": "Geräte gekoppelt.",
|
||||||
|
"display-name-random-again": "Anzeigename wird ab jetzt wieder zufällig generiert.",
|
||||||
|
"pairing-tabs-error": "Es können keine zwei Webbrowser Tabs gekoppelt werden.",
|
||||||
|
"pairing-not-persistent": "Gekoppelte Geräte sind nicht persistent.",
|
||||||
|
"pairing-key-invalid": "Ungültiger Schlüssel",
|
||||||
|
"pairing-key-invalidated": "Schlüssel {{key}} wurde ungültig gemacht.",
|
||||||
|
"copied-to-clipboard": "In die Zwischenablage kopiert",
|
||||||
|
"text-content-incorrect": "Textinhalt ist fehlerhaft.",
|
||||||
|
"clipboard-content-incorrect": "Inhalt der Zwischenablage ist fehlerhaft.",
|
||||||
|
"copied-text-error": "Konnte nicht in die Zwischenablage schreiben. Kopiere manuell!",
|
||||||
|
"file-content-incorrect": "Dateiinhalt ist fehlerhaft.",
|
||||||
|
"notifications-enabled": "Benachrichtigungen aktiviert.",
|
||||||
|
"offline": "Du bist offline",
|
||||||
|
"online": "Du bist wieder Online",
|
||||||
|
"unfinished-transfers-warning": "Es wurden noch nicht alle Übertragungen fertiggestellt. Möchtest du PairDrop wirklich schließen?",
|
||||||
|
"display-name-changed-permanently": "Anzeigename wurde dauerhaft geändert.",
|
||||||
|
"download-successful": "{{descriptor}} heruntergeladen",
|
||||||
|
"pairing-cleared": "Alle Geräte entkoppelt.",
|
||||||
|
"click-to-show": "Klicken zum Anzeigen",
|
||||||
|
"online-requirement": "Du musst online sein um Geräte zu koppeln.",
|
||||||
|
"display-name-changed-temporarily": "Anzeigename wurde nur für diese Sitzung geändert.",
|
||||||
|
"request-title": "{{name}} möchte {{count}}{{descriptor}} übertragen",
|
||||||
|
"connecting": "Verbindung wird aufgebaut…",
|
||||||
|
"files-incorrect": "Dateien sind fehlerhaft.",
|
||||||
|
"file-transfer-completed": "Dateiübertragung fertiggestellt.",
|
||||||
|
"message-transfer-completed": "Nachrichtenübertragung fertiggestellt.",
|
||||||
|
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
|
||||||
|
"selected-peer-left": "Ausgewählter Peer ist gegangen.",
|
||||||
|
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
|
||||||
|
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
|
||||||
|
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
|
||||||
|
"public-room-id-invalid": "Ungültige Raum ID",
|
||||||
|
"online-requirement-pairing": "Du musst online sein, um Geräte zu koppeln.",
|
||||||
|
"online-requirement-public-room": "Du musst online sein, um öffentliche Räume erstellen zu können."
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_desktop": "Klicke, um Dateien zu Senden oder klicke mit der rechten Maustaste, um Textnachrichten zu senden",
|
||||||
|
"no-peers-title": "Öffne PairDrop auf anderen Geräten, um Dateien zu senden",
|
||||||
|
"no-peers_data-drop-bg": "Hier ablegen, um Empfänger auszuwählen",
|
||||||
|
"no-peers-subtitle": "Kopple Geräte oder besuche einen öffentlichen Raum, damit du in anderen Netzwerken sichtbar bist",
|
||||||
|
"click-to-send": "Klicke zum Senden von",
|
||||||
|
"tap-to-send": "Tippe zum Senden von",
|
||||||
|
"x-instructions_data-drop-peer": "Hier ablegen, um an Peer zu senden",
|
||||||
|
"x-instructions_data-drop-bg": "Loslassen um Empfänger auszuwählen",
|
||||||
|
"x-instructions_mobile": "Tippe zum Senden von Dateien oder tippe lange zum Senden von Nachrichten",
|
||||||
|
"activate-paste-mode-base": "Öffne PairDrop auf anderen Geräten zum Senden von",
|
||||||
|
"activate-paste-mode-and-other-files": "und {{count}} anderen Dateien",
|
||||||
|
"activate-paste-mode-shared-text": "freigegebenem Text"
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-transfer-requested": "Datenübertagung angefordert",
|
||||||
|
"file-received": "Datei erhalten",
|
||||||
|
"file-received-plural": "{{count}} Dateien erhalten",
|
||||||
|
"message-received": "Nachricht erhalten",
|
||||||
|
"message-received-plural": "{{count}} Nachrichten erhalten"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send": "Klicke um Dateien zu senden oder nutze einen Rechtsklick um eine Textnachricht zu senden",
|
||||||
|
"connection-hash": "Um die Ende-zu-Ende Verschlüsselung zu verifizieren, vergleiche die Sicherheitsnummer auf beiden Geräten",
|
||||||
|
"waiting": "Warte…",
|
||||||
|
"click-to-send-paste-mode": "Klicken um {{descriptor}} zu senden",
|
||||||
|
"transferring": "Übertragung läuft…",
|
||||||
|
"processing": "Bearbeitung läuft…",
|
||||||
|
"preparing": "Vorbereitung läuft…"
|
||||||
|
}
|
||||||
|
}
|
154
public_included_ws_fallback/lang/en.json
Normal file
154
public_included_ws_fallback/lang/en.json
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "About PairDrop",
|
||||||
|
"language-selector_title": "Select Language",
|
||||||
|
"about_aria-label": "Open About PairDrop",
|
||||||
|
"theme-auto_title": "Adapt Theme to System",
|
||||||
|
"theme-light_title": "Always Use Light-Theme",
|
||||||
|
"theme-dark_title": "Always Use Dark-Theme",
|
||||||
|
"notification_title": "Enable Notifications",
|
||||||
|
"install_title": "Install PairDrop",
|
||||||
|
"pair-device_title": "Pair Your Devices Permanently",
|
||||||
|
"edit-paired-devices_title": "Edit Paired Devices",
|
||||||
|
"join-public-room_title": "Join Public Room Temporarily",
|
||||||
|
"cancel-paste-mode": "Done"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"no-peers_data-drop-bg": "Release to select recipient",
|
||||||
|
"no-peers-title": "Open PairDrop on other devices to send files",
|
||||||
|
"no-peers-subtitle": "Pair devices or enter a public room to be discoverable on other networks",
|
||||||
|
"x-instructions_desktop": "Click to send files or right click to send a message",
|
||||||
|
"x-instructions_mobile": "Tap to send files or long tap to send a message",
|
||||||
|
"x-instructions_data-drop-peer": "Release to send to peer",
|
||||||
|
"x-instructions_data-drop-bg": "Release to select recipient",
|
||||||
|
"click-to-send": "Click to send",
|
||||||
|
"tap-to-send": "Tap to send",
|
||||||
|
"activate-paste-mode-base": "Open PairDrop on other devices to send",
|
||||||
|
"activate-paste-mode-and-other-files": "and {{count}} other files",
|
||||||
|
"activate-paste-mode-shared-text": "shared text"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"known-as": "You are known as:",
|
||||||
|
"display-name_data-placeholder": "Loading…",
|
||||||
|
"display-name_title": "Edit your device name permanently",
|
||||||
|
"discovery": "You can be discovered:",
|
||||||
|
"on-this-network": "on this network",
|
||||||
|
"on-this-network_title": "You can be discovered by everyone on this network.",
|
||||||
|
"paired-devices": "by paired devices",
|
||||||
|
"paired-devices_title": "You can be discovered by paired devices at all times independent of the network.",
|
||||||
|
"public-room-devices": "in room {{roomId}}",
|
||||||
|
"public-room-devices_title": "You can be discovered by devices in this public room independent of the network.",
|
||||||
|
"traffic": "Traffic is",
|
||||||
|
"routed": "routed through the server",
|
||||||
|
"webrtc": "if WebRTC is not available."
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"pair-devices-title": "Pair Devices Permanently",
|
||||||
|
"input-key-on-this-device": "Input this key on another device",
|
||||||
|
"scan-qr-code": "or scan the QR-code.",
|
||||||
|
"enter-key-from-another-device": "Enter key from another device here.",
|
||||||
|
"input-room-id-on-another-device": "Input this room id on another device",
|
||||||
|
"hr-or": "OR",
|
||||||
|
"pair": "Pair",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"edit-paired-devices-title": "Edit Paired Devices",
|
||||||
|
"unpair": "Unpair",
|
||||||
|
"paired-devices-wrapper_data-empty": "No paired devices.",
|
||||||
|
"auto-accept-instructions-1": "Activate",
|
||||||
|
"auto-accept": "auto-accept",
|
||||||
|
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
|
||||||
|
"close": "Close",
|
||||||
|
"would-like-to-share": "would like to share",
|
||||||
|
"accept": "Accept",
|
||||||
|
"decline": "Decline",
|
||||||
|
"has-sent": "has sent:",
|
||||||
|
"share": "Share",
|
||||||
|
"download": "Download",
|
||||||
|
"send-message-title": "Send Message",
|
||||||
|
"send-message-to": "Send a Message to",
|
||||||
|
"send": "Send",
|
||||||
|
"receive-text-title": "Message Received",
|
||||||
|
"copy": "Copy",
|
||||||
|
"base64-processing": "Processing…",
|
||||||
|
"base64-tap-to-paste": "Tap here to paste {{type}}",
|
||||||
|
"base64-paste-to-send": "Paste here to send {{type}}",
|
||||||
|
"base64-text": "text",
|
||||||
|
"base64-files": "files",
|
||||||
|
"file-other-description-image": "and 1 other image",
|
||||||
|
"file-other-description-file": "and 1 other file",
|
||||||
|
"file-other-description-image-plural": "and {{count}} other images",
|
||||||
|
"file-other-description-file-plural": "and {{count}} other files",
|
||||||
|
"title-image": "Image",
|
||||||
|
"title-file": "File",
|
||||||
|
"title-image-plural": "Images",
|
||||||
|
"title-file-plural": "Files",
|
||||||
|
"receive-title": "{{descriptor}} Received",
|
||||||
|
"download-again": "Download again",
|
||||||
|
"language-selector-title": "Select Language",
|
||||||
|
"system-language": "System Language"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"close-about_aria-label": "Close About PairDrop",
|
||||||
|
"claim": "The easiest way to transfer files across devices",
|
||||||
|
"github_title": "PairDrop on GitHub",
|
||||||
|
"buy-me-a-coffee_title": "Buy me a coffee!",
|
||||||
|
"tweet_title": "Tweet about PairDrop",
|
||||||
|
"faq_title": "Frequently asked questions"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"display-name-changed-permanently": "Display name is changed permanently.",
|
||||||
|
"display-name-changed-temporarily": "Display name is changed only for this session.",
|
||||||
|
"display-name-random-again": "Display name is randomly generated again.",
|
||||||
|
"download-successful": "{{descriptor}} downloaded",
|
||||||
|
"pairing-tabs-error": "Pairing two web browser tabs is impossible.",
|
||||||
|
"pairing-success": "Devices paired.",
|
||||||
|
"pairing-not-persistent": "Paired devices are not persistent.",
|
||||||
|
"pairing-key-invalid": "Invalid key",
|
||||||
|
"pairing-key-invalidated": "Key {{key}} invalidated.",
|
||||||
|
"pairing-cleared": "All Devices unpaired.",
|
||||||
|
"public-room-id-invalid": "Invalid room id",
|
||||||
|
"public-room-left": "Left public room {{publicRoomId}}",
|
||||||
|
"copied-to-clipboard": "Copied to clipboard",
|
||||||
|
"copied-to-clipboard-error": "Copying not possible. Copy manually.",
|
||||||
|
"text-content-incorrect": "Text content is incorrect.",
|
||||||
|
"file-content-incorrect": "File content is incorrect.",
|
||||||
|
"clipboard-content-incorrect": "Clipboard content is incorrect.",
|
||||||
|
"notifications-enabled": "Notifications enabled.",
|
||||||
|
"link-received": "Link received by {{name}} - Click to open",
|
||||||
|
"message-received": "Message received by {{name}} - Click to copy",
|
||||||
|
"click-to-download": "Click to download",
|
||||||
|
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
|
||||||
|
"click-to-show": "Click to show",
|
||||||
|
"copied-text": "Copied text to clipboard",
|
||||||
|
"copied-text-error": "Writing to clipboard failed. Copy manually!",
|
||||||
|
"offline": "You are offline",
|
||||||
|
"online": "You are back online",
|
||||||
|
"connected": "Connected.",
|
||||||
|
"online-requirement-pairing": "You need to be online to pair devices.",
|
||||||
|
"online-requirement-public-room": "You need to be online to create a public room.",
|
||||||
|
"connecting": "Connecting…",
|
||||||
|
"files-incorrect": "Files are incorrect.",
|
||||||
|
"file-transfer-completed": "File transfer completed.",
|
||||||
|
"ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once",
|
||||||
|
"message-transfer-completed": "Message transfer completed.",
|
||||||
|
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
|
||||||
|
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
|
||||||
|
"selected-peer-left": "Selected peer left."
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-received": "File Received",
|
||||||
|
"file-received-plural": "{{count}} Files Received",
|
||||||
|
"file-transfer-requested": "File Transfer Requested",
|
||||||
|
"message-received": "Message Received",
|
||||||
|
"message-received-plural": "{{count}} Messages Received"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send-paste-mode": "Click to send {{descriptor}}",
|
||||||
|
"click-to-send": "Click to send files or right click to send a message",
|
||||||
|
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
|
||||||
|
"preparing": "Preparing…",
|
||||||
|
"waiting": "Waiting…",
|
||||||
|
"processing": "Processing…",
|
||||||
|
"transferring": "Transferring…"
|
||||||
|
}
|
||||||
|
}
|
138
public_included_ws_fallback/lang/nb.json
Normal file
138
public_included_ws_fallback/lang/nb.json
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"edit-paired-devices_title": "Rediger sammenkoblede enheter",
|
||||||
|
"about_title": "Om PairDrop",
|
||||||
|
"about_aria-label": "Åpne «Om PairDrop»",
|
||||||
|
"theme-auto_title": "Juster drakt til system",
|
||||||
|
"theme-light_title": "Alltid bruk lys drakt",
|
||||||
|
"theme-dark_title": "Alltid bruk mørk drakt",
|
||||||
|
"notification_title": "Skru på merknader",
|
||||||
|
"cancel-paste-mode": "Ferdig",
|
||||||
|
"install_title": "Installer PairDrop",
|
||||||
|
"pair-device_title": "Sammenkoble enhet"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
|
||||||
|
"display-name_data-placeholder": "Laster inn…",
|
||||||
|
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
|
||||||
|
"traffic": "Trafikken",
|
||||||
|
"on-this-network": "på dette nettverket",
|
||||||
|
"known-as": "Du er kjent som:",
|
||||||
|
"paired-devices": "sammenkoblede enheter",
|
||||||
|
"routed": "Sendes gjennom tjeneren"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||||
|
"x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding",
|
||||||
|
"x-instructions_data-drop-bg": "Slipp for å velge mottager",
|
||||||
|
"click-to-send": "Klikk for å sende",
|
||||||
|
"no-peers_data-drop-bg": "Slipp for å velge mottager",
|
||||||
|
"no-peers-title": "Åpne PairDrop på andre enheter for å sende filer",
|
||||||
|
"no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk",
|
||||||
|
"x-instructions_data-drop-peer": "Slipp for å sende til likemann",
|
||||||
|
"tap-to-send": "Trykk for å sende",
|
||||||
|
"activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende",
|
||||||
|
"activate-paste-mode-and-other-files": "og {{count}} andre filer",
|
||||||
|
"activate-paste-mode-shared-text": "delt tekst"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet",
|
||||||
|
"pair-devices-title": "Sammenkoble enheter",
|
||||||
|
"would-like-to-share": "ønsker å dele",
|
||||||
|
"auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.",
|
||||||
|
"paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter",
|
||||||
|
"enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.",
|
||||||
|
"edit-paired-devices-title": "Rediger sammenkoblede enheter",
|
||||||
|
"accept": "Godta",
|
||||||
|
"has-sent": "har sendt:",
|
||||||
|
"base64-paste-to-send": "Trykk her for å sende {{type}}",
|
||||||
|
"base64-text": "tekst",
|
||||||
|
"base64-files": "filer",
|
||||||
|
"file-other-description-image-plural": "og {{count}} andre bilder",
|
||||||
|
"receive-title": "{{descriptor}} mottatt",
|
||||||
|
"send-message-title": "Send melding",
|
||||||
|
"base64-processing": "Behandler…",
|
||||||
|
"close": "Lukk",
|
||||||
|
"decline": "Avslå",
|
||||||
|
"download": "Last ned",
|
||||||
|
"copy": "Kopier",
|
||||||
|
"pair": "Sammenkoble",
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"scan-qr-code": "eller skann QR-koden.",
|
||||||
|
"auto-accept-instructions-1": "Aktiver",
|
||||||
|
"receive-text-title": "Melding mottatt",
|
||||||
|
"auto-accept": "auto-godkjenn",
|
||||||
|
"share": "Del",
|
||||||
|
"send-message-to": "Send en melding til",
|
||||||
|
"send": "Send",
|
||||||
|
"base64-tap-to-paste": "Trykk her for å lime inn {{type}}",
|
||||||
|
"file-other-description-image": "og ett annet bilde",
|
||||||
|
"file-other-description-file-plural": "og {{count}} andre filer",
|
||||||
|
"title-file-plural": "Filer",
|
||||||
|
"download-again": "Last ned igjen",
|
||||||
|
"file-other-description-file": "og én annen fil",
|
||||||
|
"title-image": "Bilde",
|
||||||
|
"title-file": "Fil",
|
||||||
|
"title-image-plural": "Bilder"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"close-about_aria-label": "Lukk «Om PairDrop»",
|
||||||
|
"faq_title": "Ofte stilte spørsmål",
|
||||||
|
"claim": "Den enkleste måten å overføre filer mellom enheter",
|
||||||
|
"buy-me-a-coffee_title": "Spander drikke.",
|
||||||
|
"tweet_title": "Tvitre om PairDrop",
|
||||||
|
"github_title": "PairDrop på GitHub"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"copied-to-clipboard": "Kopiert til utklippstavlen",
|
||||||
|
"pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.",
|
||||||
|
"notifications-enabled": "Merknader påskrudd.",
|
||||||
|
"click-to-show": "Klikk for å vise",
|
||||||
|
"copied-text": "Tekst kopiert til utklippstavlen",
|
||||||
|
"connected": "Tilkoblet.",
|
||||||
|
"online": "Du er tilbake på nett",
|
||||||
|
"file-transfer-completed": "Filoverføring utført.",
|
||||||
|
"selected-peer-left": "Valgt likemann dro.",
|
||||||
|
"pairing-key-invalid": "Ugyldig nøkkel",
|
||||||
|
"connecting": "Kobler til …",
|
||||||
|
"pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.",
|
||||||
|
"offline": "Du er frakoblet",
|
||||||
|
"online-requirement": "Du må være på nett for å koble sammen enheter.",
|
||||||
|
"display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.",
|
||||||
|
"display-name-changed-permanently": "Visningsnavnet er endret for godt.",
|
||||||
|
"display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.",
|
||||||
|
"text-content-incorrect": "Tekstinnholdet er uriktig.",
|
||||||
|
"file-content-incorrect": "Filinnholdet er uriktig.",
|
||||||
|
"click-to-download": "Klikk for å laste ned",
|
||||||
|
"message-transfer-completed": "Meldingsoverføring utført.",
|
||||||
|
"download-successful": "{{descriptor}} nedlastet",
|
||||||
|
"pairing-success": "Enheter sammenkoblet.",
|
||||||
|
"pairing-cleared": "Sammenkobling av alle enheter opphevet.",
|
||||||
|
"pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.",
|
||||||
|
"copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt!",
|
||||||
|
"clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.",
|
||||||
|
"link-received": "Lenke mottatt av {{name}} - Klikk for å åpne.",
|
||||||
|
"request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}",
|
||||||
|
"message-received": "Melding mottatt av {{name}} - Klikk for å åpne.",
|
||||||
|
"files-incorrect": "Filene er uriktige.",
|
||||||
|
"ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.",
|
||||||
|
"unfinished-transfers-warning": "Lukk med ufullførte overføringer?",
|
||||||
|
"rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen."
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-received": "Fil mottatt",
|
||||||
|
"file-received-plural": "{{count}} filer mottatt",
|
||||||
|
"message-received": "Melding mottatt",
|
||||||
|
"file-transfer-requested": "Filoverføring forespurt",
|
||||||
|
"message-received-plural": "{{count}} meldinger mottatt"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"preparing": "Forbereder …",
|
||||||
|
"waiting": "Venter…",
|
||||||
|
"processing": "Behandler …",
|
||||||
|
"transferring": "Overfører …",
|
||||||
|
"click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding",
|
||||||
|
"click-to-send-paste-mode": "Klikk for å sende {{descriptor}}",
|
||||||
|
"connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen."
|
||||||
|
}
|
||||||
|
}
|
156
public_included_ws_fallback/lang/ru.json
Normal file
156
public_included_ws_fallback/lang/ru.json
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_aria-label": "Открыть страницу \"О сервисе\"",
|
||||||
|
"pair-device_title": "Связать ваши устройства навсегда",
|
||||||
|
"install_title": "Установить PairDrop",
|
||||||
|
"cancel-paste-mode": "Выполнено",
|
||||||
|
"edit-paired-devices_title": "Редактировать сопряженные устройства",
|
||||||
|
"notification_title": "Включить уведомления",
|
||||||
|
"about_title": "О сервисе",
|
||||||
|
"theme-auto_title": "Адаптировать тему к системной",
|
||||||
|
"theme-dark_title": "Всегда использовать темную тему",
|
||||||
|
"theme-light_title": "Всегда использовать светлую тему",
|
||||||
|
"join-public-room_title": "Войти на время в публичную комнату",
|
||||||
|
"language-selector_title": "Выбрать язык"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение",
|
||||||
|
"no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||||
|
"click-to-send": "Нажмите, чтобы отправить",
|
||||||
|
"x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя",
|
||||||
|
"tap-to-send": "Прикоснитесь, чтобы отправить",
|
||||||
|
"x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу",
|
||||||
|
"x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение",
|
||||||
|
"no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы",
|
||||||
|
"no-peers-subtitle": "Сопрягите устройства или войдите в публичную комнату, чтобы вас могли обнаружить из других сетей",
|
||||||
|
"activate-paste-mode-and-other-files": "и {{count}} других файлов",
|
||||||
|
"activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить",
|
||||||
|
"activate-paste-mode-shared-text": "общий текст"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"display-name_data-placeholder": "Загрузка…",
|
||||||
|
"routed": "направляется через сервер",
|
||||||
|
"webrtc": ", если WebRTC недоступен.",
|
||||||
|
"traffic": "Трафик",
|
||||||
|
"paired-devices": "сопряженными устройствами",
|
||||||
|
"known-as": "Вы известны под именем:",
|
||||||
|
"on-this-network": "в этой сети",
|
||||||
|
"display-name_title": "Изменить имя вашего устройства навсегда",
|
||||||
|
"public-room-devices_title": "Вы можете быть обнаружены устройствами в этой публичной комнате вне зависимости от сети.",
|
||||||
|
"paired-devices_title": "Вы можете быть обнаружены сопряженными устройствами в любое время вне зависимости от сети.",
|
||||||
|
"public-room-devices": "в комнате {{roomId}}",
|
||||||
|
"discovery": "Вы можете быть обнаружены:",
|
||||||
|
"on-this-network_title": "Вы можете быть обнаружены кем угодно в этой сети."
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"edit-paired-devices-title": "Редактировать сопряженные устройства",
|
||||||
|
"auto-accept": "автоприем",
|
||||||
|
"close": "Закрыть",
|
||||||
|
"decline": "Отклонить",
|
||||||
|
"share": "Поделиться",
|
||||||
|
"would-like-to-share": "хотел бы поделиться",
|
||||||
|
"has-sent": "отправил:",
|
||||||
|
"paired-devices-wrapper_data-empty": "Нет сопряженных устройств.",
|
||||||
|
"download": "Скачать",
|
||||||
|
"receive-text-title": "Сообщение получено",
|
||||||
|
"send": "Отправить",
|
||||||
|
"send-message-to": "Отправить сообщение",
|
||||||
|
"send-message-title": "Отправить сообщение",
|
||||||
|
"copy": "Копировать",
|
||||||
|
"base64-files": "файлы",
|
||||||
|
"base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}",
|
||||||
|
"base64-processing": "Обработка…",
|
||||||
|
"base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}",
|
||||||
|
"base64-text": "текст",
|
||||||
|
"title-file": "Файл",
|
||||||
|
"title-file-plural": "Файлы",
|
||||||
|
"title-image": "Изображение",
|
||||||
|
"title-image-plural": "Изображения",
|
||||||
|
"download-again": "Скачать еще раз",
|
||||||
|
"auto-accept-instructions-2": ", чтобы автоматически принимать все файлы, отправленные с того устройства.",
|
||||||
|
"enter-key-from-another-device": "Введите сюда ключ с другого устройства.",
|
||||||
|
"pair-devices-title": "Сопрягите устройства навсегда",
|
||||||
|
"input-key-on-this-device": "Введите этот ключ на другом устройстве",
|
||||||
|
"scan-qr-code": "или отсканируйте QR-код.",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"pair": "Подключить",
|
||||||
|
"accept": "Принять",
|
||||||
|
"auto-accept-instructions-1": "Активировать",
|
||||||
|
"file-other-description-file": "и 1 другой файл",
|
||||||
|
"file-other-description-image-plural": "и {{count}} других изображений",
|
||||||
|
"file-other-description-image": "и 1 другое изображение",
|
||||||
|
"file-other-description-file-plural": "и {{count}} других файлов",
|
||||||
|
"receive-title": "{{descriptor}} получен",
|
||||||
|
"system-language": "Язык системы",
|
||||||
|
"unpair": "Разорвать сопряжение",
|
||||||
|
"language-selector-title": "Выберите язык",
|
||||||
|
"hr-or": "ИЛИ",
|
||||||
|
"input-room-id-on-another-device": "Введите этот ID комнаты на другом устройстве"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"close-about-aria-label": "Закрыть страницу \"О сервисе\"",
|
||||||
|
"claim": "Самый простой способ передачи файлов между устройствами",
|
||||||
|
"close-about_aria-label": "Закрыть страницу \"О сервисе\"",
|
||||||
|
"buy-me-a-coffee_title": "Купить мне кофе!",
|
||||||
|
"github_title": "PairDrop на GitHub",
|
||||||
|
"tweet_title": "Твит о PairDrop",
|
||||||
|
"faq_title": "Часто задаваемые вопросы"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"display-name-changed-permanently": "Отображаемое имя было изменено навсегда.",
|
||||||
|
"display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.",
|
||||||
|
"pairing-success": "Устройства сопряжены.",
|
||||||
|
"pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.",
|
||||||
|
"copied-to-clipboard": "Скопировано в буфер обмена",
|
||||||
|
"pairing-not-persistent": "Сопряженные устройства непостоянны.",
|
||||||
|
"link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть",
|
||||||
|
"notifications-enabled": "Уведомления включены.",
|
||||||
|
"text-content-incorrect": "Содержание текста неверно.",
|
||||||
|
"message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать",
|
||||||
|
"connected": "Подключено.",
|
||||||
|
"copied-text": "Текст скопирован в буфер обмена",
|
||||||
|
"online": "Вы снова в сети",
|
||||||
|
"offline": "Вы находитесь вне сети",
|
||||||
|
"online-requirement": "Для сопряжения устройств вам нужно быть в сети.",
|
||||||
|
"files-incorrect": "Файлы неверны.",
|
||||||
|
"message-transfer-completed": "Передача сообщения завершена.",
|
||||||
|
"ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз",
|
||||||
|
"selected-peer-left": "Выбранный узел вышел.",
|
||||||
|
"request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}",
|
||||||
|
"rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.",
|
||||||
|
"unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть PairDrop?",
|
||||||
|
"copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!",
|
||||||
|
"pairing-cleared": "Все устройства не сопряжены.",
|
||||||
|
"pairing-key-invalid": "Неверный ключ",
|
||||||
|
"pairing-key-invalidated": "Ключ {{key}} признан недействительным.",
|
||||||
|
"click-to-download": "Нажмите, чтобы скачать",
|
||||||
|
"clipboard-content-incorrect": "Содержание буфера обмена неверно.",
|
||||||
|
"click-to-show": "Нажмите, чтобы показать",
|
||||||
|
"connecting": "Подключение…",
|
||||||
|
"download-successful": "{{descriptor}} загружен",
|
||||||
|
"display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.",
|
||||||
|
"file-content-incorrect": "Содержимое файла неверно.",
|
||||||
|
"file-transfer-completed": "Передача файла завершена.",
|
||||||
|
"public-room-left": "Покинуть публичную комнату {{publicRoomId}}",
|
||||||
|
"copied-to-clipboard-error": "Копирование невозможно. Скопируйте вручную.",
|
||||||
|
"public-room-id-invalid": "Неверный ID комнаты",
|
||||||
|
"online-requirement-pairing": "Для сопряжения устройств необходимо находиться быть онлайн.",
|
||||||
|
"online-requirement-public-room": "Для создания публичной комнаты необходимо быть онлайн."
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}",
|
||||||
|
"preparing": "Подготовка…",
|
||||||
|
"transferring": "Передача…",
|
||||||
|
"processing": "Обработка…",
|
||||||
|
"waiting": "Ожидание…",
|
||||||
|
"connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах",
|
||||||
|
"click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение"
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"file-received-plural": "{{count}} файлов получено",
|
||||||
|
"message-received-plural": "{{count}} сообщений получено",
|
||||||
|
"file-received": "Файл получен",
|
||||||
|
"file-transfer-requested": "Запрошена передача файлов",
|
||||||
|
"message-received": "Сообщение получено"
|
||||||
|
}
|
||||||
|
}
|
25
public_included_ws_fallback/lang/tr.json
Normal file
25
public_included_ws_fallback/lang/tr.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "PairDrop Hakkında",
|
||||||
|
"about_aria-label": "PairDrop Hakkında Aç",
|
||||||
|
"theme-auto_title": "Temayı Sisteme Uyarla",
|
||||||
|
"theme-light_title": "Daima Açık Tema Kullan",
|
||||||
|
"theme-dark_title": "Daima Koyu Tema Kullan",
|
||||||
|
"notification_title": "Bildirimleri Etkinleştir",
|
||||||
|
"install_title": "PairDrop'u Yükle",
|
||||||
|
"pair-device_title": "Cihaz Eşleştir",
|
||||||
|
"edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle",
|
||||||
|
"cancel-paste-mode": "Bitti"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"display-name_data-placeholder": "Yükleniyor…",
|
||||||
|
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"cancel": "İptal",
|
||||||
|
"edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle"
|
||||||
|
}
|
||||||
|
}
|
155
public_included_ws_fallback/lang/zh-CN.json
Normal file
155
public_included_ws_fallback/lang/zh-CN.json
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"about_title": "关于 PairDrop",
|
||||||
|
"about_aria-label": "打开 关于 PairDrop",
|
||||||
|
"theme-light_title": "总是使用明亮主题",
|
||||||
|
"install_title": "安装 PairDrop",
|
||||||
|
"pair-device_title": "长久配对您的设备",
|
||||||
|
"theme-auto_title": "主题适应系统",
|
||||||
|
"theme-dark_title": "总是使用暗黑主题",
|
||||||
|
"notification_title": "开启通知",
|
||||||
|
"edit-paired-devices_title": "管理已配对设备",
|
||||||
|
"cancel-paste-mode": "完成",
|
||||||
|
"join-public-room_title": "暂时加入公共房间",
|
||||||
|
"language-selector_title": "选择语言"
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"x-instructions_data-drop-peer": "释放以发送到此设备",
|
||||||
|
"no-peers_data-drop-bg": "释放来选择接收者",
|
||||||
|
"no-peers-subtitle": "配对新设备 或 加入一个公共房间 使在其他网络上可见",
|
||||||
|
"no-peers-title": "在其他设备上打开 PairDrop 来发送文件",
|
||||||
|
"x-instructions_desktop": "点击以发送文件 或 右键来发送信息",
|
||||||
|
"x-instructions_mobile": "轻触以发送文件 或 长按来发送信息",
|
||||||
|
"x-instructions_data-drop-bg": "释放来选择接收者",
|
||||||
|
"click-to-send": "点击发送",
|
||||||
|
"tap-to-send": "轻触发送",
|
||||||
|
"activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送",
|
||||||
|
"activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件",
|
||||||
|
"activate-paste-mode-shared-text": "分享文本"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"routed": "途径服务器",
|
||||||
|
"webrtc": "如果 WebRTC 不可用。",
|
||||||
|
"known-as": "你的名字是:",
|
||||||
|
"display-name_data-placeholder": "加载中…",
|
||||||
|
"display-name_title": "修改你的默认设备名",
|
||||||
|
"on-this-network": "在此网络上",
|
||||||
|
"paired-devices": "已配对的设备",
|
||||||
|
"traffic": "流量将",
|
||||||
|
"public-room-devices_title": "您可以被这个独立于网络的公共房间中的设备发现。",
|
||||||
|
"paired-devices_title": "您可以在任何时候被已配对的设备发现,而不依赖于网络。",
|
||||||
|
"public-room-devices": "在房间 {{roomId}} 中",
|
||||||
|
"discovery": "您可以被发现:",
|
||||||
|
"on-this-network_title": "您可以被这个网络上的每个人发现。"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"pair-devices-title": "配对新设备(常驻)",
|
||||||
|
"input-key-on-this-device": "在另一个设备上输入这串数字",
|
||||||
|
"base64-text": "信息",
|
||||||
|
"enter-key-from-another-device": "在此处输入从另一个设备上获得的数字。",
|
||||||
|
"edit-paired-devices-title": "管理已配对的设备",
|
||||||
|
"pair": "配对",
|
||||||
|
"cancel": "取消",
|
||||||
|
"scan-qr-code": "或者 扫描二维码。",
|
||||||
|
"paired-devices-wrapper_data-empty": "无已配对设备。",
|
||||||
|
"auto-accept-instructions-1": "启用",
|
||||||
|
"auto-accept": "自动接收",
|
||||||
|
"decline": "拒绝",
|
||||||
|
"base64-processing": "处理中…",
|
||||||
|
"base64-tap-to-paste": "轻触此处粘贴{{type}}",
|
||||||
|
"base64-paste-to-send": "粘贴到此处以发送 {{type}}",
|
||||||
|
"auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。",
|
||||||
|
"would-like-to-share": "想要分享",
|
||||||
|
"accept": "接收",
|
||||||
|
"close": "关闭",
|
||||||
|
"share": "分享",
|
||||||
|
"download": "保存",
|
||||||
|
"send": "发送",
|
||||||
|
"receive-text-title": "收到信息",
|
||||||
|
"copy": "复制",
|
||||||
|
"send-message-title": "发送信息",
|
||||||
|
"send-message-to": "发了一条信息给",
|
||||||
|
"has-sent": "发送了:",
|
||||||
|
"base64-files": "文件",
|
||||||
|
"file-other-description-file": "和 1 个其他的文件",
|
||||||
|
"file-other-description-image": "和 1 个其他的图片",
|
||||||
|
"file-other-description-image-plural": "和 {{count}} 个其他的图片",
|
||||||
|
"file-other-description-file-plural": "和 {{count}} 个其他的文件",
|
||||||
|
"title-image-plural": "图片",
|
||||||
|
"receive-title": "收到 {{descriptor}}",
|
||||||
|
"title-image": "图片",
|
||||||
|
"title-file": "文件",
|
||||||
|
"title-file-plural": "文件",
|
||||||
|
"download-again": "再次保存",
|
||||||
|
"system-language": "跟随系统语言",
|
||||||
|
"unpair": "取消配对",
|
||||||
|
"language-selector-title": "选择语言",
|
||||||
|
"hr-or": "或者",
|
||||||
|
"input-room-id-on-another-device": "在另一个设备上输入这串房间号"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"faq_title": "常见问题",
|
||||||
|
"close-about_aria-label": "关闭 关于 PairDrop",
|
||||||
|
"github_title": "PairDrop 在 GitHub 上开源",
|
||||||
|
"claim": "最简单的跨设备传输方案",
|
||||||
|
"buy-me-a-coffee_title": "帮我买杯咖啡!",
|
||||||
|
"tweet_title": "关于 PairDrop 的推特"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"display-name-changed-permanently": "展示的名字已经长久变更。",
|
||||||
|
"display-name-changed-temporarily": "展示的名字已经变更 仅在此会话中。",
|
||||||
|
"display-name-random-again": "展示的名字再次随机生成。",
|
||||||
|
"download-successful": "{{descriptor}} 已下载",
|
||||||
|
"pairing-tabs-error": "无法配对两个浏览器标签页。",
|
||||||
|
"pairing-success": "新设备已配对。",
|
||||||
|
"pairing-not-persistent": "配对的设备不是持久的。",
|
||||||
|
"pairing-key-invalid": "无效配对码",
|
||||||
|
"pairing-key-invalidated": "配对码 {{key}} 已失效。",
|
||||||
|
"text-content-incorrect": "文本内容不合法。",
|
||||||
|
"file-content-incorrect": "文件内容不合法。",
|
||||||
|
"clipboard-content-incorrect": "剪贴板内容不合法。",
|
||||||
|
"link-received": "收到来自 {{name}} 的链接 - 点击打开",
|
||||||
|
"message-received": "收到来自 {{name}} 的信息 - 点击复制",
|
||||||
|
"request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}",
|
||||||
|
"click-to-show": "点击展示",
|
||||||
|
"copied-text": "复制到剪贴板",
|
||||||
|
"selected-peer-left": "选择的设备已离开。",
|
||||||
|
"pairing-cleared": "所有设备已解除配对。",
|
||||||
|
"copied-to-clipboard": "已复制到剪贴板",
|
||||||
|
"notifications-enabled": "通知已启用。",
|
||||||
|
"copied-text-error": "写入剪贴板失败。请手动复制!",
|
||||||
|
"click-to-download": "点击以保存",
|
||||||
|
"unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭 PairDrop 吗?",
|
||||||
|
"message-transfer-completed": "信息传输已完成。",
|
||||||
|
"offline": "你未连接到网络",
|
||||||
|
"online": "你已重新连接到网络",
|
||||||
|
"connected": "已连接。",
|
||||||
|
"online-requirement": "你需要连接网络来配对新设备。",
|
||||||
|
"files-incorrect": "文件不合法。",
|
||||||
|
"file-transfer-completed": "文件传输已完成。",
|
||||||
|
"connecting": "连接中…",
|
||||||
|
"ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB",
|
||||||
|
"rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。",
|
||||||
|
"public-room-left": "已退出公共房间 {{publicRoomId}}",
|
||||||
|
"copied-to-clipboard-error": "无法复制。请手动复制。",
|
||||||
|
"public-room-id-invalid": "无效的房间号",
|
||||||
|
"online-requirement-pairing": "您需要连接到互联网来配对新设备。",
|
||||||
|
"online-requirement-public-room": "您需要连接到互联网来创建一个公共房间。"
|
||||||
|
},
|
||||||
|
"document-titles": {
|
||||||
|
"message-received": "收到信息",
|
||||||
|
"message-received-plural": "收到 {{count}} 条信息",
|
||||||
|
"file-transfer-requested": "文件传输请求",
|
||||||
|
"file-received-plural": "收到 {{count}} 个文件",
|
||||||
|
"file-received": "收到文件"
|
||||||
|
},
|
||||||
|
"peer-ui": {
|
||||||
|
"click-to-send-paste-mode": "点击发送 {{descriptor}}",
|
||||||
|
"click-to-send": "点击以发送文件 或 右键来发送信息",
|
||||||
|
"connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号",
|
||||||
|
"preparing": "准备中…",
|
||||||
|
"waiting": "请等待…",
|
||||||
|
"transferring": "传输中…",
|
||||||
|
"processing": "处理中…"
|
||||||
|
}
|
||||||
|
}
|
144
public_included_ws_fallback/scripts/localization.js
Normal file
144
public_included_ws_fallback/scripts/localization.js
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
class Localization {
|
||||||
|
constructor() {
|
||||||
|
Localization.defaultLocale = "en";
|
||||||
|
Localization.supportedLocales = ["en", "nb", "ru", "zh-CN", "de"];
|
||||||
|
Localization.translations = {};
|
||||||
|
Localization.defaultTranslations = {};
|
||||||
|
|
||||||
|
Localization.systemLocale = Localization.supportedOrDefault(navigator.languages);
|
||||||
|
|
||||||
|
let storedLanguageCode = localStorage.getItem("language-code");
|
||||||
|
|
||||||
|
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
|
||||||
|
? storedLanguageCode
|
||||||
|
: Localization.systemLocale;
|
||||||
|
|
||||||
|
Localization.setTranslation(Localization.initialLocale)
|
||||||
|
.then(_ => {
|
||||||
|
console.log("Initial translation successful.");
|
||||||
|
Events.fire("initial-translation-loaded");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static isSupported(locale) {
|
||||||
|
return Localization.supportedLocales.indexOf(locale) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static supportedOrDefault(locales) {
|
||||||
|
return locales.find(Localization.isSupported) || Localization.defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setTranslation(locale) {
|
||||||
|
if (!locale) locale = Localization.systemLocale;
|
||||||
|
|
||||||
|
await Localization.setLocale(locale)
|
||||||
|
await Localization.translatePage();
|
||||||
|
|
||||||
|
console.log("Page successfully translated",
|
||||||
|
`System language: ${Localization.systemLocale}`,
|
||||||
|
`Selected language: ${locale}`
|
||||||
|
);
|
||||||
|
|
||||||
|
Events.fire("translation-loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setLocale(newLocale) {
|
||||||
|
if (newLocale === Localization.locale) return false;
|
||||||
|
|
||||||
|
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
|
||||||
|
|
||||||
|
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
|
||||||
|
|
||||||
|
if(!newTranslations) return false;
|
||||||
|
|
||||||
|
Localization.locale = newLocale;
|
||||||
|
Localization.translations = newTranslations;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getLocale() {
|
||||||
|
return Localization.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isSystemLocale() {
|
||||||
|
return !localStorage.getItem('language-code');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fetchTranslationsFor(newLocale) {
|
||||||
|
const response = await fetch(`lang/${newLocale}.json`)
|
||||||
|
|
||||||
|
if (response.redirected === true || response.status !== 200) return false;
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async translatePage() {
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-i18n-key]")
|
||||||
|
.forEach(element => Localization.translateElement(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async translateElement(element) {
|
||||||
|
const key = element.getAttribute("data-i18n-key");
|
||||||
|
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
|
||||||
|
|
||||||
|
for (let i in attrs) {
|
||||||
|
let attr = attrs[i];
|
||||||
|
if (attr === "text") {
|
||||||
|
element.innerText = Localization.getTranslation(key);
|
||||||
|
} else {
|
||||||
|
if (attr.startsWith("data-")) {
|
||||||
|
let dataAttr = attr.substring(5);
|
||||||
|
element.dataset.dataAttr = Localization.getTranslation(key, attr);
|
||||||
|
} {
|
||||||
|
element.setAttribute(attr, Localization.getTranslation(key, attr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTranslation(key, attr=null, data={}, useDefault=false) {
|
||||||
|
const keys = key.split(".");
|
||||||
|
|
||||||
|
let translationCandidates = useDefault
|
||||||
|
? Localization.defaultTranslations
|
||||||
|
: Localization.translations;
|
||||||
|
|
||||||
|
let translation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
translationCandidates = translationCandidates[keys[i]]
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastKey = keys[keys.length - 1];
|
||||||
|
|
||||||
|
if (attr) lastKey += "_" + attr;
|
||||||
|
|
||||||
|
translation = translationCandidates[lastKey];
|
||||||
|
|
||||||
|
for (let j in data) {
|
||||||
|
translation = translation.replace(`{{${j}}}`, data[j]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
translation = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!translation) {
|
||||||
|
if (!useDefault) {
|
||||||
|
translation = this.getTranslation(key, attr, data, true);
|
||||||
|
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr);
|
||||||
|
console.warn("Help translating PairDrop: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/");
|
||||||
|
} else {
|
||||||
|
console.warn("Missing translation in default language:", key, attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Localization.escapeHTML(translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
static escapeHTML(unsafeText) {
|
||||||
|
let div = document.createElement('div');
|
||||||
|
div.innerText = unsafeText;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
window.URL = window.URL || window.webkitURL;
|
window.URL = window.URL || window.webkitURL;
|
||||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
window.isRtcSupported = false; //!!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||||
|
|
||||||
window.hiddenProperty = 'hidden' in document ? 'hidden' :
|
window.hiddenProperty = 'hidden' in document ? 'hidden' :
|
||||||
'webkitHidden' in document ? 'webkitHidden' :
|
'webkitHidden' in document ? 'webkitHidden' :
|
||||||
|
@ -21,10 +21,14 @@ class ServerConnection {
|
||||||
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
|
||||||
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
|
||||||
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
|
||||||
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
|
||||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||||
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
||||||
|
|
||||||
|
Events.on('create-public-room', _ => this._onCreatePublicRoom());
|
||||||
|
Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid));
|
||||||
|
Events.on('leave-public-room', _ => this._onLeavePublicRoom());
|
||||||
|
|
||||||
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
||||||
Events.on('online', _ => this._connect());
|
Events.on('online', _ => this._connect());
|
||||||
}
|
}
|
||||||
|
@ -44,23 +48,47 @@ class ServerConnection {
|
||||||
_onOpen() {
|
_onOpen() {
|
||||||
console.log('WS: server connected');
|
console.log('WS: server connected');
|
||||||
Events.fire('ws-connected');
|
Events.fire('ws-connected');
|
||||||
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
|
if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPairDeviceInitiate() {
|
_onPairDeviceInitiate() {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-pairing"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-initiate' })
|
this.send({ type: 'pair-device-initiate' });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPairDeviceJoin(roomKey) {
|
_onPairDeviceJoin(pairKey) {
|
||||||
if (!this._isConnected()) {
|
if (!this._isConnected()) {
|
||||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
this.send({ type: 'pair-device-join', pairKey: pairKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCreatePublicRoom() {
|
||||||
|
if (!this._isConnected()) {
|
||||||
|
Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-public-room"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send({ type: 'create-public-room' });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onJoinPublicRoom(roomId, createIfInvalid) {
|
||||||
|
if (!this._isConnected()) {
|
||||||
|
setTimeout(_ => this._onJoinPublicRoom(roomId), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLeavePublicRoom() {
|
||||||
|
if (!this._isConnected()) {
|
||||||
|
setTimeout(_ => this._onLeavePublicRoom(), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send({ type: 'leave-public-room' });
|
||||||
}
|
}
|
||||||
|
|
||||||
_setRtcConfig(config) {
|
_setRtcConfig(config) {
|
||||||
|
@ -102,10 +130,10 @@ class ServerConnection {
|
||||||
Events.fire('pair-device-join-key-invalid');
|
Events.fire('pair-device-join-key-invalid');
|
||||||
break;
|
break;
|
||||||
case 'pair-device-canceled':
|
case 'pair-device-canceled':
|
||||||
Events.fire('pair-device-canceled', msg.roomKey);
|
Events.fire('pair-device-canceled', msg.pairKey);
|
||||||
break;
|
break;
|
||||||
case 'pair-device-join-key-rate-limit':
|
case 'join-key-rate-limit':
|
||||||
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
|
||||||
break;
|
break;
|
||||||
case 'secret-room-deleted':
|
case 'secret-room-deleted':
|
||||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||||
|
@ -113,6 +141,15 @@ class ServerConnection {
|
||||||
case 'room-secret-regenerated':
|
case 'room-secret-regenerated':
|
||||||
Events.fire('room-secret-regenerated', msg);
|
Events.fire('room-secret-regenerated', msg);
|
||||||
break;
|
break;
|
||||||
|
case 'public-room-id-invalid':
|
||||||
|
Events.fire('public-room-id-invalid', msg.publicRoomId);
|
||||||
|
break;
|
||||||
|
case 'public-room-created':
|
||||||
|
Events.fire('public-room-created', msg.roomId);
|
||||||
|
break;
|
||||||
|
case 'public-room-left':
|
||||||
|
Events.fire('public-room-left');
|
||||||
|
break;
|
||||||
case 'request':
|
case 'request':
|
||||||
case 'header':
|
case 'header':
|
||||||
case 'partition':
|
case 'partition':
|
||||||
|
@ -139,18 +176,12 @@ class ServerConnection {
|
||||||
|
|
||||||
_onPeers(msg) {
|
_onPeers(msg) {
|
||||||
Events.fire('peers', msg);
|
Events.fire('peers', msg);
|
||||||
if (msg.roomType === "ip" && msg.peers.length === 0) {
|
|
||||||
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => {
|
|
||||||
if (!peerId) return;
|
|
||||||
console.log("successfully removed other peerIds from localStorage");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDisplayName(msg) {
|
_onDisplayName(msg) {
|
||||||
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
|
||||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
sessionStorage.setItem('peer_id', msg.message.peerId);
|
||||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash);
|
||||||
|
|
||||||
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
|
||||||
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
|
||||||
|
@ -172,8 +203,8 @@ class ServerConnection {
|
||||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||||
const peerId = sessionStorage.getItem("peerId");
|
const peerId = sessionStorage.getItem('peer_id');
|
||||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
const peerIdHash = sessionStorage.getItem('peer_id_hash');
|
||||||
if (peerId && peerIdHash) {
|
if (peerId && peerIdHash) {
|
||||||
ws_url.searchParams.append('peer_id', peerId);
|
ws_url.searchParams.append('peer_id', peerId);
|
||||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||||
|
@ -184,7 +215,7 @@ class ServerConnection {
|
||||||
_disconnect() {
|
_disconnect() {
|
||||||
this.send({ type: 'disconnect' });
|
this.send({ type: 'disconnect' });
|
||||||
|
|
||||||
const peerId = sessionStorage.getItem("peerId");
|
const peerId = sessionStorage.getItem('peer_id');
|
||||||
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
|
||||||
console.log("successfully removed peerId from localStorage");
|
console.log("successfully removed peerId from localStorage");
|
||||||
});
|
});
|
||||||
|
@ -200,7 +231,7 @@ class ServerConnection {
|
||||||
|
|
||||||
_onDisconnect() {
|
_onDisconnect() {
|
||||||
console.log('WS: server disconnected');
|
console.log('WS: server disconnected');
|
||||||
Events.fire('notify-user', 'Connecting..');
|
Events.fire('notify-user', Localization.getTranslation("notifications.connecting"));
|
||||||
clearTimeout(this._reconnectTimer);
|
clearTimeout(this._reconnectTimer);
|
||||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||||
Events.fire('ws-disconnected');
|
Events.fire('ws-disconnected');
|
||||||
|
@ -232,12 +263,13 @@ class ServerConnection {
|
||||||
|
|
||||||
class Peer {
|
class Peer {
|
||||||
|
|
||||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||||
this._server = serverConnection;
|
this._server = serverConnection;
|
||||||
this._isCaller = isCaller;
|
this._isCaller = isCaller;
|
||||||
this._peerId = peerId;
|
this._peerId = peerId;
|
||||||
this._roomType = roomType;
|
|
||||||
this._updateRoomSecret(roomSecret);
|
this._roomIds = {};
|
||||||
|
this._updateRoomIds(roomType, roomId);
|
||||||
|
|
||||||
this._filesQueue = [];
|
this._filesQueue = [];
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
|
@ -258,34 +290,58 @@ class Peer {
|
||||||
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateRoomSecret(roomSecret) {
|
_isPaired() {
|
||||||
|
return !!this._roomIds['secret'];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPairSecret() {
|
||||||
|
return this._roomIds['secret'];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getRoomTypes() {
|
||||||
|
return Object.keys(this._roomIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateRoomIds(roomType, roomId) {
|
||||||
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
|
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
|
||||||
// -> do not delete duplicates and do not regenerate room secrets
|
// -> do not delete duplicates and do not regenerate room secrets
|
||||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
|
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
|
||||||
// remove old roomSecrets to prevent multiple pairings with same peer
|
// multiple roomSecrets with same peer -> delete old roomSecret
|
||||||
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
|
PersistentStorage.deleteRoomSecret(this._getPairSecret())
|
||||||
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
.then(deletedRoomSecret => {
|
||||||
})
|
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._roomSecret = roomSecret;
|
this._roomIds[roomType] = roomId;
|
||||||
|
|
||||||
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
|
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
|
||||||
// increase security by increasing roomSecret length
|
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
|
||||||
console.log('RoomSecret is regenerated to increase security')
|
console.log('RoomSecret is regenerated to increase security')
|
||||||
Events.fire('regenerate-room-secret', this._roomSecret);
|
Events.fire('regenerate-room-secret', this._getPairSecret());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_removeRoomType(roomType) {
|
||||||
|
delete this._roomIds[roomType];
|
||||||
|
|
||||||
|
Events.fire('room-type-removed', {
|
||||||
|
peerId: this._peerId,
|
||||||
|
roomType: roomType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_evaluateAutoAccept() {
|
_evaluateAutoAccept() {
|
||||||
if (!this._roomSecret) {
|
if (!this._isPaired()) {
|
||||||
this._setAutoAccept(false);
|
this._setAutoAccept(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistentStorage.getRoomSecretEntry(this._roomSecret)
|
PersistentStorage.getRoomSecretEntry(this._getPairSecret())
|
||||||
.then(roomSecretEntry => {
|
.then(roomSecretEntry => {
|
||||||
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
|
const autoAccept = roomSecretEntry
|
||||||
|
? roomSecretEntry.entry.auto_accept
|
||||||
|
: false;
|
||||||
this._setAutoAccept(autoAccept);
|
this._setAutoAccept(autoAccept);
|
||||||
})
|
})
|
||||||
.catch(_ => {
|
.catch(_ => {
|
||||||
|
@ -294,7 +350,9 @@ class Peer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_setAutoAccept(autoAccept) {
|
_setAutoAccept(autoAccept) {
|
||||||
this._autoAccept = autoAccept;
|
this._autoAccept = !this._isSameBrowser()
|
||||||
|
? autoAccept
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||||
|
@ -505,7 +563,7 @@ class Peer {
|
||||||
|
|
||||||
_abortTransfer() {
|
_abortTransfer() {
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||||
Events.fire('notify-user', 'Files are incorrect.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
|
||||||
this._filesReceived = [];
|
this._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
this._digester = null;
|
this._digester = null;
|
||||||
|
@ -546,14 +604,14 @@ class Peer {
|
||||||
this._abortTransfer();
|
this._abortTransfer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// include for compatibility with Snapdrop for Android app
|
// include for compatibility with 'Snapdrop & PairDrop for Android' app
|
||||||
Events.fire('file-received', fileBlob);
|
Events.fire('file-received', fileBlob);
|
||||||
|
|
||||||
this._filesReceived.push(fileBlob);
|
this._filesReceived.push(fileBlob);
|
||||||
if (!this._requestAccepted.header.length) {
|
if (!this._requestAccepted.header.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
|
||||||
this._filesReceived = [];
|
this._filesReceived = [];
|
||||||
this._requestAccepted = null;
|
this._requestAccepted = null;
|
||||||
}
|
}
|
||||||
|
@ -563,7 +621,8 @@ class Peer {
|
||||||
this._chunker = null;
|
this._chunker = null;
|
||||||
if (!this._filesQueue.length) {
|
if (!this._filesQueue.length) {
|
||||||
this._busy = false;
|
this._busy = false;
|
||||||
Events.fire('notify-user', 'File transfer completed.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
|
||||||
|
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
|
||||||
} else {
|
} else {
|
||||||
this._dequeueFile();
|
this._dequeueFile();
|
||||||
}
|
}
|
||||||
|
@ -574,7 +633,7 @@ class Peer {
|
||||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||||
this._filesRequested = null;
|
this._filesRequested = null;
|
||||||
if (message.reason === 'ios-memory-limit') {
|
if (message.reason === 'ios-memory-limit') {
|
||||||
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once");
|
Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -584,7 +643,7 @@ class Peer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMessageTransferCompleted() {
|
_onMessageTransferCompleted() {
|
||||||
Events.fire('notify-user', 'Message transfer completed.');
|
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
sendText(text) {
|
sendText(text) {
|
||||||
|
@ -615,8 +674,8 @@ class Peer {
|
||||||
|
|
||||||
class RTCPeer extends Peer {
|
class RTCPeer extends Peer {
|
||||||
|
|
||||||
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
|
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
|
||||||
super(serverConnection, isCaller, peerId, roomType, roomSecret);
|
super(serverConnection, isCaller, peerId, roomType, roomId);
|
||||||
this.rtcSupported = true;
|
this.rtcSupported = true;
|
||||||
if (!this._isCaller) return; // we will listen for a caller
|
if (!this._isCaller) return; // we will listen for a caller
|
||||||
this._connect();
|
this._connect();
|
||||||
|
@ -642,13 +701,17 @@ class RTCPeer extends Peer {
|
||||||
|
|
||||||
_openChannel() {
|
_openChannel() {
|
||||||
if (!this._conn) return;
|
if (!this._conn) return;
|
||||||
|
|
||||||
const channel = this._conn.createDataChannel('data-channel', {
|
const channel = this._conn.createDataChannel('data-channel', {
|
||||||
ordered: true,
|
ordered: true,
|
||||||
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
|
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
|
||||||
});
|
});
|
||||||
channel.onopen = e => this._onChannelOpened(e);
|
channel.onopen = e => this._onChannelOpened(e);
|
||||||
channel.onerror = e => this._onError(e);
|
channel.onerror = e => this._onError(e);
|
||||||
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
|
|
||||||
|
this._conn.createOffer()
|
||||||
|
.then(d => this._onDescription(d))
|
||||||
|
.catch(e => this._onError(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDescription(description) {
|
_onDescription(description) {
|
||||||
|
@ -729,7 +792,7 @@ class RTCPeer extends Peer {
|
||||||
_onBeforeUnload(e) {
|
_onBeforeUnload(e) {
|
||||||
if (this._busy) {
|
if (this._busy) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return "There are unfinished transfers. Are you sure you want to close?";
|
return Localization.getTranslation("notifications.unfinished-transfers-warning");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -788,8 +851,8 @@ class RTCPeer extends Peer {
|
||||||
_sendSignal(signal) {
|
_sendSignal(signal) {
|
||||||
signal.type = 'signal';
|
signal.type = 'signal';
|
||||||
signal.to = this._peerId;
|
signal.to = this._peerId;
|
||||||
signal.roomType = this._roomType;
|
signal.roomType = this._getRoomTypes()[0];
|
||||||
signal.roomSecret = this._roomSecret;
|
signal.roomId = this._roomIds[this._getRoomTypes()[0]];
|
||||||
this._server.send(signal);
|
this._server.send(signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -835,8 +898,8 @@ class WSPeer extends Peer {
|
||||||
|
|
||||||
sendJSON(message) {
|
sendJSON(message) {
|
||||||
message.to = this._peerId;
|
message.to = this._peerId;
|
||||||
message.roomType = this._roomType;
|
message.roomType = this._getRoomTypes()[0];
|
||||||
message.roomSecret = this._roomSecret;
|
message.roomId = this._roomIds[this._getRoomTypes()[0]];
|
||||||
this._server.send(message);
|
this._server.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -871,7 +934,14 @@ class PeersManager {
|
||||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
|
||||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||||
|
|
||||||
|
// this device closes connection
|
||||||
|
Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));
|
||||||
|
Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));
|
||||||
|
|
||||||
|
// peer closes connection
|
||||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||||
|
|
||||||
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
|
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
|
||||||
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||||
|
@ -886,51 +956,45 @@ class PeersManager {
|
||||||
this.peers[peerId].onServerMessage(message);
|
this.peers[peerId].onServerMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshPeer(peer, roomType, roomSecret) {
|
_refreshPeer(peer, roomType, roomId) {
|
||||||
if (!peer) return false;
|
if (!peer) return false;
|
||||||
|
|
||||||
const roomTypeIsSecret = roomType === "secret";
|
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
|
||||||
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
|
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
|
||||||
|
|
||||||
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
|
// if roomType or roomId for roomType differs peer is already connected
|
||||||
if (roomTypeIsSecret && roomSecretsDiffer) {
|
// -> only update roomSecret and reevaluate auto accept
|
||||||
peer._updateRoomSecret(roomSecret);
|
if (roomTypesDiffer || roomIdsDiffer) {
|
||||||
|
peer._updateRoomIds(roomType, roomId);
|
||||||
peer._evaluateAutoAccept();
|
peer._evaluateAutoAccept();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomTypesDiffer = peer._roomType !== roomType;
|
|
||||||
|
|
||||||
// if roomTypes differ peer is already connected -> abort
|
|
||||||
if (roomTypesDiffer) return true;
|
|
||||||
|
|
||||||
peer.refresh();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret, rtcSupported) {
|
_createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
if (peer) {
|
if (peer) {
|
||||||
this._refreshPeer(peer, roomType, roomSecret);
|
this._refreshPeer(peer, roomType, roomId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.isRtcSupported && rtcSupported) {
|
if (window.isRtcSupported && rtcSupported) {
|
||||||
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret);
|
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomId);
|
||||||
} else {
|
} else {
|
||||||
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret);
|
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPeerJoined(message) {
|
_onPeerJoined(message) {
|
||||||
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported);
|
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPeers(message) {
|
_onPeers(message) {
|
||||||
message.peers.forEach(peer => {
|
message.peers.forEach(peer => {
|
||||||
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported);
|
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -970,7 +1034,7 @@ class PeersManager {
|
||||||
}
|
}
|
||||||
if (message.disconnect === true) {
|
if (message.disconnect === true) {
|
||||||
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
|
||||||
Events.fire('peer-disconnected', message.peerId);
|
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
|
||||||
|
|
||||||
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
|
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
|
||||||
// Tidy up peerIds in localStorage
|
// Tidy up peerIds in localStorage
|
||||||
|
@ -1002,14 +1066,42 @@ class PeersManager {
|
||||||
if (peer._channel) peer._channel.onclose = null;
|
if (peer._channel) peer._channel.onclose = null;
|
||||||
peer._conn.close();
|
peer._conn.close();
|
||||||
peer._busy = false;
|
peer._busy = false;
|
||||||
|
peer._roomIds = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRoomSecretsDeleted(roomSecrets) {
|
||||||
|
for (let i=0; i<roomSecrets.length; i++) {
|
||||||
|
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecrets[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLeavePublicRoom(publicRoomId) {
|
||||||
|
this._disconnectOrRemoveRoomTypeByRoomId('public-id', publicRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSecretRoomDeleted(roomSecret) {
|
_onSecretRoomDeleted(roomSecret) {
|
||||||
for (const peerId in this.peers) {
|
this._disconnectOrRemoveRoomTypeByRoomId('secret', roomSecret);
|
||||||
const peer = this.peers[peerId];
|
}
|
||||||
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
|
|
||||||
this._onPeerDisconnected(peerId);
|
_disconnectOrRemoveRoomTypeByRoomId(roomType, roomId) {
|
||||||
}
|
const peerIds = this._getPeerIdsFromRoomId(roomId);
|
||||||
|
|
||||||
|
if (!peerIds.length) return;
|
||||||
|
|
||||||
|
for (let i=0; i<peerIds.length; i++) {
|
||||||
|
this._disconnectOrRemoveRoomTypeByPeerId(peerIds[i], roomType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_disconnectOrRemoveRoomTypeByPeerId(peerId, roomType) {
|
||||||
|
const peer = this.peers[peerId];
|
||||||
|
|
||||||
|
if (!peer) return;
|
||||||
|
|
||||||
|
if (peer._getRoomTypes().length > 1) {
|
||||||
|
peer._removeRoomType(roomType);
|
||||||
|
} else {
|
||||||
|
Events.fire('peer-disconnected', peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1040,20 +1132,26 @@ class PeersManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
_onAutoAcceptUpdated(roomSecret, autoAccept) {
|
||||||
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
|
const peerId = this._getPeerIdsFromRoomId(roomSecret)[0];
|
||||||
|
|
||||||
if (!peerId) return;
|
if (!peerId) return;
|
||||||
|
|
||||||
this.peers[peerId]._setAutoAccept(autoAccept);
|
this.peers[peerId]._setAutoAccept(autoAccept);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPeerIdFromRoomSecret(roomSecret) {
|
_getPeerIdsFromRoomId(roomId) {
|
||||||
|
if (!roomId) return [];
|
||||||
|
|
||||||
|
let peerIds = []
|
||||||
for (const peerId in this.peers) {
|
for (const peerId in this.peers) {
|
||||||
const peer = this.peers[peerId];
|
const peer = this.peers[peerId];
|
||||||
// peer must have same roomSecret and not be on the same browser.
|
|
||||||
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
|
// peer must have same roomId.
|
||||||
return peer._peerId;
|
if (Object.values(peer._roomIds).includes(roomId)) {
|
||||||
|
peerIds.push(peer._peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return peerIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1139,7 +1237,7 @@ class FileDigester {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Events {
|
class Events {
|
||||||
static fire(type, detail) {
|
static fire(type, detail = {}) {
|
||||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,8 @@
|
||||||
--icon-size: 24px;
|
--icon-size: 24px;
|
||||||
--primary-color: #4285f4;
|
--primary-color: #4285f4;
|
||||||
--paired-device-color: #00a69c;
|
--paired-device-color: #00a69c;
|
||||||
|
--public-room-color: #db8500;
|
||||||
|
--accent-color: var(--primary-color);
|
||||||
--peer-width: 120px;
|
--peer-width: 120px;
|
||||||
--ws-peer-color: #ff6b6b;
|
--ws-peer-color: #ff6b6b;
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
|
@ -24,6 +26,7 @@ body {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
transition: color 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -41,6 +44,10 @@ html {
|
||||||
min-height: fill-available;
|
min-height: fill-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fw {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.row-reverse {
|
.row-reverse {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
@ -52,7 +59,6 @@ html {
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +85,10 @@ html {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
@ -216,10 +226,6 @@ a,
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -276,8 +282,6 @@ x-noscript {
|
||||||
margin-top: 56px;
|
margin-top: 56px;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
--footer-height: 146px;
|
|
||||||
max-height: calc(100vh - 56px - var(--footer-height));
|
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
@ -285,17 +289,7 @@ x-noscript {
|
||||||
overscroll-behavior-x: none;
|
overscroll-behavior-x: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 402px) and (max-width: 425px) {
|
|
||||||
header:has(#edit-pair-devices:not([hidden]))~#center {
|
|
||||||
--footer-height: 164px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 402px) {
|
|
||||||
#center {
|
|
||||||
--footer-height: 184px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Peers List */
|
/* Peers List */
|
||||||
|
|
||||||
#x-peers-filler {
|
#x-peers-filler {
|
||||||
|
@ -452,7 +446,7 @@ x-no-peers::before {
|
||||||
}
|
}
|
||||||
|
|
||||||
x-no-peers[drop-bg]::before {
|
x-no-peers[drop-bg]::before {
|
||||||
content: "Release to select recipient";
|
content: attr(data-drop-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-no-peers[drop-bg] * {
|
x-no-peers[drop-bg] * {
|
||||||
|
@ -471,7 +465,6 @@ x-peer {
|
||||||
|
|
||||||
x-peer label {
|
x-peer label {
|
||||||
width: var(--peer-width);
|
width: var(--peer-width);
|
||||||
cursor: pointer;
|
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -500,10 +493,14 @@ x-peer .icon-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer:not(.type-ip).type-secret .icon-wrapper {
|
x-peer.type-secret .icon-wrapper {
|
||||||
background: var(--paired-device-color);
|
background: var(--paired-device-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
|
||||||
|
background: var(--public-room-color);
|
||||||
|
}
|
||||||
|
|
||||||
x-peer x-icon > .highlight-wrapper {
|
x-peer x-icon > .highlight-wrapper {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -512,17 +509,29 @@ x-peer x-icon > .highlight-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer x-icon > .highlight-wrapper > .highlight {
|
x-peer x-icon > .highlight-wrapper > .highlight {
|
||||||
width: 6px;
|
width: 15px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 4px;
|
||||||
|
margin-left: 1px;
|
||||||
|
margin-right: 1px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer.type-secret x-icon > .highlight-wrapper > .highlight {
|
x-peer.type-ip x-icon > .highlight-wrapper > .highlight.highlight-room-ip {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-peer.type-secret x-icon > .highlight-wrapper > .highlight.highlight-room-secret {
|
||||||
background-color: var(--paired-device-color);
|
background-color: var(--paired-device-color);
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x-peer.type-public-id x-icon > .highlight-wrapper > .highlight.highlight-room-public-id {
|
||||||
|
background-color: var(--public-room-color);
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
x-peer:not([status]):hover x-icon,
|
x-peer:not([status]):hover x-icon,
|
||||||
x-peer:not([status]):focus x-icon {
|
x-peer:not([status]):focus x-icon {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
|
@ -551,6 +560,14 @@ x-peer.ws-peer .highlight-wrapper {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#websocket-fallback {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#websocket-fallback > span:nth-of-type(2) {
|
||||||
|
border-bottom: solid 2px var(--ws-peer-color);
|
||||||
|
}
|
||||||
|
|
||||||
.device-descriptor {
|
.device-descriptor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -580,22 +597,6 @@ x-peer.ws-peer .highlight-wrapper {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-peer[status=transfer] .status:before {
|
|
||||||
content: 'Transferring...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer[status=prepare] .status:before {
|
|
||||||
content: 'Preparing...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer[status=wait] .status:before {
|
|
||||||
content: 'Waiting...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer[status=process] .status:before {
|
|
||||||
content: 'Processing...';
|
|
||||||
}
|
|
||||||
|
|
||||||
x-peer:not([status]) .status,
|
x-peer:not([status]) .status,
|
||||||
x-peer[status] .device-name {
|
x-peer[status] .device-name {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -629,12 +630,11 @@ x-peer[drop] x-icon {
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: auto;
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: color 300ms;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
margin: auto 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .logo {
|
footer .logo {
|
||||||
|
@ -644,43 +644,71 @@ footer .logo {
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .font-body2 {
|
.discovery-wrapper {
|
||||||
color: var(--primary-color);
|
font-size: 12px;
|
||||||
margin: auto 18px;
|
margin: 10px auto auto;
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 2px;
|
||||||
|
background-color: rgb(var(--bg-color));
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#on-this-network {
|
/*You can be discovered wrapper*/
|
||||||
border-bottom: solid 4px var(--primary-color);
|
.discovery-wrapper > div:first-of-type {
|
||||||
padding-bottom: 1px;
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#paired-devices {
|
|
||||||
border-bottom: solid 4px var(--paired-device-color);
|
.discovery-wrapper .badge {
|
||||||
padding-bottom: 1px;
|
word-break: keep-all;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
border-radius: 0.3rem/0.3rem;
|
||||||
|
padding-right: 0.3rem;
|
||||||
|
padding-left: 0.3em;
|
||||||
|
background-color: var(--badge-color);
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-room-ip {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-room-secret {
|
||||||
|
background-color: var(--paired-device-color);
|
||||||
|
border-color: var(--paired-device-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-room-public-id {
|
||||||
|
background-color: var(--public-room-color);
|
||||||
|
border-color: var(--public-room-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#display-name {
|
#display-name {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
max-width: 15em;
|
max-width: 15em;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
margin-left: -1rem;
|
margin-left: -1rem;
|
||||||
margin-bottom: -6px;
|
margin-bottom: -6px;
|
||||||
padding-right: 0.3rem;
|
|
||||||
padding-left: 0.3em;
|
|
||||||
padding-bottom: 0.1rem;
|
padding-bottom: 0.1rem;
|
||||||
border-radius: 1.3rem/30%;
|
border-radius: 1.3rem/30%;
|
||||||
border-right: solid 1rem transparent;
|
border-right: solid 1rem transparent;
|
||||||
border-left: solid 1rem transparent;
|
border-left: solid 1rem transparent;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
background-color: rgba(var(--text-color), 43%);
|
|
||||||
color: white;
|
|
||||||
transition: background-color 0.5s ease;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-pen {
|
#edit-pen {
|
||||||
|
@ -689,7 +717,6 @@ footer .font-body2 {
|
||||||
margin-left: -1rem;
|
margin-left: -1rem;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dialog */
|
/* Dialog */
|
||||||
|
@ -707,7 +734,6 @@ x-dialog x-paper {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px 24px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -716,14 +742,33 @@ x-dialog x-paper {
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-paper {
|
x-paper > .row:first-of-type {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-bottom: solid 4px var(--border-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > .row:first-of-type h2 {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pair-device-dialog,
|
||||||
|
#edit-paired-devices-dialog {
|
||||||
|
--accent-color: var(--paired-device-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#public-room-dialog {
|
||||||
|
--accent-color: var(--public-room-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pair-device-dialog x-paper,
|
||||||
|
#public-room-dialog x-paper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: max(50%, 350px);
|
top: max(50%, 350px);
|
||||||
margin-top: -328.5px;
|
margin-top: -328.5px;
|
||||||
width: calc(100vw - 20px);
|
width: calc(100vw - 20px);
|
||||||
height: 625px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog ::-moz-selection,
|
#pair-device-dialog ::-moz-selection,
|
||||||
|
@ -732,6 +777,12 @@ x-dialog x-paper {
|
||||||
background: var(--paired-device-color);
|
background: var(--paired-device-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#public-room-dialog ::-moz-selection,
|
||||||
|
#public-room-dialog ::selection {
|
||||||
|
color: black;
|
||||||
|
background: var(--public-room-color);
|
||||||
|
}
|
||||||
|
|
||||||
x-dialog:not([show]) {
|
x-dialog:not([show]) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -749,24 +800,21 @@ x-dialog a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-dialog .font-subheading {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pair Devices Dialog */
|
/* Pair Devices Dialog */
|
||||||
|
|
||||||
#key-input-container {
|
.input-key-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container > input {
|
.input-key-container > input {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
display: -webkit-box !important;
|
display: -webkit-box !important;
|
||||||
display: -webkit-flex !important;
|
display: -webkit-flex !important;
|
||||||
display: -moz-flex !important;
|
display: -moz-flex !important;
|
||||||
|
@ -777,15 +825,15 @@ x-dialog .font-subheading {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container > input + * {
|
.input-key-container > input + * {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#key-input-container > input:nth-of-type(4) {
|
.input-key-container.six-chars > input:nth-of-type(4) {
|
||||||
margin-left: 5%;
|
margin-left: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#room-key {
|
.key {
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
-moz-user-select: text;
|
-moz-user-select: text;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
@ -796,13 +844,48 @@ x-dialog .font-subheading {
|
||||||
margin: 15px -15px;
|
margin: 15px -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#room-key-qr-code {
|
.key-qr-code {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-instructions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
x-dialog h2 {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
x-dialog hr {
|
x-dialog hr {
|
||||||
margin: 40px -24px 30px -24px;
|
height: 3px;
|
||||||
border: solid 1.25px var(--border-color);
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-note hr {
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-note > div {
|
||||||
|
height: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.hr-note > div > span {
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgb(var(--text-color));
|
||||||
|
background-color: rgb(var(--bg-color));
|
||||||
|
border: var(--border-color) solid 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pair-device-dialog x-background {
|
#pair-device-dialog x-background {
|
||||||
|
@ -811,7 +894,7 @@ x-dialog hr {
|
||||||
|
|
||||||
/* Edit Paired Devices Dialog */
|
/* Edit Paired Devices Dialog */
|
||||||
.paired-devices-wrapper:empty:before {
|
.paired-devices-wrapper:empty:before {
|
||||||
content: "No paired devices.";
|
content: attr(data-empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
.paired-devices-wrapper:empty {
|
.paired-devices-wrapper:empty {
|
||||||
|
@ -896,39 +979,34 @@ x-dialog hr {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paired-device > .auto-accept {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Receive Dialog */
|
/* Receive Dialog */
|
||||||
|
|
||||||
x-dialog .row {
|
x-paper > .row {
|
||||||
margin-top: 24px;
|
padding: 10px;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* button row*/
|
/* button row*/
|
||||||
x-paper > div:last-child {
|
x-paper > .button-row {
|
||||||
margin: auto -24px -15px;
|
border-top: solid 3px var(--border-color);
|
||||||
border-top: solid 2.5px var(--border-color);
|
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-paper > div:last-child > .button {
|
x-paper > .button-row > .button {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
x-paper > div:last-child > .button:not(:last-child) {
|
x-paper > .button-row > .button:not(:first-child) {
|
||||||
border-left: solid 2.5px var(--border-color);
|
border-right: solid 1.5px var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
x-paper > .button-row > .button:not(:last-child) {
|
||||||
|
border-left: solid 1.5px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description {
|
.file-description {
|
||||||
margin-bottom: 25px;
|
max-width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.file-description .row {
|
|
||||||
margin: 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-description span {
|
.file-description span {
|
||||||
|
@ -939,23 +1017,29 @@ x-paper > div:last-child > .button:not(:last-child) {
|
||||||
.file-name {
|
.file-name {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-stem {
|
.file-stem {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
padding-right: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send Text Dialog */
|
/* Send Text Dialog */
|
||||||
/* Todo: add pair underline to send / receive dialogs displayName */
|
|
||||||
x-dialog .dialog-subheader {
|
x-dialog .dialog-subheader {
|
||||||
margin-bottom: 25px;
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-text-dialog .display-name-wrapper {
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#text-input {
|
#text-input {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
margin: 14px auto;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receive Text Dialog */
|
/* Receive Text Dialog */
|
||||||
|
@ -970,7 +1054,6 @@ x-dialog .dialog-subheader {
|
||||||
-moz-user-select: text;
|
-moz-user-select: text;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
padding: 15px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#receive-text-dialog #text a {
|
#receive-text-dialog #text a {
|
||||||
|
@ -997,12 +1080,11 @@ x-dialog .dialog-subheader {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
border: solid 12px #438cff;
|
border: solid 12px #438cff;
|
||||||
text-align: center;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-dialog .textarea {
|
#base64-paste-dialog .textarea {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -1014,21 +1096,9 @@ x-dialog .dialog-subheader {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
content: attr(placeholder);
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base64-paste-dialog button {
|
|
||||||
margin: auto;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#base64-paste-dialog button[close] {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#base64-paste-dialog button[close]:before {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button */
|
/* Button */
|
||||||
|
|
||||||
|
@ -1045,12 +1115,13 @@ x-dialog .dialog-subheader {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: var(--primary-color);
|
color: var(--accent-color);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
color: #5B5B66;
|
color: #5B5B66;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1084,6 +1155,11 @@ x-dialog .dialog-subheader {
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button[selected],
|
||||||
|
.icon-button[selected] {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
#cancel-paste-mode {
|
#cancel-paste-mode {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -1126,8 +1202,7 @@ button::-moz-focus-inner {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
margin: 10px 0;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background: #f1f3f4;
|
background: #f1f3f4;
|
||||||
|
@ -1314,11 +1389,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before {
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions[drop-peer]:before {
|
x-instructions[drop-peer]:before {
|
||||||
content: "Release to send to peer";
|
content: attr(data-drop-peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions[drop-bg]:not([drop-peer]):before {
|
x-instructions[drop-bg]:not([drop-peer]):before {
|
||||||
content: "Release to select recipient";
|
content: attr(data-drop-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
x-instructions p {
|
x-instructions p {
|
||||||
|
@ -1336,36 +1411,11 @@ x-peers:empty~x-instructions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#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 */
|
/* Responsive Styles */
|
||||||
@media screen and (max-width: 360px) {
|
|
||||||
x-dialog x-paper {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
x-paper > div:last-child {
|
|
||||||
margin: auto -15px -15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-height: 800px) {
|
@media screen and (min-height: 800px) {
|
||||||
#websocket-fallback {
|
footer {
|
||||||
padding-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1384,8 +1434,9 @@ body {
|
||||||
--text-color: 51,51,51;
|
--text-color: 51,51,51;
|
||||||
--bg-color: 250,250,250; /*rgb code*/
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
--bg-color-test: 18,18,18;
|
--bg-color-test: 18,18,18;
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #e4e4e4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: rgb(169, 169, 169);
|
||||||
|
--badge-color: #a5a5a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme colors */
|
/* Dark theme colors */
|
||||||
|
@ -1393,7 +1444,8 @@ body.dark-theme {
|
||||||
--text-color: 238,238,238;
|
--text-color: 238,238,238;
|
||||||
--bg-color: 18,18,18; /*rgb code*/
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: rgb(238,238,238);
|
||||||
|
--badge-color: #717171;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colored Elements */
|
/* Colored Elements */
|
||||||
|
@ -1427,7 +1479,7 @@ x-dialog x-paper {
|
||||||
|
|
||||||
/* Image/Video/Audio Preview */
|
/* Image/Video/Audio Preview */
|
||||||
.file-preview {
|
.file-preview {
|
||||||
margin: 10px -24px 40px -24px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-preview:empty {
|
.file-preview:empty {
|
||||||
|
@ -1451,15 +1503,17 @@ x-dialog x-paper {
|
||||||
--text-color: 238,238,238;
|
--text-color: 238,238,238;
|
||||||
--bg-color: 18,18,18; /*rgb code*/
|
--bg-color: 18,18,18; /*rgb code*/
|
||||||
--bg-color-secondary: #333;
|
--bg-color-secondary: #333;
|
||||||
--border-color: #252525;
|
--border-color: rgb(238,238,238);
|
||||||
|
--badge-color: #717171;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override dark mode with light mode styles if the user decides to swap */
|
/* Override dark mode with light mode styles if the user decides to swap */
|
||||||
body.light-theme {
|
body.light-theme {
|
||||||
--text-color: 51,51,51;
|
--text-color: 51,51,51;
|
||||||
--bg-color: 250,250,250; /*rgb code*/
|
--bg-color: 250,250,250; /*rgb code*/
|
||||||
--bg-color-secondary: #f1f3f4;
|
--bg-color-secondary: #e4e4e4;
|
||||||
--border-color: #e7e8e8;
|
--border-color: rgb(169, 169, 169);
|
||||||
|
--badge-color: #a5a5a5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue