move files for node only implementation
BIN
public/images/android-chrome-192x192-maskable.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
public/images/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
public/images/android-chrome-512x512-maskable.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
public/images/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
public/images/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
public/images/favicon-96x96.png
Normal file
After Width: | Height: | Size: 7 KiB |
BIN
public/images/logo_blue_512x512.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
public/images/logo_transparent_128x128.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/images/logo_transparent_512x512.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
public/images/logo_transparent_white_512x512.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/images/logo_white_512x512.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
public/images/mstile-150x150.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
251
public/images/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,251 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2325 4214 c-225 -34 -366 -76 -544 -161 -361 -172 -651 -455 -830
|
||||
-809 -135 -268 -194 -532 -186 -839 2 -88 6 -173 9 -190 3 -16 8 -43 11 -60 3
|
||||
-16 7 -41 10 -55 3 -14 7 -36 10 -50 9 -51 54 -188 86 -265 94 -229 217 -413
|
||||
389 -585 111 -111 139 -136 212 -186 29 -20 60 -42 68 -49 8 -7 34 -22 57 -35
|
||||
36 -20 43 -21 55 -9 7 8 14 20 15 26 2 7 20 38 40 70 21 32 38 63 38 68 0 6
|
||||
10 9 22 9 12 -1 21 4 20 10 -1 6 3 10 8 9 6 -1 12 7 12 18 2 18 -1 17 -19 -6
|
||||
-29 -38 -32 -25 -5 24 13 22 26 38 30 35 3 -3 4 2 2 13 -2 10 -4 20 -4 23 -1
|
||||
3 -21 16 -46 29 -28 15 -44 29 -41 37 3 8 0 11 -10 7 -18 -7 -46 16 -37 31 3
|
||||
6 3 8 -2 4 -5 -5 -24 4 -44 19 -55 40 -180 166 -181 181 0 7 -4 10 -10 7 -5
|
||||
-3 -10 1 -10 9 0 9 -4 16 -9 16 -4 0 -27 26 -49 58 -29 42 -38 62 -30 70 7 7
|
||||
5 10 -7 9 -10 -1 -20 4 -23 12 -3 9 0 11 9 6 8 -5 11 -4 6 1 -5 5 -14 9 -20 9
|
||||
-7 0 -13 9 -15 19 -2 11 -10 26 -18 34 -8 7 -12 18 -9 23 4 5 2 9 -2 9 -5 0
|
||||
-14 9 -21 21 -10 15 -10 24 -2 34 8 10 8 15 -1 21 -8 4 -9 3 -5 -4 4 -7 3 -12
|
||||
-2 -12 -9 0 -39 69 -47 108 -2 11 -8 29 -13 39 -5 10 -8 28 -7 40 1 12 -2 20
|
||||
-7 17 -4 -3 -9 4 -9 15 -2 23 -3 29 -12 51 -11 29 -29 161 -23 175 3 8 2 15
|
||||
-2 15 -4 0 -7 24 -8 54 -1 48 1 54 17 51 10 -2 21 0 24 4 2 5 -5 8 -18 7 -19
|
||||
-1 -22 4 -22 34 0 27 4 35 17 34 10 -1 15 3 12 8 -3 5 -12 7 -20 4 -17 -7 -18
|
||||
6 -1 23 6 8 7 11 2 8 -7 -3 -9 9 -7 34 3 25 8 37 16 32 8 -4 8 -3 0 6 -8 9 -9
|
||||
25 -2 54 5 23 12 51 14 62 2 11 6 30 9 43 3 12 7 32 10 43 11 53 28 95 43 107
|
||||
10 7 11 12 4 12 -9 0 -9 5 -3 18 5 9 22 45 38 80 19 42 32 61 42 57 11 -4 11
|
||||
-2 2 9 -7 8 -8 16 -4 18 4 1 18 23 31 48 24 47 50 85 92 130 14 15 30 36 36
|
||||
46 6 11 16 19 22 19 6 0 14 4 19 9 5 5 3 6 -4 2 -20 -11 -15 1 11 29 14 15 29
|
||||
24 34 21 6 -3 9 2 8 12 0 10 6 16 16 16 10 -1 15 3 12 8 -7 10 36 45 49 40 5
|
||||
-1 6 2 3 7 -8 12 21 34 34 26 6 -4 9 -2 8 3 -4 12 52 62 70 62 6 0 12 4 12 8
|
||||
0 4 15 16 33 27 17 10 34 21 37 25 12 16 101 51 111 44 8 -5 9 -3 4 6 -6 10
|
||||
-4 12 9 7 10 -4 15 -3 11 3 -6 10 56 40 88 43 9 1 17 5 17 10 0 4 4 6 8 3 4
|
||||
-2 14 -1 22 4 9 5 19 5 27 -2 11 -8 12 -7 7 7 -5 12 -4 16 4 11 6 -3 13 -2 16
|
||||
3 4 5 14 8 24 7 9 -2 19 0 22 5 3 4 21 10 40 12 19 3 35 6 35 7 0 3 42 10 87
|
||||
14 26 2 51 7 56 10 6 3 13 0 15 -6 4 -10 6 -10 6 0 1 16 151 17 151 1 0 -6 6
|
||||
-4 14 3 17 17 89 12 79 -6 -4 -6 1 -5 10 3 9 7 17 10 17 5 0 -5 19 -9 43 -10
|
||||
43 -1 91 -7 137 -19 14 -4 32 -8 40 -9 8 -1 22 -5 30 -8 8 -3 51 -17 95 -32
|
||||
43 -15 82 -30 85 -34 3 -4 12 -8 20 -10 16 -3 121 -53 130 -62 3 -3 17 -11 32
|
||||
-19 15 -8 36 -22 47 -32 10 -10 21 -15 25 -12 3 3 8 -2 12 -11 3 -9 11 -16 17
|
||||
-16 33 0 237 -202 320 -317 180 -253 268 -536 262 -847 -1 -83 -5 -162 -9
|
||||
-176 -3 -13 -8 -41 -11 -61 -3 -20 -10 -50 -15 -68 -5 -17 -9 -35 -9 -39 -1
|
||||
-4 -13 -43 -27 -87 -35 -107 -110 -257 -180 -357 -78 -112 -258 -290 -366
|
||||
-362 -49 -32 -88 -62 -88 -67 0 -4 10 -25 21 -46 33 -60 142 -247 146 -253 3
|
||||
-2 18 3 34 12 16 10 37 15 47 12 14 -5 15 -4 2 5 -8 6 -12 11 -7 12 4 1 10 2
|
||||
15 3 4 0 18 11 31 23 13 12 27 20 31 18 5 -3 11 2 14 11 4 9 13 14 22 10 8 -3
|
||||
12 -2 9 3 -7 12 84 83 96 75 5 -3 6 2 3 10 -4 11 0 16 11 16 9 0 13 5 10 10
|
||||
-6 10 9 15 30 10 5 -1 7 2 3 6 -11 10 12 35 25 27 5 -3 7 -2 4 4 -4 5 5 20 18
|
||||
31 14 12 25 27 25 34 0 6 3 9 6 6 6 -7 33 18 51 48 6 10 16 18 23 16 6 -1 9 2
|
||||
5 7 -3 6 2 18 12 28 10 10 30 38 46 61 15 23 32 42 37 42 6 0 9 6 8 13 -2 6 3
|
||||
11 11 9 8 -2 11 3 8 11 -7 19 21 59 35 51 6 -4 8 1 3 15 -4 15 -2 21 9 21 9 0
|
||||
13 6 10 14 -3 8 0 17 5 21 6 3 9 11 6 16 -4 5 -2 9 3 9 4 0 15 16 22 35 9 24
|
||||
19 34 28 30 12 -4 13 -3 2 10 -9 11 -9 15 -1 15 7 0 10 4 7 9 -3 5 3 28 13 52
|
||||
17 40 22 54 32 84 1 6 10 17 19 25 9 8 10 11 3 6 -10 -6 -10 1 1 32 8 23 20
|
||||
68 27 101 6 34 15 59 18 56 4 -2 5 10 3 27 -3 17 -1 28 4 25 5 -3 9 11 10 31
|
||||
1 97 5 133 12 129 4 -3 7 6 7 19 0 13 -3 24 -6 24 -4 0 -3 17 0 38 4 20 6 61
|
||||
4 90 -1 29 2 50 7 47 5 -3 12 0 16 6 4 8 3 9 -4 5 -16 -10 -34 28 -20 42 9 9
|
||||
8 12 -2 12 -13 0 -13 2 0 10 12 8 12 10 1 10 -11 0 -11 3 0 17 8 9 9 14 3 10
|
||||
-12 -7 -29 97 -19 114 4 5 2 9 -2 9 -5 0 -10 14 -11 31 -1 17 -7 39 -12 49 -6
|
||||
10 -5 21 1 28 5 7 6 10 2 7 -8 -6 -65 168 -67 202 -1 11 -5 19 -10 15 -5 -3
|
||||
-7 2 -3 11 4 10 2 17 -4 17 -6 0 -8 7 -5 16 3 8 2 12 -4 9 -9 -6 -14 8 -11 28
|
||||
0 4 -4 7 -10 7 -5 0 -8 4 -5 9 4 5 1 11 -4 13 -6 2 -12 10 -14 18 -1 8 -8 25
|
||||
-14 38 -7 12 -9 22 -5 22 4 0 -1 6 -12 13 -11 9 -15 19 -10 27 5 9 4 11 -3 6
|
||||
-7 -4 -12 -2 -12 4 0 6 -11 27 -25 47 -13 20 -21 41 -18 47 3 6 2 8 -2 3 -11
|
||||
-9 -46 43 -38 57 3 6 3 8 -2 4 -4 -4 -24 14 -44 40 -45 59 -49 64 -89 107 -19
|
||||
20 -30 40 -26 47 4 6 3 8 -4 5 -11 -8 -112 85 -112 103 0 6 -4 9 -9 5 -5 -3
|
||||
-17 5 -25 17 -9 12 -16 18 -16 13 0 -6 -3 -6 -8 0 -11 16 -111 89 -182 134
|
||||
-36 22 -69 44 -75 48 -5 5 -45 24 -87 44 -43 20 -74 40 -70 44 4 5 2 5 -4 2
|
||||
-11 -6 -54 7 -109 33 -23 11 -128 43 -195 58 -84 20 -104 24 -230 41 -91 13
|
||||
-339 10 -435 -5z"/>
|
||||
<path d="M2470 3856 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M2333 3825 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2365 3820 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M1993 3715 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M1936 3658 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1710 3555 c5 -7 6 -16 2 -21 -4 -5 -3 -5 2 -1 15 12 15 34 0 34 -9
|
||||
0 -11 -4 -4 -12z"/>
|
||||
<path d="M1650 3515 c5 -7 6 -16 2 -21 -4 -5 -3 -5 2 -1 15 12 15 34 0 34 -9
|
||||
0 -11 -4 -4 -12z"/>
|
||||
<path d="M2425 3508 c-152 -17 -331 -84 -468 -175 -88 -57 -238 -206 -292
|
||||
-288 -86 -132 -146 -280 -170 -423 -16 -99 -14 -299 5 -377 6 -27 14 -61 16
|
||||
-74 10 -48 65 -178 108 -254 48 -84 140 -202 181 -233 14 -10 25 -25 25 -32 0
|
||||
-7 4 -11 8 -8 4 2 23 -9 42 -26 31 -27 94 -72 127 -91 7 -5 24 13 44 47 18 30
|
||||
36 53 40 50 4 -2 6 4 5 14 0 9 4 16 11 15 7 -2 10 3 7 11 -3 7 2 21 11 30 9 9
|
||||
13 16 9 16 -3 0 -2 6 3 13 22 28 75 130 65 123 -13 -8 -100 48 -109 70 -3 8
|
||||
-9 14 -14 14 -17 0 -99 95 -94 108 3 8 0 10 -7 6 -8 -5 -9 -2 -5 10 4 10 3 15
|
||||
-3 11 -12 -7 -44 41 -35 55 4 6 1 9 -5 8 -12 -3 -51 87 -42 96 3 4 1 6 -5 6
|
||||
-18 0 -37 121 -36 225 2 84 7 140 13 140 2 0 5 16 14 59 3 16 13 38 23 49 10
|
||||
11 13 17 6 14 -8 -5 -8 -1 0 18 8 16 17 23 27 19 11 -4 12 -2 3 7 -9 9 -6 20
|
||||
10 49 34 58 79 110 90 103 6 -3 8 -2 4 3 -3 5 1 18 9 29 8 10 14 16 14 12 0
|
||||
-4 13 7 29 24 17 17 35 28 41 24 6 -3 9 -1 8 7 -2 7 5 12 14 12 10 -1 16 3 15
|
||||
9 -2 11 124 77 136 70 4 -2 7 1 7 7 0 7 6 10 14 7 8 -3 16 -2 18 3 2 4 19 11
|
||||
38 15 19 3 51 9 70 12 46 9 186 9 230 0 19 -4 50 -10 67 -13 19 -4 30 -11 26
|
||||
-17 -3 -6 -1 -7 6 -3 16 10 53 -3 45 -16 -4 -7 -2 -8 5 -4 16 11 83 -22 75
|
||||
-36 -4 -6 -3 -8 4 -5 14 9 43 -3 36 -14 -3 -5 1 -7 8 -4 7 3 27 -8 44 -23 17
|
||||
-15 47 -41 66 -58 53 -47 114 -133 152 -215 96 -207 83 -452 -34 -649 -43 -74
|
||||
-145 -181 -213 -227 -53 -35 -60 -12 57 -210 l78 -132 24 15 c13 9 24 13 24 9
|
||||
0 -5 8 2 19 13 10 12 21 19 25 15 3 -3 6 -1 6 6 0 8 6 11 16 7 8 -3 12 -2 9 4
|
||||
-3 6 -1 10 6 10 7 0 9 3 6 7 -4 3 9 17 28 30 19 13 35 29 35 35 0 6 10 3 22
|
||||
-8 16 -14 19 -14 10 -2 -11 14 -9 21 18 50 18 18 28 26 24 18 -4 -9 -3 -12 2
|
||||
-7 5 5 9 16 9 25 0 9 11 22 25 29 14 7 19 13 12 13 -10 0 -9 4 2 16 9 8 21 24
|
||||
27 35 9 17 13 18 27 7 15 -12 16 -11 4 4 -12 15 -11 22 7 47 12 16 21 32 21
|
||||
36 0 4 9 20 21 36 11 16 17 29 13 29 -4 0 1 7 12 16 10 8 12 12 4 8 -12 -6
|
||||
-13 -5 -4 7 6 8 17 35 25 62 7 26 16 47 20 47 4 0 6 6 6 13 -3 34 28 165 37
|
||||
160 8 -5 8 -3 0 8 -9 13 -10 25 -4 47 6 21 4 172 -3 172 -4 0 -3 10 4 21 6 12
|
||||
7 19 1 15 -5 -3 -12 15 -16 42 -3 26 -9 61 -12 77 -4 17 -7 36 -7 43 0 6 -4
|
||||
12 -9 12 -5 0 -7 4 -3 9 3 5 1 12 -4 15 -5 4 -12 24 -16 46 -4 22 -10 40 -14
|
||||
40 -5 0 -15 18 -23 39 -15 36 -14 39 1 35 10 -4 9 -1 -4 7 -29 18 -45 42 -39
|
||||
59 3 8 2 11 -2 7 -4 -4 -16 5 -25 20 -14 21 -15 29 -6 36 8 6 6 7 -6 3 -11 -4
|
||||
-20 1 -24 12 -4 9 -13 22 -21 28 -8 6 -12 16 -8 23 4 6 4 10 -1 9 -5 -2 -38
|
||||
25 -73 59 -36 34 -89 80 -117 101 -29 21 -49 43 -45 47 4 5 2 5 -5 2 -6 -4
|
||||
-19 0 -27 9 -9 8 -16 13 -16 10 0 -4 -17 4 -37 17 -162 99 -432 151 -658 125z"/>
|
||||
<path d="M1435 3301 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M1461 3276 c-9 -11 -9 -16 1 -22 7 -4 10 -4 6 1 -4 4 -3 14 3 22 6 7
|
||||
9 13 6 13 -2 0 -10 -6 -16 -14z"/>
|
||||
<path d="M1389 3217 c6 -8 7 -18 3 -22 -4 -5 -1 -5 6 -1 10 6 10 11 1 22 -6 8
|
||||
-14 14 -16 14 -3 0 0 -6 6 -13z"/>
|
||||
<path d="M1350 3200 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0
|
||||
-4 -4 -4 -10z"/>
|
||||
<path d="M2520 3140 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
|
||||
<path d="M1339 3113 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M2595 3120 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M1336 3075 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
|
||||
<path d="M2330 3079 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M2239 3068 c-5 -18 -6 -38 -1 -34 7 8 12 36 6 36 -2 0 -4 -1 -5 -2z"/>
|
||||
<path d="M1274 3049 c-3 -6 -2 -15 3 -20 5 -5 9 -1 9 11 0 23 -2 24 -12 9z"/>
|
||||
<path d="M2185 3020 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M2255 3019 c-3 -4 2 -6 10 -5 21 3 28 13 10 13 -9 0 -18 -4 -20 -8z"/>
|
||||
<path d="M2153 2995 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2116 2982 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M2086 2962 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M1216 2922 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M2070 2919 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1210 2896 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M1195 2860 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M1256 2858 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1993 2835 c0 -8 4 -12 9 -9 4 3 8 9 8 15 0 5 -4 9 -8 9 -5 0 -9 -7
|
||||
-9 -15z"/>
|
||||
<path d="M2030 2839 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1190 2825 c0 -7 30 -13 34 -7 3 4 -4 9 -15 9 -10 1 -19 0 -19 -2z"/>
|
||||
<path d="M1193 2803 c4 -3 1 -13 -6 -22 -11 -14 -10 -14 5 -2 16 12 16 31 1
|
||||
31 -4 0 -3 -3 0 -7z"/>
|
||||
<path d="M2493 2795 c-122 -27 -209 -94 -260 -202 -64 -138 -24 -318 92 -415
|
||||
39 -33 101 -68 120 -68 8 0 15 -4 15 -8 0 -13 143 -14 196 -1 27 7 68 25 91
|
||||
41 23 15 47 28 53 28 6 0 9 3 7 8 -3 4 1 13 9 20 8 7 14 9 14 5 1 -4 10 9 21
|
||||
30 11 20 24 38 29 38 6 1 14 2 19 3 5 0 13 4 17 8 3 4 -3 6 -15 3 -23 -4 -29
|
||||
10 -7 18 7 3 14 16 14 29 2 41 9 63 20 63 6 0 14 4 19 8 4 5 1 7 -7 5 -12 -2
|
||||
-15 7 -15 45 0 26 -4 47 -9 47 -5 0 -2 8 5 17 10 11 10 14 2 9 -8 -4 -13 -2
|
||||
-13 8 0 8 -5 27 -11 43 -6 15 -11 30 -12 33 -2 7 -24 34 -64 80 -40 45 -132
|
||||
96 -193 105 -25 3 -52 8 -60 10 -8 2 -43 -3 -77 -10z"/>
|
||||
<path d="M1953 2775 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M1987 2779 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M4375 2761 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M3630 2739 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1929 2723 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M1987 2719 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M1949 2683 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M1910 2681 c0 -6 4 -12 8 -15 5 -3 9 1 9 9 0 8 -4 15 -9 15 -4 0 -8
|
||||
-4 -8 -9z"/>
|
||||
<path d="M1155 2661 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M1894 2640 c0 -13 4 -16 10 -10 7 7 7 13 0 20 -6 6 -10 3 -10 -10z"/>
|
||||
<path d="M3667 2639 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M1879 2593 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M4416 2542 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M1894 2476 c1 -8 5 -18 8 -22 4 -3 5 1 4 10 -1 8 -5 18 -8 22 -4 3
|
||||
-5 -1 -4 -10z"/>
|
||||
<path d="M2936 2447 c3 -10 9 -15 12 -12 3 3 0 11 -7 18 -10 9 -11 8 -5 -6z"/>
|
||||
<path d="M4415 2441 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M1890 2421 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
|
||||
<path d="M1186 2358 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1865 2360 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M2926 2258 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1153 2235 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M3633 2215 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2873 2175 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M1186 2158 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M3653 2149 c-2 -23 3 -25 10 -4 4 8 3 16 -1 19 -4 3 -9 -4 -9 -15z"/>
|
||||
<path d="M4385 2120 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M2770 2099 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1230 1939 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3519 1833 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M4260 1846 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M4251 1804 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
|
||||
<path d="M2210 1779 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3467 1779 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M3430 1739 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M4230 1746 c0 -2 7 -7 16 -10 8 -3 12 -2 9 4 -6 10 -25 14 -25 6z"/>
|
||||
<path d="M3396 1713 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
|
||||
<path d="M2136 1679 c4 -8 30 -6 38 2 3 3 -5 5 -19 5 -13 0 -22 -3 -19 -7z"/>
|
||||
<path d="M4255 1680 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M3354 1662 c4 -3 14 -7 22 -8 9 -1 13 0 10 4 -4 3 -14 7 -22 8 -9 1
|
||||
-13 0 -10 -4z"/>
|
||||
<path d="M3296 1638 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M4236 1638 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M3294 1595 c0 -13 3 -22 7 -19 8 4 6 30 -2 38 -3 3 -5 -5 -5 -19z"/>
|
||||
<path d="M1435 1600 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M2076 1601 c-3 -5 2 -15 12 -22 15 -12 16 -12 5 2 -7 9 -10 19 -6 22
|
||||
3 4 4 7 0 7 -3 0 -8 -4 -11 -9z"/>
|
||||
<path d="M3253 1595 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2115 1580 c3 -5 8 -10 11 -10 2 0 4 5 4 10 0 6 -5 10 -11 10 -5 0
|
||||
-7 -4 -4 -10z"/>
|
||||
<path d="M3199 1553 c-13 -16 -12 -17 4 -4 9 7 17 15 17 17 0 8 -8 3 -21 -13z"/>
|
||||
<path d="M3240 1520 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
|
||||
<path d="M4105 1479 c-3 -4 2 -6 10 -5 21 3 28 13 10 13 -9 0 -18 -4 -20 -8z"/>
|
||||
<path d="M4033 1355 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M4006 1298 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M3940 1285 c0 -2 6 -8 13 -14 10 -8 14 -7 14 2 0 8 -6 14 -14 14 -7
|
||||
0 -13 -1 -13 -2z"/>
|
||||
<path d="M3827 1139 c7 -9 10 -19 6 -22 -3 -4 -1 -7 5 -7 17 0 15 16 -5 31
|
||||
-16 12 -17 12 -6 -2z"/>
|
||||
<path d="M1810 1059 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3719 1053 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M1774 1049 c-3 -6 -2 -15 3 -20 5 -5 9 -1 9 11 0 23 -2 24 -12 9z"/>
|
||||
<path d="M3679 1028 c-5 -16 -4 -46 2 -42 4 2 7 13 6 24 -1 17 -5 26 -8 18z"/>
|
||||
<path d="M1710 965 c0 -7 30 -13 34 -7 3 4 -4 9 -15 9 -10 1 -19 0 -19 -2z"/>
|
||||
<path d="M3610 939 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3570 879 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/snapdrop-graphics.sketch
Executable file
BIN
public/images/twitter-stream.jpg
Normal file
After Width: | Height: | Size: 38 KiB |
242
public/index.html
Normal file
|
@ -0,0 +1,242 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<!-- Web App Config -->
|
||||
<title>Snapdrop</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#3367d6">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="apple-mobile-web-app-capable" content="no">
|
||||
<meta name="apple-mobile-web-app-title" content="Snapdrop">
|
||||
<!-- Descriptions -->
|
||||
<meta name="description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
|
||||
<meta name="keywords" content="File, Transfer, Share, Peer2Peer">
|
||||
<meta name="author" content="RobinLinus">
|
||||
<meta property="og:title" content="Snapdrop">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://snapdrop.net/">
|
||||
<meta property="og:author" content="https://facebook.com/RobinLinus">
|
||||
<meta name="twitter:author" content="@RobinLinus">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
|
||||
<meta name="og:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
|
||||
<!-- Icons -->
|
||||
<link rel="icon" sizes="96x96" href="images/favicon-96x96.png">
|
||||
<link rel="shortcut icon" href="images/favicon-96x96.png">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<meta name="msapplication-TileImage" content="images/mstile-150x150.png">
|
||||
<link rel="fluid-icon" type="image/png" href="images/android-chrome-192x192.png">
|
||||
<meta name="twitter:image" content="https://snapdrop.net/images/twitter-stream.jpg">
|
||||
<meta property="og:image" content="https://snapdrop.net/images/twitter-stream.jpg">
|
||||
<!-- Resources -->
|
||||
<link rel="stylesheet" type="text/css" href="styles.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
|
||||
<body translate="no">
|
||||
<header class="row-reverse">
|
||||
<a href="#about" class="icon-button" title="About Snapdrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" id="theme" class="icon-button" title="Switch Darkmode/Lightmode" >
|
||||
<svg class="icon">
|
||||
<use xlink:href="#icon-theme" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" id="notification" class="icon-button" title="Enable Notifications" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" id="install" class="icon-button" title="Install Snapdrop" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open Snapdrop on other devices to send files</h2>
|
||||
</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>
|
||||
<!-- Footer -->
|
||||
<footer class="column">
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="displayName" placeholder="The easiest way to transfer data across devices"></div>
|
||||
<div class="font-body2">You can be discovered by everyone on this network</div>
|
||||
</footer>
|
||||
<!-- Receive Dialog -->
|
||||
<x-dialog id="receiveDialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h3>File Received</h3>
|
||||
<div class="font-subheading" id="fileName">Filename</div>
|
||||
<div class="font-body2" id="fileSize"></div>
|
||||
<div class='preview' style="visibility: hidden;">
|
||||
<img id='img-preview' src="">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="autoDownload" class="grow">Ask to save each file before downloading</label>
|
||||
<input type="checkbox" id="autoDownload" checked="">
|
||||
</div>
|
||||
<div class="row-reverse">
|
||||
<a class="button" close id="download" title="Download File" autofocus>Save</a>
|
||||
<button class="button" close>Ignore</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Send Text Dialog -->
|
||||
<x-dialog id="sendTextDialog">
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h3>Send a Message</h3>
|
||||
<div id="textInput" class="textarea" role="textbox" placeholder="Send a message" autocomplete="off" autofocus contenteditable></div>
|
||||
<div class="row-reverse">
|
||||
<button class="button" type="submit" close>Send</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Text Dialog -->
|
||||
<x-dialog id="receiveTextDialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h3>Message Received</h3>
|
||||
<div class="font-subheading" id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<button class="button" id="copy" close autofocus>Copy</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast class="row" shadow="1" id="toast">File Transfer Completed</x-toast>
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<section class="center column fade-in">
|
||||
<header class="row-reverse">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<h1>Snapdrop</h1>
|
||||
<div class="font-subheading">The easiest way to transfer files across devices</div>
|
||||
<div class="row">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop" title="Snapdrop on Github" rel="noreferrer">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#github" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://www.paypal.com/donate?hosted_button_id=FTP9DXUR7LA7Q" title="Help cover the server costs!" rel="noreferrer">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#monetarization" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https://snapdrop.net%20by%20@robin_linus%20&" title="Tweet about Snapdrop" rel="noreferrer">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#twitter" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#help-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<x-background></x-background>
|
||||
</x-about>
|
||||
<!-- SVG Icon Library -->
|
||||
<svg style="display: none;">
|
||||
<symbol id=wifi-tethering viewBox="0 0 24 24">
|
||||
<path d="M12 11c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 2c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19zM12 3C6.48 3 2 7.48 2 13c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 18.53 4 15.96 4 13c0-4.42 3.58-8 8-8s8 3.58 8 8c0 2.96-1.61 5.53-4 6.92l1 1.73c2.99-1.73 5-4.95 5-8.65 0-5.52-4.48-10-10-10z"></path>
|
||||
</symbol>
|
||||
<symbol id=desktop-mac viewBox="0 0 24 24">
|
||||
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H3V4h18v10z"></path>
|
||||
</symbol>
|
||||
<symbol id=phone-iphone viewBox="0 0 24 24">
|
||||
<path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z"></path>
|
||||
</symbol>
|
||||
<symbol id=tablet-mac viewBox="0 0 24 24">
|
||||
<path d="M18.5 0h-14C3.12 0 2 1.12 2 2.5v19C2 22.88 3.12 24 4.5 24h14c1.38 0 2.5-1.12 2.5-2.5v-19C21 1.12 19.88 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z"></path>
|
||||
</symbol>
|
||||
<symbol id=info-outline viewBox="0 0 24 24">
|
||||
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path>
|
||||
</symbol>
|
||||
<symbol id=close viewBox="0 0 24 24">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
|
||||
</symbol>
|
||||
<symbol id=help-outline viewBox="0 0 24 24">
|
||||
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"></path>
|
||||
</symbol>
|
||||
<symbol id="twitter">
|
||||
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z" />
|
||||
</symbol>
|
||||
<symbol id="github">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</symbol>
|
||||
<g id="notifications">
|
||||
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
|
||||
</g>
|
||||
<symbol id="homescreen">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z" />
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
</symbol>
|
||||
<symbol id="monetarization">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z" />
|
||||
</symbol>
|
||||
<symbol id="icon-theme" viewBox="0 0 24 24">
|
||||
<rect fill="none" height="24" width="24"/><path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/network.js"></script>
|
||||
<script src="scripts/ui.js"></script>
|
||||
<script src="scripts/theme.js" async></script>
|
||||
<script src="scripts/clipboard.js" async></script>
|
||||
<!-- Sounds -->
|
||||
<audio id="blop" autobuffer="true">
|
||||
<source src="/sounds/blop.mp3" type="audio/mpeg">
|
||||
<source src="/sounds/blop.ogg" type="audio/ogg">
|
||||
</audio>
|
||||
<!-- no script -->
|
||||
<noscript>
|
||||
<x-noscript class="full center column">
|
||||
<h1>Enable JavaScript</h1>
|
||||
<h3>Snapdrop works only with JavaScript</h3>
|
||||
</x-noscript>
|
||||
<style>
|
||||
x-noscript {
|
||||
background: #599cfc;
|
||||
color: white;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a[href="#info"] {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
</body>
|
40
public/manifest.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "Snapdrop",
|
||||
"short_name": "Snapdrop",
|
||||
"icons": [{
|
||||
"src": "images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},{
|
||||
"src": "images/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},{
|
||||
"src": "images/android-chrome-192x192-maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},{
|
||||
"src": "images/android-chrome-512x512-maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},{
|
||||
"src": "images/favicon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}],
|
||||
"background_color": "#efefef",
|
||||
"start_url": "/",
|
||||
"display": "minimal-ui",
|
||||
"theme_color": "#3367d6",
|
||||
"share_target": {
|
||||
"method":"GET",
|
||||
"action": "/?share_target",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
}
|
38
public/scripts/clipboard.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Polyfill for Navigator.clipboard.writeText
|
||||
if (!navigator.clipboard) {
|
||||
navigator.clipboard = {
|
||||
writeText: text => {
|
||||
|
||||
// A <span> contains the text to copy
|
||||
const span = document.createElement('span');
|
||||
span.textContent = text;
|
||||
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
|
||||
|
||||
// Paint the span outside the viewport
|
||||
span.style.position = 'absolute';
|
||||
span.style.left = '-9999px';
|
||||
span.style.top = '-9999px';
|
||||
|
||||
const win = window;
|
||||
const selection = win.getSelection();
|
||||
win.document.body.appendChild(span);
|
||||
|
||||
const range = win.document.createRange();
|
||||
selection.removeAllRanges();
|
||||
range.selectNode(span);
|
||||
selection.addRange(range);
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
success = win.document.execCommand('copy');
|
||||
} catch (err) {
|
||||
return Promise.error();
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
span.remove();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
597
public/scripts/network.js
Normal file
|
@ -0,0 +1,597 @@
|
|||
window.URL = window.URL || window.webkitURL;
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
class ServerConnection {
|
||||
|
||||
constructor() {
|
||||
this._connect();
|
||||
Events.on('beforeunload', e => this._disconnect());
|
||||
Events.on('pagehide', e => this._disconnect());
|
||||
document.addEventListener('visibilitychange', e => this._onVisibilityChange());
|
||||
Events.on('online', this._reconnect);
|
||||
}
|
||||
|
||||
_connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => console.log('WS: server connected');
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
ws.onclose = _ => this._onDisconnect();
|
||||
ws.onerror = e => this._onError(e);
|
||||
this._socket = ws;
|
||||
|
||||
Events.on('force-disconnect', this._onForceDisconnect);
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'peers':
|
||||
Events.fire('peers', msg.peers);
|
||||
break;
|
||||
case 'peer-joined':
|
||||
Events.fire('peer-joined', msg.peer);
|
||||
break;
|
||||
case 'peer-left':
|
||||
Events.fire('peer-left', msg.peerId);
|
||||
break;
|
||||
case 'signal':
|
||||
Events.fire('signal', msg);
|
||||
break;
|
||||
case 'ping':
|
||||
this.send({ type: 'pong' });
|
||||
break;
|
||||
case 'display-name':
|
||||
Events.fire('display-name', msg);
|
||||
break;
|
||||
default:
|
||||
console.error('WS: unknown message type', msg);
|
||||
}
|
||||
}
|
||||
|
||||
send(message) {
|
||||
if (!this._isConnected()) return;
|
||||
this._socket.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
return protocol + '://' + location.host + location.pathname + 'server' + webrtc;
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
this._socket.onclose = null;
|
||||
this._socket.close();
|
||||
Events.fire('disconnect');
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(this._connect, 5000);
|
||||
Events.fire('disconnect');
|
||||
}
|
||||
|
||||
_onForceDisconnect() {
|
||||
document.cookie = "peerid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
this._disconnect();
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
if (document.hidden) return;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_isConnected() {
|
||||
return this._socket && this._socket.readyState === this._socket.OPEN;
|
||||
}
|
||||
|
||||
_isConnecting() {
|
||||
return this._socket && this._socket.readyState === this._socket.CONNECTING;
|
||||
}
|
||||
|
||||
_reconnect() {
|
||||
console.log("reconnect")
|
||||
this._disconnect();
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_onError(e) {
|
||||
console.error(e);
|
||||
this._onForceDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, peerId) {
|
||||
this._server = serverConnection;
|
||||
this._peerId = peerId;
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
this._send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
sendFiles(files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
this._filesQueue.push(files[i]);
|
||||
}
|
||||
if (this._busy) return;
|
||||
this._dequeueFile();
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
if (!this._filesQueue.length) return;
|
||||
this._busy = true;
|
||||
const file = this._filesQueue.shift();
|
||||
this._sendFile(file);
|
||||
}
|
||||
|
||||
_sendFile(file) {
|
||||
this.sendJSON({
|
||||
type: 'header',
|
||||
name: file.name,
|
||||
mime: file.type,
|
||||
size: file.size
|
||||
});
|
||||
this._chunker = new FileChunker(file,
|
||||
chunk => this._send(chunk),
|
||||
offset => this._onPartitionEnd(offset));
|
||||
this._chunker.nextPartition();
|
||||
}
|
||||
|
||||
_onPartitionEnd(offset) {
|
||||
this.sendJSON({ type: 'partition', offset: offset });
|
||||
}
|
||||
|
||||
_onReceivedPartitionEnd(offset) {
|
||||
this.sendJSON({ type: 'partition-received', offset: offset });
|
||||
}
|
||||
|
||||
_sendNextPartition() {
|
||||
if (!this._chunker || this._chunker.isFileEnd()) return;
|
||||
this._chunker.nextPartition();
|
||||
}
|
||||
|
||||
_sendProgress(progress) {
|
||||
this.sendJSON({ type: 'progress', progress: progress });
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
case 'header':
|
||||
this._onFileHeader(message);
|
||||
break;
|
||||
case 'partition':
|
||||
this._onReceivedPartitionEnd(message);
|
||||
break;
|
||||
case 'partition-received':
|
||||
this._sendNextPartition();
|
||||
break;
|
||||
case 'progress':
|
||||
this._onDownloadProgress(message.progress);
|
||||
break;
|
||||
case 'file-transfer-complete':
|
||||
this._onFileTransferCompleted();
|
||||
break;
|
||||
case 'message-transfer-complete':
|
||||
this._onMessageTransferCompleted();
|
||||
break;
|
||||
case 'text':
|
||||
this._onTextReceived(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onFileHeader(header) {
|
||||
this._lastProgress = 0;
|
||||
this._digester = new FileDigester({
|
||||
name: header.name,
|
||||
mime: header.mime,
|
||||
size: header.size
|
||||
}, file => this._onFileReceived(file));
|
||||
}
|
||||
|
||||
_onChunkReceived(chunk) {
|
||||
if(!(chunk.byteLength || chunk.size)) return;
|
||||
|
||||
this._digester.unchunk(chunk);
|
||||
const progress = this._digester.progress;
|
||||
this._onDownloadProgress(progress);
|
||||
|
||||
// occasionally notify sender about our progress
|
||||
if (progress - this._lastProgress < 0.01) return;
|
||||
this._lastProgress = progress;
|
||||
this._sendProgress(progress);
|
||||
}
|
||||
|
||||
_onDownloadProgress(progress) {
|
||||
Events.fire('file-progress', { sender: this._peerId, progress: progress });
|
||||
}
|
||||
|
||||
_onFileReceived(proxyFile) {
|
||||
Events.fire('file-received', proxyFile);
|
||||
this.sendJSON({ type: 'file-transfer-complete' });
|
||||
}
|
||||
|
||||
_onFileTransferCompleted() {
|
||||
this._onDownloadProgress(1);
|
||||
this._reader = null;
|
||||
this._busy = false;
|
||||
this._dequeueFile();
|
||||
Events.fire('notify-user', 'File transfer completed.');
|
||||
}
|
||||
|
||||
_onMessageTransferCompleted() {
|
||||
Events.fire('notify-user', 'Message transfer completed.');
|
||||
}
|
||||
|
||||
sendText(text) {
|
||||
const unescaped = btoa(unescape(encodeURIComponent(text)));
|
||||
this.sendJSON({ type: 'text', text: unescaped });
|
||||
}
|
||||
|
||||
_onTextReceived(message) {
|
||||
const escaped = decodeURIComponent(escape(atob(message.text)));
|
||||
Events.fire('text-received', { text: escaped, sender: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId) {
|
||||
super(serverConnection, peerId);
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._connect(peerId, true);
|
||||
}
|
||||
|
||||
_connect(peerId, isCaller) {
|
||||
if (!this._conn) this._openConnection(peerId, isCaller);
|
||||
|
||||
if (isCaller) {
|
||||
this._openChannel();
|
||||
} else {
|
||||
this._conn.ondatachannel = e => this._onChannelOpened(e);
|
||||
}
|
||||
}
|
||||
|
||||
_openConnection(peerId, isCaller) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._conn = new RTCPeerConnection(RTCPeer.config);
|
||||
this._conn.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._conn.onconnectionstatechange = e => this._onConnectionStateChange(e);
|
||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||
}
|
||||
|
||||
_openChannel() {
|
||||
const channel = this._conn.createDataChannel('data-channel', {
|
||||
ordered: true,
|
||||
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
|
||||
});
|
||||
channel.onopen = e => this._onChannelOpened(e);
|
||||
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
|
||||
}
|
||||
|
||||
_onDescription(description) {
|
||||
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
|
||||
this._conn.setLocalDescription(description)
|
||||
.then(_ => this._sendSignal({ sdp: description }))
|
||||
.catch(e => this._onError(e));
|
||||
}
|
||||
|
||||
_onIceCandidate(event) {
|
||||
if (!event.candidate) return;
|
||||
this._sendSignal({ ice: event.candidate });
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
if (!this._conn) this._connect(message.sender, false);
|
||||
|
||||
if (message.sdp) {
|
||||
this._conn.setRemoteDescription(new RTCSessionDescription(message.sdp))
|
||||
.then( _ => {
|
||||
if (message.sdp.type === 'offer') {
|
||||
return this._conn.createAnswer()
|
||||
.then(d => this._onDescription(d));
|
||||
}
|
||||
})
|
||||
.catch(e => this._onError(e));
|
||||
} else if (message.ice) {
|
||||
this._conn.addIceCandidate(new RTCIceCandidate(message.ice));
|
||||
}
|
||||
}
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', this._peerId);
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = e => this._onChannelClosed();
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
console.log('RTC: channel closed', this._peerId);
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
if (!this._isCaller) return;
|
||||
this._connect(this._peerId, true); // reopen the channel
|
||||
}
|
||||
|
||||
_onConnectionStateChange(e) {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
this._onChannelClosed();
|
||||
break;
|
||||
case 'failed':
|
||||
this._conn = null;
|
||||
this._onChannelClosed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onIceConnectionStateChange() {
|
||||
switch (this._conn.iceConnectionState) {
|
||||
case 'failed':
|
||||
console.error('ICE Gathering failed');
|
||||
Events.fire('force-disconnect');
|
||||
break;
|
||||
default:
|
||||
console.log('ICE Gathering', this._conn.iceConnectionState);
|
||||
}
|
||||
}
|
||||
|
||||
_onError(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
_send(message) {
|
||||
if (!this._channel) return this.refresh();
|
||||
this._channel.send(message);
|
||||
}
|
||||
|
||||
_sendSignal(signal) {
|
||||
signal.type = 'signal';
|
||||
signal.to = this._peerId;
|
||||
this._server.send(signal);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// check if channel is open. otherwise create one
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
this._connect(this._peerId, this._isCaller);
|
||||
}
|
||||
|
||||
_isConnected() {
|
||||
return this._channel && this._channel.readyState === 'open';
|
||||
}
|
||||
|
||||
_isConnecting() {
|
||||
return this._channel && this._channel.readyState === 'connecting';
|
||||
}
|
||||
}
|
||||
|
||||
class PeersManager {
|
||||
|
||||
constructor(serverConnection) {
|
||||
this.peers = {};
|
||||
this._server = serverConnection;
|
||||
Events.on('signal', e => this._onMessage(e.detail));
|
||||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('files-selected', e => this._onFilesSelected(e.detail));
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('disconnect', this._clearPeers);
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (!this.peers[message.sender]) {
|
||||
this.peers[message.sender] = new RTCPeer(this._server);
|
||||
}
|
||||
this.peers[message.sender].onServerMessage(message);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
peers.forEach(peer => {
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].refresh();
|
||||
return;
|
||||
}
|
||||
if (window.isRtcSupported && peer.rtcSupported) {
|
||||
this.peers[peer.id] = new RTCPeer(this._server, peer.id);
|
||||
} else {
|
||||
this.peers[peer.id] = new WSPeer(this._server, peer.id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onFilesSelected(message) {
|
||||
this.peers[message.to].sendFiles(message.files);
|
||||
}
|
||||
|
||||
_onSendText(message) {
|
||||
this.peers[message.to].sendText(message.text);
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
if (!peer || !peer._peer) return;
|
||||
peer._peer.close();
|
||||
}
|
||||
|
||||
_clearPeers() {
|
||||
if (this.peers) {
|
||||
Object.keys(this.peers).forEach(peerId => this._onPeerLeft(peerId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WSPeer extends Peer {
|
||||
_send(message) {
|
||||
message.to = this._peerId;
|
||||
this._server.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
|
||||
constructor(file, onChunk, onPartitionEnd) {
|
||||
this._chunkSize = 64000; // 64 KB
|
||||
this._maxPartitionSize = 1e6; // 1 MB
|
||||
this._offset = 0;
|
||||
this._partitionSize = 0;
|
||||
this._file = file;
|
||||
this._onChunk = onChunk;
|
||||
this._onPartitionEnd = onPartitionEnd;
|
||||
this._reader = new FileReader();
|
||||
this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));
|
||||
}
|
||||
|
||||
nextPartition() {
|
||||
this._partitionSize = 0;
|
||||
this._readChunk();
|
||||
}
|
||||
|
||||
_readChunk() {
|
||||
const chunk = this._file.slice(this._offset, this._offset + this._chunkSize);
|
||||
this._reader.readAsArrayBuffer(chunk);
|
||||
}
|
||||
|
||||
_onChunkRead(chunk) {
|
||||
this._offset += chunk.byteLength;
|
||||
this._partitionSize += chunk.byteLength;
|
||||
this._onChunk(chunk);
|
||||
if (this.isFileEnd()) return;
|
||||
if (this._isPartitionEnd()) {
|
||||
this._onPartitionEnd(this._offset);
|
||||
return;
|
||||
}
|
||||
this._readChunk();
|
||||
}
|
||||
|
||||
repeatPartition() {
|
||||
this._offset -= this._partitionSize;
|
||||
this._nextPartition();
|
||||
}
|
||||
|
||||
_isPartitionEnd() {
|
||||
return this._partitionSize >= this._maxPartitionSize;
|
||||
}
|
||||
|
||||
isFileEnd() {
|
||||
return this._offset >= this._file.size;
|
||||
}
|
||||
|
||||
get progress() {
|
||||
return this._offset / this._file.size;
|
||||
}
|
||||
}
|
||||
|
||||
class FileDigester {
|
||||
|
||||
constructor(meta, callback) {
|
||||
this._buffer = [];
|
||||
this._bytesReceived = 0;
|
||||
this._size = meta.size;
|
||||
this._mime = meta.mime || 'application/octet-stream';
|
||||
this._name = meta.name;
|
||||
this._callback = callback;
|
||||
}
|
||||
|
||||
unchunk(chunk) {
|
||||
this._buffer.push(chunk);
|
||||
this._bytesReceived += chunk.byteLength || chunk.size;
|
||||
const totalChunks = this._buffer.length;
|
||||
this.progress = this._bytesReceived / this._size;
|
||||
if (isNaN(this.progress)) this.progress = 1
|
||||
|
||||
if (this._bytesReceived < this._size) return;
|
||||
// we are done
|
||||
let blob = new Blob(this._buffer, { type: this._mime });
|
||||
this._callback({
|
||||
name: this._name,
|
||||
mime: this._mime,
|
||||
size: this._size,
|
||||
blob: blob
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Events {
|
||||
static fire(type, detail) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback) {
|
||||
return window.addEventListener(type, callback, false);
|
||||
}
|
||||
|
||||
static off(type, callback) {
|
||||
return window.removeEventListener(type, callback, false);
|
||||
}
|
||||
}
|
||||
|
||||
RTCPeer.config = {
|
||||
'sdpSemantics': 'unified-plan',
|
||||
// iceServers: [
|
||||
// {
|
||||
// urls: 'stun:127.0.0.1:3478',
|
||||
// },
|
||||
// {
|
||||
// urls: 'turn:127.0.0.1:3478',
|
||||
// username: 'snapdrop',
|
||||
// credential: 'ifupvrwelijmoyjxmefcsvfxxmcphvxo'
|
||||
// }
|
||||
// ]
|
||||
iceServers: [
|
||||
{
|
||||
urls: "stun:relay.metered.ca:80",
|
||||
},
|
||||
{
|
||||
urls: "turn:relay.metered.ca:80",
|
||||
username: "411061cd290de7ca6cc1a753",
|
||||
credential: "CuCIGdVfA9Gias1E",
|
||||
},
|
||||
{
|
||||
urls: "turn:relay.metered.ca:443",
|
||||
username: "411061cd290de7ca6cc1a753",
|
||||
credential: "CuCIGdVfA9Gias1E",
|
||||
},
|
||||
],
|
||||
// iceServers: [
|
||||
// {
|
||||
// urls: 'stun:stun.l.google.com:19302'
|
||||
// },
|
||||
// {
|
||||
// urls: 'turn:om.wulingate.com',
|
||||
// username: 'hmzJ0OHZivkod703',
|
||||
// credential: 'KDF04PBYD9xHAp0s'
|
||||
// },
|
||||
// ]
|
||||
}
|
37
public/scripts/theme.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
(function(){
|
||||
|
||||
// Select the button
|
||||
const btnTheme = document.getElementById('theme');
|
||||
// Check for dark mode preference at the OS level
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
// Get the user's theme preference from local storage, if it's available
|
||||
const currentTheme = localStorage.getItem('theme');
|
||||
// If the user's preference in localStorage is dark...
|
||||
if (currentTheme == 'dark') {
|
||||
// ...let's toggle the .dark-theme class on the body
|
||||
document.body.classList.toggle('dark-theme');
|
||||
// Otherwise, if the user's preference in localStorage is light...
|
||||
} else if (currentTheme == 'light') {
|
||||
// ...let's toggle the .light-theme class on the body
|
||||
document.body.classList.toggle('light-theme');
|
||||
}
|
||||
|
||||
// Listen for a click on the button
|
||||
btnTheme.addEventListener('click', function() {
|
||||
// If the user's OS setting is dark and matches our .dark-theme class...
|
||||
if (prefersDarkScheme.matches) {
|
||||
// ...then toggle the light mode class
|
||||
document.body.classList.toggle('light-theme');
|
||||
// ...but use .dark-theme if the .light-theme class is already on the body,
|
||||
var theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
|
||||
} else {
|
||||
// Otherwise, let's do the same thing, but for .dark-theme
|
||||
document.body.classList.toggle('dark-theme');
|
||||
var theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||
}
|
||||
// Finally, let's save the current preference to localStorage to keep using it
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
})();
|
666
public/scripts/ui.js
Normal file
|
@ -0,0 +1,666 @@
|
|||
const $ = query => document.getElementById(query);
|
||||
const $$ = query => document.body.querySelector(query);
|
||||
const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase());
|
||||
window.isDownloadSupported = (typeof document.createElement('a').download !== 'undefined');
|
||||
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
|
||||
// set display name
|
||||
Events.on('display-name', e => {
|
||||
const me = e.detail.message;
|
||||
const $displayName = $('displayName')
|
||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
||||
$displayName.title = me.deviceName;
|
||||
});
|
||||
|
||||
class PeersUI {
|
||||
|
||||
constructor() {
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peer-connected', e => this._onPeerConnected(e.detail));
|
||||
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('file-progress', e => this._onFileProgress(e.detail));
|
||||
Events.on('paste', e => this._onPaste(e));
|
||||
Events.on('offline', () => this._clearPeers());
|
||||
this.peers = {};
|
||||
}
|
||||
|
||||
_onPeerJoined(peer) {
|
||||
if (this.peers[peer.id]) return; // peer already exists
|
||||
this.peers[peer.id] = peer;
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
if(this.peers[peerId])
|
||||
new PeerUI(this.peers[peerId]);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
this._clearPeers();
|
||||
peers.forEach(peer => this._onPeerJoined(peer));
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
this._onPeerDisconnected(peerId);
|
||||
delete this.peers[peerId];
|
||||
}
|
||||
|
||||
_onFileProgress(progress) {
|
||||
const peerId = progress.sender || progress.recipient;
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.ui.setProgress(progress.progress);
|
||||
}
|
||||
|
||||
_clearPeers() {
|
||||
const $peers = $$('x-peers').innerHTML = '';
|
||||
Object.keys(this.peers).forEach(peerId => delete this.peers[peerId]);
|
||||
}
|
||||
|
||||
_onPaste(e) {
|
||||
const files = e.clipboardData.files || e.clipboardData.items
|
||||
.filter(i => i.type.indexOf('image') > -1)
|
||||
.map(i => i.getAsFile());
|
||||
const peers = document.querySelectorAll('x-peer');
|
||||
// send the pasted image content to the only peer if there is one
|
||||
// otherwise, select the peer somehow by notifying the client that
|
||||
// "image data has been pasted, click the client to which to send it"
|
||||
// not implemented
|
||||
if (files.length > 0 && peers.length === 1) {
|
||||
Events.fire('files-selected', {
|
||||
files: files,
|
||||
to: $$('x-peer').id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PeerUI {
|
||||
|
||||
html() {
|
||||
return `
|
||||
<label class="column center" title="Click to send files or right click to send a text">
|
||||
<input type="file" multiple>
|
||||
<x-icon shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
</x-icon>
|
||||
<div class="progress">
|
||||
<div class="circle"></div>
|
||||
<div class="circle right"></div>
|
||||
</div>
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="device-name font-body2"></div>
|
||||
<div class="status font-body2"></div>
|
||||
</label>`
|
||||
}
|
||||
|
||||
constructor(peer) {
|
||||
this._peer = peer;
|
||||
this._initDom();
|
||||
this._bindListeners(this.$el);
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
setTimeout(e => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
const el = document.createElement('x-peer');
|
||||
el.id = this._peer.id;
|
||||
el.innerHTML = this.html();
|
||||
el.ui = this;
|
||||
el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
el.querySelector('.name').textContent = this._displayName();
|
||||
el.querySelector('.device-name').textContent = this._deviceName();
|
||||
this.$el = el;
|
||||
this.$progress = el.querySelector('.progress');
|
||||
}
|
||||
|
||||
_bindListeners(el) {
|
||||
el.querySelector('input').addEventListener('change', e => this._onFilesSelected(e));
|
||||
el.addEventListener('drop', e => this._onDrop(e));
|
||||
el.addEventListener('dragend', e => this._onDragEnd(e));
|
||||
el.addEventListener('dragleave', e => this._onDragEnd(e));
|
||||
el.addEventListener('dragover', e => this._onDragOver(e));
|
||||
el.addEventListener('contextmenu', e => this._onRightClick(e));
|
||||
el.addEventListener('touchstart', e => this._onTouchStart(e));
|
||||
el.addEventListener('touchend', e => this._onTouchEnd(e));
|
||||
// prevent browser's default file drop behavior
|
||||
Events.on('dragover', e => e.preventDefault());
|
||||
Events.on('drop', e => e.preventDefault());
|
||||
}
|
||||
|
||||
_displayName() {
|
||||
return this._peer.name.displayName;
|
||||
}
|
||||
|
||||
_deviceName() {
|
||||
return this._peer.name.deviceName;
|
||||
}
|
||||
|
||||
_icon() {
|
||||
const device = this._peer.name.device || this._peer.name;
|
||||
if (device.type === 'mobile') {
|
||||
return '#phone-iphone';
|
||||
}
|
||||
if (device.type === 'tablet') {
|
||||
return '#tablet-mac';
|
||||
}
|
||||
return '#desktop-mac';
|
||||
}
|
||||
|
||||
_onFilesSelected(e) {
|
||||
const $input = e.target;
|
||||
const files = $input.files;
|
||||
Events.fire('files-selected', {
|
||||
files: files,
|
||||
to: this._peer.id
|
||||
});
|
||||
$input.value = null; // reset input
|
||||
}
|
||||
|
||||
setProgress(progress) {
|
||||
if (progress > 0) {
|
||||
this.$el.setAttribute('transfer', '1');
|
||||
}
|
||||
if (progress > 0.5) {
|
||||
this.$progress.classList.add('over50');
|
||||
} else {
|
||||
this.$progress.classList.remove('over50');
|
||||
}
|
||||
const degrees = `rotate(${360 * progress}deg)`;
|
||||
this.$progress.style.setProperty('--progress', degrees);
|
||||
if (progress >= 1) {
|
||||
this.setProgress(0);
|
||||
this.$el.removeAttribute('transfer');
|
||||
}
|
||||
}
|
||||
|
||||
_onDrop(e) {
|
||||
e.preventDefault();
|
||||
const files = e.dataTransfer.files;
|
||||
Events.fire('files-selected', {
|
||||
files: files,
|
||||
to: this._peer.id
|
||||
});
|
||||
this._onDragEnd();
|
||||
}
|
||||
|
||||
_onDragOver() {
|
||||
this.$el.setAttribute('drop', 1);
|
||||
}
|
||||
|
||||
_onDragEnd() {
|
||||
this.$el.removeAttribute('drop');
|
||||
}
|
||||
|
||||
_onRightClick(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
}
|
||||
|
||||
_onTouchStart(e) {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
}
|
||||
|
||||
_onTouchEnd(e) {
|
||||
if (Date.now() - this._touchStart < 500) {
|
||||
clearTimeout(this._touchTimer);
|
||||
} else { // this was a long tap
|
||||
if (e) e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Dialog {
|
||||
constructor(id) {
|
||||
this.$el = $(id);
|
||||
this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', e => this.hide()))
|
||||
this.$el.querySelectorAll('[role="textbox"]').forEach((el) => {
|
||||
el.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
})
|
||||
this.$autoFocus = this.$el.querySelector('[autofocus]');
|
||||
}
|
||||
|
||||
show() {
|
||||
this.$el.setAttribute('show', 1);
|
||||
if (this.$autoFocus) this.$autoFocus.focus();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$el.removeAttribute('show');
|
||||
document.activeElement.blur();
|
||||
window.blur();
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveDialog extends Dialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveDialog');
|
||||
Events.on('file-received', e => {
|
||||
this._nextFile(e.detail);
|
||||
window.blop.play();
|
||||
});
|
||||
this._filesQueue = [];
|
||||
}
|
||||
|
||||
_nextFile(nextFile) {
|
||||
if (nextFile) this._filesQueue.push(nextFile);
|
||||
if (this._busy) return;
|
||||
this._busy = true;
|
||||
const file = this._filesQueue.shift();
|
||||
this._displayFile(file);
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
if (!this._filesQueue.length) { // nothing to do
|
||||
this._busy = false;
|
||||
return;
|
||||
}
|
||||
// dequeue next file
|
||||
setTimeout(_ => {
|
||||
this._busy = false;
|
||||
this._nextFile();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_displayFile(file) {
|
||||
const $a = this.$el.querySelector('#download');
|
||||
const url = URL.createObjectURL(file.blob);
|
||||
$a.href = url;
|
||||
$a.download = file.name;
|
||||
|
||||
if(this._autoDownload()){
|
||||
$a.click()
|
||||
return
|
||||
}
|
||||
if(file.mime.split('/')[0] === 'image'){
|
||||
console.log('the file is image');
|
||||
this.$el.querySelector('.preview').style.visibility = 'inherit';
|
||||
this.$el.querySelector("#img-preview").src = url;
|
||||
}
|
||||
|
||||
this.$el.querySelector('#fileName').textContent = file.name;
|
||||
this.$el.querySelector('#fileSize').textContent = this._formatFileSize(file.size);
|
||||
this.show();
|
||||
|
||||
if (window.isDownloadSupported) return;
|
||||
// fallback for iOS
|
||||
$a.target = '_blank';
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => $a.href = reader.result;
|
||||
reader.readAsDataURL(file.blob);
|
||||
}
|
||||
|
||||
_formatFileSize(bytes) {
|
||||
if (bytes >= 1e9) {
|
||||
return (Math.round(bytes / 1e8) / 10) + ' GB';
|
||||
} else if (bytes >= 1e6) {
|
||||
return (Math.round(bytes / 1e5) / 10) + ' MB';
|
||||
} else if (bytes > 1000) {
|
||||
return Math.round(bytes / 1000) + ' KB';
|
||||
} else {
|
||||
return bytes + ' Bytes';
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$el.querySelector('.preview').style.visibility = 'hidden';
|
||||
this.$el.querySelector("#img-preview").src = "";
|
||||
super.hide();
|
||||
this._dequeueFile();
|
||||
}
|
||||
|
||||
|
||||
_autoDownload(){
|
||||
return !this.$el.querySelector('#autoDownload').checked
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail))
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
const button = this.$el.querySelector('form');
|
||||
button.addEventListener('submit', e => this._send(e));
|
||||
}
|
||||
|
||||
_onRecipient(recipient) {
|
||||
this._recipient = recipient;
|
||||
this._handleShareTargetText();
|
||||
this.show();
|
||||
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
|
||||
range.selectNodeContents(this.$text);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
}
|
||||
|
||||
_handleShareTargetText() {
|
||||
if (!window.shareTargetText) return;
|
||||
this.$text.textContent = window.shareTargetText;
|
||||
window.shareTargetText = '';
|
||||
}
|
||||
|
||||
_send(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('send-text', {
|
||||
to: this._recipient,
|
||||
text: this.$text.innerText
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('receiveTextDialog');
|
||||
Events.on('text-received', e => this._onText(e.detail))
|
||||
this.$text = this.$el.querySelector('#text');
|
||||
const $copy = this.$el.querySelector('#copy');
|
||||
copy.addEventListener('click', _ => this._onCopy());
|
||||
}
|
||||
|
||||
_onText(e) {
|
||||
this.$text.innerHTML = '';
|
||||
const text = e.text;
|
||||
if (isURL(text)) {
|
||||
const $a = document.createElement('a');
|
||||
$a.href = text;
|
||||
$a.target = '_blank';
|
||||
$a.textContent = text;
|
||||
this.$text.appendChild($a);
|
||||
} else {
|
||||
this.$text.textContent = text;
|
||||
}
|
||||
this.show();
|
||||
window.blop.play();
|
||||
}
|
||||
|
||||
async _onCopy() {
|
||||
await navigator.clipboard.writeText(this.$text.textContent);
|
||||
Events.fire('notify-user', 'Copied to clipboard');
|
||||
}
|
||||
}
|
||||
|
||||
class Toast extends Dialog {
|
||||
constructor() {
|
||||
super('toast');
|
||||
Events.on('notify-user', e => this._onNotfiy(e.detail));
|
||||
}
|
||||
|
||||
_onNotfiy(message) {
|
||||
this.$el.textContent = message;
|
||||
this.show();
|
||||
setTimeout(_ => this.hide(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Notifications {
|
||||
|
||||
constructor() {
|
||||
// Check if the browser supports notifications
|
||||
if (!('Notification' in window)) return;
|
||||
|
||||
// Check whether notification permissions have already been granted
|
||||
if (Notification.permission !== 'granted') {
|
||||
this.$button = $('notification');
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', e => this._requestPermission());
|
||||
}
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text));
|
||||
Events.on('file-received', e => this._downloadNotification(e.detail.name));
|
||||
}
|
||||
|
||||
_requestPermission() {
|
||||
Notification.requestPermission(permission => {
|
||||
if (permission !== 'granted') {
|
||||
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
|
||||
return;
|
||||
}
|
||||
this._notify('Even more snappy sharing!');
|
||||
this.$button.setAttribute('hidden', 1);
|
||||
});
|
||||
}
|
||||
|
||||
_notify(message, body) {
|
||||
const config = {
|
||||
body: body,
|
||||
icon: '/images/logo_transparent_128x128.png',
|
||||
}
|
||||
let notification;
|
||||
try {
|
||||
notification = new Notification(message, config);
|
||||
} catch (e) {
|
||||
// Android doesn't support "new Notification" if service worker is installed
|
||||
if (!serviceWorker || !serviceWorker.showNotification) return;
|
||||
notification = serviceWorker.showNotification(message, config);
|
||||
}
|
||||
|
||||
// Notification is persistent on Android. We have to close it manually
|
||||
const visibilitychangeHandler = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
notification.close();
|
||||
Events.off('visibilitychange', visibilitychangeHandler);
|
||||
}
|
||||
};
|
||||
Events.on('visibilitychange', visibilitychangeHandler);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
_messageNotification(message) {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
if (isURL(message)) {
|
||||
const notification = this._notify(message, 'Click to open link');
|
||||
this._bind(notification, e => window.open(message, '_blank', null, true));
|
||||
} else {
|
||||
const notification = this._notify(message, 'Click to copy text');
|
||||
this._bind(notification, e => this._copyText(message, notification));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_downloadNotification(message) {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
const notification = this._notify(message, 'Click to download');
|
||||
if (!window.isDownloadSupported) return;
|
||||
this._bind(notification, e => this._download(notification));
|
||||
}
|
||||
}
|
||||
|
||||
_download(notification) {
|
||||
document.querySelector('x-dialog [download]').click();
|
||||
notification.close();
|
||||
}
|
||||
|
||||
_copyText(message, notification) {
|
||||
notification.close();
|
||||
if (!navigator.clipboard.writeText(message)) return;
|
||||
this._notify('Copied text to clipboard');
|
||||
}
|
||||
|
||||
_bind(notification, handler) {
|
||||
if (notification.then) {
|
||||
notification.then(e => serviceWorker.getNotifications().then(notifications => {
|
||||
serviceWorker.addEventListener('notificationclick', handler);
|
||||
}));
|
||||
} else {
|
||||
notification.onclick = handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NetworkStatusUI {
|
||||
|
||||
constructor() {
|
||||
Events.on('offline', this._showOfflineMessage);
|
||||
Events.on('online', this._showOnlineMessage);
|
||||
if (!navigator.onLine) this._showOfflineMessage();
|
||||
}
|
||||
|
||||
_showOfflineMessage() {
|
||||
Events.fire('notify-user', 'You are offline');
|
||||
}
|
||||
|
||||
_showOnlineMessage() {
|
||||
Events.fire('notify-user', 'You are back online');
|
||||
}
|
||||
}
|
||||
|
||||
class WebShareTargetUI {
|
||||
constructor() {
|
||||
const parsedUrl = new URL(window.location);
|
||||
const title = parsedUrl.searchParams.get('title');
|
||||
const text = parsedUrl.searchParams.get('text');
|
||||
const url = parsedUrl.searchParams.get('url');
|
||||
|
||||
let shareTargetText = title ? title : '';
|
||||
shareTargetText += text ? shareTargetText ? ' ' + text : text : '';
|
||||
|
||||
if(url) shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable.
|
||||
|
||||
if (!shareTargetText) return;
|
||||
window.shareTargetText = shareTargetText;
|
||||
history.pushState({}, 'URL Rewrite', '/');
|
||||
console.log('Shared Target Text:', '"' + shareTargetText + '"');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Snapdrop {
|
||||
constructor() {
|
||||
const server = new ServerConnection();
|
||||
const peers = new PeersManager(server);
|
||||
const peersUI = new PeersUI();
|
||||
Events.on('load', e => {
|
||||
const receiveDialog = new ReceiveDialog();
|
||||
const sendTextDialog = new SendTextDialog();
|
||||
const receiveTextDialog = new ReceiveTextDialog();
|
||||
const toast = new Toast();
|
||||
const notifications = new Notifications();
|
||||
const networkStatusUI = new NetworkStatusUI();
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const snapdrop = new Snapdrop();
|
||||
|
||||
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.then(serviceWorker => {
|
||||
console.log('Service Worker registered');
|
||||
window.serviceWorker = serviceWorker
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', e => {
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
// don't display install banner when installed
|
||||
return e.preventDefault();
|
||||
} else {
|
||||
const btn = document.querySelector('#install')
|
||||
btn.hidden = false;
|
||||
btn.onclick = _ => e.prompt();
|
||||
return e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Background Animation
|
||||
Events.on('load', () => {
|
||||
let c = document.createElement('canvas');
|
||||
document.body.appendChild(c);
|
||||
let style = c.style;
|
||||
style.width = '100%';
|
||||
style.position = 'absolute';
|
||||
style.zIndex = -1;
|
||||
style.top = 0;
|
||||
style.left = 0;
|
||||
let ctx = c.getContext('2d');
|
||||
let x0, y0, w, h, dw;
|
||||
|
||||
function init() {
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
c.width = w;
|
||||
c.height = h;
|
||||
let offset = h > 380 ? 100 : 65;
|
||||
offset = h > 800 ? 116 : offset;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.max(w, h, 1000) / 13;
|
||||
drawCircles();
|
||||
}
|
||||
window.onresize = init;
|
||||
|
||||
function drawCircle(radius) {
|
||||
ctx.beginPath();
|
||||
let color = Math.round(255 * (1 - radius / Math.max(w, h)));
|
||||
ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)';
|
||||
ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
ctx.lineWidth = 2;
|
||||
}
|
||||
|
||||
let step = 0;
|
||||
|
||||
function drawCircles() {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
drawCircle(dw * i + step % dw);
|
||||
}
|
||||
step += 1;
|
||||
}
|
||||
|
||||
let loading = true;
|
||||
|
||||
function animate() {
|
||||
if (loading || step % dw < dw - 5) {
|
||||
requestAnimationFrame(function() {
|
||||
drawCircles();
|
||||
animate();
|
||||
});
|
||||
}
|
||||
}
|
||||
window.animateBackground = function(l) {
|
||||
loading = l;
|
||||
animate();
|
||||
};
|
||||
init();
|
||||
animate();
|
||||
});
|
||||
|
||||
Notifications.PERMISSION_ERROR = `
|
||||
Notifications permission has been blocked
|
||||
as the user has dismissed the permission prompt several times.
|
||||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = e => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
}
|
57
public/service-worker.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
var CACHE_NAME = 'snapdrop-cache-v2';
|
||||
var urlsToCache = [
|
||||
'index.html',
|
||||
'./',
|
||||
'styles.css',
|
||||
'scripts/network.js',
|
||||
'scripts/ui.js',
|
||||
'scripts/clipboard.js',
|
||||
'scripts/theme.js',
|
||||
'sounds/blop.mp3',
|
||||
'images/favicon-96x96.png'
|
||||
];
|
||||
|
||||
self.addEventListener('install', function(event) {
|
||||
// Perform install steps
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(function(cache) {
|
||||
console.log('Opened cache');
|
||||
return cache.addAll(urlsToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
self.addEventListener('fetch', function(event) {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(function(response) {
|
||||
// Cache hit - return response
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
self.addEventListener('activate', function(event) {
|
||||
console.log('Updating Service Worker...')
|
||||
event.waitUntil(
|
||||
caches.keys().then(function(cacheNames) {
|
||||
return Promise.all(
|
||||
cacheNames.filter(function(cacheName) {
|
||||
// Return true if you want to remove this cache,
|
||||
// but remember that caches are shared across
|
||||
// the whole origin
|
||||
return true
|
||||
}).map(function(cacheName) {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
BIN
public/sounds/blop.mp3
Normal file
BIN
public/sounds/blop.ogg
Normal file
757
public/styles.css
Normal file
|
@ -0,0 +1,757 @@
|
|||
/* Constants */
|
||||
|
||||
:root {
|
||||
--icon-size: 24px;
|
||||
--primary-color: #4285f4;
|
||||
--peer-width: 120px;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
body {
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.full {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Typography */
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 34px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -.01em;
|
||||
line-height: 40px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -.012em;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.font-subheading {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.font-body1,
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.font-body2 {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Icons */
|
||||
|
||||
.icon {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Shadows */
|
||||
|
||||
[shadow="1"] {
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 8px 0 rgba(0, 0, 0, 0.12),
|
||||
0 3px 3px -2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[shadow="2"] {
|
||||
box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 10px 0 rgba(0, 0, 0, 0.12),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Animations */
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Main Header */
|
||||
|
||||
body>header a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Peers List */
|
||||
|
||||
x-peers {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex-flow: row wrap;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Empty Peers List */
|
||||
|
||||
x-no-peers {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
animation: fade-in 300ms;
|
||||
animation-delay: 500ms;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
|
||||
x-no-peers h2,
|
||||
x-no-peers a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
x-peers:not(:empty)+x-no-peers {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Peer */
|
||||
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
x-peer .name {
|
||||
width: var(--peer-width);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
x-peer x-icon {
|
||||
--icon-size: 40px;
|
||||
width: var(--icon-size);
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer:not([transfer]):hover x-icon,
|
||||
x-peer:not([transfer]):focus x-icon {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
x-peer[transfer] x-icon {
|
||||
box-shadow: none;
|
||||
opacity: 0.8;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.status,
|
||||
.device-name {
|
||||
height: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
x-peer[transfer] .status:before {
|
||||
content: 'Transferring...';
|
||||
}
|
||||
|
||||
x-peer:not([transfer]) .status,
|
||||
x-peer[transfer] .device-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer x-icon {
|
||||
animation: pop 600ms ease-out 1;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
x-peer[drop] x-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Footer */
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 80px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
background: rgba(0, 0, 0, 0.61);
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
z-index: 3;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) x-paper {
|
||||
transform: scale(0.1);
|
||||
}
|
||||
|
||||
x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 16px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
x-dialog a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Receive Dialog */
|
||||
#receiveDialog .row {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Receive Text Dialog */
|
||||
|
||||
#receiveTextDialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#receiveTextDialog #text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#receiveTextDialog h3 {
|
||||
/* Select the received text when double-clicking the dialog */
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
|
||||
.button {
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.button,
|
||||
.icon-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
touch-action: manipulation;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button:before,
|
||||
.icon-button:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: currentColor;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
.button:hover:before,
|
||||
.icon-button:hover:before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.button:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.button:focus:before,
|
||||
.icon-button:focus:before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.icon-button:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
.textarea {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16px 24px;
|
||||
border-radius: 16px;
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
background: #f1f3f4;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
|
||||
/* Info Animation */
|
||||
|
||||
#about {
|
||||
color: white;
|
||||
z-index: 11;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#about .fade-in {
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
transition-delay: 300ms;
|
||||
z-index: 11;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#about:not(:target) .fade-in {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition-delay: 0;
|
||||
}
|
||||
|
||||
#about .logo {
|
||||
--icon-size: 96px;
|
||||
}
|
||||
|
||||
#about x-background {
|
||||
position: absolute;
|
||||
top: calc(32px - 250px);
|
||||
right: calc(32px - 250px);
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
transform: scale(0);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Hack such that initial scale(0) isn't animated */
|
||||
#about x-background {
|
||||
will-change: transform;
|
||||
transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
|
||||
#about:target x-background {
|
||||
transform: scale(10);
|
||||
}
|
||||
|
||||
#about .row a {
|
||||
margin: 8px 8px -16px;
|
||||
}
|
||||
|
||||
|
||||
/* Loading Indicator */
|
||||
|
||||
.progress {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border: 4px solid var(--primary-color);
|
||||
border-radius: 40px;
|
||||
position: absolute;
|
||||
clip: rect(0px, 40px, 80px, 0px);
|
||||
will-change: transform;
|
||||
transform: var(--progress);
|
||||
}
|
||||
|
||||
.over50 {
|
||||
clip: rect(auto, auto, auto, auto);
|
||||
}
|
||||
|
||||
.over50 .circle.right {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
/* Generic placeholder */
|
||||
[placeholder]:empty:before {
|
||||
content: attr(placeholder);
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
|
||||
.toast-container {
|
||||
padding: 0 8px 24px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
z-index: 20;
|
||||
transition: opacity 200ms, transform 300ms ease-out;
|
||||
cursor: default;
|
||||
line-height: 24px;
|
||||
border-radius: 8px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
}
|
||||
|
||||
|
||||
/* Instructions */
|
||||
|
||||
x-instructions {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
z-index: -1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive Styles */
|
||||
|
||||
@media (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px),
|
||||
screen and (min-width: 1100px) {
|
||||
x-instructions:before {
|
||||
content: attr(desktop);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 420px) {
|
||||
x-instructions {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS specific styles
|
||||
*/
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
|
||||
|
||||
html {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Color Themes
|
||||
*/
|
||||
|
||||
/* Default colors */
|
||||
body {
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
body.dark-theme {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--bg-color-secondary: #333;
|
||||
}
|
||||
|
||||
/* Colored Elements */
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color-secondary);
|
||||
}
|
||||
/* Image Preview */
|
||||
#img-preview{
|
||||
max-height: 50vh;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* Styles for users who prefer dark mode at the OS level */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
/* defaults to dark theme */
|
||||
body {
|
||||
--text-color: #eee;
|
||||
--bg-color: #121212;
|
||||
--bg-color-secondary: #333;
|
||||
}
|
||||
|
||||
/* Override dark mode with light mode styles if the user decides to swap */
|
||||
body.light-theme {
|
||||
--text-color: #333;
|
||||
--bg-color: #fafafa;
|
||||
--bg-color-secondary: #f1f3f4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Edge specific styles
|
||||
*/
|
||||
@supports (-ms-ime-align: auto) {
|
||||
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|