Version 2 initial commit
BIN
client/images/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
client/images/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
client/images/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
client/images/favicon-96x96.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
client/images/logo_blue_512x512.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
client/images/logo_transparent_128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
client/images/logo_transparent_512x512.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
client/images/logo_transparent_white_512x512.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
client/images/logo_white_512x512.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
client/images/mstile-150x150.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
251
client/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
client/images/snapdrop-graphics.sketch
Executable file
BIN
client/images/twitter-stream.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
200
client/index.html
Normal file
|
@ -0,0 +1,200 @@
|
|||
<!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="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Snapdrop">
|
||||
<meta name="msapplication-TileColor" content="#3372DF">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<!-- Descriptions -->
|
||||
<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>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open Snapdrop on other devices to send files.</h2>
|
||||
<div class="font-body1">Short link: <a href="http://yg.gl">yg.gl</a></div>
|
||||
</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>The easiest way to transfer data across devices.</div>
|
||||
<div class="font-body2">Allow me to be discovered by: Everyone in 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="row-reverse">
|
||||
<a class="button" close id="download" title="Download File" autofocus>Download</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>
|
||||
<input id="textInput" placeholder="Send a message" autocomplete="off" autofocus>
|
||||
<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>
|
||||
<!-- Info Page -->
|
||||
<div id="info" class="full center column">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close" />
|
||||
</svg>
|
||||
</a>
|
||||
<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">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#github" />
|
||||
</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">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#twitter" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop#frequently-asked-questions" title="frequently asked questions">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#help-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#info" class="icon-button" title="about Snapdrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
<div class="info-background"></div>
|
||||
</a>
|
||||
<a id="notification" class="icon-button" hidden title="enable Notifications">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</a>
|
||||
<!-- 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>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script type="text/javascript" src="scripts/network.js"></script>
|
||||
<script type="text/javascript" src="scripts/ui.js"></script>
|
||||
<!-- Sounds -->
|
||||
<audio id="blop" preload="auto" 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>
|
29
client/manifest.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "Snapdrop",
|
||||
"short_name": "Snapdrop",
|
||||
"icons": [{
|
||||
"src": "images/favicon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "images/apple-touch-icon.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "images/mstile-150x150.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "logo_transparent_white_512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}],
|
||||
"background_color": "#3367d6",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3367d6"
|
||||
}
|
34
client/scripts/network-v2.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
class ServerConnection {
|
||||
|
||||
}
|
||||
|
||||
class Connection {
|
||||
|
||||
}
|
||||
|
||||
class WSConnection extends Connection {
|
||||
|
||||
}
|
||||
|
||||
class RTCConnection extends Connection {
|
||||
|
||||
}
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection) {
|
||||
this._ws = new WSConnection(serverConnection);
|
||||
this._rtc = new RTCConnection(serverConnection);
|
||||
this._fileReceiver = new FileReceiver(this);
|
||||
this._fileSender = new FileSender(this);
|
||||
}
|
||||
|
||||
send(message) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Peers {
|
||||
|
||||
}
|
475
client/scripts/network.js
Normal file
|
@ -0,0 +1,475 @@
|
|||
class ServerConnection {
|
||||
|
||||
constructor() {
|
||||
this._connect();
|
||||
Events.on('beforeunload', e => this._disconnect(), false);
|
||||
}
|
||||
|
||||
_connect() {
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = e => console.log('WS: server connection opened');
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
ws.onclose = e => this._onDisconnect();
|
||||
ws.onerror = e => console.error(e);
|
||||
this._socket = ws;
|
||||
clearTimeout(this._reconnectTimer);
|
||||
}
|
||||
|
||||
_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;
|
||||
default:
|
||||
console.error('WS: unkown message type', msg)
|
||||
}
|
||||
}
|
||||
|
||||
send(message) {
|
||||
if (this._socket.readyState !== this._socket.OPEN) 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 host = location.hostname.startsWith('localhost') ? 'localhost:3000' : (location.host + '/server');
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
const url = protocol + '://' + host + webrtc;
|
||||
return url;
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
this._socket.close();
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
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 'transfer-complete':
|
||||
this._onTransferCompleted();
|
||||
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) {
|
||||
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: 'transfer-complete' });
|
||||
// this._digester = null;
|
||||
}
|
||||
|
||||
_onTransferCompleted() {
|
||||
this._onDownloadProgress(1);
|
||||
this._reader = null;
|
||||
this._busy = false;
|
||||
this._dequeueFile();
|
||||
Events.fire('notify-user', 'File transfer completed.');
|
||||
}
|
||||
|
||||
sendText(text) {
|
||||
this.sendJSON({
|
||||
type: 'text',
|
||||
text: btoa(unescape(encodeURIComponent(text)))
|
||||
});
|
||||
}
|
||||
|
||||
_onTextReceived(message) {
|
||||
Events.fire('text-received', {
|
||||
text: decodeURIComponent(escape(atob(message.text))),
|
||||
sender: this._peerId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId) {
|
||||
super(serverConnection, peerId);
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._start(peerId, true);
|
||||
}
|
||||
|
||||
_start(peerId, isCaller) {
|
||||
if (!this._peer) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._peer = new RTCPeerConnection(RTCPeer.config);
|
||||
this._peer.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._peer.onconnectionstatechange = e => console.log('RTC: state changed:', this._peer.connectionState);
|
||||
}
|
||||
|
||||
if (isCaller) {
|
||||
this._createChannel();
|
||||
} else {
|
||||
this._peer.ondatachannel = e => this._onChannelOpened(e);
|
||||
}
|
||||
}
|
||||
|
||||
_createChannel() {
|
||||
const channel = this._peer.createDataChannel('data-channel', { reliable: true });
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onopen = e => this._onChannelOpened(e)
|
||||
this._peer.createOffer(d => this._onDescription(d), e => this._onError(e));
|
||||
}
|
||||
|
||||
_onDescription(description) {
|
||||
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
|
||||
this._peer.setLocalDescription(description,
|
||||
_ => this._sendSignal({ sdp: description }),
|
||||
e => this._onError(e));
|
||||
}
|
||||
|
||||
_onIceCandidate(event) {
|
||||
if (!event.candidate) return;
|
||||
this._sendSignal({ ice: event.candidate });
|
||||
}
|
||||
|
||||
_sendSignal(signal) {
|
||||
signal.type = 'signal';
|
||||
signal.to = this._peerId;
|
||||
this._server.send(signal);
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
if (!this._peer) this._start(message.sender, false);
|
||||
const conn = this._peer;
|
||||
|
||||
if (message.sdp) {
|
||||
this._peer.setRemoteDescription(new RTCSessionDescription(message.sdp), () => {
|
||||
if (message.sdp.type !== 'offer') return;
|
||||
this._peer.createAnswer(d => this._onDescription(d), e => this._onError(e));
|
||||
}, e => this._onError(e));
|
||||
} else if (message.ice) {
|
||||
this._peer.addIceCandidate(new RTCIceCandidate(message.ice));
|
||||
}
|
||||
}
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
const channel = event.channel || event.target;
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = e => this._onChannelClosed();
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
console.log('RTC: channel closed ', this._peerId);
|
||||
if (!this.isCaller) return;
|
||||
this._start(this._peerId, true); // reopen the channel
|
||||
}
|
||||
|
||||
_send(message) {
|
||||
this._channel.send(message);
|
||||
}
|
||||
|
||||
_onError(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// check if channel open. otherwise create one
|
||||
if (this._peer && this._channel && this._channel.readyState !== 'open') return;
|
||||
this._createChannel(this._peerId, this._isCaller);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
_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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WSPeer {
|
||||
_send(message) {
|
||||
message.to = this._peerId;
|
||||
this._server.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
|
||||
constructor(file, onChunk, onPartitionEnd) {
|
||||
this._chunkSize = 64000;
|
||||
this._maxPartitionSize = 1e6;
|
||||
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._isPartitionEnd() || this.isFileEnd()) {
|
||||
this._onPartitionEnd(this._partitionSize);
|
||||
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 (this._bytesReceived < this._size) return;
|
||||
|
||||
let received = new Blob(this._buffer, { type: this._mime }); // pass a useful mime type here
|
||||
let url = URL.createObjectURL(received);
|
||||
this._callback({
|
||||
name: this._name,
|
||||
mime: this._mime,
|
||||
size: this._size,
|
||||
url: url
|
||||
});
|
||||
this._callback = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Events {
|
||||
static fire(type, detail) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback) {
|
||||
return window.addEventListener(type, callback, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
RTCPeer.config = {
|
||||
'iceServers': [{
|
||||
urls: 'stun:stun.stunprotocol.org:3478'
|
||||
}, {
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
}, {
|
||||
urls: 'turn:turn.bistri.com:80',
|
||||
credential: 'homeo',
|
||||
username: 'homeo'
|
||||
}, {
|
||||
urls: 'turn:turn.anyfirewall.com:443?transport=tcp',
|
||||
credential: 'webrtc',
|
||||
username: 'webrtc'
|
||||
}]
|
||||
}
|
521
client/scripts/ui.js
Normal file
|
@ -0,0 +1,521 @@
|
|||
const $ = query => document.getElementById(query);
|
||||
const $$ = query => document.body.querySelector(query);
|
||||
const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase());
|
||||
const isDownloadSupported = (typeof document.createElement('a').download !== 'undefined');
|
||||
const isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||
|
||||
class PeersUI {
|
||||
|
||||
constructor() {
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('file-progress', e => this._onFileProgress(e.detail));
|
||||
}
|
||||
|
||||
_onPeerJoined(peer) {
|
||||
if (document.getElementById(peer.id)) return;
|
||||
const peerUI = new PeerUI(peer);
|
||||
$$('x-peers').appendChild(peerUI.$el);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
this._clearPeers();
|
||||
peers.forEach(peer => this._onPeerJoined(peer));
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
const peer = $(peerId);
|
||||
if (!peer) return;
|
||||
peer.remove();
|
||||
}
|
||||
|
||||
_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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
class PeerUI {
|
||||
|
||||
html() {
|
||||
return `
|
||||
<label class="column center">
|
||||
<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="status font-body2"></div>
|
||||
</label>`
|
||||
}
|
||||
|
||||
constructor(peer) {
|
||||
this._peer = peer;
|
||||
this._initDom();
|
||||
this._bindListeners(this.$el);
|
||||
}
|
||||
|
||||
_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._name();
|
||||
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());
|
||||
}
|
||||
|
||||
_name() {
|
||||
if (this._peer.name.model) {
|
||||
return this._peer.name.os + ' ' + this._peer.name.model;
|
||||
}
|
||||
this._peer.name.os = this._peer.name.os.replace('Mac OS', 'Mac');
|
||||
return this._peer.name.os + ' ' + this._peer.name.browser;
|
||||
}
|
||||
|
||||
_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
|
||||
this.setProgress(0.01);
|
||||
}
|
||||
|
||||
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.$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');
|
||||
$a.href = file.url;
|
||||
$a.download = file.name;
|
||||
|
||||
this.$el.querySelector('#fileName').textContent = file.name;
|
||||
this.$el.querySelector('#fileSize').textContent = this._formatFileSize(file.size);
|
||||
this.show();
|
||||
|
||||
if (!isDownloadSupported) return;
|
||||
// $a.target = "_blank"; // fallback
|
||||
$a.target = "_system"; // fallback
|
||||
$a.href = 'external:' + $a.href;
|
||||
}
|
||||
|
||||
_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() {
|
||||
super.hide();
|
||||
this._dequeueFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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.show();
|
||||
this.$text.setSelectionRange(0, this.$text.value.length)
|
||||
}
|
||||
|
||||
_send(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('send-text', {
|
||||
to: this._recipient,
|
||||
text: this.$text.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
_onCopy() {
|
||||
if (!document.copy(this.$text.textContent)) return;
|
||||
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) {
|
||||
var img = '/images/logo_transparent_128x128.png';
|
||||
return new Notification(message, {
|
||||
body: body,
|
||||
icon: img,
|
||||
vibrate: [200, 100, 200, 100, 200, 100, 400],
|
||||
});
|
||||
}
|
||||
|
||||
_messageNotification(message) {
|
||||
if (isURL(message)) {
|
||||
const notification = this._notify(message, 'Click to open link');
|
||||
notification.onclick = e => window.open(message, '_blank', null, true);
|
||||
} else {
|
||||
const notification = this._notify(message, 'Click to copy text');
|
||||
notification.onclick = e => document.copy(message);
|
||||
}
|
||||
}
|
||||
|
||||
_downloadNotification(message) {
|
||||
const notification = this._notify(message, 'Click to download');
|
||||
if (window.isDownloadSupported) return;
|
||||
notification.onclick = e => {
|
||||
document.querySelector('x-dialog [download]').click();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 snapdrop = new Snapdrop();
|
||||
|
||||
document.copy = 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) {}
|
||||
|
||||
selection.removeAllRanges();
|
||||
span.remove();
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator && isProductionEnvironment) {
|
||||
navigator.serviceWorker
|
||||
.register('/service-worker.js')
|
||||
.then(e => console.log("Service Worker Registered"));
|
||||
}
|
||||
|
||||
// Background Animation
|
||||
Events.on('load', () => {
|
||||
var requestAnimFrame = (function() {
|
||||
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
|
||||
function(callback) {
|
||||
window.setTimeout(callback, 1000 / 60);
|
||||
};
|
||||
})();
|
||||
var c = document.createElement('canvas');
|
||||
document.body.appendChild(c);
|
||||
var style = c.style;
|
||||
style.width = '100%';
|
||||
style.position = 'absolute';
|
||||
style.zIndex = -1;
|
||||
var ctx = c.getContext('2d');
|
||||
var x0, y0, w, h, dw;
|
||||
|
||||
function init() {
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
c.width = w;
|
||||
c.height = h;
|
||||
var offset = h > 380 ? 100 : 65;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
dw = Math.max(w, h, 1000) / 13;
|
||||
drawCircles();
|
||||
}
|
||||
window.onresize = init;
|
||||
|
||||
function drawCicrle(radius) {
|
||||
ctx.beginPath();
|
||||
var 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;
|
||||
}
|
||||
|
||||
var step = 0;
|
||||
|
||||
function drawCircles() {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
for (var i = 0; i < 8; i++) {
|
||||
drawCicrle(dw * i + step % dw);
|
||||
}
|
||||
step += 1;
|
||||
}
|
||||
|
||||
var loading = true;
|
||||
|
||||
function animate() {
|
||||
if (loading || step % dw < dw - 5) {
|
||||
requestAnimFrame(function() {
|
||||
drawCircles();
|
||||
animate();
|
||||
});
|
||||
}
|
||||
}
|
||||
window.animateBackground = function(l) {
|
||||
loading = l;
|
||||
animate();
|
||||
};
|
||||
init();
|
||||
animate();
|
||||
setTimeout(e => window.animateBackground(false), 3000);
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
0
client/service-worker.js
Normal file
BIN
client/sounds/blop.mp3
Normal file
BIN
client/sounds/blop.ogg
Normal file
640
client/styles.css
Normal file
|
@ -0,0 +1,640 @@
|
|||
/* Constants */
|
||||
|
||||
:root {
|
||||
--icon-size: 24px;
|
||||
--primary-color: #4285f4;
|
||||
--peer-width: 120px;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.full {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
color: #333;
|
||||
-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;
|
||||
}
|
||||
|
||||
.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: var(--primary-color);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 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 {
|
||||
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 {
|
||||
height: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
x-peer[transfer] .status:before {
|
||||
content: 'Transfering...';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Receive 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Button */
|
||||
|
||||
.button {
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
border: none;
|
||||
outline: none;
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 */
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16px 24px;
|
||||
background: #f1f3f4;
|
||||
border-radius: 50px;
|
||||
margin: 8px 0;
|
||||
line-height: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Info Animation */
|
||||
|
||||
#info {
|
||||
text-align: center;
|
||||
color: white;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
z-index: 11;
|
||||
transition-delay: 300ms;
|
||||
}
|
||||
|
||||
#info:not(:target) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition-delay: 0;
|
||||
}
|
||||
|
||||
#info .logo {
|
||||
--icon-size: 96px;
|
||||
}
|
||||
|
||||
#info .close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info-background {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info-background:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
top: -20px;
|
||||
left: -32px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
transform: scale(0);
|
||||
transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#info:target+a>.info-background:before {
|
||||
transform: scale(100);
|
||||
}
|
||||
|
||||
a[href="#info"] {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#info .row a {
|
||||
color: currentColor;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 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;
|
||||
border-radius: 8px;
|
||||
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: 6px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
}
|
||||
|
||||
#notification {
|
||||
position: absolute;
|
||||
right: 56px;
|
||||
top: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
|
||||
x-instructions {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
|
||||
@media (min-height: 800px) {
|
||||
x-toast {
|
||||
right: 24px;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
/* CSS specific to iOS devices */
|
||||
html {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
}
|