Version 2 initial commit

This commit is contained in:
RobinLinus 2018-09-21 16:05:03 +02:00
parent 81252d301c
commit 663db5cbb3
99 changed files with 2448 additions and 27650 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

200
client/index.html Normal file
View 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
View 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"
}

View 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
View 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
View 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
View file

BIN
client/sounds/blop.mp3 Normal file

Binary file not shown.

BIN
client/sounds/blop.ogg Normal file

Binary file not shown.

640
client/styles.css Normal file
View 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);
}
}