mirror of
https://github.com/amark/gun.git
synced 2025-07-30 15:23:20 +00:00
Compare commits
850 Commits
0.2019.413
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
58d0318f3d | ||
![]() |
938697dee9 | ||
![]() |
4b43fa7f2c | ||
![]() |
ff4bf9293c | ||
![]() |
0c423c999c | ||
![]() |
e3a5a52506 | ||
![]() |
9a0e259a9b | ||
![]() |
5305f01011 | ||
![]() |
90b88959d0 | ||
![]() |
7cc4cce1a3 | ||
![]() |
03735dc09c | ||
![]() |
1c095b13e9 | ||
![]() |
faff9efaca | ||
![]() |
7a2767a763 | ||
![]() |
c47800f4d8 | ||
![]() |
e584906a65 | ||
![]() |
3070627c83 | ||
![]() |
3bd809818f | ||
![]() |
61df63c96e | ||
![]() |
7eb6d38cfc | ||
![]() |
638c2c3c23 | ||
![]() |
5c52df2eee | ||
![]() |
7cb337c158 | ||
![]() |
e07c9b21ec | ||
![]() |
203bd40932 | ||
![]() |
3688ba1cc6 | ||
![]() |
78a40daf46 | ||
![]() |
d7f19473a4 | ||
![]() |
dc5f90ad61 | ||
![]() |
2b4f750392 | ||
![]() |
c440a7cc88 | ||
![]() |
6dfaaf229b | ||
![]() |
5ff33b7ac3 | ||
![]() |
efb2552997 | ||
![]() |
5d3cbaca19 | ||
![]() |
96b1402a65 | ||
![]() |
0251de11ec | ||
![]() |
1862fb69fd | ||
![]() |
f882b86889 | ||
![]() |
7f6a0b27c3 | ||
![]() |
0cb9706393 | ||
![]() |
4c01db9a67 | ||
![]() |
1304ef90e8 | ||
![]() |
8c4d38e30e | ||
![]() |
30eff267ea | ||
![]() |
580058056f | ||
![]() |
bfedfc027c | ||
![]() |
bf1565398b | ||
![]() |
8b1f055cd1 | ||
![]() |
ca0e7e35ae | ||
![]() |
e15416ff2d | ||
![]() |
0e7121b8f4 | ||
![]() |
2183e6acdf | ||
![]() |
48b5a0e765 | ||
![]() |
1ddf797fff | ||
![]() |
2afde8db4a | ||
![]() |
c6b9fa8d13 | ||
![]() |
c9283d8fa6 | ||
![]() |
58f9135f6b | ||
![]() |
b79e5bbad7 | ||
![]() |
b14d5da558 | ||
![]() |
9433e5ca06 | ||
![]() |
73b661a6a5 | ||
![]() |
9e616ea3a9 | ||
![]() |
1bdab7394a | ||
![]() |
d732a9e48b | ||
![]() |
c274977eb6 | ||
![]() |
81a29cdd60 | ||
![]() |
713380ccd2 | ||
![]() |
6f6d8b533d | ||
![]() |
0665787e2b | ||
![]() |
2ee0bed0c1 | ||
![]() |
9abebd7673 | ||
![]() |
8778ca4138 | ||
![]() |
eca0451c4e | ||
![]() |
9ceb86b208 | ||
![]() |
47c070932a | ||
![]() |
73d54b6271 | ||
![]() |
2d3993125d | ||
![]() |
ad8e67e3c3 | ||
![]() |
6d7e980b5f | ||
![]() |
e9439daa51 | ||
![]() |
94c6a97ae7 | ||
![]() |
f25747443e | ||
![]() |
680f871aa3 | ||
![]() |
b125d8d150 | ||
![]() |
7123207c66 | ||
![]() |
89b24d3862 | ||
![]() |
d5c8a02980 | ||
![]() |
451c33a69a | ||
![]() |
07b30ed602 | ||
![]() |
d06359f45c | ||
![]() |
cedf9b8809 | ||
![]() |
e840df43af | ||
![]() |
ef59ea185c | ||
![]() |
233d2612d1 | ||
![]() |
cfad98b0c6 | ||
![]() |
3dbdd4e43c | ||
![]() |
db99ad65fb | ||
![]() |
5efedd7308 | ||
![]() |
8f6322efc0 | ||
![]() |
7ec5805d8d | ||
![]() |
faff04ff63 | ||
![]() |
3749bf74cf | ||
![]() |
027edcb54e | ||
![]() |
cdfe87c2b6 | ||
![]() |
9ca6e59d32 | ||
![]() |
8b52cbb2cb | ||
![]() |
e4e703c957 | ||
![]() |
d6d50040dc | ||
![]() |
809eae4f98 | ||
![]() |
081931a6d4 | ||
![]() |
5687bddc90 | ||
![]() |
ac52811880 | ||
![]() |
19cb91ee4e | ||
![]() |
46d926e831 | ||
![]() |
9c9a5fd293 | ||
![]() |
87652467b5 | ||
![]() |
de46cccc1e | ||
![]() |
fa1e1578f7 | ||
![]() |
42720c57ea | ||
![]() |
8facbcc095 | ||
![]() |
a634b37b1a | ||
![]() |
525d834784 | ||
![]() |
2beb258b4c | ||
![]() |
39337338bd | ||
![]() |
c85cb4365d | ||
![]() |
210a5834c6 | ||
![]() |
55682b6f4b | ||
![]() |
7335f8c866 | ||
![]() |
904b2f8e7f | ||
![]() |
ced9cde41b | ||
![]() |
94ab05b032 | ||
![]() |
589a7784dc | ||
![]() |
440bbffb73 | ||
![]() |
e3eaf5e268 | ||
![]() |
a092f5a725 | ||
![]() |
8f79ff7bb0 | ||
![]() |
ebe6f0cf3b | ||
![]() |
9d94b433d0 | ||
![]() |
ff3c4f6e69 | ||
![]() |
f062fc330b | ||
![]() |
4f19440262 | ||
![]() |
e4bb977d5d | ||
![]() |
0e8b4549df | ||
![]() |
f6b65c8e7e | ||
![]() |
5ea98f48e3 | ||
![]() |
41c2f64ef2 | ||
![]() |
2ff35aa316 | ||
![]() |
c4a613a971 | ||
![]() |
2bbbe36e13 | ||
![]() |
532b59b098 | ||
![]() |
d53d157f19 | ||
![]() |
1051e477ba | ||
![]() |
2c3e7d55fd | ||
![]() |
ba7c3e8bda | ||
![]() |
ddaf523a98 | ||
![]() |
666569f1d7 | ||
![]() |
05f497778a | ||
![]() |
f85f55c5a1 | ||
![]() |
ce20e0787d | ||
![]() |
852b77f49a | ||
![]() |
6780cb4334 | ||
![]() |
77162fcb68 | ||
![]() |
d1f13c86c6 | ||
![]() |
9413142c80 | ||
![]() |
6d3ea2ecc5 | ||
![]() |
3c5de47c42 | ||
![]() |
396b367e4e | ||
![]() |
1969d48e0f | ||
![]() |
0441aa332d | ||
![]() |
009be7e534 | ||
![]() |
74d2b077cd | ||
![]() |
50af2d52ad | ||
![]() |
3b8eb16960 | ||
![]() |
395c2b8f98 | ||
![]() |
17af355ac1 | ||
![]() |
49973ac522 | ||
![]() |
90c7fb813e | ||
![]() |
34f53a20fb | ||
![]() |
0b158667a7 | ||
![]() |
edf6c5f38d | ||
![]() |
c97ac0002c | ||
![]() |
d7282be14b | ||
![]() |
f8f2fc502d | ||
![]() |
63b0043076 | ||
![]() |
89c5286a52 | ||
![]() |
31971c0c51 | ||
![]() |
7dedd0797c | ||
![]() |
e52e5821c1 | ||
![]() |
eafab334a9 | ||
![]() |
d1a59e5d6f | ||
![]() |
d178d4a095 | ||
![]() |
8cd8c4f6fb | ||
![]() |
e63504a8df | ||
![]() |
ae0ffadc29 | ||
![]() |
bdf4717d2d | ||
![]() |
084e2907da | ||
![]() |
0eec835c31 | ||
![]() |
8e2f12542d | ||
![]() |
5df08f91cb | ||
![]() |
b8450ceab9 | ||
![]() |
1be6c5ed62 | ||
![]() |
4db88079b5 | ||
![]() |
6cb7261ac2 | ||
![]() |
dac9e8d059 | ||
![]() |
6336cc66ae | ||
![]() |
8e4128b474 | ||
![]() |
cd45008433 | ||
![]() |
087704ec6b | ||
![]() |
e7afd231eb | ||
![]() |
da573640b3 | ||
![]() |
44fd7bee89 | ||
![]() |
5ba2d9b44f | ||
![]() |
5d5432a9b5 | ||
![]() |
70eb769209 | ||
![]() |
8c8b513716 | ||
![]() |
5bafcea9f3 | ||
![]() |
a362460b0f | ||
![]() |
617c7ac2f1 | ||
![]() |
fdedf8e59d | ||
![]() |
b1b0432983 | ||
![]() |
fe9ea62f61 | ||
![]() |
10b42525ad | ||
![]() |
fc10c250c9 | ||
![]() |
0f9ebfc454 | ||
![]() |
7925598cd8 | ||
![]() |
8b9f8915f2 | ||
![]() |
804bf04e50 | ||
![]() |
2168e3e2d7 | ||
![]() |
5ca97d88a3 | ||
![]() |
429b011955 | ||
![]() |
e9ed2e5a02 | ||
![]() |
331ea85c86 | ||
![]() |
183555d793 | ||
![]() |
655bec7b16 | ||
![]() |
4a84984699 | ||
![]() |
4dc672eeb4 | ||
![]() |
508d38cd1b | ||
![]() |
45dd008cb7 | ||
![]() |
1c25266935 | ||
![]() |
ae6d5cb6bf | ||
![]() |
5be57c3fb1 | ||
![]() |
4ded14186f | ||
![]() |
88cfae52b4 | ||
![]() |
8de6ce5ca2 | ||
![]() |
60d94180e2 | ||
![]() |
43eae41c35 | ||
![]() |
d141c6e403 | ||
![]() |
edccff60a7 | ||
![]() |
cb3f8600d9 | ||
![]() |
00f75aff9a | ||
![]() |
1bbc308ceb | ||
![]() |
6e0a6fe225 | ||
![]() |
a6b6b7f2ee | ||
![]() |
49b2937421 | ||
![]() |
be0b1cefb2 | ||
![]() |
0a91b8079b | ||
![]() |
c17a14b53a | ||
![]() |
e2a92bd61e | ||
![]() |
34d7f6fc70 | ||
![]() |
d8d037fc81 | ||
![]() |
7d12a82458 | ||
![]() |
227685cedf | ||
![]() |
1e1cad8bbd | ||
![]() |
4d0bcab3b8 | ||
![]() |
b3e5b21f86 | ||
![]() |
deeb87e31b | ||
![]() |
0770648219 | ||
![]() |
5ee816191a | ||
![]() |
ab5e38b367 | ||
![]() |
002680ede4 | ||
![]() |
e1de0c875e | ||
![]() |
d7dd65babf | ||
![]() |
fece267263 | ||
![]() |
bff7804b5b | ||
![]() |
1eed73af48 | ||
![]() |
afa7abc646 | ||
![]() |
8a7d1ce2fc | ||
![]() |
52590d96b9 | ||
![]() |
4f3c5f0f3d | ||
![]() |
57d1919c68 | ||
![]() |
1703e6003c | ||
![]() |
4a66bface6 | ||
![]() |
badb490d50 | ||
![]() |
807ec9ef25 | ||
![]() |
bafb25dcfa | ||
![]() |
cf5c00d82c | ||
![]() |
77c39a2035 | ||
![]() |
564c6f3100 | ||
![]() |
c254f4c699 | ||
![]() |
0875201558 | ||
![]() |
803e1d4938 | ||
![]() |
5701b8b581 | ||
![]() |
2777d87c4d | ||
![]() |
ca003747b7 | ||
![]() |
c95b99d376 | ||
![]() |
c686a15980 | ||
![]() |
04bbfdd2eb | ||
![]() |
5d755af301 | ||
![]() |
bac237fa89 | ||
![]() |
37da3cc8ea | ||
![]() |
be6dcf0f99 | ||
![]() |
ebe8345090 | ||
![]() |
4dc88e9ea8 | ||
![]() |
80c74d254b | ||
![]() |
97bbdd9b74 | ||
![]() |
622a252722 | ||
![]() |
37726f6e68 | ||
![]() |
84097d7e68 | ||
![]() |
69165f69f5 | ||
![]() |
05349a5c68 | ||
![]() |
fe57813721 | ||
![]() |
773d5cba25 | ||
![]() |
7e9ec5d93a | ||
![]() |
866e8594d6 | ||
![]() |
11bdf2211e | ||
![]() |
811a1b8963 | ||
![]() |
a607bdd004 | ||
![]() |
33b29f4ee6 | ||
![]() |
fee7473ccb | ||
![]() |
fd6752cbd6 | ||
![]() |
a89ca678bb | ||
![]() |
c82e0da589 | ||
![]() |
b78bbc322b | ||
![]() |
35c8a75a00 | ||
![]() |
bf68704063 | ||
![]() |
5cbd5e3943 | ||
![]() |
bd417b9f04 | ||
![]() |
f723c69d7c | ||
![]() |
a394a23aa1 | ||
![]() |
8161f14553 | ||
![]() |
87b2dbe03d | ||
![]() |
85191e8042 | ||
![]() |
18aebaa4a1 | ||
![]() |
34f1be198e | ||
![]() |
97aa976c97 | ||
![]() |
2ddb526323 | ||
![]() |
a7bb4a840f | ||
![]() |
feb54f024b | ||
![]() |
e74f4a00e2 | ||
![]() |
ef837a1018 | ||
![]() |
6d8f776200 | ||
![]() |
01cd2050d2 | ||
![]() |
edc122f63c | ||
![]() |
3e678b8568 | ||
![]() |
7c45ddb558 | ||
![]() |
e88a120a4e | ||
![]() |
486184e767 | ||
![]() |
823df19593 | ||
![]() |
0d53db7877 | ||
![]() |
f759ab1394 | ||
![]() |
9bc25e3647 | ||
![]() |
461bb7990a | ||
![]() |
bfb43bf7a9 | ||
![]() |
5706f46b00 | ||
![]() |
6d02684036 | ||
![]() |
16d286538f | ||
![]() |
4a74c65019 | ||
![]() |
b356877a23 | ||
![]() |
113fd7727d | ||
![]() |
bcad8f7ef3 | ||
![]() |
47a841fbcd | ||
![]() |
c45950a771 | ||
![]() |
d56e06fec7 | ||
![]() |
fcf105aaab | ||
![]() |
a48fd8c0a3 | ||
![]() |
e4333397e4 | ||
![]() |
022741dd90 | ||
![]() |
98e0554944 | ||
![]() |
d333738b0a | ||
![]() |
00c48f172b | ||
![]() |
4eb7d0d383 | ||
![]() |
9e811e882c | ||
![]() |
125e1384c1 | ||
![]() |
55b9231d47 | ||
![]() |
213a14b0c8 | ||
![]() |
d69e6d43fe | ||
![]() |
99cb0e92df | ||
![]() |
d681c5ca91 | ||
![]() |
5372279fb2 | ||
![]() |
2501b5cdea | ||
![]() |
116d485379 | ||
![]() |
c7f017191e | ||
![]() |
0af7379ed6 | ||
![]() |
9f92b014f4 | ||
![]() |
eb8985b2fd | ||
![]() |
49bc20ee95 | ||
![]() |
880947afe8 | ||
![]() |
9d03ce83d0 | ||
![]() |
284a028e1c | ||
![]() |
5a430bc812 | ||
![]() |
985cfa2b4d | ||
![]() |
51be588a74 | ||
![]() |
c93305ae11 | ||
![]() |
ff99a4639d | ||
![]() |
06a4a0bfeb | ||
![]() |
29d596baf7 | ||
![]() |
018fa8cc1e | ||
![]() |
1d85346770 | ||
![]() |
f8e2230b04 | ||
![]() |
550b0633b0 | ||
![]() |
5bedffc864 | ||
![]() |
4124a2db50 | ||
![]() |
47b29768c7 | ||
![]() |
1a71145b4d | ||
![]() |
46b0fb6dc5 | ||
![]() |
17f90d6d10 | ||
![]() |
b0de4027e3 | ||
![]() |
f0b041cdb1 | ||
![]() |
964b1c9fcf | ||
![]() |
60a62d867c | ||
![]() |
ce78d7a4c9 | ||
![]() |
2f34d8cb4e | ||
![]() |
e2f4f11e6a | ||
![]() |
31abc3eaa0 | ||
![]() |
6a3c55cc69 | ||
![]() |
47e7c495d7 | ||
![]() |
87955288a0 | ||
![]() |
fc58d3552a | ||
![]() |
ce865da613 | ||
![]() |
879cd84ccc | ||
![]() |
8ec8e394b8 | ||
![]() |
156b84acee | ||
![]() |
ec8808735b | ||
![]() |
7857c70b6e | ||
![]() |
22a76f489d | ||
![]() |
3520f04b36 | ||
![]() |
7ba9aa1c3d | ||
![]() |
cba32c1040 | ||
![]() |
12ff85aa2f | ||
![]() |
28205fe69e | ||
![]() |
ee7a01675f | ||
![]() |
54a50d62ff | ||
![]() |
dec75be45e | ||
![]() |
1b002bd49d | ||
![]() |
6d05150ae8 | ||
![]() |
fbcb369aa8 | ||
![]() |
6bdc99da1a | ||
![]() |
4163687acb | ||
![]() |
9de7d0f38d | ||
![]() |
da7c243fb0 | ||
![]() |
97065b70fa | ||
![]() |
b4568e52c3 | ||
![]() |
c58e412208 | ||
![]() |
2540818665 | ||
![]() |
ccf2c303b9 | ||
![]() |
3733473115 | ||
![]() |
6f053b6327 | ||
![]() |
5a70f506d3 | ||
![]() |
8f78669997 | ||
![]() |
e5c06e7491 | ||
![]() |
74f6638227 | ||
![]() |
9a3225e387 | ||
![]() |
105d81f7a6 | ||
![]() |
5bf425d2c4 | ||
![]() |
79fac4cb45 | ||
![]() |
50a7373d00 | ||
![]() |
1c29092af8 | ||
![]() |
ff04e23b64 | ||
![]() |
c7f0acb0df | ||
![]() |
3ab14ffc42 | ||
![]() |
699823d578 | ||
![]() |
c785a184c1 | ||
![]() |
63d8114791 | ||
![]() |
f84af3fcd7 | ||
![]() |
cac59fbc12 | ||
![]() |
fb0e44f4a6 | ||
![]() |
947ee8455c | ||
![]() |
5016298b45 | ||
![]() |
72c699f20b | ||
![]() |
77263e40b7 | ||
![]() |
3085579915 | ||
![]() |
35414daa20 | ||
![]() |
4aadd43248 | ||
![]() |
a3383e5617 | ||
![]() |
40b47a2621 | ||
![]() |
d15721c46d | ||
![]() |
5a1f8350cb | ||
![]() |
e355d41f60 | ||
![]() |
ca4b5ae016 | ||
![]() |
a72342431f | ||
![]() |
82d8192700 | ||
![]() |
5253e2084a | ||
![]() |
c4fc859789 | ||
![]() |
4137c52598 | ||
![]() |
2801198691 | ||
![]() |
3668f5099f | ||
![]() |
1f3b090f81 | ||
![]() |
77fda9d42c | ||
![]() |
ed82e28109 | ||
![]() |
3c30c52f01 | ||
![]() |
d075423ffb | ||
![]() |
48b288280d | ||
![]() |
b45be18ad3 | ||
![]() |
58f8cd0e03 | ||
![]() |
b101b492d4 | ||
![]() |
bc40109e61 | ||
![]() |
d6dae4871e | ||
![]() |
0936c326f8 | ||
![]() |
6f65c6ef26 | ||
![]() |
fd26e39acc | ||
![]() |
3127b41f38 | ||
![]() |
9b188dea72 | ||
![]() |
e614100c47 | ||
![]() |
e98e51a0ea | ||
![]() |
7cf2d42dcd | ||
![]() |
c59e0e95f9 | ||
![]() |
273f7fce8d | ||
![]() |
16f911e126 | ||
![]() |
89b8f01cda | ||
![]() |
d2512f2842 | ||
![]() |
90ca38ee7e | ||
![]() |
4a42a4f1ce | ||
![]() |
4d252a96f7 | ||
![]() |
4e94d7f04e | ||
![]() |
c93149a3a0 | ||
![]() |
248e09c573 | ||
![]() |
82d7bfad61 | ||
![]() |
60c98a5ba0 | ||
![]() |
fa8a63606c | ||
![]() |
efd055c2b0 | ||
![]() |
827197dbb7 | ||
![]() |
a5a0eab24c | ||
![]() |
782750d728 | ||
![]() |
a980f4efc1 | ||
![]() |
75148ec540 | ||
![]() |
d8f3ed7edb | ||
![]() |
c97526a36c | ||
![]() |
cae8264832 | ||
![]() |
6281fed309 | ||
![]() |
ebfd82eac6 | ||
![]() |
cc6415fa44 | ||
![]() |
c5d3ef47e7 | ||
![]() |
d3e89c2adc | ||
![]() |
328ae52c2c | ||
![]() |
9dfdf608dc | ||
![]() |
e6b61f667e | ||
![]() |
56488b567e | ||
![]() |
bf907d5f87 | ||
![]() |
b3f87b180e | ||
![]() |
9dbbf856d3 | ||
![]() |
66361cb6f3 | ||
![]() |
fa5fe8a325 | ||
![]() |
6593844ed0 | ||
![]() |
958d736e0c | ||
![]() |
faa776a8e5 | ||
![]() |
39d467314a | ||
![]() |
9890fb88aa | ||
![]() |
ff9ff71cb2 | ||
![]() |
fe3a25682a | ||
![]() |
8f874d6f32 | ||
![]() |
ba2b207dd6 | ||
![]() |
9f446a08d4 | ||
![]() |
b5fd757db4 | ||
![]() |
de85106c42 | ||
![]() |
f8f9cddafe | ||
![]() |
5c4126bc0a | ||
![]() |
dc94252f20 | ||
![]() |
08fc562eac | ||
![]() |
80159888fb | ||
![]() |
3d8c8a2f47 | ||
![]() |
23420c9f60 | ||
![]() |
ecf4fda285 | ||
![]() |
06099bf881 | ||
![]() |
277147acc0 | ||
![]() |
86c74737f5 | ||
![]() |
50ee5c766f | ||
![]() |
db868d0e3a | ||
![]() |
dab91974c1 | ||
![]() |
761030d721 | ||
![]() |
af43b394c3 | ||
![]() |
77be32167f | ||
![]() |
3547039cba | ||
![]() |
8d5d6a3ee6 | ||
![]() |
08c45c08d4 | ||
![]() |
e8b5587b99 | ||
![]() |
bbf7791893 | ||
![]() |
47b0519117 | ||
![]() |
787ea543b4 | ||
![]() |
ccea6a030c | ||
![]() |
5d7e1dd226 | ||
![]() |
a3784d7930 | ||
![]() |
05ba89888f | ||
![]() |
2a450bf3cc | ||
![]() |
e4b4611856 | ||
![]() |
6eab251438 | ||
![]() |
0c27b81993 | ||
![]() |
f287887e87 | ||
![]() |
7a08fc400a | ||
![]() |
de43978382 | ||
![]() |
4b14e7c645 | ||
![]() |
fcce25df48 | ||
![]() |
4619e57968 | ||
![]() |
c7dcbfbd7e | ||
![]() |
d9d9e5c5d0 | ||
![]() |
4bb9d2dd51 | ||
![]() |
14ebd73d80 | ||
![]() |
8f5ab18fa3 | ||
![]() |
88a5c8fea0 | ||
![]() |
9db8adea2a | ||
![]() |
46066fee0d | ||
![]() |
9caf77dd0c | ||
![]() |
fc00ed0239 | ||
![]() |
dedab599d6 | ||
![]() |
8dbd9db17b | ||
![]() |
bc87faa8d9 | ||
![]() |
512d2c8c32 | ||
![]() |
29ed57e955 | ||
![]() |
af1590cf60 | ||
![]() |
1cd0c08ab0 | ||
![]() |
b2709e2eb7 | ||
![]() |
d2c4e7c8e8 | ||
![]() |
1a4a621fd1 | ||
![]() |
14b1ea1896 | ||
![]() |
c1c7c9ec74 | ||
![]() |
eece7f91ac | ||
![]() |
3a0b084449 | ||
![]() |
b41022ca91 | ||
![]() |
79216341e3 | ||
![]() |
b57a7a8013 | ||
![]() |
22547099bd | ||
![]() |
24aec9f466 | ||
![]() |
5a12e30996 | ||
![]() |
48ebe38e08 | ||
![]() |
3f3e973f39 | ||
![]() |
5aafbbbff0 | ||
![]() |
7ac8e29f67 | ||
![]() |
ef49a73e4a | ||
![]() |
5172c38913 | ||
![]() |
21fcdf8693 | ||
![]() |
79bd5b9827 | ||
![]() |
8dbe77f4d0 | ||
![]() |
56678aa7f5 | ||
![]() |
e048b897da | ||
![]() |
875de844fb | ||
![]() |
1895010188 | ||
![]() |
5a58f77a32 | ||
![]() |
301f8910fb | ||
![]() |
c99ad39c56 | ||
![]() |
0a02e7bfaa | ||
![]() |
1c3ea91011 | ||
![]() |
8dc8b27cd0 | ||
![]() |
8da4689fea | ||
![]() |
1ba73dd47b | ||
![]() |
f8bb17e27f | ||
![]() |
b5b4357a5d | ||
![]() |
fa724b5abb | ||
![]() |
ef4b4ff795 | ||
![]() |
8dc043c44f | ||
![]() |
c0224c4d28 | ||
![]() |
7c6886857c | ||
![]() |
42926ec09d | ||
![]() |
325c7b48ed | ||
![]() |
92a4f7ad24 | ||
![]() |
f15281dcd0 | ||
![]() |
209fe60c76 | ||
![]() |
e8f15b27e3 | ||
![]() |
1c6d519162 | ||
![]() |
e92bf7b940 | ||
![]() |
b916aea3c1 | ||
![]() |
d69c315023 | ||
![]() |
d193a332bf | ||
![]() |
528db20161 | ||
![]() |
026b278311 | ||
![]() |
0050eeb89a | ||
![]() |
2b82d6211f | ||
![]() |
76a5231498 | ||
![]() |
418b0b22b6 | ||
![]() |
d387a4f790 | ||
![]() |
e997b4d2d1 | ||
![]() |
4ffcc5b17a | ||
![]() |
df272501c0 | ||
![]() |
d7de5e7a67 | ||
![]() |
b40a6a50fc | ||
![]() |
247ddee578 | ||
![]() |
b3399a4c01 | ||
![]() |
6f2c246146 | ||
![]() |
4de57d4014 | ||
![]() |
382cbc4798 | ||
![]() |
a2086cd09c | ||
![]() |
8e7a5369cc | ||
![]() |
814d330b8a | ||
![]() |
7cd19b3d67 | ||
![]() |
93cd24caa7 | ||
![]() |
a6fc7fd5c5 | ||
![]() |
0804c5ef24 | ||
![]() |
6d798a136f | ||
![]() |
87f69aebc9 | ||
![]() |
14f6ba098a | ||
![]() |
ef29f181fe | ||
![]() |
1f0d0d62b0 | ||
![]() |
68f4308664 | ||
![]() |
f4526238f3 | ||
![]() |
3953c17130 | ||
![]() |
99ed43f5c4 | ||
![]() |
4fb04b8f53 | ||
![]() |
89578efd68 | ||
![]() |
12d61ace2c | ||
![]() |
932ff99c58 | ||
![]() |
a0ab6931d1 | ||
![]() |
cf642b75d1 | ||
![]() |
7d7b11f9b8 | ||
![]() |
7b583f3b41 | ||
![]() |
0b03702152 | ||
![]() |
71e0be33f7 | ||
![]() |
2027012379 | ||
![]() |
502dd9a3db | ||
![]() |
0b31971678 | ||
![]() |
2f0d2e3ade | ||
![]() |
ab69258633 | ||
![]() |
78ae966813 | ||
![]() |
6af5b6c2ac | ||
![]() |
0311969060 | ||
![]() |
a2a7ef91bb | ||
![]() |
5d352d90c9 | ||
![]() |
209909ce94 | ||
![]() |
8a44eb7f62 | ||
![]() |
0725190a94 | ||
![]() |
fc4a23e148 | ||
![]() |
9fc0adbbe6 | ||
![]() |
0a4af74ea9 | ||
![]() |
d299a99a57 | ||
![]() |
a34e90e868 | ||
![]() |
aa558f0706 | ||
![]() |
0515403716 | ||
![]() |
1101c06130 | ||
![]() |
d148f46d30 | ||
![]() |
a52f9ef3f4 | ||
![]() |
597bb77a98 | ||
![]() |
e0c93228ee | ||
![]() |
203a80a430 | ||
![]() |
9ac94db8b3 | ||
![]() |
b1aab8cd0f | ||
![]() |
44f5e22bdc | ||
![]() |
a7083105b6 | ||
![]() |
37d6d47462 | ||
![]() |
b4d5f3becc | ||
![]() |
3a7975a235 | ||
![]() |
9839f65a69 | ||
![]() |
8dee34d6dc | ||
![]() |
d729ab940f | ||
![]() |
f3c925a857 | ||
![]() |
cc276a6862 | ||
![]() |
e1ae0158a4 | ||
![]() |
e5589d435c | ||
![]() |
c4864c214b | ||
![]() |
8431dce0b9 | ||
![]() |
959e8e36bb | ||
![]() |
68ba4dcc65 | ||
![]() |
3277ba4127 | ||
![]() |
ced1e20ecc | ||
![]() |
9a6495dc21 | ||
![]() |
eb313d6f65 | ||
![]() |
ab36283b2e | ||
![]() |
0c718c8658 | ||
![]() |
e52496ae76 | ||
![]() |
7ce2dc9056 | ||
![]() |
b2db5f6b43 | ||
![]() |
2741996002 | ||
![]() |
5b38d06bcb | ||
![]() |
6033abbdd9 | ||
![]() |
7bec7ce5a0 | ||
![]() |
162061259a | ||
![]() |
6eaa0f5fa6 | ||
![]() |
48f9d10e2a | ||
![]() |
dc42f63204 | ||
![]() |
86043aa89e | ||
![]() |
c4ba41c4b8 | ||
![]() |
866593343d | ||
![]() |
9f7c530922 | ||
![]() |
cfd7d1042d | ||
![]() |
4aac986e94 | ||
![]() |
209bdf2b06 | ||
![]() |
a786944ed6 | ||
![]() |
a6a5aec76f | ||
![]() |
5b1a2d1b28 | ||
![]() |
c9b0a05ee7 | ||
![]() |
2945c42ba2 | ||
![]() |
f82cb32a54 | ||
![]() |
455e9f9c91 | ||
![]() |
ed30911ba7 | ||
![]() |
9b820287d6 | ||
![]() |
abac11d959 | ||
![]() |
124d0146c8 | ||
![]() |
083c1c59f8 | ||
![]() |
2b899aeb93 | ||
![]() |
3bee20dd8a | ||
![]() |
86e78f1e5d | ||
![]() |
ed9b234c21 | ||
![]() |
1c44bf900e | ||
![]() |
4c8eb2c112 | ||
![]() |
a111e20f27 | ||
![]() |
b78e73a32b | ||
![]() |
bf40e7e152 | ||
![]() |
2071512b20 | ||
![]() |
e9fc38ddd1 | ||
![]() |
16634cf023 | ||
![]() |
2d541f9529 | ||
![]() |
da431c06bc | ||
![]() |
ecffdce786 | ||
![]() |
782f66cd49 | ||
![]() |
33893e40b8 | ||
![]() |
2c8abba79c | ||
![]() |
32a68c2d4d | ||
![]() |
1abcfe0c83 | ||
![]() |
4b247628e7 | ||
![]() |
4085a4053c | ||
![]() |
98a1290cbc | ||
![]() |
f7d9089ba7 | ||
![]() |
e7e9c35d69 | ||
![]() |
29519b0ebd | ||
![]() |
b99202f0ec | ||
![]() |
9cb1a52fc0 | ||
![]() |
0bdaf053f0 | ||
![]() |
d8ce093e9f | ||
![]() |
8bc815e827 | ||
![]() |
2e41cef16b | ||
![]() |
7ae2f3d1ec | ||
![]() |
8a36d0863a | ||
![]() |
d947059d5c | ||
![]() |
1e20fd5761 | ||
![]() |
d11abeb781 | ||
![]() |
fde74e1254 | ||
![]() |
d3f0d326b2 | ||
![]() |
ed509d0ae3 | ||
![]() |
f7a5937ed5 | ||
![]() |
f92da9b14d | ||
![]() |
16a34c1d0c | ||
![]() |
b4f760f1b5 | ||
![]() |
be4574840a | ||
![]() |
6795ef04a8 | ||
![]() |
5f153a4912 | ||
![]() |
7bc146bcce | ||
![]() |
803c5b6ee0 | ||
![]() |
259857453a | ||
![]() |
7bb3aa250e | ||
![]() |
1869f09a4c | ||
![]() |
25db64527c | ||
![]() |
a6f12af8da | ||
![]() |
dd3d4c3066 | ||
![]() |
fe7576ee66 | ||
![]() |
6d12b3d5a9 | ||
![]() |
51b98ba370 | ||
![]() |
83f2aec1e3 | ||
![]() |
6fd0e88dd5 | ||
![]() |
e2a7dba29f | ||
![]() |
1e8bece479 | ||
![]() |
fc4f3c5157 | ||
![]() |
960031ca4f |
@ -1,4 +1,6 @@
|
||||
node_modules
|
||||
radata
|
||||
stats.radata
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
|
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: amark
|
||||
patreon: gunDB
|
||||
open_collective: gun
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
108
.github/workflows/ci.yml
vendored
Normal file
108
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# checkout the code
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4 # Updated to v4 (latest as of 2025)
|
||||
|
||||
# verify the version in package.json matches the release tag
|
||||
- name: Version
|
||||
uses: tcurdt/action-verify-version-npm@master # No version update as it's using @master
|
||||
|
||||
# setup the node version
|
||||
- name: Setup Node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4 # Updated to v4 (latest as of 2025)
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
# cache node_modules if we can
|
||||
- name: Cache
|
||||
id: cache-modules
|
||||
uses: actions/cache@v4 # Updated to v4 (latest as of 2025)
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ matrix.node-version }}-${{ runner.os }}-build-${{ hashFiles('package.json') }}
|
||||
|
||||
# otherwise run install
|
||||
- name: Install
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
run: npm install
|
||||
|
||||
# run tests
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
# create github release
|
||||
release:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# create github release (which triggers the release workflows)
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2 # Updated to v2 (latest stable version as of 2025)
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
|
||||
# # publish latest master or release to dockerhub
|
||||
# dockerhub:
|
||||
# if: github.event_name == 'push'
|
||||
# needs: [test]
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# image: ${{ secrets.DOCKERHUB_USERNAME }}/gun
|
||||
# steps:
|
||||
#
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4 # Updated to v4
|
||||
#
|
||||
# - name: Login
|
||||
# env:
|
||||
# DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
# DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
# run: echo -n ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
|
||||
#
|
||||
# - name: Build
|
||||
# run: |
|
||||
# echo "SHA=$GITHUB_SHA"
|
||||
# docker build --build-arg SHA=$GITHUB_SHA \
|
||||
# BUILD_DATE=$(date +'%Y-%m-%dT%H:%M:%S') \
|
||||
# VCS_REF=${GITHUB_REF} \
|
||||
# VCS_URL=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} \
|
||||
# VERSION=${GITHUB_REF##*/} \
|
||||
# SHA=$GITHUB_SHA \
|
||||
# --label "SHA=$GITHUB_SHA" \
|
||||
# --tag ${{ env.image }}:${GITHUB_REF##*/} \
|
||||
# --tag ${{ env.image }}:latest \
|
||||
# .
|
||||
#
|
||||
# - name: Push
|
||||
# run: docker push ${{ env.image }}
|
||||
|
||||
# publish release to npm
|
||||
npm:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4 # Updated to v4
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN
|
||||
npm install
|
||||
npm publish --access=public
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -7,7 +7,15 @@ yarn.lock
|
||||
.idea/
|
||||
*.bak
|
||||
*.new
|
||||
*.log
|
||||
v8.json
|
||||
*.DS_store
|
||||
isolate*.log
|
||||
.esm-cache
|
||||
.sessionStorage
|
||||
.localStorage
|
||||
/types/**/*.ts
|
||||
!/types/**/*.d.ts
|
||||
!/types/**/*.test-d.ts
|
||||
/gun.ts
|
||||
/temp/
|
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
||||
*.ts
|
||||
/temp/
|
||||
!*.d.ts
|
||||
*.radata
|
||||
isolate-*
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
*
|
@ -2,9 +2,9 @@ language: node_js
|
||||
branches:
|
||||
except:
|
||||
- debug
|
||||
- manhattan
|
||||
node_js:
|
||||
- 8
|
||||
- 10
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- node_modules
|
||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -1,20 +1,26 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 0.2020.x
|
||||
|
||||
`>0.2020.520` may break in-process `gun1` `gun2` message passing. Check `test/common.js` "Check multi instance message passing" for a hint and/or complain on community chat.
|
||||
|
||||
- No breaking changes to core API.
|
||||
- Storage adapter `put` event breaking change (temporary?), RAD is official now and storage adapters should be RAD plugins instead of GUN adapters.
|
||||
- GUN soul format changed from being a random UUID to being a more predictable graph path (of where initially created) to support even better offline behavior. This means `null`ing & replacing an object will not create a new but re-merge.
|
||||
- Pretty much all internal GUN utility will be deleted, these are mostly undocumented but will affect some people - they will still be available as a separate file but deprecated.
|
||||
- As the DHT gets implemented, your relay peers may automatically connect to it, so do not assume your peer is standalone. `Gun({axe: false` should help prevent this but loses you most scaling properties.
|
||||
- The 2019 -> 2020 "changes" are happening gradually, based on experimental in-production tests.
|
||||
- As always, **most important** is to ask in the [community chat](http://chat.gun.eco) if you have any issues, and to keep up to date with changes.
|
||||
|
||||
## 0.2019.x
|
||||
|
||||
Some RAD & SEA data format changes, but with as much backward compatibility as possible, tho ideally should be dropped.
|
||||
|
||||
## 0.9.x
|
||||
|
||||
No breaking changes, but the new Radix Storage Engine (RSE) has been finally integrated and works with S3 as a backup. Expect an order of magnitude or more in cost savings, we'll report our December bill compared to November when we get it.
|
||||
No breaking changes, but the new Radix Storage Engine (RSE) has been finally integrated and works with S3 as a backup.
|
||||
|
||||
We have successfully benchmarked it against **1,000,000 records** doing end-to-end triple verification using our Jepsen-inspired [PANIC](https://github.com/gundb/panic-server) distritubed testing framework, doing **~4K acked writes/second** on a Macbook Air dev machine. For more information on PANIC, check out this [5 minute presentation](https://youtu.be/nTbUCTgLmkY) we did in Sweden (and the prior talk, for those interested in [porting GUN](https://github.com/amark/gun/wiki/porting-gun) out of its reference implementation of JS).
|
||||
|
||||
> Warning: There is a known rare edge case in RSE currently, if data is split between two chunked files, a GET will only return from the first chunk. This will be fixed soon, but we still encourage developers to run and test against it, please report any problems.
|
||||
|
||||
To use RSE, initialize a gun server peer with the default storage disabled, like `Gun({localStorage: false})`. Want to use it with S3? All you need to do is make sure that your environment variables are configured and it will automatically use S3, here is a [template](https://github.com/amark/gun/wiki/Using-Amazon-S3-for-Storage). This works especially well for our 1-click-deploy Heroku [demo server](http://gunjs.herokuapp.com/) with the example apps.
|
||||
|
||||
Finally, with **end-to-end encryption** being enabled with our Security, Encryption, Authorization (SEA) framework (check out our [P2P/decentralized crypto-identity blockchain](https://github.com/amark/gun/wiki/auth)), gun is marching towards a stable v1.0 production-ready system (it is already being used in production by a Northern European government's Navy). So if you are able to [work around the remaining bugs](https://github.com/amark/gun/issues), we would appreciate everybody efforts in experimenting and testing out gun and reporting any last hiccups **in our lead up to the v1.0**!
|
||||
|
||||
We will be **overhauling documentation in this v0.9.x series**, please make complaints about what is missing, and how we can make it better, so it will be polished for the v1.0! The [chatroom is actively and friendly for help](https://gitter.im/amark/gun), [StackOverflow](https://stackoverflow.com/questions/tagged/gun) for questions. And we're looking for [sponsors](https://www.patreon.com/gunDB), we **regularly get 1,200+ uniques every 2 weeks** on this repo, we've had **53% monthly growth** on our installs, and GUN is ranked in the top quarter of the top 1% of the top 1% fastest growing projects across all GitHub! If you are an Enterprise, this would be a great time to chat with us about our IoT, AI/ML, edge computing, graph, and cybersecurity solutions.
|
||||
|
||||
Here is towards a v1.0! Cheers.
|
||||
// Edit: commentary removed.
|
||||
|
||||
## 0.8.x
|
||||
|
||||
|
29
Dockerfile
29
Dockerfile
@ -1,5 +1,15 @@
|
||||
FROM alpine:latest
|
||||
# Build-time metadata as defined at http://label-schema.org
|
||||
# install packages
|
||||
FROM node:lts-alpine as builder
|
||||
RUN mkdir /work
|
||||
WORKDIR /work
|
||||
RUN apk add --no-cache alpine-sdk python3
|
||||
COPY package*.json ./
|
||||
RUN mkdir -p node_modules
|
||||
RUN npm ci --only=production
|
||||
|
||||
# fresh image without dev packages
|
||||
FROM node:lts-alpine
|
||||
# build-time metadata as defined at http://label-schema.org
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VCS_URL
|
||||
@ -12,15 +22,14 @@ LABEL org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.vendor="The Gun Database Team" \
|
||||
org.label-schema.version=$VERSION \
|
||||
org.label-schema.schema-version="1.0"
|
||||
# org.label-schema.description="Let it be pulled from Readme.md..." \
|
||||
WORKDIR /app
|
||||
ARG SHA
|
||||
RUN mkdir /work
|
||||
WORKDIR /work
|
||||
COPY --from=builder /work/node_modules ./node_modules
|
||||
RUN npm rebuild -q
|
||||
ADD . .
|
||||
ENV NPM_CONFIG_LOGLEVEL warn
|
||||
RUN apk update && apk upgrade \
|
||||
&& apk add --no-cache ca-certificates nodejs-npm \
|
||||
&& apk add --no-cache --virtual .build-dependencies python make g++ \
|
||||
&& npm install \
|
||||
&& apk del .build-dependencies && rm -rf /var/cache/* /tmp/npm*
|
||||
RUN echo "{ \"sha\": \"$SHA\" }" > version.json
|
||||
RUN cat version.json
|
||||
EXPOSE 8080
|
||||
EXPOSE 8765
|
||||
CMD ["npm","start"]
|
||||
|
2
Procfile
2
Procfile
@ -1 +1 @@
|
||||
web: node --optimize_for_size --gc_interval=100 examples/http.js
|
||||
web: node --inspect examples/http.js
|
207
README.md
207
README.md
@ -1,64 +1,73 @@
|
||||
<p><a href="https://gun.eco/"><img width="40%" src="https://cldup.com/TEy9yGh45l.svg"/></a><img width="50%" align="right" vspace="25" src="https://gun.eco/see/demo.gif"/></p>
|
||||
<p id="readme"><a href="https://gun.eco/"><img width="40%" src="https://cldup.com/TEy9yGh45l.svg"/></a><img width="50%" align="right" vspace="25" src="https://gun.eco/see/demo.gif"/></p>
|
||||
|
||||
[](https://www.npmjs.com/package/gun)
|
||||
[](https://travis-ci.org/amark/gun)
|
||||
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Famark%2Fgun?ref=badge_shield)
|
||||
[](https://gitter.im/amark/gun)
|
||||
[](https://www.jsdelivr.com/package/npm/gun)
|
||||
[](https://www.jsdelivr.com/package/npm/gun)
|
||||

|
||||
[](http://chat.gun.eco)
|
||||
|
||||
**GUN** is an _ecosystem_ of tools that let you <u>build tomorrow's dApps, today</u>.
|
||||
**GUN** is an [ecosystem](https://gun.eco/docs/Ecosystem) of **tools** that let you build [community run](https://www.nbcnews.com/tech/tech-news/these-technologists-think-internet-broken-so-they-re-building-another-n1030136) and [encrypted applications](https://gun.eco/docs/Cartoon-Cryptography) - like an Open Source Firebase or a Decentralized Dropbox.
|
||||
|
||||
Decentralized alternatives to [Reddit](https://notabug.io/t/whatever/comments/36588a16b9008da4e3f15663c2225e949eca4a15/gpu-bot-test), [YouTube](https://d.tube/), [Wikipedia](https://news.ycombinator.com/item?id=17685682), etc. are already pushing terabytes of daily P2P traffic on GUN. We are a [friendly community](https://gitter.im/amark/gun) creating a free fun future for freedom:
|
||||
The [Internet Archive](https://news.ycombinator.com/item?id=17685682) and [100s of other apps](https://github.com/amark/gun/wiki/awesome-gun) run GUN in-production.
|
||||
|
||||
+ Multiplayer by default with realtime p2p state synchronization!
|
||||
+ Graph data lets you use key/value, tables, documents, videos, & more!
|
||||
+ Local-first, offline, and decentralized with end-to-end encryption.
|
||||
|
||||
Decentralized alternatives to [Zoom](https://www.zdnet.com/article/era-hatches-meething-an-open-source-browser-based-video-conferencing-system/), [Reddit](https://notabug.io/t/whatever/comments/36588a16b9008da4e3f15663c2225e949eca4a15/gpu-bot-test), [Instagram](https://iris.to/), [Slack](https://iris.to/), [YouTube](https://d.tube/), [Stripe](https://twitter.com/marknadal/status/1422717427427647489), [Wikipedia](https://news.ycombinator.com/item?id=17685682), Facebook [Horizon](https://twitter.com/marknadal/status/1424476179189305347) and more have already pushed terabytes of daily P2P traffic on GUN. We are a [friendly community](http://chat.gun.eco/) creating a [free fun future for freedom](https://youtu.be/1HJdrBk3BlE):
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<a href=""><img width="31%" align="left" src="https://gun.eco/see/3dvr.gif" title="3D VR"/></a>
|
||||
<a href="https://github.com/cstefanache/cstefanache.github.io/blob/master/_posts/2016-08-02-gun-db-artificial-knowledge-sharing.md#gundb"><img width="31%" align="left" src="https://gun.eco/see/aiml.gif" title="AI/ML"/></a>
|
||||
<a href="http://gps.gunDB.io/"><img width="31%" align="left" src="https://gun.eco/see/gps.gif" title="GPS"/></a>
|
||||
<a href="https://github.com/lmangani/gun-scape#gun-scape"><img width="31%" align="left" src="https://gun.eco/see/dataviz.gif" title="Data Viz"/></a>
|
||||
<a href="https://github.com/amark/gun/wiki/Auth"><img width="31%" align="left" src="https://gun.eco/see/p2p.gif" title="P2P"/></a>
|
||||
<a href="https://github.com/Stefdv/gun-ui-lcd#okay-what-about-gundb-"><img width="31%" align="left" src="https://gun.eco/see/iot.gif" title="IoT"/></a>
|
||||
<a href="https://youtu.be/s_m16-w6bBI"><img width="31%" src="https://gun.eco/see/3dvr.gif" title="3D VR"/></a>
|
||||
<a href="https://github.com/cstefanache/cstefanache.github.io/blob/06697003449e4fc531fd32ee068bab532976f47b/_posts/2016-08-02-gun-db-artificial-knowledge-sharing.md"><img width="31%" src="https://gun.eco/see/aiml.gif" title="AI/ML"/></a>
|
||||
<a href="http://gps.gunDB.io/"><img width="31%" src="https://gun.eco/see/gps.gif" title="GPS"/></a>
|
||||
</tr>
|
||||
<tr>
|
||||
<a href="https://github.com/lmangani/gun-scape#gun-scape"><img width="31%" src="https://gun.eco/see/dataviz.gif" title="Data Viz"/></a>
|
||||
<a href="https://github.com/amark/gun/wiki/Auth"><img width="31%" src="https://gun.eco/see/p2p.gif" title="P2P"/></a>
|
||||
<a href="https://github.com/Stefdv/gun-ui-lcd#okay-what-about-gundb-"><img width="31%" src="https://gun.eco/see/iot.gif" title="IoT"/></a>
|
||||
</tr>
|
||||
<tr>
|
||||
<a href="http://chat.gun.eco"><img width="31%" src="https://gun.eco/see/vr-world.gif" title="VR World"/></a>
|
||||
<a href="https://youtu.be/1ASrmQ-CwX4"><img width="31%" src="https://gun.eco/see/ar.gif" title="AR"/></a>
|
||||
<a href="https://meething.space/"><img width="31%" src="https://gun.eco/see/video-conf.gif" title="Video Confernece"/></a>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
The ecosystem is one nice stack of technologies that looks like this:
|
||||
|
||||
<div><img width="48%" src="https://gun.eco/see/stack.png"/>
|
||||
<img width="48%" align="right" src="https://gun.eco/see/layers.png"/></div>
|
||||
|
||||
For now, it is best to start with GUN and _just use it_ to learn the basics, since it is _**so easy**_: (**or** want to read more? Skip ahead to the "[What is GUN?](#what-is-gun)" section.)
|
||||
|
||||
## Quickstart
|
||||
|
||||
GUN is *super easy* to get started with:
|
||||
|
||||
- Try the [interactive tutorial](https://gun.eco/docs/Todo-Dapp) in the browser (**5min** ~ average developer).
|
||||
- Or `npm install gun` and run the examples with `cd node_modules/gun && npm start` (**5min** ~ average developer).
|
||||
|
||||
> **Note:** If you don't have [node](http://nodejs.org/) or [npm](https://www.npmjs.com/), read [this](https://github.com/amark/gun/blob/master/examples/install.sh) first.
|
||||
> If the `npm` command line didn't work, you may need to `mkdir node_modules` first or use `sudo`.
|
||||
|
||||
- An online demo of the examples are available here: http://gunjs.herokuapp.com/
|
||||
- Or write a quick app: ([try now in jsbin](http://jsbin.com/sovihaveso/edit?js,console))
|
||||
- An online demo of the examples are available here: http://try.axe.eco/
|
||||
- Or write a quick app: ([try now in a playground](https://jsbin.com/kadobamevo/edit?js,console))
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
<script>
|
||||
// var Gun = require('gun'); // in NodeJS
|
||||
// var Gun = require('gun/gun'); // in React
|
||||
var gun = Gun();
|
||||
// import GUN from 'gun'; // in ESM
|
||||
// GUN = require('gun'); // in NodeJS
|
||||
// GUN = require('gun/gun'); // in React
|
||||
gun = GUN();
|
||||
|
||||
gun.get('mark').put({
|
||||
name: "Mark",
|
||||
email: "mark@gunDB.io",
|
||||
email: "mark@gun.eco",
|
||||
});
|
||||
|
||||
gun.get('mark').on(function(data, key){
|
||||
console.log("update:", data);
|
||||
gun.get('mark').on((data, key) => {
|
||||
console.log("realtime updates:", data);
|
||||
});
|
||||
|
||||
setInterval(() => { gun.get('mark').get('live').put(Math.random()) }, 9);
|
||||
</script>
|
||||
```
|
||||
- Or try something **mind blowing**, like saving circular references to a table of documents! ([play](http://jsbin.com/wefozepume/edit?js,console))
|
||||
```javascript
|
||||
var cat = {name: "Fluffy", species: "kitty"};
|
||||
var mark = {boss: cat};
|
||||
cat = {name: "Fluffy", species: "kitty"};
|
||||
mark = {boss: cat};
|
||||
cat.slave = mark;
|
||||
|
||||
// partial updates merge with existing data!
|
||||
@ -66,13 +75,13 @@ gun.get('mark').put(mark);
|
||||
|
||||
// access the data as if it is a document.
|
||||
gun.get('mark').get('boss').get('name').once(function(data, key){
|
||||
// `val` grabs the data once, no subscriptions.
|
||||
// `once` grabs the data once, no subscriptions.
|
||||
console.log("Mark's boss is", data);
|
||||
});
|
||||
|
||||
// traverse a graph of circular references!
|
||||
gun.get('mark').get('boss').get('slave').once(function(data, key){
|
||||
console.log("Mark is the slave!", data);
|
||||
console.log("Mark is the cat's slave!", data);
|
||||
});
|
||||
|
||||
// add both of them to a table!
|
||||
@ -85,21 +94,41 @@ gun.get('list').map().once(function(data, key){
|
||||
});
|
||||
|
||||
// live update the table!
|
||||
gun.get('list').set({type: "cucumber", goal: "scare cat"});
|
||||
gun.get('list').set({type: "cucumber", goal: "jumping cat"});
|
||||
```
|
||||
|
||||
Want to keep building more? **Jump to [THE DOCUMENTATION](#documentation)!**
|
||||
|
||||
# What is GUN?
|
||||
# About
|
||||
First & foremost, GUN is **a community of the nicest and most helpful people** out there. So [I want to invite you](http://chat.gun.eco) to come tell us about what **you** are working on & wanting to build (new or old school alike! Just be nice as well.) and ask us your questions directly. :)
|
||||
|
||||
First & foremost, GUN is **a community of the nicest and most helpful people** out there. So [I want to invite you](https://gitter.im/amark/gun) to come tell us about what **you** are working on & wanting to build (new or old school alike! Just be nice as well.) and ask us your questions directly. :)
|
||||
<p align="center"><a href="https://www.youtube.com/watch?v=oTQXzhm8w_8"><img width="250" src="https://img.youtube.com/vi/oTQXzhm8w_8/0.jpg"><br/>Watch the 100 second intro!</a></p>
|
||||
|
||||
The GUN ecosystem stack is a collection of independent and modular tools covering everything from [CRDT](https://crdt.tech/) [conflict resolution](https://gun.eco/distributed/matters.html), [cryptographic security](https://gun.eco/docs/Cartoon-Cryptography) & [encryption](https://gun.eco/docs/SEA), [radix storage serialization](https://gun.eco/docs/RAD), [mesh networking](https://gun.eco/docs/DAM) & [routing algorithms](https://gun.eco/docs/Routing), to distributed systems [correctness & load testing](https://github.com/gundb/panic-server), CPU scheduled [JSON parser](https://github.com/amark/gun/blob/master/lib/yson.js) to prevent UI lag, and more!
|
||||
|
||||
<div><img width="48%" src="https://gun.eco/see/stack.png"/>
|
||||
<img width="48%" align="right" src="https://gun.eco/see/layers.png"/></div>
|
||||
|
||||
On that note, let's get some official shout outs covered first:
|
||||
|
||||
### Support
|
||||
|
||||
<p align="center">
|
||||
Thanks to:<br/>
|
||||
Thanks to:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td vlign="center"><a href="https://mozilla.org/builders"><img height="100" src="https://user-images.githubusercontent.com/1423657/81992335-85346480-9643-11ea-8754-8275e98e06bc.png"></a></td>
|
||||
<td vlign="center"><a href="http://unstoppabledomains.com/"><img src="https://gun.eco/img/unstoppable.png"></a></td>
|
||||
<td vlign="center"><a href="https://mask.io/"><img src="https://dimensiondev.github.io/Mask-VI/assets/Logo/MB--Logo--CombH-Circle--Blue.svg" width="250"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td vlign="center"> <a href="https://www.ajar.org/"><img src="https://www.ajar.org/logo.png" height="120"></a></td>
|
||||
<td vlign="center"><a href="https://wallie.io/"><img src="https://raw.githubusercontent.com/gundb/gun-site/master/img/wallie.png" width="250"></a></td>
|
||||
<td vlign="center"> <a href="https://ghostdrive.com/"><img src="https://gun.eco/img/ghostdrive.png" height="120"></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a href="https://github.com/robertheessels">Robert Heessels</a>,
|
||||
<a href="http://qxip.net/">Lorenzo Mangani</a>,
|
||||
<a href="https://nlnet.nl/">NLnet Foundation</a>,
|
||||
@ -113,13 +142,28 @@ Thanks to:<br/>
|
||||
<a href="http://github.com/alanmimms">Alan Mimms</a>,
|
||||
<a href="https://github.com/dfreire">Dário Freire</a>,
|
||||
<a href="http://github.com/velua">John Williamson</a>,
|
||||
<a href="http://github.com/finwo">Robin Bron</a>
|
||||
<a href="http://github.com/finwo">Robin Bron</a>,
|
||||
<a href="http://github.com/ElieMakhoul">Elie Makhoul</a>,
|
||||
<a href="http://github.com/mikestaub">Mike Staub</a>,
|
||||
<a href="http://github.com/bmatusiak">Bradley Matusiak</a>,
|
||||
<a href="https://github.com/sjuxax">Jeff Cook</a>,
|
||||
<a href="https://github.com/nmauersberg">Nico</a>,
|
||||
<a href="https://github.com/ajartille">Aaron Artille</a>,
|
||||
<a href="https://github.com/timjrobinson">Tim Robinson</a>,
|
||||
<a href="https://github.com/hibas123">Fabian Stamm</a>,
|
||||
<a href="https://twitter.com/mikestaub">Mike Staub</a>,
|
||||
<a href="https://hunterowens.com/">Hunter Owens</a>,
|
||||
<a href="https://github.com/JacobMillner">Jacob Millner</a>,
|
||||
<a href="https://github.com/b-lack">Gerrit Balindt</a>,
|
||||
<a href="https://github.com/gabriellemon">Gabriel Lemon</a>,
|
||||
<a href="https://github.com/murageyun">Murage Martin</a>,
|
||||
<a href="https://github.com/octalmage">Jason Stallings</a>
|
||||
</p>
|
||||
|
||||
- Join others in sponsoring code: https://www.patreon.com/gunDB !
|
||||
- Ask questions: http://stackoverflow.com/questions/tagged/gun ?
|
||||
- Found a bug? Report at: https://github.com/amark/gun/issues ;
|
||||
- **Need help**? Chat with us: https://gitter.im/amark/gun .
|
||||
- **Need help**? Chat with us: http://chat.gun.eco .
|
||||
|
||||
### History
|
||||
|
||||
@ -146,11 +190,11 @@ Technically, **GUN is a graph synchronization protocol** with a *lightweight emb
|
||||
<tr>
|
||||
<td style="border: 0;"><h3><a href="https://github.com/brysgo/graphql-gun">GraphQL</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://github.com/PenguinMan98/electrontest">Electron</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://gun.eco/docs/React-Native">React Native</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://gun.eco/docs/React-Native">React & Native</a></h3></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 0;"><h3><a href="https://github.com/sjones6/vue-gun">Vue</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://github.com/amark/gun/wiki/React-Tutorial">React</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://gun.eco/docs/Svelte">Svelte</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://github.com/Stefdv/gun-ui-lcd#syncing">Webcomponents</a></h3></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -160,54 +204,74 @@ Technically, **GUN is a graph synchronization protocol** with a *lightweight emb
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 0;"><h3><a href="https://gun.eco/docs/Auth">Crypto Auth</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://gun.eco/docs/Awesome-GUN">Modules</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://github.com/amark/gun/wiki/Awesome-GUN">Modules</a></h3></td>
|
||||
<td style="border: 0;"><h3><a href="https://gun.eco/docs/Roadmap">Roadmap</a></h3></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
This would not be possible without **community contributors**, big shout out to:
|
||||
|
||||
**[ajmeyghani](https://github.com/ajmeyghani) ([Learn GUN Basics with Diagrams](https://medium.com/@ajmeyghani/gundb-a-graph-database-in-javascript-3860a08d873c))**; **[anywhichway](https://github.com/anywhichway) ([Block Storage](https://github.com/anywhichway/gun-block))**; **[beebase](https://github.com/beebase) ([Quasar](https://github.com/beebase/gun-vuex-quasar))**; **[BrockAtkinson](https://github.com/BrockAtkinson) ([brunch config](https://github.com/BrockAtkinson/brunch-gun))**; **[Brysgo](https://github.com/brysgo) ([GraphQL](https://github.com/brysgo/graphql-gun))**; **[d3x0r](https://github.com/d3x0r) ([SQLite](https://github.com/d3x0r/gun-db))**; **[forrestjt](https://github.com/forrestjt) ([file.js](https://github.com/amark/gun/blob/master/lib/file.js))**; **[hillct](https://github.com/hillct) (Docker)**; **[JosePedroDias](https://github.com/josepedrodias) ([graph visualizer](http://acor.sl.pt:9966))**; **[JuniperChicago](https://github.com/JuniperChicago) ([cycle.js bindings](https://github.com/JuniperChicago/cycle-gun))**; **[jveres](https://github.com/jveres) ([todoMVC](https://github.com/jveres/todomvc))**; **[kristianmandrup](https://github.com/kristianmandrup) ([edge](https://github.com/kristianmandrup/gun-edge))**; **[Lightnet](https://github.com/Lightnet)** ([Awesome Vue User Examples](https://glitch.com/edit/#!/jsvuegunui?path=README.md:1:0) & [User Kitchen Sink Playground](https://gdb-auth-vue-node.glitch.me/)); **[lmangani](https://github.com/lmangani) ([Cytoscape Visualizer](https://github.com/lmangani/gun-scape), [Cassandra](https://github.com/lmangani/gun-cassandra), [Fastify](https://github.com/lmangani/fastify-gundb), [LetsEncrypt](https://github.com/lmangani/polyGun-letsencrypt))**; **[mhelander](https://github.com/mhelander) ([SEA](https://github.com/amark/gun/blob/master/sea.js))**; [omarzion](https://github.com/omarzion) ([Sticky Note App](https://github.com/omarzion/stickies)); [PsychoLlama](https://github.com/PsychoLlama) ([LevelDB](https://github.com/PsychoLlama/gun-level)); **[RangerMauve](https://github.com/RangerMauve) ([schema](https://github.com/gundb/gun-schema))**; **[robertheessels](https://github.com/swifty) ([gun-p2p-auth](https://github.com/swifty/gun-p2p-auth))**; **[rogowski](https://github.com/rogowski) (AXE)**; [sbeleidy](https://github.com/sbeleidy); **[sbiaudet](https://github.com/sbiaudet) ([C# Port](https://github.com/sbiaudet/cs-gun))**; **[Sean Matheson](https://github.com/ctrlplusb) ([Observable/RxJS/Most.js bindings](https://github.com/ctrlplusb/gun-most))**; **[Shadyzpop](https://github.com/Shadyzpop) ([React Native example](https://github.com/amark/gun/tree/master/examples/react-native))**; **[sjones6](https://github.com/sjones6) ([Flint](https://github.com/sjones6/gun-flint))**; **[Stefdv](https://github.com/stefdv) (Polymer/web components)**; **[zrrrzzt](https://github.com/zrrrzzt) ([JWT Auth](https://gist.github.com/zrrrzzt/6f88dc3cedee4ee18588236756d2cfce))**; **[xmonader](https://github.com/xmonader) ([Python Port](https://github.com/xmonader/pygundb))**; **[88dev](https://github.com/88dev) ([Database Viewer](https://github.com/88dev/gun-show))**;
|
||||
**[ajmeyghani](https://github.com/ajmeyghani) ([Learn GUN Basics with Diagrams](https://medium.com/@ajmeyghani/gundb-a-graph-database-in-javascript-3860a08d873c))**; **[anywhichway](https://github.com/anywhichway) ([Block Storage](https://github.com/anywhichway/gun-block))**; **[beebase](https://github.com/beebase) ([Quasar](https://github.com/beebase/gun-vuex-quasar))**; **[BrockAtkinson](https://github.com/BrockAtkinson) ([brunch config](https://github.com/BrockAtkinson/brunch-gun))**; **[Brysgo](https://github.com/brysgo) ([GraphQL](https://github.com/brysgo/graphql-gun))**; **[d3x0r](https://github.com/d3x0r) ([SQLite](https://github.com/d3x0r/gun-db))**; **[forrestjt](https://github.com/forrestjt) ([file.js](https://github.com/amark/gun/blob/master/lib/file.js))**; **[hillct](https://github.com/hillct) (Docker)**; **[JosePedroDias](https://github.com/josepedrodias) ([graph visualizer](http://acor.sl.pt:9966))**; **[JuniperChicago](https://github.com/JuniperChicago) ([cycle.js bindings](https://github.com/JuniperChicago/cycle-gun))**; **[jveres](https://github.com/jveres) ([todoMVC](https://github.com/jveres/todomvc))**; **[kristianmandrup](https://github.com/kristianmandrup) ([edge](https://github.com/kristianmandrup/gun-edge))**; **[Lightnet](https://github.com/Lightnet)** ([Awesome Vue User Examples](https://glitch.com/edit/#!/jsvuegunui?path=README.md:1:0) & [User Kitchen Sink Playground](https://gdb-auth-vue-node.glitch.me/)); **[lmangani](https://github.com/lmangani) ([Cytoscape Visualizer](https://github.com/lmangani/gun-scape), [Cassandra](https://github.com/lmangani/gun-cassandra), [Fastify](https://github.com/lmangani/fastify-gundb), [LetsEncrypt](https://github.com/lmangani/polyGun-letsencrypt))**; **[mhelander](https://github.com/mhelander) ([SEA](https://github.com/amark/gun/blob/master/sea.js))**; [omarzion](https://github.com/omarzion) ([Sticky Note App](https://github.com/omarzion/stickies)); [PsychoLlama](https://github.com/PsychoLlama) ([LevelDB](https://github.com/PsychoLlama/gun-level)); **[RangerMauve](https://github.com/RangerMauve) ([schema](https://github.com/gundb/gun-schema))**; **[robertheessels](https://github.com/swifty) ([gun-p2p-auth](https://github.com/swifty/gun-p2p-auth))**; **[rogowski](https://github.com/rogowski) (AXE)**; [sbeleidy](https://github.com/sbeleidy); **[sbiaudet](https://github.com/sbiaudet) ([C# Port](https://github.com/sbiaudet/cs-gun))**; **[Sean Matheson](https://github.com/ctrlplusb) ([Observable/RxJS/Most.js bindings](https://github.com/ctrlplusb/gun-most))**; **[Shadyzpop](https://github.com/Shadyzpop) ([React Native example](https://github.com/amark/gun/tree/master/examples/react-native))**; **[sjones6](https://github.com/sjones6) ([Flint](https://github.com/sjones6/gun-flint))**; RIP **[Stefdv](https://github.com/stefdv) (Polymer/web components)**; **[zrrrzzt](https://github.com/zrrrzzt) ([JWT Auth](https://gist.github.com/zrrrzzt/6f88dc3cedee4ee18588236756d2cfce))**; **[xmonader](https://github.com/xmonader) ([Python Port](https://github.com/xmonader/pygundb))**;
|
||||
|
||||
I am missing many others, apologies, will be adding them soon!
|
||||
I am missing many others, apologies, will be adding them soon! This list is infinitely old & way out of date, if you want to be listed in it please make a PR! :)
|
||||
|
||||
## Testing
|
||||
|
||||
Tests may be run with `npm test`. Tests will trigger persistent writes to the DB, so subsequent runs of the test will fail. You must clear the DB before running the tests again. This can be done by running the following command in the project directory.
|
||||
You will need to `npm install -g mocha` first. Then in the gun root folder run `npm test`. Tests will trigger persistent writes to the DB, so subsequent runs of the test will fail. You must clear the DB before running the tests again. This can be done by running `rm -rf *data*` command in the project directory.
|
||||
|
||||
```bash
|
||||
rm -rf *data*
|
||||
```
|
||||
## Shims
|
||||
|
||||
### Additional Cryptography Libraries
|
||||
> These are only needed for NodeJS & React Native, they shim the native Browser WebCrypto API.
|
||||
|
||||
To install with npm, first install `npm install gun -S`.
|
||||
For just the networking layer, import Gun:
|
||||
If you want to use [SEA](https://gun.eco/docs/SEA) for `User` auth and security, you will need to install:
|
||||
|
||||
`npm install @peculiar/webcrypto --save`
|
||||
|
||||
Please see [our React Native docs](https://gun.eco/docs/React-Native) for installation instructions!
|
||||
|
||||
Then you can require [SEA](https://gun.eco/docs/SEA) without an error:
|
||||
|
||||
```javascript
|
||||
var Gun = require('gun/gun');
|
||||
GUN = require('gun/gun');
|
||||
SEA = require('gun/sea');
|
||||
```
|
||||
|
||||
If you also need to install SEA for user auth and crypto, also install some of its dependencies like this:
|
||||
|
||||
`npm install @trust/crypto text-encoding node-webcrypto-ossl --save`
|
||||
|
||||
You will need to require it too (it will be automatically added to the Gun object):
|
||||
|
||||
```javascript
|
||||
var Gun = require('gun/gun');
|
||||
var Sea = require('gun/sea');
|
||||
```
|
||||
|
||||
|
||||
## Deploy
|
||||
|
||||
To quickly spin up a Gun test server for your development team, utilize either [Heroku](http://heroku.com) or [Docker](http://docker.com) or any variant thereof [Dokku](http://dokku.viewdocs.io/dokku/), [Flynn.io](http://flynn.io), [now.sh](https://zeit.co/now), etc. !
|
||||
> Note: The default examples that get auto-deployed on `npm start` CDN-ify all GUN files, modules, & storage.
|
||||
|
||||
> Note: Moving forward, AXE will start to automatically cluster your peer into a shared DHT. You may want to disable this to run an isolated network.
|
||||
|
||||
> Note: When deploying a web application using GUN on a cloud provider, you may have to set `CI=false` in your `.env`. This prevents GUN-specific warnings from being treated as errors when deploying your app. You may also resolve this by modifying your webpack config to not try to build the GUN dependencies.
|
||||
|
||||
To quickly spin up a GUN relay peer for your development team, utilize [Heroku](http://heroku.com), [Docker](http://docker.com), or any others listed below. Or some variant thereof [Dokku](http://dokku.viewdocs.io/dokku/), K8s, etc. ! Or use all of them so your relays are decentralized too!
|
||||
|
||||
### Linux
|
||||
|
||||
`SSH` into the home directory of a clean OS install with `sudo` ability. Set any environment variables you need (see below), then do:
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/amark/gun/master/examples/install.sh | bash
|
||||
```
|
||||
|
||||
> Read [install.sh](https://github.com/amark/gun/blob/master/examples/install.sh) first!
|
||||
> If `curl` is not found, *copy&paste* the contents of install.sh into your ssh.
|
||||
|
||||
You can now safely `CTRL+A+D` to escape without stopping the peer. To stop everything `killall screen` or `killall node`.
|
||||
|
||||
Environment variables may need to be set like `export HTTPS_CERT=~/cert.pem HTTPS_KEY=~/key.pem PORT=443`. You can also look at a sample [nginx](https://gun.eco/docs/nginx) config. For production deployments, you probably will want to use something like `pm2` or better to keep the peer alive after machine reboots.
|
||||
|
||||
### [Dome](https://www.trydome.io/)
|
||||
[Deploy GUN in one-click](https://app.trydome.io/signup?package=gun) with [Dome](https://trydome.io) and receive a free trial:
|
||||
|
||||
[](https://app.trydome.io/signup?package=gun)
|
||||
|
||||
### [Heroku](https://www.heroku.com/)
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/amark/gun)
|
||||
|
||||
> Heroku deletes your data every 15 minutes, one way to fix this is by adding [cheap storage](https://gun.eco/docs/Using-Amazon-S3-for-Storage).
|
||||
|
||||
Or:
|
||||
|
||||
```bash
|
||||
@ -217,20 +281,19 @@ heroku create
|
||||
git push -f heroku HEAD:master
|
||||
```
|
||||
|
||||
Then visit the URL in the output of the 'heroku create' step, in a browser.
|
||||
Then visit the URL in the output of the 'heroku create' step, in a browser. Make sure to set any environment config vars in the settings tab.
|
||||
|
||||
### [Now.sh](https://zeit.co/now/)
|
||||
### [Zeet.co](https://www.zeet.co/)
|
||||
|
||||
```bash
|
||||
npm install -g now
|
||||
now --npm amark/gun
|
||||
```
|
||||
[](https://deploy.zeet.co/?url=https://github.com/amark/gun)
|
||||
|
||||
Then visit the URL in the output of the 'now --npm' step, in your browser.
|
||||
|
||||
### [Docker](https://www.docker.com/)
|
||||
|
||||
[](https://hub.docker.com/r/gundb/gun/) [](https://microbadger.com/images/gundb/gun "Get your own image badge on microbadger.com") [](https://hub.docker.com/r/gundb/gun/) [](https://hub.docker.com/r/gundb/gun/)
|
||||
> Warning: Docker image is community contributed and may be old with missing security updates, please check version numbers to compare.
|
||||
|
||||
[](https://hub.docker.com/r/gundb/gun/) [](https://microbadger.com/images/gundb/gun "Get your own image badge on microbadger.com") [](https://hub.docker.com/r/gundb/gun/) [](https://hub.docker.com/r/gundb/gun/)
|
||||
|
||||
Pull from the [Docker Hub](https://hub.docker.com/r/gundb/gun/) [](https://microbadger.com/images/gundb/gun). Or:
|
||||
|
||||
|
25
RELEASE.md
Normal file
25
RELEASE.md
Normal file
@ -0,0 +1,25 @@
|
||||
Every push or pull request will
|
||||
|
||||
- run the tests
|
||||
|
||||
Every push to master will
|
||||
|
||||
- run the tests
|
||||
- publish the latest docker image to dockerhub
|
||||
|
||||
Creating a tag that starts with `v` will
|
||||
|
||||
- create a new github release
|
||||
- publish the release to npm
|
||||
- publish the release to dockerhub
|
||||
|
||||
Creating a release from the github web interface will
|
||||
|
||||
- publish the release to npm
|
||||
- publish the release to dockerhub
|
||||
|
||||
Creating the release for version `0.2021.001` from the command line works as follows
|
||||
|
||||
git tag v0.2021.001
|
||||
git push --tags
|
||||
|
47
SECURITY.md
Normal file
47
SECURITY.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Security Policy
|
||||
|
||||
## Introduction
|
||||
|
||||
Security is our top priority. We are committed to ensuring that our project is as secure as possible for everyone who uses it. This document outlines our security policy and procedures for dealing with security issues.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We provide security updates for the following versions of our project:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.2020.x| :white_check_mark: |
|
||||
| < 0.2020| :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible.
|
||||
|
||||
### Report Format
|
||||
|
||||
When reporting vulnerabilities, please include the following details:
|
||||
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce the issue
|
||||
- Potential impact if left unaddressed
|
||||
- Suggested mitigation or resolution if any
|
||||
|
||||
### Response Time
|
||||
|
||||
We aim to confirm the receipt of your vulnerability report within 48 hours. Depending on the severity and complexity of the issue, we strive to investigate the issue and provide an initial response within a week.
|
||||
|
||||
### Disclosure Policy
|
||||
|
||||
If the vulnerability is confirmed, we will work on a fix and plan a release. We ask that you do not publicly disclose the issue until it has been addressed by us.
|
||||
|
||||
## Security Practices
|
||||
|
||||
We follow industry-standard security practices, including regular audits of the services and features we provide, to maintain the trust of our users.
|
||||
|
||||
## Security Updates
|
||||
|
||||
We will communicate any security updates through our standard communication channels, including our project's release notes and official website.
|
||||
|
||||
## Conclusion
|
||||
|
||||
We greatly value the work of security researchers and believe that responsible disclosure of vulnerabilities is a valuable contribution to the security of the Internet. We encourage users to contribute to the security of our project by reporting any security-related issues to us.
|
14
app.json
14
app.json
@ -1,8 +1,18 @@
|
||||
{
|
||||
"name": "gun-server",
|
||||
"website": "http://gun.js.org",
|
||||
"website": "http://gun.eco/",
|
||||
"repository": "https://github.com/amark/gun",
|
||||
"logo": "https://avatars3.githubusercontent.com/u/8811914",
|
||||
"keywords": ["node", "gun", "gunDB", "database","graph","offline-first"],
|
||||
"description": "Javascript, Offline-First Javascript Graph Database Server Peer"
|
||||
"description": "Javascript, Offline-First Javascript Graph Database Server Peer",
|
||||
"env": {
|
||||
"NPM_CONFIG_PRODUCTION": {
|
||||
"description": "If you do not want default features, set to \"true\".",
|
||||
"value": "false"
|
||||
},
|
||||
"PEERS": {
|
||||
"description": "Comma-separated list of peer urls to connect to",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
110
as.js
110
as.js
@ -1,7 +1,73 @@
|
||||
;(function(){
|
||||
function as(el, gun, cb){
|
||||
function as(el, gun, cb, opt){
|
||||
el = $(el);
|
||||
if(gun === as.gui && as.el && as.el.is(el)){ return }
|
||||
|
||||
opt = opt || {};
|
||||
opt.match = opt.match || '{{ ';
|
||||
opt.end = opt.end || ' }}';
|
||||
;(function(){ // experimental
|
||||
function nest(t, s,e, r, i,tmp,u){
|
||||
if(r && !r.length){ return t||'' }
|
||||
if(!t){ return [] }
|
||||
e = e || s;
|
||||
i = t.indexOf(s, i||0);
|
||||
if(0 > i){ return [] }
|
||||
tmp = t.indexOf(e, i+1);
|
||||
if(!r){ return [t.slice(i+s.length, tmp)].concat(nest(t, s,e, r, tmp,tmp,u)) }
|
||||
return t.slice(0,i)+r[0]+nest(t.slice(tmp+e.length), s,e, r.slice(1), 0,tmp,u);
|
||||
}
|
||||
|
||||
/* experimental */
|
||||
function template(tag, attr){
|
||||
var html = (tag = $(tag))[0].outerHTML, sub, tmp;
|
||||
if(html && (0 > html.indexOf(opt.match))){ return }
|
||||
if(!attr){
|
||||
$.each(tag[0].attributes, function(i,v){
|
||||
if(!v){ return }
|
||||
if(!nest(v.value, opt.match, opt.end).length){ return }
|
||||
template(tag, v.name)
|
||||
});
|
||||
if((sub = tag.children()).length){
|
||||
return sub.each(function(){ template(this) });
|
||||
}
|
||||
}
|
||||
var data = [], plate = attr? tag.attr(attr) : tag.html();
|
||||
tmp = nest(plate, opt.match, opt.end);
|
||||
if(!tmp.length){ return }
|
||||
$.each(tmp, function(pos, match){
|
||||
var expr = match.split(' ');
|
||||
var path = (expr[0]).split('.');
|
||||
if(expr = expr.slice(1).join(' ')){
|
||||
expr = new Function("_", "b", "return (_)" + expr);
|
||||
}
|
||||
var val = (expr && expr('')) || '';
|
||||
data.push(val);
|
||||
if(!attr){ tag.text(val) }
|
||||
|
||||
var ref = gun, sup = [], tmp;
|
||||
if(tmp = tag.attr('name')){ sup.push(tmp) }
|
||||
tag.parents("[name]").each(function(){
|
||||
sup.push($(this).attr('name'));
|
||||
});
|
||||
$.each(path = sup.reverse().concat(path), function(i,v){
|
||||
ref = ref.get(v);
|
||||
});
|
||||
ref.on(function(v){
|
||||
v = data[pos] = expr? expr(v) : v;
|
||||
var tmp = nest(plate, opt.match, opt.end, data);
|
||||
if(attr){
|
||||
tag.attr(attr, tmp);
|
||||
} else {
|
||||
tag.text(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
template(el);
|
||||
|
||||
}());
|
||||
|
||||
as.gui = gun;
|
||||
as.el = el;
|
||||
if(el.data('as')){
|
||||
@ -59,10 +125,12 @@
|
||||
if(many && ui.is('.sort')){
|
||||
var up = ui.closest("[name='#']");
|
||||
var tmp = as.sort(data, up.parent().children().last());
|
||||
up.insertAfter(tmp);
|
||||
tmp? up.insertAfter(tmp) : up.prependTo(up.parent());
|
||||
}
|
||||
if(as.lock === gui){ return }
|
||||
(ui[0] && u === ui[0].value)? ui.text(data) : ui.val(data);
|
||||
if(!(data && data instanceof Object)){
|
||||
(ui[0] && u === ui[0].value)? ui.text(data) : ui.val(data);
|
||||
}
|
||||
ui.data('was', data);
|
||||
if(cb){
|
||||
cb(data, key, ui);
|
||||
@ -80,12 +148,7 @@
|
||||
}, wait || 200);
|
||||
}
|
||||
}
|
||||
as.sort = function sort(id, li){
|
||||
var num = parseFloat(id);
|
||||
var id = $(li).find('.sort').text() || -Infinity;
|
||||
var at = num >= parseFloat(id);
|
||||
return at ? li : sort(id, li.prev());
|
||||
}
|
||||
as.sort = function sort(num, li){ return parseFloat(num) >= parseFloat($(li).find('.sort').text() || -Infinity)? li : sort(num, li.prev()) }
|
||||
$(document).on('keyup', 'input, textarea, [contenteditable]', as.wait(function(){
|
||||
var el = $(this);
|
||||
var data = (el[0] && u === el[0].value)? el.text() : el.val();
|
||||
@ -94,7 +157,7 @@
|
||||
as.lock = g;
|
||||
g.put(data);
|
||||
}, 99));
|
||||
$(document).on('submit', 'form', function(e){ e.preventDefault() });
|
||||
//$(document).on('submit', 'form', function(e){ e.preventDefault() });
|
||||
var u;
|
||||
window.as = as;
|
||||
$.as = as;
|
||||
@ -146,4 +209,31 @@
|
||||
;$(function(){
|
||||
$('.page').not(':first').hide();
|
||||
$.as.route(location.hash.slice(1));
|
||||
$(JOY.start = JOY.start || function(){ $.as(document, gun, null, JOY.opt) });
|
||||
|
||||
if($('body').attr('peers')){ (console.warn || console.log)('Warning: Please upgrade <body peers=""> to https://github.com/eraeco/joydb#peers !') }
|
||||
|
||||
});
|
||||
;(function(){ // need to isolate into separate module!
|
||||
var joy = window.JOY = function(){};
|
||||
joy.auth = function(a,b,cb,o){
|
||||
if(!o){ o = cb ; cb = 0 }
|
||||
if(o === true){
|
||||
gun.user().create(a, b);
|
||||
return;
|
||||
}
|
||||
gun.user().auth(a,b, cb,o);
|
||||
}
|
||||
|
||||
var opt = joy.opt = window.CONFIG || {}, peers;
|
||||
$('link[type=peer]').each(function(){ (peers || (peers = [])).push($(this).attr('href')) });
|
||||
!window.gun && (opt.peers = opt.peers || peers || (function(){
|
||||
(console.warn || console.log)('Warning: No peer provided, defaulting to DEMO peer. Do not run in production, or your data will be regularly wiped, reset, or deleted. For more info, check https://github.com/eraeco/joydb#peers !');
|
||||
return ['https://gunjs.herokuapp.com/gun'];
|
||||
}()));
|
||||
window.gun = window.gun || Gun(opt);
|
||||
|
||||
gun.on('auth', function(ack){
|
||||
console.log("Your namespace is publicly available at", ack.soul);
|
||||
});
|
||||
}());
|
188
axe.js
188
axe.js
@ -1,99 +1,109 @@
|
||||
;(function(){
|
||||
|
||||
/* UNBUILD */
|
||||
var root;
|
||||
if(typeof window !== "undefined"){ root = window }
|
||||
if(typeof global !== "undefined"){ root = global }
|
||||
root = root || {};
|
||||
var console = root.console || {log: function(){}};
|
||||
function USE(arg, req){
|
||||
return req? require(arg) : arg.slice? USE[R(arg)] : function(mod, path){
|
||||
arg(mod = {exports: {}});
|
||||
USE[R(path)] = mod.exports;
|
||||
}
|
||||
function R(p){
|
||||
return p.split('/').slice(-1).toString().replace('.js','');
|
||||
}
|
||||
}
|
||||
if(typeof module !== "undefined"){ var common = module }
|
||||
/* UNBUILD */
|
||||
var sT = setTimeout || {}, u;
|
||||
if(typeof window !== ''+u){ sT.window = window }
|
||||
var AXE = (sT.window||'').AXE || function(){};
|
||||
if(AXE.window = sT.window){ AXE.window.AXE = AXE }
|
||||
|
||||
;USE(function(module){
|
||||
if(typeof window !== "undefined"){ module.window = window }
|
||||
var tmp = module.window || module;
|
||||
var AXE = tmp.AXE || function(){};
|
||||
var Gun = (AXE.window||'').GUN || require('./gun');
|
||||
(Gun.AXE = AXE).GUN = AXE.Gun = Gun;
|
||||
|
||||
if(AXE.window = module.window){ try{
|
||||
AXE.window.AXE = AXE;
|
||||
tmp = document.createEvent('CustomEvent');
|
||||
tmp.initCustomEvent('extension', false, false, {type: "AXE"});
|
||||
(window.dispatchEvent || window.fireEvent)(tmp);
|
||||
window.postMessage({type: "AXE"}, '*');
|
||||
} catch(e){} }
|
||||
//if(!Gun.window){ try{ require('./lib/axe') }catch(e){} }
|
||||
if(!Gun.window){ require('./lib/axe') }
|
||||
|
||||
try{ if(typeof common !== "undefined"){ common.exports = AXE } }catch(e){}
|
||||
module.exports = AXE;
|
||||
})(USE, './root');
|
||||
|
||||
;USE(function(module){
|
||||
Gun.on('opt', function(at){ start(at) ; this.to.next(at) }); // make sure to call the "next" middleware adapter.
|
||||
|
||||
var AXE = USE('./root'), Gun = (AXE.window||{}).Gun || USE('./gun', 1);
|
||||
(Gun.AXE = AXE).GUN = AXE.Gun = Gun;
|
||||
function start(root){
|
||||
if(root.axe){ return }
|
||||
var opt = root.opt, peers = opt.peers;
|
||||
if(false === opt.axe){ return }
|
||||
if(!Gun.window){ return } // handled by ^ lib/axe.js
|
||||
var w = Gun.window, lS = w.localStorage || opt.localStorage || {}, loc = w.location || opt.location || {}, nav = w.navigator || opt.navigator || {};
|
||||
var axe = root.axe = {}, tmp, id;
|
||||
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
|
||||
|
||||
Gun.on('opt', function(at){
|
||||
if(!at.axe){
|
||||
at.axe = {};
|
||||
var p = at.opt.peers, tmp;
|
||||
// 1. If any remembered peers or from last cache or extension
|
||||
// 2. Fallback to use hard coded peers from dApp
|
||||
// 3. Or any offered peers.
|
||||
//if(Gun.obj.empty(p)){
|
||||
// Gun.obj.map(['http://localhost:8765/gun'/*, 'https://guntest.herokuapp.com/gun'*/], function(url){
|
||||
// p[url] = {url: url, axe: {}};
|
||||
// });
|
||||
//}
|
||||
// Our current hypothesis is that it is most optimal
|
||||
// to take peers in a common network, and align
|
||||
// them in a line, where you only have left and right
|
||||
// peers, so messages propagate left and right in
|
||||
// a linear manner with reduced overlap, and
|
||||
// with one common superpeer (with ready failovers)
|
||||
// in case the p2p linear latency is high.
|
||||
// Or there could be plenty of other better options.
|
||||
console.log("axe", at.opt);
|
||||
if(at.opt.super){
|
||||
function verify(msg, send, at) {
|
||||
var peers = Object.keys(p), puts = Object.keys(msg.put), i, j, peer;
|
||||
var soul = puts[0]; /// TODO: verify all souls in puts. Copy the msg only with subscribed souls?
|
||||
for (i=0; i < peers.length; ++i) {
|
||||
peer = p[peers[i]];
|
||||
//if (peer.url) {console.log('AXE do not reject superpeers'); send(msg, peer); continue;} /// always send to superpeers?
|
||||
if (!peer.id) {console.log('AXE peer without id: ', peer); continue;}
|
||||
if (!Gun.subscribe[soul] || !Gun.subscribe[soul][peer.id]) { console.log('AXE SAY reject msg to peer: %s, soul: %s', peer.id, soul); continue; }
|
||||
send(msg, peer);
|
||||
}
|
||||
}
|
||||
AXE.say = function(msg, send, at) {
|
||||
if (!msg.put) { send(msg); return; }
|
||||
console.log('AXE HOOK!! ', msg);
|
||||
verify(msg, send, at);
|
||||
};
|
||||
/// TODO: remove peer from all Gun.subscribe. On `mesh.bye` event?
|
||||
}
|
||||
if(at.opt.super){
|
||||
at.on('in', USE('./lib/super', 1), at);
|
||||
} else {
|
||||
//at.on('in', input, at);
|
||||
}
|
||||
}
|
||||
this.to.next(at); // make sure to call the "next" middleware adapter.
|
||||
});
|
||||
tmp = peers[id = loc.origin + '/gun'] = peers[id] || {};
|
||||
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
|
||||
tmp = peers[id = 'http://localhost:8765/gun'] = peers[id] || {};
|
||||
tmp.id = tmp.url = id; tmp.retry = tmp.retry || 0;
|
||||
Gun.log.once("AXE", "AXE enabled: Trying to find network via (1) local peer (2) last used peers (3) a URL parameter, and last (4) hard coded peers.");
|
||||
Gun.log.once("AXEWarn", "Warning: AXE is in alpha, use only for testing!");
|
||||
var last = lS.peers || ''; if(last){ last += ' ' }
|
||||
last += ((loc.search||'').split('peers=')[1]||'').split('&')[0];
|
||||
|
||||
function input(msg){
|
||||
var at = this.as, to = this.to;
|
||||
}
|
||||
root.on('bye', function(peer){
|
||||
this.to.next(peer);
|
||||
if(!peer.url){ return } // ignore WebRTC disconnects for now.
|
||||
if(!nav.onLine){ peer.retry = 1 }
|
||||
if(peer.retry){ return }
|
||||
if(axe.fall){ delete axe.fall[peer.url || peer.id] }
|
||||
(function next(){
|
||||
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
|
||||
var fall = Object.keys(axe.fall||''), one = fall[(Math.random()*fall.length) >> 0];
|
||||
if(!fall.length){ lS.peers = ''; one = 'https://gunjs.herokuapp.com/gun' } // out of peers
|
||||
if(peers[one]){ next(); return } // already choose
|
||||
mesh.hi(one);
|
||||
}());
|
||||
});
|
||||
|
||||
module.exports = AXE;
|
||||
})(USE, './axe');
|
||||
root.on('hi', function(peer){ // TEMPORARY! Try to connect all peers.
|
||||
this.to.next(peer);
|
||||
if(!peer.url){ return } // ignore WebRTC disconnects for now.
|
||||
return; // DO NOT COMMIT THIS FEATURE YET! KEEP TESTING NETWORK PERFORMANCE FIRST!
|
||||
(function next(){
|
||||
if(!peer.wire){ return }
|
||||
if(!axe.fall){ setTimeout(next, 9); return } // not found yet
|
||||
var one = (next.fall = next.fall || Object.keys(axe.fall||'')).pop();
|
||||
if(!one){ return }
|
||||
setTimeout(next, 99);
|
||||
mesh.say({dam: 'opt', opt: {peers: one}}, peer);
|
||||
}());
|
||||
});
|
||||
|
||||
}());
|
||||
function found(text){
|
||||
|
||||
axe.fall = {};
|
||||
((text||'').match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/ig)||[]).forEach(function(url){
|
||||
axe.fall[url] = {url: url, id: url, retry: 0}; // RETRY
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
// TODO: Finish porting below? Maybe not.
|
||||
|
||||
Object.keys(last.peers||'').forEach(function(key){
|
||||
tmp = peers[id = key] = peers[id] || {};
|
||||
tmp.id = tmp.url = id;
|
||||
});
|
||||
tmp = peers[id = 'https://guntest.herokuapp.com/gun'] = peers[id] || {};
|
||||
tmp.id = tmp.url = id;
|
||||
|
||||
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root); // DAM!
|
||||
mesh.way = function(msg){
|
||||
if(root.$ === msg.$ || (msg._||'').via){
|
||||
mesh.say(msg, opt.peers);
|
||||
return;
|
||||
}
|
||||
var at = (msg.$||'')._;
|
||||
if(!at){ mesh.say(msg, opt.peers); return }
|
||||
if(msg.get){
|
||||
if(at.axe){ return } // don't ask for it again!
|
||||
at.axe = {};
|
||||
}
|
||||
mesh.say(msg, opt.peers);
|
||||
}
|
||||
}
|
||||
|
||||
if(last){ found(last); return }
|
||||
try{ fetch(((loc.search||'').split('axe=')[1]||'').split('&')[0] || loc.axe || 'https://raw.githubusercontent.com/wiki/amark/gun/volunteer.dht.md').then(function(res){
|
||||
return res.text()
|
||||
}).then(function(text){
|
||||
found(lS.peers = text);
|
||||
}).catch(function(){
|
||||
found(); // nothing
|
||||
})}catch(e){found()}
|
||||
}
|
||||
|
||||
var empty = {}, yes = true;
|
||||
try{ if(typeof module != ''+u){ module.exports = AXE } }catch(e){}
|
||||
}());
|
4
browser.js
Normal file
4
browser.js
Normal file
@ -0,0 +1,4 @@
|
||||
// if(!(typeof navigator == "undefined") && navigator.product == "ReactNative"){
|
||||
// require("./lib/mobile.js");
|
||||
// }
|
||||
module.exports = require('./gun.js');
|
217
examples/Main.js
Normal file
217
examples/Main.js
Normal file
@ -0,0 +1,217 @@
|
||||
import { render } from './iris/js/lib/preact.js';
|
||||
import { Router, route } from './iris/js/lib/preact-router.es.js';
|
||||
import { createHashHistory } from './iris/js/lib/history.production.min.js';
|
||||
import { Component } from './iris/js/lib/preact.js';
|
||||
import { Link } from './iris/js/lib/preact.match.js';
|
||||
|
||||
import Helpers from './iris/js/Helpers.js';
|
||||
import { html } from './iris/js/Helpers.js';
|
||||
import QRScanner from './iris/js/QRScanner.js';
|
||||
import PeerManager from './iris/js/PeerManager.js';
|
||||
import Session from './iris/js/Session.js';
|
||||
import { translate as t } from './iris/js/Translation.js';
|
||||
|
||||
import Settings from './iris/js/views/Settings.js';
|
||||
import LogoutConfirmation from './iris/js/views/LogoutConfirmation.js';
|
||||
import Chat from './iris/js/views/Chat.js';
|
||||
import Store from './iris/js/views/Store.js';
|
||||
import Checkout from './iris/js/views/Checkout.js';
|
||||
import Product from './iris/js/views/Product.js';
|
||||
import Login from './iris/js/views/Login.js';
|
||||
import Profile from './iris/js/views/Profile.js';
|
||||
import Group from './iris/js/views/Group.js';
|
||||
import Message from './iris/js/views/Message.js';
|
||||
import Follows from './iris/js/views/Follows.js';
|
||||
import Feed from './iris/js/views/Feed.js';
|
||||
import About from './iris/js/views/About.js';
|
||||
import Explorer from './iris/js/views/Explorer.js';
|
||||
import Contacts from './iris/js/views/Contacts.js';
|
||||
import Torrent from './iris/js/views/Torrent.js';
|
||||
|
||||
import VideoCall from './iris/js/components/VideoCall.js';
|
||||
import Identicon from './iris/js/components/Identicon.js';
|
||||
import MediaPlayer from './iris/js/components/MediaPlayer.js';
|
||||
import Footer from './iris/js/components/Footer.js';
|
||||
import State from './iris/js/State.js';
|
||||
import Icons from './iris/js/Icons.js';
|
||||
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const isElectron = (userAgent.indexOf(' electron/') > -1);
|
||||
if (!isElectron && ('serviceWorker' in navigator)) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('iris/serviceworker.js')
|
||||
.catch(function(err) {
|
||||
// registration failed :(
|
||||
console.log('ServiceWorker registration failed: ', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
State.init();
|
||||
Session.init({autologin: true});
|
||||
PeerManager.init();
|
||||
|
||||
Helpers.checkColorScheme();
|
||||
|
||||
const APPLICATIONS = [ // TODO: move editable shortcuts to State.local gun
|
||||
{url: '/', text: t('home'), icon: Icons.home},
|
||||
{url: '/feed', text: t('feed'), icon: Icons.feed},
|
||||
{url: '/media', text: t('media'), icon: Icons.play},
|
||||
{url: '/settings', text: t('settings'), icon: Icons.settings},
|
||||
{url: '/store', text: t('store'), icon: Icons.store},
|
||||
{url: '/explorer', text: t('explorer'), icon: Icons.folder},
|
||||
{url: '/chat', text: t('messages'), icon: Icons.chat},
|
||||
// {url: '/store', text: t('store'), icon: Icons.store}, // restore when it works!
|
||||
{},
|
||||
{url: '../stats.html', text: 'Gun node stats'},
|
||||
{url: '../iris/index.html', text: 'Iris', icon: html`<img src="iris/img/icon128.png" width=24/>`},
|
||||
{url: '../infinite-scroll/index.html', text: 'Infinite scroll'},
|
||||
{url: '../chat/index.html', text: 'Chat'},
|
||||
{url: '../game/space.html', text: 'Space'},
|
||||
{},
|
||||
{url: 'https://gun.eco/docs/', text: 'Gun documentation'},
|
||||
{url: 'https://examples.iris.to/components/', text: 'Iris web components'}
|
||||
];
|
||||
|
||||
const HomeView = () => {
|
||||
return html`
|
||||
<div class="main-view">
|
||||
<div class="centered-container public-messages-view">
|
||||
<h1>Hello, world!</h1>
|
||||
<p>Here you can find sample applications and utilities for <a href="https://github.com/amark/gun">GUN</a>.</p>
|
||||
<p>If you need any help, please feel free to join the GUN community chat: <a href="http://chat.gun.eco">http://chat.gun.eco</a></p>
|
||||
<a href="/explorer" class="msg"><div class="msg-content">
|
||||
<b>Explorer</b>
|
||||
<p>Explore the data saved on the GUN database. Open to the side while using an application and see the data change in real-time.</p>
|
||||
</div></a>
|
||||
<a class="msg" href="game/space.html"><div class="msg-content">
|
||||
<div class="img-container"><img src="iris/img/space-game.jpg"/></div>
|
||||
<b>Space</b>
|
||||
<p>Spaceflight game. Open in 2 or more browser windows.</p>
|
||||
</div></a>
|
||||
<a class="msg" href="/iris/index.html"><div class="msg-content">
|
||||
<div class="img-container"><img src="iris/img/screenshot.png"/></div>
|
||||
<b>Iris</b>
|
||||
<p>Decentralized Twitter/Instagram. Provides modular components that can be reused in other applications (including this one).</p>
|
||||
</div></a>
|
||||
<a native class="msg" href="/chat/index.html"><div class="msg-content">
|
||||
<div class="img-container"><img src="iris/img/gun-chat.jpg"/></div>
|
||||
<b>Chat</b>
|
||||
<p>Shoutbox!</p>
|
||||
</div></a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
class MenuView extends Component {
|
||||
componentDidMount() {
|
||||
State.local.get('showMenu').on(showMenu => this.setState({showMenu}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const pub = Session.getPubKey();
|
||||
return html`
|
||||
<div class="application-list ${this.state.showMenu ? 'menu-visible-xs' : ''}">
|
||||
<a href="/profile/${pub}">
|
||||
<span class="icon"><${Identicon} str=${pub} width=40/></span>
|
||||
<span class="text" style="font-size: 1.2em;border:0;margin-left: 7px;"><iris-text user="${pub}" path="profile/name" editable="false"/></span>
|
||||
</a>
|
||||
<br/><br/>
|
||||
${APPLICATIONS.map(a => {
|
||||
if (a.url) {
|
||||
return html`
|
||||
<a href=${a.url}>
|
||||
<span class="icon">${a.icon || Icons.circle}</span>
|
||||
<span class="text">${a.text}</span>
|
||||
</a>`;
|
||||
} else {
|
||||
return html`<br/><br/>`;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
class Main extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
State.local.get('loggedIn').on(loggedIn => this.setState({loggedIn}));
|
||||
}
|
||||
|
||||
handleRoute(e) {
|
||||
let activeRoute = e.url;
|
||||
if (!activeRoute && window.location.hash) {
|
||||
return route(window.location.hash.replace('#', '')); // bubblegum fix back navigation
|
||||
}
|
||||
document.title = 'Iris';
|
||||
if (activeRoute && activeRoute.length > 1) { document.title += ' - ' + Helpers.capitalize(activeRoute.replace('/', '')); }
|
||||
State.local.get('activeRoute').put(activeRoute);
|
||||
QRScanner.cleanupScanner();
|
||||
}
|
||||
|
||||
onClickOverlay() {
|
||||
if (this.state.showMenu) {
|
||||
this.setState({showMenu: false});
|
||||
}
|
||||
}
|
||||
|
||||
toggleMenu(show) {
|
||||
this.setState({showMenu: typeof show === 'undefined' ? !this.state.showMenu : show});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.state.loggedIn ? html`
|
||||
<div class="visible-xs-flex" style="border-bottom:var(--sidebar-border-right)">
|
||||
<svg onClick=${() => State.local.get('showMenu').put(this.showMenu = !this.showMenu)} style="padding: 5px;cursor:pointer;" viewBox="0 -53 384 384" width="40px"><path d="m368 154.667969h-352c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h352c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/><path d="m368 32h-352c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h352c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/><path d="m368 277.332031h-352c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h352c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/></svg>
|
||||
</div>
|
||||
<section class="main" style="flex-direction: row;">
|
||||
<${MenuView}/>
|
||||
<div style="flex: 3; display: flex">
|
||||
<${Router} history=${createHashHistory()} onChange=${e => this.handleRoute(e)}>
|
||||
<${HomeView} path="/"/>
|
||||
<${Feed} path="/feed"/>
|
||||
<${Feed} path="/search/:term?/:type?"/>
|
||||
<${Feed} path="/media" index="media"/>
|
||||
<${Login} path="/login"/>
|
||||
<${Chat} path="/chat/:id?"/>
|
||||
<${Message} path="/post/:hash"/>
|
||||
<${Torrent} path="/torrent/:id"/>
|
||||
<${About} path="/about"/>
|
||||
<${Settings} path="/settings"/>
|
||||
<${LogoutConfirmation} path="/logout"/>
|
||||
<${Profile} path="/profile/:id?" tab="profile"/>
|
||||
<${Profile} path="/replies/:id?" tab="replies"/>
|
||||
<${Profile} path="/likes/:id?" tab="likes"/>
|
||||
<${Profile} path="/media/:id" tab="media"/>
|
||||
<${Group} path="/group/:id?"/>
|
||||
<${Store} path="/store/:store?"/>
|
||||
<${Checkout} path="/checkout/:store?"/>
|
||||
<${Product} path="/product/:product/:store"/>
|
||||
<${Product} path="/product/new" store=Session.getPubKey()/>
|
||||
<${Explorer} path="/explorer/:node"/>
|
||||
<${Explorer} path="/explorer"/>
|
||||
<${Follows} path="/follows/:id"/>
|
||||
<${Follows} followers=${true} path="/followers/:id"/>
|
||||
<${Contacts} path="/contacts"/>
|
||||
</${Router}>
|
||||
</div>
|
||||
</section>
|
||||
<${VideoCall}/>
|
||||
` : '';
|
||||
return html`
|
||||
<div id="main-content">
|
||||
${content}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
render(html`<${Main}/>`, document.body);
|
||||
|
||||
$('body').css('opacity', 1); // use opacity because setting focus on display: none elements fails
|
@ -22,7 +22,7 @@
|
||||
"@angular/router": "^4.1.0",
|
||||
"core-js": "^2.4.1",
|
||||
"express-http-proxy": "^1.0.1",
|
||||
"gun": "^0.7.4",
|
||||
"gun": "https://github.com/amark/gun.git#master",
|
||||
"ngx-pipes": "^2.0.5",
|
||||
"rxjs": "^5.3.0",
|
||||
"underscore": "^1.8.3",
|
10047
examples/angular/package-lock.json
generated
10047
examples/angular/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ import { pick } from 'underscore';
|
||||
export function on$(node, cleanup = true): Observable<any> {
|
||||
return Observable.fromEventPattern(
|
||||
h => {
|
||||
// there is no way to off() an on() until at least one value is trigerred
|
||||
// there is no way to off() an on() until at least one value is triggered
|
||||
// so that we can access the event listener to off() it
|
||||
const signal = { stop: false };
|
||||
node.on((data, key, at, ev) => {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,8 +8,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3 id="pid"></h3>
|
||||
<script src="../gun.js"></script>
|
||||
<!-- <script src="../axe.js"></script> -->
|
||||
<script src="../axe.js"></script>
|
||||
<script src="../lib/radix.js"></script>
|
||||
<script src="../lib/webrtc.js"></script>
|
||||
<!-- <script src="../sea.js"></script> -->
|
||||
<script>
|
||||
var pid = location.hash.slice(1);
|
||||
@ -23,14 +26,17 @@
|
||||
Gun.on('opt', function(ctx) {
|
||||
this.to.next(ctx);
|
||||
ctx.on('hi', function(opt) {
|
||||
console.log('HI!! PEER', new Date(), opt.pid);
|
||||
// console.log('HI!! PEER', new Date(), opt.pid);
|
||||
setTimeout(function() {
|
||||
document.getElementById('pid').innerHTML = gun._.opt.pid;
|
||||
});
|
||||
});
|
||||
if (pid) {
|
||||
ctx.on('out', function(msg) {
|
||||
msg.pid = pid;
|
||||
this.to.next(msg);
|
||||
});
|
||||
}
|
||||
// if (pid) {
|
||||
// ctx.on('out', function(msg) {
|
||||
// msg.pid = pid;
|
||||
// this.to.next(msg);
|
||||
// });
|
||||
// }
|
||||
});
|
||||
|
||||
var gun = Gun(opt);
|
||||
|
21
examples/basic/chat.html
Normal file
21
examples/basic/chat.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<ul id='list'></ul>
|
||||
<form id='form'>
|
||||
<input id='who' placeholder='name'>
|
||||
<input id='what' placeholder='say'>
|
||||
<input type='submit' value='send'>
|
||||
</form>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/axe.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/emojione@4.0.0/lib/js/emojione.min.js"></script>
|
||||
<script>
|
||||
gun = GUN(), chat = gun.get("note" + location.hash.replace('#','/')), view = document;
|
||||
form.onsubmit = (eve) => { chat.set(who.value+': '+what.value), eve.preventDefault(what.value = "") }
|
||||
chat.map().on(function show(data, id){
|
||||
(view.line = view.getElementById(id) || view.createElement("li")).id = id;
|
||||
list.appendChild(view.line).innerText = emojione.shortnameToUnicode(data);
|
||||
window.scroll(0, list.offsetHeight);
|
||||
(list.beep = new SpeechSynthesisUtterance()).text = "new";
|
||||
list.beep.rate = 10, list.beep.pitch = 2, window.speechSynthesis.speak(list.beep);
|
||||
});
|
||||
</script>
|
2
examples/basic/emoji.html
Normal file
2
examples/basic/emoji.html
Normal file
@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<p>Moved to <a href="./chat.html">./chat.html</a>!</p>
|
38
examples/basic/meet.html
Normal file
38
examples/basic/meet.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<center>must press play or unmute on new videos to accept meeting</center>
|
||||
<center id="videos">
|
||||
<video id="me" width="100%" controls autoplay playsinline muted></video>
|
||||
</center>
|
||||
<center>Stream <select id="select"><option id="from">from</option></select></center>
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/sea.js"></script>
|
||||
<script src="../../../gun/lib/webrtc.js"></script>
|
||||
|
||||
<script>;(async function(){
|
||||
streams = {}, gun = Gun(location.origin + '/gun'); //gun = GUN();
|
||||
mesh = gun.back('opt.mesh');
|
||||
|
||||
(await (me.stream = navigator.mediaDevices).enumerateDevices()).forEach((device,i) => {
|
||||
if('videoinput' !== device.kind){ return }
|
||||
var opt = $(from).clone().prependTo('select').get(0);
|
||||
$(opt).text(opt.id = device.label || 'Camera '+i);
|
||||
opt.value = device.deviceId;
|
||||
});
|
||||
|
||||
$('select').on('change', async eve => { $(from).text('Off'); // update label
|
||||
if('Off' == select.value){ return me.srcObject.getTracks()[0].stop() }
|
||||
mesh.hi(me.srcObject = await me.stream.getUserMedia({ audio: true,
|
||||
video: (select.value && {deviceId: {exact: select.value}}) || {facingMode: "environment"}
|
||||
}));
|
||||
});
|
||||
|
||||
gun.on('rtc', async function(eve){ var ui, src;
|
||||
console.log("?RTC?", eve.peer && eve.peer.connectionState, eve);
|
||||
if(!(src = eve.streams)){ return }
|
||||
ui = $('#v'+(src=src[0]).id).get(0) || $(me).clone().attr('id', 'v'+src.id).prependTo('#videos').get(0); // reuse or create video element
|
||||
ui.srcObject = src;
|
||||
});
|
||||
}());</script>
|
9
examples/basic/note.html
Normal file
9
examples/basic/note.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<style>html, body, textarea { width: 100%; height: 100%; padding: 0; margin: 0; }</style>
|
||||
<textarea id="view" placeholder="write here..."></textarea>
|
||||
<script src="../../../gun/gun.js"></script><script>
|
||||
gun = GUN(location.origin + '/gun');
|
||||
note = gun.get('note').get(location.hash.replace('#','')||1);
|
||||
view.oninput = () => { note.put(view.value) };
|
||||
note.on((data) => { view.value = data });
|
||||
</script>
|
2
examples/basic/paste.html
Normal file
2
examples/basic/paste.html
Normal file
@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<p>Moved to <a href="./note.html">./note.html</a>!</p>
|
148
examples/basic/poll.html
Normal file
148
examples/basic/poll.html
Normal file
@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/sea.js"></script>
|
||||
<!-- script src="../../../gun/axe.js"></script -->
|
||||
<script> // main init!
|
||||
var app = {
|
||||
view: $, // replace with not jquery!
|
||||
data: GUN('http://localhost:8765/gun'), // peer-to-peer database!
|
||||
};
|
||||
app.user = app.data.user().recall({sessionStorage: true});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="login" class="center pad">
|
||||
<style>
|
||||
#login input {
|
||||
max-width: 6em;
|
||||
}
|
||||
</style>
|
||||
<form id="sign" onsubmit="app.login(event)">
|
||||
<input id="alias" placeholder="username" class="jot rim">
|
||||
<input id="pass" type="password" placeholder="passphrase" class="jot rim">
|
||||
<input id="in" type="submit" value="sign in" class="green whitet act gap sap rim">
|
||||
<input id="up" type="button" value="sign up" onclick="app.register()" class="act gap sap rim">
|
||||
</form>
|
||||
<script>
|
||||
app.login = function(eve){
|
||||
if(app.error(eve)){ return }
|
||||
app.data.user().auth(
|
||||
app.view('#alias').val(),
|
||||
app.view('#pass').val(),
|
||||
app.error
|
||||
);
|
||||
};
|
||||
|
||||
app.register = function(eve){
|
||||
app.data.user().create(
|
||||
app.view('#alias').val(),
|
||||
app.view('#pass').val(),
|
||||
app.login
|
||||
);
|
||||
};
|
||||
|
||||
app.data.on('auth', function(eve){
|
||||
app.view('#sign').hide(); // hide login form upon logging in.
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="poll" class="pad">
|
||||
<style>
|
||||
#poll {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#poll div {
|
||||
margin: 1%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(window.onhashchange = async function(){
|
||||
app.poll = app.data.get(location.hash.slice(1));
|
||||
app.poll.map().on(function(data, id){
|
||||
app.render(id = 'p'+String.hash(id), '.q', '#poll', data).css({order: data.how}).data('as',{$:this});
|
||||
console.log("poll?", id, data);
|
||||
});
|
||||
})();
|
||||
app.render = function(id, model, onto, data){
|
||||
var ui = $(
|
||||
$('#'+id).get(0) ||
|
||||
$('.model').find(model).clone(true).attr('id', id).appendTo(onto)
|
||||
);
|
||||
$.each(data, function(field, val){
|
||||
if($.isPlainObject(val)){ return }
|
||||
ui.find("[name='" + field + "']").val(val).text(val);
|
||||
});
|
||||
return ui;
|
||||
}
|
||||
</script>
|
||||
<div class="model">
|
||||
<div class="q">
|
||||
<span name="what"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="make" class="pad">
|
||||
<style>
|
||||
#make #add {
|
||||
border-radius: 100%;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
line-height: 0em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<button id="add" onclick="app.add()" class="green whitet act">+</button>
|
||||
<span class="hint">add new title, text, question...</span>
|
||||
<script>
|
||||
app.add = async function(){
|
||||
if(app.error(app.user)){ return }
|
||||
var tmp = await (app.poll = app.poll || app.data.get(location.hash.slice(1)));
|
||||
if(!tmp){ app.poll = app.user.get('poll').set({}) }
|
||||
app.poll.set({how: tmp = Object.keys(tmp||'').length || 1, what: "Question " + tmp });
|
||||
if(!location.hash){ location.hash = (await app.poll)._['#'] }
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<span id="error">
|
||||
<span id="err"></span>
|
||||
<script>
|
||||
app.error = function(eve){
|
||||
app.view('#err').text('').hide();
|
||||
if(!eve){ return }
|
||||
if(eve.preventDefault){
|
||||
eve.preventDefault();
|
||||
return;
|
||||
}
|
||||
if(eve._ && !eve.is){ eve = {err: "Not signed in!"} }
|
||||
if(!eve.err){ return }
|
||||
app.view('#err').text(eve.err).show();
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
#error { position: fixed; top: 0; width: 100%; text-align: center; background: white; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="../style.css"/>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Oxygen');
|
||||
html, body { font-family: "Oxygen", sans-serif; }
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
45
examples/basic/post.html
Normal file
45
examples/basic/post.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<h1>Posts</h1>
|
||||
|
||||
<form id="sign">
|
||||
<input id="alias" placeholder="username">
|
||||
<input id="pass" type="password" placeholder="passphrase">
|
||||
<input id="in" type="submit" value="sign in">
|
||||
<input id="up" type="button" value="sign up">
|
||||
</form>
|
||||
|
||||
<form id="said">
|
||||
<input id="say" placeholder="write here...">
|
||||
<input id="speak" type="submit" value="say">
|
||||
</form>
|
||||
|
||||
<ul></ul>
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/sea.js"></script>
|
||||
<script src="../../../gun/axe.js"></script>
|
||||
|
||||
<script>
|
||||
gun = GUN(), user = gun.user().recall({sessionStorage: true});
|
||||
|
||||
$('#sign').on('submit', login);
|
||||
$('#up').on('click', () => { user.create($('#alias').val(), $('#pass').val(), login) });
|
||||
function login(eve){
|
||||
eve.preventDefault();
|
||||
user.auth($('#alias').val(), $('#pass').val());
|
||||
};
|
||||
|
||||
gun.on('auth', () => { $('#sign').hide(), user.get('said').map().on(show) });
|
||||
function show(data, id){
|
||||
return ($('#' + id).get(0) || $('<li>').attr('id', id).prependTo('ul')).text(data);
|
||||
};
|
||||
|
||||
$('#said').on('submit', (eve) => {
|
||||
eve.preventDefault();
|
||||
if(!user.is){ return }
|
||||
user.get('said').set($('#say').val());
|
||||
$('#say').val("");
|
||||
});
|
||||
</script>
|
@ -1,4 +1,4 @@
|
||||
<html><body>
|
||||
<!DOCTYPE html>
|
||||
<style>
|
||||
html, body {
|
||||
background: rgb(245, 245, 245);
|
||||
@ -70,13 +70,98 @@
|
||||
Public Key: <input id="pub">
|
||||
</div></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/examples/jquery.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/sea.js"></script>
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/sea.js"></script>
|
||||
|
||||
<script>
|
||||
//var gun = Gun();
|
||||
var gun = Gun('http://localhost:8080/gun');
|
||||
|
||||
// extend SEA functions to base64 encode encrypted data
|
||||
// workaround for https://github.com/amark/gun/issues/783
|
||||
|
||||
(() => {
|
||||
const _encrypt = SEA.encrypt;
|
||||
SEA.encrypt = function(...args) {
|
||||
return _encrypt.apply(this, args).then(enc => btoa(JSON.stringify(enc)));
|
||||
}
|
||||
|
||||
const _decrypt = SEA.decrypt;
|
||||
SEA.decrypt = function(data, ...args) {
|
||||
try { data = JSON.parse(atob(data)); }
|
||||
finally { return _decrypt.apply(this, [data, ...args]); }
|
||||
}
|
||||
})();
|
||||
|
||||
// override User functions to fix several issues
|
||||
// see https://github.com/amark/gun/issues/808
|
||||
|
||||
SEA.Gun.User.prototype.grant = function grant(to, cb) {
|
||||
const gun = this; const user = gun.back(-1).user();
|
||||
const pair = user._.sea; let path = '';
|
||||
|
||||
gun.back(at => { if (at.has) { path += at.get; } });
|
||||
|
||||
(async () => {
|
||||
let enc, sec;
|
||||
|
||||
if (sec = await user.get('trust').get(pair.pub).get(path).then()) {
|
||||
sec = await SEA.decrypt(sec, pair);
|
||||
|
||||
} else {
|
||||
sec = SEA.random(24).toString();
|
||||
enc = await SEA.encrypt(sec, pair);
|
||||
|
||||
user.get('trust').get(pair.pub).get(path).put(enc);
|
||||
}
|
||||
|
||||
let pub = to.get('pub') .then();
|
||||
let epub = to.get('epub').then();
|
||||
|
||||
pub = await pub; epub = await epub;
|
||||
|
||||
const dh = await SEA.secret (epub, pair);
|
||||
enc = await SEA.encrypt(sec, dh);
|
||||
|
||||
// if pub is not already in trust, first put an empty node
|
||||
// workaround for https://github.com/amark/gun/issues/844
|
||||
|
||||
if (!await user.get('trust').get(pub).then()) {
|
||||
await user.get('trust').get(pub).get(path).put({}).then();
|
||||
}
|
||||
|
||||
user.get('trust').get(pub).get(path).put(enc, cb);
|
||||
})();
|
||||
|
||||
return gun;
|
||||
}
|
||||
|
||||
SEA.Gun.User.prototype.secret = function(data, cb) {
|
||||
const gun = this; const user = gun.back(-1).user();
|
||||
const pair = user._.sea; let path = '';
|
||||
|
||||
gun.back(at => { if (at.has) { path += at.get; } });
|
||||
|
||||
(async () => {
|
||||
let enc, sec;
|
||||
|
||||
if (sec = await user.get('trust').get(pair.pub).get(path).then()) {
|
||||
sec = await SEA.decrypt(sec, pair);
|
||||
|
||||
} else {
|
||||
sec = SEA.random(24).toString();
|
||||
enc = await SEA.encrypt(sec, pair);
|
||||
|
||||
user.get('trust').get(pair.pub).get(path).put(enc);
|
||||
}
|
||||
|
||||
enc = await SEA.encrypt(data, sec);
|
||||
gun.put(enc, cb);
|
||||
})();
|
||||
|
||||
return gun;
|
||||
}
|
||||
|
||||
var gun = Gun('http://localhost:8765/gun');
|
||||
var user = gun.user();
|
||||
var LI = {};
|
||||
|
||||
@ -94,7 +179,7 @@ $('#sign').on('submit', function(e){
|
||||
gun.on('auth', function(){
|
||||
$('#sign').hide();
|
||||
$('#profile').show();
|
||||
var pub = user.pair().pub;
|
||||
var pub = user._.sea.pub;
|
||||
$('#pub').val(pub);
|
||||
return;
|
||||
$("#search").val(pub).trigger('blur');
|
||||
@ -126,15 +211,25 @@ $('#search').on('blur', function(e){
|
||||
ev.off();
|
||||
return;
|
||||
}
|
||||
Gun.node.is(data, async function(v, k){
|
||||
if(k === LI.busy){ return }
|
||||
var key = await find.get('trust').get(user.pair().pub).get(k+'profile').then();
|
||||
var mix = await Gun.SEA.secret(await find.get('epub').then(), user.pair());
|
||||
key = await Gun.SEA.decrypt(key, mix);
|
||||
var val = await Gun.SEA.decrypt(v, key);
|
||||
$('#'+k).val(val || v);
|
||||
|
||||
Gun.node.is(data, async (enc, id) => {
|
||||
if (id === LI.busy) { return; }
|
||||
|
||||
const pair = user._.sea;
|
||||
let key, val;
|
||||
|
||||
if (key =
|
||||
await find.get('trust').get(pair.pub).get(id + 'profile').then()) {
|
||||
const mix = await Gun.SEA.secret(await find.get('epub').then(), pair);
|
||||
|
||||
key = await Gun.SEA.decrypt(key, mix);
|
||||
val = await Gun.SEA.decrypt(enc, key);
|
||||
|
||||
// decode encrypted data to show 'SEA{...}'
|
||||
} else { val = JSON.parse(atob(enc)); }
|
||||
|
||||
$('#' + id).val(val);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
</script>
|
121
examples/basic/schedule.html
Normal file
121
examples/basic/schedule.html
Normal file
@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
<link href='https://fonts.googleapis.com/css?family=Poiret+One' rel='stylesheet' type='text/css'>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1><button id="left">←</button> <span id="date"></span> Schedule <button id="right">→</button></h1>
|
||||
|
||||
<form id="add">
|
||||
<style>input[type="number"]{ width: 4em; }</style>
|
||||
<input id="what" placeholder="What?">
|
||||
<input id="where" placeholder="Where?">
|
||||
<input type="number" id="hour"><script>hour.value = new Date().getHours() % 12 || 12</script> :
|
||||
<input type="number" id="min" value="0">
|
||||
<select id="ampm">
|
||||
<option value="">am</option>
|
||||
<option value="1">pm</option>
|
||||
<script>ampm.children[new Date().getHours() < 12? 0 : 1].selected='selected'</script>
|
||||
</select>
|
||||
<input id="id" type="hidden">
|
||||
<input id="go" type="submit" value="add">
|
||||
<div id="err"></div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.none { display: none; }
|
||||
p, ul, li { list-style-type: none; margin: 0; padding: 0; }
|
||||
</style>
|
||||
|
||||
<ul></ul>
|
||||
|
||||
<div class="model none">
|
||||
<li>
|
||||
<b class="when"></b>
|
||||
<span class="what"></span>
|
||||
<u class="where"></u>
|
||||
<span class="sort none">0</span>
|
||||
<button class="edit"><</button>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/nts.js"></script>
|
||||
<script src="../../../gun/lib/webrtc.js"></script>
|
||||
|
||||
<script>
|
||||
var name = 'schedule/' + location.hash.slice(1);
|
||||
var gun = Gun(location.origin + '/gun');
|
||||
//var gun = Gun('http://localhost:8765/gun');
|
||||
//var gun = Gun();
|
||||
|
||||
$('#add').on('submit', function(event){
|
||||
event.preventDefault();
|
||||
event = {};
|
||||
if(!schedule.on){ return err.innerText = "No date!" }
|
||||
event.when = new Date(schedule.on.getFullYear(), schedule.on.getMonth(), schedule.on.getDate(), hour.value % 12 + (ampm.value? 12 : 0), min.value).getTime();
|
||||
if(!(event.what = what.value)){ return err.innerText = "No description!" }
|
||||
if(!(event.where = where.value)){ return err.innerText = "No location!" }
|
||||
var day = gun.get(name+now(event.when));
|
||||
day.get(id.value || String.random(9)).put(event);
|
||||
what.value = where.value = id.value = err.innerText = '';
|
||||
go.value = 'add';
|
||||
schedule(event.when);
|
||||
});
|
||||
|
||||
function schedule(ms){
|
||||
var day = new Date(ms);
|
||||
if(schedule.on && schedule.on.toLocaleDateString() === day.toLocaleDateString()){ return } schedule.on = day;
|
||||
$('#date').text(day.getFullYear()+' '+ day.toString().split(' ')[1] +' '+day.getDate());
|
||||
day = gun.get(name+now(ms));
|
||||
$('ul').empty();
|
||||
day.map().on(UI);
|
||||
}
|
||||
schedule(+new Date());
|
||||
|
||||
$('#left').on('click', function(){ schedule(+new Date(schedule.on.getFullYear(), schedule.on.getMonth(), schedule.on.getDate() - 1)) });
|
||||
$('#right').on('click', function(){ schedule(+new Date(schedule.on.getFullYear(), schedule.on.getMonth(), schedule.on.getDate() + 1)) });
|
||||
|
||||
function UI(event, id){
|
||||
if(!event){ return }
|
||||
var when = new Date(event.when);
|
||||
if(schedule.on && when.toLocaleDateString() !== schedule.on.toLocaleDateString()){ return }
|
||||
var ul = $('ul')
|
||||
var li = $("#cal-" + id)[0]; // grab if exists
|
||||
if(!li){
|
||||
li = $('.model li').clone(true) // else create it
|
||||
.attr('id', 'cal-' + id);
|
||||
}
|
||||
li = (UI.last = sort(event.when, ul.children('li').last())[0])? $(li).insertAfter(UI.last) : $(li).prependTo(ul);
|
||||
li.find('.what').text(event.what);
|
||||
li.find('.where').text(event.where);
|
||||
li.find('.sort').text(event.when);
|
||||
li.find('.edit').val(id);
|
||||
|
||||
var time = when.toLocaleTimeString();
|
||||
li.find('.when').text(time.split(':').slice(0,2).join(':') + time.slice(-2));
|
||||
};
|
||||
|
||||
$(document).on('click', '.edit', function(){
|
||||
go.value = 'update';
|
||||
id.value = this.value;
|
||||
what.value = $(this).parent().find('.what').text();
|
||||
where.value = $(this).parent().find('.where').text();
|
||||
var when = new Date(parseFloat($(this).parent().find('.sort').text()));
|
||||
hour.value = when.getHours() % 12 || 12;
|
||||
min.value = when.getMinutes();
|
||||
ampm.value = when.getHours() < 12? '' : 1;
|
||||
what.focus();
|
||||
});
|
||||
|
||||
function now(t){
|
||||
return new Date(t || Gun.state()).toLocaleDateString().split('/').reverse().join('/')
|
||||
}
|
||||
|
||||
function sort(num, li){ return parseFloat(num) >= parseFloat($(li).find('.sort').text() || -Infinity)? li : sort(num, li.prev()) }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
59
examples/basic/screen.html
Normal file
59
examples/basic/screen.html
Normal file
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<video id="video" width="100%"></video>
|
||||
<center>
|
||||
<button id="record">Record</button>
|
||||
<button id="play">Play</button>
|
||||
</center>
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
|
||||
<script>
|
||||
var gun = Gun(location.origin + '/gun');
|
||||
var record = {recorder: null, recording: false};
|
||||
|
||||
$('#record').on('click', ()=>{
|
||||
if(!record.ing){ return record.stream() }
|
||||
$('#record').text("Record");
|
||||
if(record.ing.stop){ record.ing.stop() }
|
||||
record.ing = false;
|
||||
})
|
||||
|
||||
record.stream = function(){
|
||||
navigator.mediaDevices.getDisplayMedia({ video: true }).then(stream => {
|
||||
var chunks = []; // we have a stream, we can record it
|
||||
record.ing = new MediaRecorder(stream);
|
||||
record.ing.ondataavailable = eve => chunks.push(eve.data);
|
||||
record.ing.onstop = eve => record.save(new Blob(chunks));
|
||||
record.ing.start()
|
||||
$('#record').text("End");
|
||||
}, err => { console.log(err) });
|
||||
}
|
||||
|
||||
record.save = function(data){
|
||||
record.file = record.file || new FileReader();
|
||||
record.file.readAsDataURL(data);
|
||||
record.file.onloadend = function(){
|
||||
var b64 = record.file.result;
|
||||
b64 = "data:video/webm" + b64.slice(b64.indexOf(';'));
|
||||
gun.get('test').get('screen').put(b64);
|
||||
}
|
||||
}
|
||||
|
||||
$('#play').on('click', ()=>{
|
||||
if(record.playing){
|
||||
$('#play').text("Play")
|
||||
$('#video').get(0).stop();
|
||||
record.playing = false;
|
||||
return;
|
||||
}
|
||||
$('#play').text("Stop");
|
||||
record.playing = true;
|
||||
gun.get('test').get('screen').once((data)=>{
|
||||
if(!data){ return }
|
||||
$('#video').get(0).src = data;
|
||||
$('#video').get(0).play()
|
||||
})
|
||||
})
|
||||
</script>
|
@ -1,50 +0,0 @@
|
||||
<h1>Search</h1>
|
||||
|
||||
<form id="ask">
|
||||
<input id="search" placeholder="search..." autocomplete="off">
|
||||
</form>
|
||||
|
||||
<div id="answer"></div>
|
||||
<ul></ul>
|
||||
|
||||
<small>Note: No data is indexed by default, you need to add some!</small>
|
||||
|
||||
<script src="../../examples/jquery.js"></script>
|
||||
<script src="../../gun.js"></script>
|
||||
<script src="../../sea.js"></script>
|
||||
<script src="../../lib/space.js"></script>
|
||||
|
||||
<script>
|
||||
var gun = Gun();
|
||||
var ask = {};
|
||||
|
||||
$('#search').on('keyup', function(e){
|
||||
ask.now = (this.value||'').toLowerCase().replace(/[\W_]+/g,"");
|
||||
if(ask.last === ask.now){ return }
|
||||
ask.last = ask.now;
|
||||
clearTimeout(ask.to);
|
||||
ask.to = setTimeout(search, 20);
|
||||
});
|
||||
|
||||
function search(){
|
||||
var key = ask.now;
|
||||
gun.get('Q').space(key, function(ack){
|
||||
if(!ack || key !== ask.now){ return }
|
||||
UI(ack)
|
||||
});
|
||||
}
|
||||
|
||||
function UI(ack){
|
||||
$('#answer').text(ack.data || '');
|
||||
var $ul = $('ul').empty(), tree = ack.tree;
|
||||
Gun.obj.map(tree, function(v,k){
|
||||
$('<li>').text(k +' - ' + v).appendTo($ul);
|
||||
});
|
||||
};
|
||||
|
||||
function load(DATA){
|
||||
Gun.obj.map(DATA, function(v,k){
|
||||
gun.get('Q').space(k, v);
|
||||
});
|
||||
}
|
||||
</script>
|
107
examples/basic/stream.html
Normal file
107
examples/basic/stream.html
Normal file
@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<center>
|
||||
<img id="img" width="100%"><br/>
|
||||
Stream <select id="select"><option id="from">from</option></select>
|
||||
add <input id="pass" placeholder="password" type="password">
|
||||
resolution <input id="res" value="240" step="32" max="1080" type="number" style="width:3em;">
|
||||
or <input id="upload" type="file">
|
||||
</center>
|
||||
<video id="video" width="100%" controls autoplay style="display: none;"></video>
|
||||
<canvas id="canvas" width="0" style="display: none;"></canvas>
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/sea.js"></script>
|
||||
<script src="../../../gun/lib/webrtc.js"></script>
|
||||
|
||||
<script>;(async function(){
|
||||
gun = Gun(location.origin + '/gun'); //gun = GUN();
|
||||
|
||||
stream = canvas.getContext('2d'), stream.from = navigator.mediaDevices;
|
||||
|
||||
(await stream.from.enumerateDevices()).forEach((device,i) => {
|
||||
if('videoinput' !== device.kind){ return }
|
||||
var opt = $(from).clone().prependTo('select').get(0);
|
||||
$(opt).text(opt.id = device.label || 'Camera '+i);
|
||||
opt.value = device.deviceId;
|
||||
});
|
||||
|
||||
$('select').on('change', async eve => { $(from).text('Off'); // update label
|
||||
if('Off' == select.value){ return video.srcObject.getTracks()[0].stop() }
|
||||
video.srcObject = await stream.from.getUserMedia({ audio: false,
|
||||
video: (select.value && {deviceId: {exact: select.value}}) || {facingMode: "environment"}
|
||||
});
|
||||
});
|
||||
$('#upload').on('change', async eve => { console.log("Check ./upload.html") })
|
||||
|
||||
setInterval(async tmp => {
|
||||
if(!(video.srcObject||'').active){ return }
|
||||
var size = parseInt(res.value);
|
||||
stream.drawImage(video, 0,0,
|
||||
canvas.width = size || video.videoWidth * 0.1,
|
||||
canvas.height = (size * (video.videoHeight/video.videoWidth)) || video.videoHeight * 0.1
|
||||
);
|
||||
var b64 = canvas.toDataURL('image/jpeg');
|
||||
if(pass.value){ b64 = await SEA.encrypt(b64, pass.value) }
|
||||
gun.get('test').get('video').put(b64);
|
||||
}, 99);
|
||||
|
||||
gun.get('test').get('video').on(async data => {
|
||||
if(pass.value){ data = await SEA.decrypt(data, pass.value) }
|
||||
img.src = data; // Beware: Some browsers memory leak fast src updates.
|
||||
});
|
||||
|
||||
// === AUDIO STREAMING WITH FADE-IN/OUT ===
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const mic = await stream.from.getUserMedia({ audio: true });
|
||||
const src = audioCtx.createMediaStreamSource(mic);
|
||||
const proc = audioCtx.createScriptProcessor(2048, 1, 1);
|
||||
|
||||
src.connect(proc);
|
||||
proc.connect(audioCtx.destination);
|
||||
|
||||
proc.onaudioprocess = async e => {
|
||||
const input = e.inputBuffer.getChannelData(0);
|
||||
const output = new Float32Array(input.length);
|
||||
const fade = 128;
|
||||
|
||||
for (let i = 0; i < fade; i++) {
|
||||
output[i] = input[i] * (i / fade); // fade in
|
||||
}
|
||||
for (let i = fade; i < input.length - fade; i++) {
|
||||
output[i] = input[i]; // middle
|
||||
}
|
||||
for (let i = input.length - fade; i < input.length; i++) {
|
||||
output[i] = input[i] * ((input.length - i) / fade); // fade out
|
||||
}
|
||||
|
||||
const int16 = new Int16Array(output.length);
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
int16[i] = Math.max(-32768, Math.min(32767, output[i] * 32768));
|
||||
}
|
||||
|
||||
let b64 = btoa(String.fromCharCode(...new Uint8Array(int16.buffer)));
|
||||
if(pass.value){ b64 = await SEA.encrypt(b64, pass.value) }
|
||||
gun.get('test').get('audio').put(b64);
|
||||
};
|
||||
|
||||
gun.get('test').get('audio').on(async data => {
|
||||
if(!data) return;
|
||||
if(pass.value){ data = await SEA.decrypt(data, pass.value) }
|
||||
const bin = atob(data);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for(let i=0; i<bin.length; i++){ bytes[i] = bin.charCodeAt(i) }
|
||||
const buf = audioCtx.createBuffer(1, bytes.length / 2, 44100);
|
||||
const chan = buf.getChannelData(0);
|
||||
for(let i=0; i<chan.length; i++){
|
||||
const s = (bytes[i*2+1] << 8) | bytes[i*2];
|
||||
chan[i] = (s > 32767 ? s - 65536 : s) / 32768;
|
||||
}
|
||||
const player = audioCtx.createBufferSource();
|
||||
player.buffer = buf;
|
||||
player.connect(audioCtx.destination);
|
||||
player.start();
|
||||
});
|
||||
|
||||
}());</script>
|
@ -1,3 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<h1>Tables</h1>
|
||||
|
||||
<form id="sign">
|
||||
|
35
examples/basic/upload.html
Normal file
35
examples/basic/upload.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<div class="model" style="display: none;">
|
||||
<video width="100%" controls autoplay></video>
|
||||
<audio width="100%" controls autoplay></audio>
|
||||
<img style="max-width: 100%;">
|
||||
</div>
|
||||
<center>
|
||||
<p>Drag & drop videos, songs, or images! <input id="upload" type="file" multiple></p>
|
||||
</center>
|
||||
|
||||
<script src="../../../gun/lib/yson.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/lib/dom.js"></script>
|
||||
<script src="../../../gun/lib/upload.js"></script>
|
||||
|
||||
<script>
|
||||
gun = GUN(location.origin + '/gun');
|
||||
|
||||
$('html, #upload').upload(function resize(eve, up){
|
||||
if(up){ return up.shrink(eve, resize, 1024) }
|
||||
var b64 = (eve.base64 || ((eve.event || eve).target || eve).result || eve); // which one? try all!
|
||||
gun.get('test').get((eve.id+(new Date).getUTCSeconds()) % 60).put(b64); // limit uploads to 1 of 60 slots.
|
||||
});
|
||||
|
||||
gun.get('test').map().once(function(data){
|
||||
if("string" != typeof data){ return }
|
||||
var type = data.split(';')[0], ui;
|
||||
if(type.indexOf('image') + 1){ ui = $("img").get(0) }
|
||||
if(type.indexOf('video') + 1){ ui = $('video').get(0) }
|
||||
if(type.indexOf('audio') + 1){ ui = $('audio').get(0) }
|
||||
if(!ui){ return }
|
||||
$(ui).clone().prependTo('center').get(0).src = data;
|
||||
});
|
||||
</script>
|
@ -1,52 +1,2 @@
|
||||
<h1>User</h1>
|
||||
|
||||
<form id="sign">
|
||||
<input id="alias" placeholder="username">
|
||||
<input id="pass" type="password" placeholder="passphrase">
|
||||
<input id="in" type="submit" value="sign in">
|
||||
<input id="up" type="button" value="sign up">
|
||||
<input id="mask" type="button" value="Identifi Login">
|
||||
</form>
|
||||
|
||||
<ul></ul>
|
||||
|
||||
<form id="said">
|
||||
<input id="say">
|
||||
<input id="speak" type="submit" value="speak">
|
||||
</form>
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/sea.js"></script>
|
||||
|
||||
<script>
|
||||
var gun = Gun(); //Gun(['http://localhost:8765/gun', 'https://guntest.herokuapp.com/gun']);
|
||||
var user = gun.user().recall({sessionStorage: true});
|
||||
|
||||
$('#up').on('click', function(e){
|
||||
user.create($('#alias').val(), $('#pass').val(), login);
|
||||
});
|
||||
function login(e){
|
||||
user.auth($('#alias').val(), $('#pass').val());
|
||||
return false; // e.preventDefault();
|
||||
};
|
||||
$('#sign').on('submit', login);
|
||||
$('#mask').on('click', login);
|
||||
|
||||
gun.on('auth', function(){
|
||||
$('#sign').hide();
|
||||
user.get('said').map().on(UI);
|
||||
});
|
||||
|
||||
$('#said').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
//if(!user.is){ return }
|
||||
user.get('said').set($('#say').val());
|
||||
$('#say').val("");
|
||||
});
|
||||
|
||||
function UI(say, id){
|
||||
var li = $('#' + id).get(0) || $('<li>').attr('id', id).appendTo('ul');
|
||||
$(li).text(say);
|
||||
};
|
||||
</script>
|
||||
<!DOCTYPE html>
|
||||
<p>Moved to <a href="./post.html">./post.html</a>!</p>
|
62
examples/basic/video.html
Normal file
62
examples/basic/video.html
Normal file
@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<video id="video" width="100%" controls autoplay></video>
|
||||
<center>
|
||||
<input id="pass" placeholder="password">
|
||||
Record <button class="record">Camera</button> or <button class="record">Screen</button>
|
||||
</center>
|
||||
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/sea.js"></script>
|
||||
|
||||
<script>
|
||||
var gun = Gun(location.origin + '/gun');
|
||||
|
||||
gun.get('test').get('video').on(async function(data){
|
||||
if($('#pass').val()){ data = await SEA.decrypt(data, $('#pass').val()) }
|
||||
$('#video').get(0).src = data;
|
||||
})
|
||||
|
||||
$('.record').on('click', function(){
|
||||
if(record.ing){
|
||||
if(record.ing.stop){ record.ing.stop() }
|
||||
$(this).text(record.type);
|
||||
record.ing = false;
|
||||
return;
|
||||
}
|
||||
record(record.type = $(this).text());
|
||||
$(this).text("End");
|
||||
})
|
||||
|
||||
function record(type){
|
||||
if('Camera' === type){
|
||||
navigator.getMedia({ video: true, audio: true }, load, error);
|
||||
}
|
||||
if('Screen' === type){
|
||||
navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }).then(load, error);
|
||||
}
|
||||
function load(media){
|
||||
var chunks = [];
|
||||
record.ing = new MediaRecorder(media);
|
||||
record.ing.ondataavailable = function(eve){ chunks.push(eve.data) }
|
||||
record.ing.onstop = function(eve){record.save(new Blob(chunks)) }
|
||||
record.ing.start();
|
||||
}
|
||||
function error(err){ console.log(err) }
|
||||
}
|
||||
|
||||
record.save = function(data){
|
||||
record.file = record.file || new FileReader();
|
||||
record.file.readAsDataURL(data);
|
||||
record.file.onloadend = async function(){
|
||||
var b64 = record.file.result, pass;
|
||||
b64 = $('#video').get(0).src = "data:video/webm" + b64.slice(b64.indexOf(';'));
|
||||
if($('#pass').val()){ b64 = await SEA.encrypt(b64, $('#pass').val()) }
|
||||
gun.get('test').get('video').put(b64);
|
||||
}
|
||||
}
|
||||
|
||||
navigator.getMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia
|
||||
|| navigator.mozGetUserMedia || navigator.msGetUserMedia);
|
||||
</script>
|
@ -1,161 +1,213 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Converse</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<link href='https://fonts.googleapis.com/css?family=Poiret+One' rel='stylesheet' type='text/css'>
|
||||
<style>
|
||||
#converse {
|
||||
font-size: 16pt;
|
||||
}
|
||||
#converse .box {
|
||||
margin-bottom: 0.2em;
|
||||
padding: 1em;
|
||||
border-radius: 0.1em;
|
||||
}
|
||||
#converse b:after {
|
||||
content: " ";
|
||||
}
|
||||
#converse li .when {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2em;
|
||||
padding: 0.5em 1em;
|
||||
background: rgba(100%,100%,100%,0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
#converse li:hover .when {
|
||||
opacity: 1;
|
||||
right: 0em;
|
||||
}
|
||||
.poiret {
|
||||
font-family: 'Poiret One', sans-serif;
|
||||
}
|
||||
.large {
|
||||
font-size: 200%;
|
||||
}
|
||||
#converse .what, #converse .who {
|
||||
cursor: text;
|
||||
outline: none;
|
||||
display: inline;
|
||||
min-width: 1em;
|
||||
padding-left: 1px;
|
||||
}
|
||||
[contentEditable=true]:empty:not(:focus):before{
|
||||
content:attr(data-text)
|
||||
}
|
||||
|
||||
#title {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
<head>
|
||||
<title>Converse</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<link href='https://fonts.googleapis.com/css?family=Poiret+One' rel='stylesheet' type='text/css'>
|
||||
<style>
|
||||
.chat__heading {
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.say {
|
||||
margin: 0 0 0.4em 0.4em;
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
.chat__form-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#name-input {
|
||||
margin-top: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.chat__form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
background-color: white;
|
||||
border: 2px solid white;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#message-input {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="converse" class="hue2 page">
|
||||
<div class="pad">
|
||||
<div id='title' class="poiret large rubric whitet">Have a Conversation...</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li class="none"></li>
|
||||
</ul>
|
||||
.chat__name-input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
<form class="white huet2 box">
|
||||
<div>
|
||||
<div class="say hue2 right whitet box act">say</div>
|
||||
<b id="name-input" class="jot left who" contenteditable="true" data-text="Name"></b>
|
||||
<p id="message-input" class="jot left what" contenteditable="true" data-text="Write a message..."></p>
|
||||
</div>
|
||||
</form>
|
||||
.chat__message-input {
|
||||
flex: 5;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
<div class="model">
|
||||
<li class="white huet2 box">
|
||||
<b class="who"></b>
|
||||
<p class="what"></p>
|
||||
<span class="sort none">0</span>
|
||||
<div class="when"></div>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.chat__submit {
|
||||
padding: 10px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
<script src="/jquery.js"></script>
|
||||
<script src="/gun.js"></script>
|
||||
<script src="/gun/nts.js"></script>
|
||||
<script>
|
||||
var gun = Gun(location.origin + '/gun');
|
||||
var chat = gun.get('converse/' + location.hash.slice(1));
|
||||
.chat__submit:hover::after {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
$("form .say").on('click', submit);
|
||||
$("form .what").on('keydown', enter);
|
||||
function enter(e){
|
||||
if(e.which !== 13){ return }
|
||||
submit(e);
|
||||
}
|
||||
function submit(e){
|
||||
e.preventDefault();
|
||||
.chat__submit:focus::after {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
var msg = {when: Gun.time.is()};
|
||||
.chat__submit::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
msg.who = $('form .who').text();
|
||||
if(!msg.who){
|
||||
msg.who = 'user' + Gun.text.random(3);
|
||||
$('form .who').text(msg.who);
|
||||
}
|
||||
.chat__message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 60px 20px;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
msg.what = $('form .what').text();
|
||||
if(!msg.what){ return }
|
||||
.chat__message {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
chat.set(msg);
|
||||
$('form .what').text('');
|
||||
}
|
||||
.chat__name {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
chat.map().val(function(msg, id){
|
||||
if(!msg){ return }
|
||||
var ul = $('ul');
|
||||
var last = sort(msg.when, ul.children('li').last());
|
||||
.chat__when {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2em;
|
||||
padding: 10px;
|
||||
background: rgba(100%, 100%, 100%, 0.9);
|
||||
opacity: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
var li = $("#msg-" + id)[0]; // grab if exists
|
||||
if(!li){
|
||||
li = $('.model li').clone(true) // else create it
|
||||
.attr('id', 'msg-' + id)
|
||||
.insertAfter(last);
|
||||
}
|
||||
.chat__message:hover .chat__when {
|
||||
opacity: 1;
|
||||
right: 0em;
|
||||
}
|
||||
|
||||
// bind the message data into the UI
|
||||
li = $(li);
|
||||
li.find('.who').text(msg.who);
|
||||
li.find('.what').text(msg.what);
|
||||
li.find('.sort').text(msg.when);
|
||||
@media (max-width: 567px) {
|
||||
.chat__heading {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
var time = new Date(msg.when);
|
||||
li.find('.when').text(time.toDateString() + ', ' + time.toLocaleTimeString());
|
||||
<body>
|
||||
<div class="chat hue2 page">
|
||||
<h2 id='title' class="chat__heading hue2 whitet">Have a Conversation...</h2>
|
||||
<ul class="chat__message-list">
|
||||
<li class="none"></li>
|
||||
</ul>
|
||||
|
||||
$('html, body').stop(true, true)
|
||||
.animate({scrollTop: ul.height()});
|
||||
});
|
||||
<div class="chat__form-container hue2">
|
||||
<form class="chat__form">
|
||||
<label for="name-input" class="visually-hidden">Name</label>
|
||||
<input id="name-input" class="chat__name-input" placeholder="Name"></input>
|
||||
<label for="message-input" class="visually-hidden">Message</label>
|
||||
<input id="message-input" class="chat__message-input" placeholder="Write a message..."></input>
|
||||
<button class="chat__submit say hue2">say</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
function sort(id, li){
|
||||
var num = parseFloat(id);
|
||||
var id = $(li).find('.sort').text() || -Infinity;
|
||||
var at = num >= parseFloat(id);
|
||||
return at ? li : sort(id, li.prev());
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<div class="model">
|
||||
<li class="chat__message white huet2 box">
|
||||
<b class="chat__name"></b>
|
||||
<p class="chat__message-text"></p>
|
||||
<span class="sort none">0</span>
|
||||
<div class="chat__when"></div>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/jquery.js"></script>
|
||||
<script src="/gun.js"></script>
|
||||
<script src="/gun/nts.js"></script>
|
||||
<script>
|
||||
var gun = Gun(location.origin + '/gun');
|
||||
var chat = gun.get('converse/' + location.hash.slice(1));
|
||||
|
||||
$(".chat__submit").on('click', submit);
|
||||
$(".chat_form").on('keydown', enter);
|
||||
function enter(e) {
|
||||
if (e.which !== 13) { return }
|
||||
submit(e);
|
||||
}
|
||||
function submit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var msg = { when: Gun.state() };
|
||||
|
||||
msg.who = $('.chat__name-input').val();
|
||||
if (!msg.who) {
|
||||
msg.who = 'user' + String.random(3);
|
||||
$('.chat__name-input').val(msg.who);
|
||||
}
|
||||
|
||||
msg.what = $('.chat__message-input').val();
|
||||
if (!msg.what) { return }
|
||||
|
||||
chat.set(msg);
|
||||
$('.chat__message-input').val('').focus();
|
||||
}
|
||||
|
||||
chat.map().once(function (msg, id) {
|
||||
if (!msg) { return }
|
||||
var messageList = $('.chat__message-list');
|
||||
var last = sort(msg.when, messageList.children('li').last());
|
||||
|
||||
var li = $("#msg-" + id)[0]; // grab if exists
|
||||
if (!li) {
|
||||
li = $('.model li').clone(true) // else create it
|
||||
.attr('id', 'msg-' + id)
|
||||
.insertAfter(last);
|
||||
}
|
||||
|
||||
// bind the message data into the UI
|
||||
li = $(li);
|
||||
li.find('.chat__name').text(msg.who);
|
||||
li.find('.chat__message-text').text(msg.what);
|
||||
li.find('.sort').text(msg.when);
|
||||
|
||||
var time = new Date(msg.when);
|
||||
li.find('.chat__when').text(time.toDateString() + ', ' + time.toLocaleTimeString());
|
||||
|
||||
$('html, body').stop(true, true)
|
||||
.animate({ scrollTop: messageList.height() });
|
||||
});
|
||||
|
||||
function sort(num, li) { return parseFloat(num) >= parseFloat($(li).find('.sort').text() || -Infinity) ? li : sort(num, li.prev()) }
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
379
examples/docs.html
Normal file
379
examples/docs.html
Normal file
@ -0,0 +1,379 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- always start with these two lines to set a clean baseline for different devices -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<script src="jquery.js"></script>
|
||||
|
||||
<title>Docs</title>
|
||||
</head>
|
||||
<body class="black whitet">
|
||||
<style>
|
||||
/*
|
||||
Choose white text on a black background so you can add color in.
|
||||
Pick your favorite font and choose a font size.
|
||||
*/
|
||||
@import url('https://fonts.googleapis.com/css?family=Oxygen');
|
||||
html, body {
|
||||
font-family: "Oxygen", sans-serif;
|
||||
}
|
||||
|
||||
[contenteditable]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.meta-on, div:hover, ul:hover, ol:hover, li:hover, p:hover, span:hover, form:hover, button:hover, input:hover, textarea:hover, img:hover {
|
||||
outline: 1px solid;
|
||||
animation: meta-on 3s infinite;
|
||||
transition: none !important;
|
||||
} @keyframes meta-on {
|
||||
0% {outline-color: magenta;}
|
||||
33% {outline-color: cyan;}
|
||||
66% {outline-color: yellow;}
|
||||
100% {outline-color: magenta;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="hold full hue2">
|
||||
<div id="page" class="max focus gap" style="margin-top: 9%;"></div>
|
||||
</div>
|
||||
|
||||
<script src="../../gun/gun.js"></script>
|
||||
<script src="../../gun/lib/monotype.js"></script>
|
||||
<script src="../../gun/lib/meta.js"></script>
|
||||
<script src="../../gun/lib/normalize.js"></script>
|
||||
<script async src="../../gun/lib/fun.js"></script>
|
||||
|
||||
<script async src="../../gun/lib/wave.js"></script>
|
||||
<!-- script async src="https://edide.io/music.lib"></script -->
|
||||
|
||||
<script>
|
||||
var gun = Gun();
|
||||
var page = {};
|
||||
//var gun = Gun(['https://guntest.herokuapp.com/gun', 'http://localhost:8765/gun']);
|
||||
|
||||
;(window.onhashchange = function(){
|
||||
var file = (location.hash||'').slice(1);
|
||||
var S = +new Date;
|
||||
$('#page').empty().attr('contenteditable', 'false');
|
||||
gun.get('test/gun/docs/'+file).get('what').map().on(function render(data, i, msg, eve){
|
||||
var tmp = page[i] || '';
|
||||
var last = Gun.state.is(gun._.root.graph[msg.put['#']], i);
|
||||
if(last < tmp.last){ return }
|
||||
//});
|
||||
//if(window.LOCK){ return }
|
||||
var p = $('#page').children().get(i);
|
||||
if(!p){
|
||||
$('#page').append('<p>');
|
||||
setTimeout(function(){ render(data, i, msg, eve) },0);
|
||||
return;
|
||||
}
|
||||
var DBG = {s: +new Date};
|
||||
var r = monotype(p);
|
||||
DBG.mono = +new Date;
|
||||
var safe = $.normalize(data);
|
||||
DBG.norm = +new Date;
|
||||
p.outerHTML = data;
|
||||
DBG.html = +new Date;
|
||||
r.restore();
|
||||
DBG.rest = +new Date;
|
||||
//console.log("mono:", DBG.mono - DBG.s, "norm:", DBG.norm - DBG.mono, 'html:', DBG.html - DBG.norm, 'rest:', DBG.rest - DBG.html, ':::', msg, eve);
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
window.requestAnimationFrame = window.requestAnimationFrame || setTimeout;
|
||||
window.requestAnimationFrame(function frame(){
|
||||
window.requestAnimationFrame(frame, 16);
|
||||
|
||||
}, 16);
|
||||
|
||||
document.execCommand('defaultParagraphSeparator', false, 'p');
|
||||
meta.edit({
|
||||
name: "Edit",
|
||||
combo: ['E'],
|
||||
use: function(eve){
|
||||
console.log('on');
|
||||
}, on: function(eve){
|
||||
if($(eve.target).closest('p').length){ return }
|
||||
var edit = this;
|
||||
setTimeout(function(){ meta.flip(false) },1);
|
||||
edit.init();
|
||||
$(document).on('keydown.tmp', '[contenteditable]', function(eve){
|
||||
if(eve.which != 13){ return }
|
||||
eve.preventDefault();
|
||||
var r = window.getSelection().getRangeAt(0);
|
||||
var c = r.commonAncestorContainer, p;
|
||||
r.deleteContents();
|
||||
var p = c.splitText? $(c.splitText(r.startOffset)).parent() : $(c);
|
||||
var n = $("<"+p.get(0).tagName+">"), f;
|
||||
p.contents().each(function(){
|
||||
if(this === c){ return f = true }
|
||||
if(!f){ return }
|
||||
n.append(this);
|
||||
});
|
||||
p.after(n);
|
||||
edit.select(n.get(0));
|
||||
// make sure we re-save & sync each changed paragraph.
|
||||
edit.save(p);
|
||||
p.nextAll().each(function(){
|
||||
edit.save(this);
|
||||
});
|
||||
}).on('keyup.tmp', '[contenteditable]', function(eve){
|
||||
//$('#debug').val(doc.html());
|
||||
var p = $(window.getSelection().anchorNode).closest('p'), tmp;
|
||||
(tmp = page[p.index()] || (page[p.index()] = {})).last = (+new Date) + 99;
|
||||
clearTimeout(tmp.to); tmp.to = setTimeout(function(){
|
||||
var DBG = {s: +new Date};
|
||||
var r = monotype(p);
|
||||
DBG.m = +new Date;
|
||||
var html = p.html() || '';
|
||||
DBG.g = +new Date;
|
||||
if(!html && !p.prev().length && !p.next().length && !$('#page').html()){
|
||||
edit.init();
|
||||
}
|
||||
DBG.i = +new Date;
|
||||
var safe = $.normalize(html);
|
||||
DBG.n = +new Date;
|
||||
p.html(safe);
|
||||
DBG.h = +new Date;
|
||||
r.restore();
|
||||
DBG.r = +new Date;
|
||||
edit.save(p);
|
||||
DBG.p = +new Date;
|
||||
//console.log("save:", DBG.p - DBG.r, "rest:", DBG.r - DBG.h, "html:", DBG.h - DBG.n, "norm:", DBG.n - DBG.i, 'init:', DBG.i - DBG.g, 'grab:', DBG.g - DBG.m, 'mono:', DBG.m - DBG.s);
|
||||
},50)});
|
||||
},
|
||||
up: function(){
|
||||
console.log("UP");
|
||||
$('[contenteditable=true]').off('.tmp');
|
||||
},
|
||||
init: function(){
|
||||
var edit = this;
|
||||
var doc = $('#page').attr('contenteditable', 'true');
|
||||
if(!doc.text()){
|
||||
doc.html('<p class="loud crack"></p>');
|
||||
}
|
||||
edit.select(doc.children().first().get(0));
|
||||
},
|
||||
save: function(p){
|
||||
p = $(p);
|
||||
var i = p.index();// = Array.prototype.indexOf.call(parent.children, child);
|
||||
var file = (location.hash||'').slice(1);
|
||||
var data = (p.get(0)||{}).outerHTML||'';
|
||||
//data = $.normalize(data); // GOOD TO DO SECURITY ON SENDING SIDE TOO!!!
|
||||
window.LOCK = true;
|
||||
gun.get('test/gun/docs/'+file).get('what').get(i).put(data);
|
||||
window.LOCK = false;
|
||||
},
|
||||
select: function(p){
|
||||
var s = window.getSelection(),
|
||||
r = document.createRange();
|
||||
if(p.innerHTML){
|
||||
r.setStart(p, 0);
|
||||
r.collapse(true);
|
||||
s.removeAllRanges();
|
||||
s.addRange(r);
|
||||
return;
|
||||
}
|
||||
p.innerHTML = '\u00a0';
|
||||
r.selectNodeContents(p);
|
||||
s.removeAllRanges();
|
||||
s.addRange(r);
|
||||
document.execCommand('delete', false, null);
|
||||
}
|
||||
});
|
||||
|
||||
;(function(){
|
||||
|
||||
meta.edit({name: "Design", combo: ['D']});
|
||||
meta.edit({name: "Fill", combo: ['D','F'], // TODO!
|
||||
use: function(eve){},
|
||||
on: function(eve){
|
||||
var on = meta.tap();
|
||||
meta.ask('Color name, code, or URL?', function(color){
|
||||
on.css('background', color);
|
||||
}, true);
|
||||
},
|
||||
up: function(eve){}
|
||||
});
|
||||
|
||||
meta.edit({name: "Add", combo: ['D','A']});
|
||||
meta.edit({name: "Row", combo: ['D','A', 'R'],
|
||||
on: function(eve){
|
||||
meta.tap().append('<div style="min-height: 9em; padding: 2%;">');
|
||||
}
|
||||
});
|
||||
meta.edit({name: "Columns", combo: ['D','A','C'],
|
||||
on: function(eve){
|
||||
var on = meta.tap().addClass('center'), tmp, c;
|
||||
var html = '<div class="unit col" style="min-height: 9em; padding: 2%;"></div>';
|
||||
if(!on.children('.col').length){ html += html }
|
||||
c = (tmp = on.append(html).children('.col')).length;
|
||||
tmp.each(function(){
|
||||
$(this).css('width', (100/c)+'%');
|
||||
})
|
||||
}
|
||||
});
|
||||
meta.edit({name: "Text", combo: ['D','A','T'],
|
||||
on: function(eve){
|
||||
var tag = $('<p>text</p>');
|
||||
meta.tap().append(tag);
|
||||
tag.focus();
|
||||
}
|
||||
});
|
||||
meta.edit({name: "Delete", combo: ['D','A','D'],
|
||||
on: function(eve){
|
||||
meta.tap().remove();
|
||||
}
|
||||
});
|
||||
|
||||
meta.edit({name: "Turn", combo: ['D','T']});
|
||||
meta.edit({name: "Size", combo: ['D','S']});
|
||||
meta.edit({name: "X", combo: ['D','S','X'],
|
||||
on: function(eve){
|
||||
var on = this.a = meta.tap().addClass('meta-on'), was = on.width();
|
||||
$(document).on('mousemove.tmp', function(eve){
|
||||
var be = was + ((eve.pageX||0) - was);
|
||||
on.css({'max-width': be, width: '100%'});
|
||||
});
|
||||
meta.ask('Width in px, %, or other unit?', function(w){
|
||||
if(!w){ return }
|
||||
on.css({'max-width': w, width: '100%'});
|
||||
}, true);
|
||||
}, up: function(){
|
||||
$(document).off('mousemove.tmp');
|
||||
this.a.removeClass('meta-on');
|
||||
}
|
||||
});
|
||||
meta.edit({name: "Y", combo: ['D','S','Y'],
|
||||
//on: function(eve){ console.log('on Y') },
|
||||
on: function(eve){ console.log('use Y')
|
||||
var on = this.a = meta.tap().addClass('meta-on'), was = on.height();
|
||||
$(document).on('mousemove.tmp', function(eve){
|
||||
var be = was + ((eve.pageY||0) - was);
|
||||
on.css({'min-height': be});
|
||||
})
|
||||
}, up: function(){ console.log('up Y')
|
||||
$(document).off('mousemove.tmp');
|
||||
this.a.removeClass('meta-on');
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
||||
;(function(){
|
||||
var logic = {};
|
||||
|
||||
meta.edit({name: "Logic", combo: ['L']});
|
||||
|
||||
meta.edit({name: "Symbol", combo: ['L','S'],
|
||||
on: function(eve){
|
||||
console.log(1);
|
||||
}
|
||||
});
|
||||
meta.edit({name: "Action", combo: ['L','A'],
|
||||
on: function(eve){
|
||||
console.log(2);
|
||||
}
|
||||
});
|
||||
meta.edit({name: "Data", combo: ['L','D'],
|
||||
on: function(eve){
|
||||
console.log(3);
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
||||
;(function(){
|
||||
var song = {};
|
||||
// TODO:
|
||||
// 1. Manually OR automatically load music.js API, dependencies, and modules. - FINE for now
|
||||
// 2. only export music API, not meta, not dom, not mouselock system, not UI/html, etc. better module isolation and export.
|
||||
// 3. `var wave = Wave('a').play()` // also on `Music.now`
|
||||
// defaults... instrument: pure tones, volume curve: |\_ , speed curve: 0.5
|
||||
// 4. `wave.blur(0.5).itch(0.5);`
|
||||
// 5. wave.long(2); // how long in seconds each note plays, optionally: wave.pace(60) is bpm
|
||||
// 6. wave.loud(0.5); // 0% to 100% volume loudness of device output.
|
||||
// 7. wave.vary(0.5); // slows down or speeds up wiggle per harmonic
|
||||
// 8:
|
||||
// wave structure, does ToneJS allow us to change the sine wave smoothness/type continuously or is it a pre-fixed type?
|
||||
// wave structure: /\/\/, |_|, /|/, \|\| do some research with ToneJS whether these are dynamic or fixed
|
||||
// wave.itch(); // changes the shape of the wiggle from smooth sine to square or triangle
|
||||
// wave.blur(220hz); // blur may not apply/work on pure notes other than filtering them.
|
||||
|
||||
meta.edit({name: "Music", combo: ['M']});
|
||||
|
||||
meta.edit({name: "Play", combo: ['M','P'],
|
||||
on: function(eve){
|
||||
// TODO: We still need to add to meta API ability to change name.
|
||||
if(song.play){
|
||||
music.stop();
|
||||
song.play = false;
|
||||
return;
|
||||
}
|
||||
song.play = true;
|
||||
music.stop();
|
||||
setTimeout(function(){
|
||||
song.now = wave($('#page').text()).play();
|
||||
},250);
|
||||
}
|
||||
});
|
||||
|
||||
meta.edit({name: "Blur", combo: ['M','B'],
|
||||
on: function(eve){
|
||||
$(document).on('mousemove.tmp', function(eve){
|
||||
var x = eve.pageX;
|
||||
song.now.loud(x/$('body').innerWidth());
|
||||
});
|
||||
},
|
||||
up: function(){
|
||||
$(document).off('.tmp');
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('keydown', function(eve){
|
||||
if(eve.which === music.which){ return }
|
||||
music.play(String.fromCharCode(music.which = eve.which));
|
||||
});
|
||||
|
||||
}());
|
||||
;(function(){
|
||||
/*
|
||||
Edit
|
||||
Bold
|
||||
Italic
|
||||
Link
|
||||
?
|
||||
Left
|
||||
Middle
|
||||
Right
|
||||
Justify
|
||||
?
|
||||
Small
|
||||
Normal
|
||||
Header
|
||||
Title
|
||||
Design
|
||||
Add
|
||||
Row
|
||||
Column
|
||||
Text
|
||||
Delete
|
||||
Turn
|
||||
Grab
|
||||
Size
|
||||
X
|
||||
Y
|
||||
Fill
|
||||
Logic
|
||||
Symbol
|
||||
Action
|
||||
Data
|
||||
*/
|
||||
/*
|
||||
|
||||
*/
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
439
examples/game/furball.html
Normal file
439
examples/game/furball.html
Normal file
@ -0,0 +1,439 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- always start with these two lines to set a clean baseline for different devices -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="../style.css">
|
||||
<!-- link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/gun/examples/style.css" -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/examples/jquery.js"></script>
|
||||
|
||||
<title>Furball</title>
|
||||
</head>
|
||||
<body class="black whitet">
|
||||
<style>
|
||||
/*
|
||||
Choose white text on a black background so you can add color in.
|
||||
Pick your favorite font and choose a font size.
|
||||
*/
|
||||
@import url('https://fonts.googleapis.com/css?family=Mali');
|
||||
html, body {
|
||||
font-family: "Mali", sans-serif;
|
||||
}
|
||||
|
||||
.huef {
|
||||
background: #4D79D8;
|
||||
-webkit-animation: huef 9s infinite;
|
||||
animation: huef 9s infinite;
|
||||
} @keyframes huef {
|
||||
0% {background-color: #4D79D8;}
|
||||
25% {background-color: #33cc33;}
|
||||
50% {background-color: #f2b919;}
|
||||
75% {background-color: #ea3224;}
|
||||
100% {background-color: #4D79D8;}
|
||||
} @-webkit-keyframes huef {
|
||||
0% {background-color: #4D79D8;}
|
||||
25% {background-color: #33cc33;}
|
||||
50% {background-color: #f2b919;}
|
||||
75% {background-color: #ea3224;}
|
||||
100% {background-color: #4D79D8;}
|
||||
}
|
||||
|
||||
button, input {
|
||||
padding: 1em;
|
||||
background: transparent;
|
||||
border: 1px solid white;
|
||||
border-radius: 1.5em;
|
||||
color: white;
|
||||
margin: 0.5em;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover, input:hover {
|
||||
background: white;
|
||||
color: black;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.air { padding-top: 9%; }
|
||||
.yak button { font-size: 80%; }
|
||||
|
||||
.wag {
|
||||
-webkit-animation: wag 3s infinite;
|
||||
animation: wag 3s infinite;
|
||||
} @keyframes wag {
|
||||
0% {transform: rotate(0deg);}
|
||||
50% {transform: rotate(-1deg);}
|
||||
100% {transform: rotate(0deg);}
|
||||
}
|
||||
|
||||
@keyframes print {
|
||||
0% { overflow: hidden; height: 0vh; }
|
||||
99% { overflow: hidden; height: 100vh; }
|
||||
100% { overflow: visible; height: auto; }
|
||||
}
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<!-- for educational sites, consider starting with a nice full screen welcome message -->
|
||||
<div class="home hold full huef center air">
|
||||
<div class="focus row">
|
||||
<p><i>Neon ERA presents</i></p>
|
||||
<p class="shout wag">Furball Forces</p>
|
||||
<!-- just like in real life, say who you are and give a concise reason why you add value to someone's life and then make a call to action, if they want to learn more they can always scroll to learn more -->
|
||||
<div>
|
||||
<!-- a class="unit hold" href="#fullscreen"><button>WATCH TRAILER</button></a -->
|
||||
<a class="unit yak" href="#choose"><button>PLAY GAME</button></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="focus center row leak">
|
||||
<!-- just like in real life, looking pretty attracts attention, so show off and look glamorous! -->
|
||||
<img class="unit blink" src="file:///Users/mark/Pictures/supercatdog.png" style="min-width: 10em; width: 80%;">
|
||||
</div>
|
||||
<script>location.hash = ''</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/lib/fun.js"></script>
|
||||
<script>;(function(){
|
||||
// OPTIONAL MUSIC:
|
||||
$('.home button').on('click', function(){
|
||||
if(window.screen.height > window.screen.width){ return }
|
||||
$('body').append("<div id='audio' onclick='$(this).remove();'><iframe width='0' height='0' src='https://www.youtube-nocookie.com/embed/LLPoZGX0qZk?autoplay=1' frameborder='0'></iframe></div>");
|
||||
})
|
||||
}());
|
||||
</script>
|
||||
<style>#audio { padding: 0.5em; position: fixed; bottom: 0; left: 0; } #audio:before { content: '\25BA'; } #audio:hover:before { content: '\25FC'; }</style>
|
||||
</div>
|
||||
|
||||
<div id="choose" class="hold full hue4 center air">
|
||||
<div class="focus row">
|
||||
<p class="shout wag fur">Choose Team:</p>
|
||||
<div>
|
||||
<a class="unit yak" href="#automecha"><button style="background: white; color: black;">#AutoMecha</button></a>
|
||||
<a class="unit yak" href="#cyberninjas"><button style="background: black; color: white; border-color: black;">#CyberNinjas</button></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="focus center row leak">
|
||||
<img class="unit blink" src="file:///Users/mark/Pictures/supercatdog.png" style="transform: scaleX(-1); filter: invert(1); min-width: 10em; width: 80%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cyberninjas" class="hold full black">
|
||||
<style>
|
||||
#cyberninjas:target .story {
|
||||
animation: print 3s steps(50, end);
|
||||
}
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Episode 1: Waking</p>
|
||||
<p>"How long until they're online?"</p>
|
||||
<p>"We're copying the soul files, almost done."</p>
|
||||
<p>"Monsters are on the bridge, we do not have time!"</p>
|
||||
<p>"The new body is printing now, it'll be able to outrun them all, just hold on."</p>
|
||||
<p>"It won't know where to run! We're risking ruining the whole resistance, I need to talk to it now."</p>
|
||||
<p>"95% done." The voice behind the glass turns to the soul in the body, "My cub, can you hear me?"</p>
|
||||
<p>...</p>
|
||||
<a class="unit yak" href="#cyberninjas2"><button>Reply "Yes, Mom?"</button></a>
|
||||
</div>
|
||||
<script>
|
||||
;(function(){
|
||||
$('#cyberninjas a').on('click', function(){
|
||||
$('#hud .life').removeClass('down');
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="cyberninjas2" class="hold full red">
|
||||
<style>
|
||||
#cyberninjas2:target .story {
|
||||
animation: print 3s steps(50, end);
|
||||
}
|
||||
|
||||
#hud {
|
||||
opacity: 0.4;
|
||||
font-family: 'Audiowide', cursive;
|
||||
z-index: 999999999999;
|
||||
transition: all 3s;
|
||||
}
|
||||
#hud .life {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 0px;
|
||||
padding: 0.25em 1em 0.1em;
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
transform: translateX(-50%);
|
||||
background: black;
|
||||
text-shadow: 0em -0.125em 0.75em white;
|
||||
}
|
||||
#hud .score {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 0px;
|
||||
padding: 0.1em 1em 0.25em;
|
||||
border-radius: 0 0 0.5em 0.5em;
|
||||
transform: translateX(-50%);
|
||||
background: black;
|
||||
text-shadow: 0em 0.1em 0.75em white;
|
||||
}
|
||||
#hud .down {
|
||||
bottom: -2em !important;
|
||||
}
|
||||
#hud .up {
|
||||
top: -2em !important;
|
||||
}
|
||||
</style>
|
||||
<div id="hud">
|
||||
<div class="score shade up">
|
||||
SCORE: <span id="hudscore">0</span>%
|
||||
</div>
|
||||
<div class="life shade down">
|
||||
LIFE: <span id="hudlife">50</span>%
|
||||
</div>
|
||||
</div>
|
||||
<div class="story pad">
|
||||
<p>A fire explodes in the room behind the glass as an AutoMecha blows the door open.</p>
|
||||
<p>The floor shakes and the bed crashes through the wall, flying out of the building.</p>
|
||||
<p class="center">"Mom!!!"</p>
|
||||
<p>There is a total free fall from 10 levels up, water down below.</p>
|
||||
<p>...</p>
|
||||
<a class="unit yak" href="#cyberninjas3"><button>Dive or Die</button></a>
|
||||
</div>
|
||||
<script>
|
||||
;(function(){
|
||||
$('#cyberninjas2 a').on('click', function(){
|
||||
$('#hudlife').text($('#hudlife').data().is = 50);
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="cyberninjas3" class="hold full blue">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Audiowide');
|
||||
|
||||
#cyberninjas3:target .story {
|
||||
animation: print 3s steps(50, end);
|
||||
}
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p>The water splashes, swelling and swirling all around.</p>
|
||||
<p>...</p>
|
||||
<p class="center">Rapidly tap to swim up to air:</p>
|
||||
<a class="unit yak"><button>Swim</button></a>
|
||||
</div>
|
||||
<script>
|
||||
;(function(){
|
||||
var go, life = $('#hudlife').data();
|
||||
$('#cyberninjas3 a').on('click', function(){
|
||||
$('#hudlife').text(life.is += 5);
|
||||
if(100 <= life.is){
|
||||
location.hash = 'cyberninjas4';
|
||||
clearInterval(go);
|
||||
go = false;
|
||||
return;
|
||||
}
|
||||
if(go){ return }
|
||||
go = setInterval(function(){
|
||||
if(0 >= life.is){
|
||||
location.hash = 'cyberninjas2';
|
||||
$('#hudlife').text(life.is = 50);
|
||||
clearInterval(go);
|
||||
go = false;
|
||||
return;
|
||||
}
|
||||
$('#hudlife').text(life.is -= 5);
|
||||
}, 1000); // 1 second
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="cyberninjas4" class="hold full black">
|
||||
<style>
|
||||
#cyberninjas4:target .story {
|
||||
animation: print 3s steps(50, end);
|
||||
}
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Episode 2: Who Am I?</p>
|
||||
<p>"Grab on!" A voice calls out from the darkness.</p>
|
||||
<p>A life vest hits the water and floats within arm's distance.</p>
|
||||
<p>The shivering body is pulled up onto the boat.</p>
|
||||
<p>"Wow, you're heavier than you look. Are you OK? What's your name?"</p>
|
||||
<p>...</p>
|
||||
<p class="center">Write your reply & hit enter:</p>
|
||||
<form class="center">
|
||||
<input class="loud" style="width: 60%;">
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
;(function(){
|
||||
$('form').on('submit', function(eve){ eve.preventDefault() });
|
||||
$('#cyberninjas4').on('submit', function(){
|
||||
var name = $(this).find('input').val();
|
||||
if(!name.length){ return }
|
||||
$('.story-name').text(' '+(window.NAME = name));
|
||||
$('#hud .score').removeClass('up');
|
||||
location.hash = 'cyberninjas5';
|
||||
})
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="cyberninjas5" class="hold full green">
|
||||
<style>
|
||||
#cyberninjas5:target .story {
|
||||
animation: print 3s steps(50, end);
|
||||
}
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p>"Well<span class="story-name"></span>, it's a miracle you did not die in the building explosion or from that fall."</p>
|
||||
<p>"What is going on? What happened?"</p>
|
||||
<p>"You can't remember? Your brain must be knocked up pretty hard."</p>
|
||||
<p>"No, I was mid copy into this body and now my memories are glitching."</p>
|
||||
<p>"Woah, you're one of those pro elite AREION revolutionaries? All flesh & blood! Dense, too. I would've assumed they were stealing AutoMecha tech for that instead."</p>
|
||||
<p>"I was about to be told vital data for the resistance, but then they blew up the build--"</p>
|
||||
<p>...</p>
|
||||
<p>"Hey, what's the matter?"</p>
|
||||
<p>"My mom. She was in there. I need to go back. Please, help me and tell me everything you know."</p>
|
||||
<p>"I'm so sorry. I can only get so close with the boat, you're gonna have to jump over a lot of broken bits. You ready?"</p>
|
||||
<p>...</p>
|
||||
<a class="unit yak" href="#cyberninjas6"><button>GO!</button></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cyberninjas6" class="hold full green">
|
||||
<style>
|
||||
#cyberninjas6:target .story {
|
||||
animation: print 3s steps(50, end);
|
||||
}
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="center">... to be continued ...</p>
|
||||
<div id="player" style="position: fixed; width: 1em; height: 1em; background: white; left: 50%; top: 50%; border-radius: 100%;"></div>
|
||||
<!-- jumping game ? like offline dinosaur ? -->
|
||||
</div>
|
||||
<script src="../../../gun/lib/meta.js"></script>
|
||||
<script>
|
||||
;(function(){
|
||||
var p = $('#player');
|
||||
p.x = 50;
|
||||
p.y = 50;
|
||||
meta.edit({
|
||||
name: "Up",
|
||||
combo: ["W"],
|
||||
on: function(){
|
||||
console.log("up");
|
||||
this.to = this.to || setInterval(this.on, 100);
|
||||
$("html, body").stop().animate({ scrollTop: $(window).scrollTop()-100 }, 100);
|
||||
p.css({top: --p.y +'%'});
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){ clearTimeout(this.to); this.to = 0 }
|
||||
});
|
||||
meta.edit({
|
||||
name: "Left",
|
||||
combo: ["A"],
|
||||
on: function(){
|
||||
console.log("left");
|
||||
this.to = this.to || setInterval(this.on, 100);
|
||||
p.css({left: --p.x +'%'});
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){ clearTimeout(this.to); this.to = 0 }
|
||||
});
|
||||
meta.edit({
|
||||
name: "Down",
|
||||
combo: ["S"],
|
||||
on: function on(){
|
||||
console.log("down");
|
||||
this.to = this.to || setInterval(this.on, 100);
|
||||
$("html, body").stop().animate({ scrollTop: $(window).scrollTop()+100 }, 100);
|
||||
p.css({top: ++p.y +'%'});
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){ clearTimeout(this.to); this.to = 0 }
|
||||
});
|
||||
meta.edit({
|
||||
name: "Right",
|
||||
combo: ["D"],
|
||||
on: function(){
|
||||
console.log("right");
|
||||
this.to = this.to || setInterval(this.on, 100);
|
||||
p.css({left: ++p.x +'%'});
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){ clearTimeout(this.to); this.to = 0 }
|
||||
});
|
||||
meta.edit({
|
||||
name: "Jump",
|
||||
combo: [32],
|
||||
on: function(){ console.log("jump") },
|
||||
use: function(){},
|
||||
up: function(){}
|
||||
});
|
||||
meta.edit({
|
||||
name: "Crouch",
|
||||
combo: [16],
|
||||
on: function(){ console.log("crouch") },
|
||||
use: function(){},
|
||||
up: function(){}
|
||||
});
|
||||
meta.edit({
|
||||
name: "Use",
|
||||
combo: ["E"],
|
||||
on: function(){ console.log("use") },
|
||||
use: function(){},
|
||||
up: function(){}
|
||||
});
|
||||
meta.edit({
|
||||
name: "Fire",
|
||||
combo: ["F"],
|
||||
on: function(){ console.log("fire") },
|
||||
use: function(){},
|
||||
up: function(){}
|
||||
});
|
||||
meta.edit({
|
||||
name: "Switch",
|
||||
combo: [9],
|
||||
on: function(){ console.log("Switch") },
|
||||
use: function(){},
|
||||
up: function(){}
|
||||
});
|
||||
window.requestAnimationFrame = window.requestAnimationFrame || setTimeout;
|
||||
window.requestAnimationFrame(function frame(){
|
||||
window.requestAnimationFrame(frame, 16);
|
||||
|
||||
}, 16);
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="automecha" class="hold full white blackt">
|
||||
<style>
|
||||
#automecha:target .story {
|
||||
animation: print 3s steps(50, end);
|
||||
}
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Episode 1: Training</p>
|
||||
<p>...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="hold black center">
|
||||
<div class="pad">
|
||||
<div class="left">
|
||||
<p class="loud">For <i>You</i>,</p>
|
||||
<p>Crafted with love, <span class="redt">♥</span> by ERA.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src="https://era.eco/media/world.png" class="row">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -3,7 +3,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="text-align: center;">
|
||||
<h1 id="when" style="font-size: 7vw; margin-top: 43vh;"></h1>
|
||||
<h1 id="when" style="font-size: 7vw; margin-top: 43vh; font-family: monospace;"></h1>
|
||||
</body>
|
||||
<script src="/gun.js"></script>
|
||||
<script src="/gun/nts.js"></script>
|
||||
@ -16,4 +16,4 @@
|
||||
when.innerHTML = print;
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
</html>
|
||||
|
@ -10,12 +10,12 @@
|
||||
<p id="debug" style="position: fixed; bottom: 0px; color: white; height: 1em;"></p>
|
||||
<script src="../jquery.js"></script>
|
||||
<script src="../../../gun/gun.js"></script>
|
||||
<script src="../../../gun/nts.js"></script>
|
||||
<!-- script src="../../../gun/nts.js"></script -->
|
||||
<script src="../../../gun/lib/webrtc.js"></script>
|
||||
<script>
|
||||
// Thanks to https://github.com/dmcinnes/HTML5-Asteroids
|
||||
//var gun = Gun();
|
||||
var gun = Gun(location.host? location.origin+'/gun' : 'http://localhost:8765/gun');
|
||||
var gun = GUN(location.origin + '/gun');
|
||||
var game = {gun: gun.get('example/game/space'), area: {}, ships: {}};
|
||||
game.keys = {38: 'up', 37: 'left', 39: 'right', 40: 'down', 32: 'space'};
|
||||
$(document).on('keydown', function(e){
|
||||
@ -50,7 +50,7 @@
|
||||
}
|
||||
game.ship = function(d, el){
|
||||
if(!d){ // spawn our ship
|
||||
var id = Gun.text.random(1, 'abcdefghijklmno');
|
||||
var id = String.random(1, 'abcdefghijklmno');
|
||||
game.me = game.ships[id] = {data: d = {id: id, t: game.now}};
|
||||
game.me.gun = game.gun.get('players').get(d.id).put(d);
|
||||
return;
|
||||
@ -101,7 +101,8 @@
|
||||
s.ay = 0;
|
||||
}
|
||||
|
||||
Gun.obj.map(game.ships, function(ship){
|
||||
Object.keys(game.ships).forEach(function(key, ship){
|
||||
ship = game.ships[key];
|
||||
if(ship.gun){ return }
|
||||
if(!ship.l){ return }
|
||||
if(ship.x-50 <= s.x && s.x <= ship.x+50
|
||||
@ -131,7 +132,7 @@
|
||||
var d = s.data;
|
||||
var dt = (now - d.t) || 0;
|
||||
if(dt > 30 * 1000){
|
||||
Gun.obj.del(game.ships, d.id);
|
||||
delete game.ships[d.id];
|
||||
s.$.remove();
|
||||
return true;
|
||||
}
|
||||
@ -143,7 +144,7 @@
|
||||
}
|
||||
s.x = d.x + d.vx * dt;
|
||||
s.y = d.y + d.vy * dt;
|
||||
|
||||
|
||||
s.x = s.x % area.x;
|
||||
if(s.x < 0){
|
||||
s.x += area.x;
|
||||
@ -159,13 +160,13 @@
|
||||
}
|
||||
return s;
|
||||
}
|
||||
localStorage.clear();
|
||||
game.sync = function(shoot){
|
||||
var me = game.me;
|
||||
if(!me || me.boom){ return }
|
||||
var keys = game.keys;
|
||||
if(shoot || keys.up || keys.right || keys.left || keys.down){
|
||||
var data = Gun.obj.to(me.data);
|
||||
var data = {};
|
||||
Object.keys(me.data).forEach(function(k){ data[k] = me.data[k] }); // 1 layer clone.
|
||||
data.x = data.x / game.area.x;
|
||||
data.y = data.y / game.area.y;
|
||||
me.gun.put(data);
|
||||
@ -182,7 +183,7 @@
|
||||
game.resize();
|
||||
game.ship();
|
||||
game.gun.get('players').map().on(function(data, id){
|
||||
data = Gun.obj.copy(data);
|
||||
data = JSON.parse(JSON.stringify(data)); // clone object, this is bad perf tho.
|
||||
data.x = data.x * game.area.x;
|
||||
data.y = data.y * game.area.y;
|
||||
data.id = data.id || id;
|
||||
@ -196,7 +197,8 @@
|
||||
now = game.now = Gun.state();
|
||||
diff = now - last;
|
||||
last = now;
|
||||
Gun.obj.map(ships, function(ship){
|
||||
Object.keys(ships).forEach(function(key, ship){
|
||||
ship = ships[key];
|
||||
if(!ship.frame){ return }
|
||||
ship.frame(diff, now);
|
||||
});
|
||||
@ -210,7 +212,9 @@
|
||||
gun.on('hi', function(peer){
|
||||
console.log("hi!", peer);
|
||||
if(peer.url){ return }
|
||||
Gun.obj.map(gun.back('opt.peers'), function(peer){
|
||||
var peers = gun.back('opt.peers');
|
||||
Object.keys(peers).forEach(function(id, peer){
|
||||
peer = peers[id];
|
||||
if(!peer.url || !peer.wire){ return }
|
||||
peer.wire._send = peer.wire.send;
|
||||
peer.wire.send = send;
|
||||
@ -221,7 +225,7 @@
|
||||
});
|
||||
function send(raw){
|
||||
if(!raw){ return }
|
||||
if(raw.indexOf('webrtc') >= 0){
|
||||
if(raw.indexOf('rtc') >= 0){
|
||||
if(!this._send){ return }
|
||||
return this._send(raw);
|
||||
}
|
||||
@ -274,4 +278,4 @@
|
||||
left: -50px;
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
</html>
|
||||
|
331
examples/game/win.html
Normal file
331
examples/game/win.html
Normal file
@ -0,0 +1,331 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- always start with these two lines to set a clean baseline for different devices -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="../style.css">
|
||||
<!-- link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/gun/examples/style.css" -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/examples/jquery.js"></script>
|
||||
|
||||
<title>Win</title>
|
||||
</head>
|
||||
<body class="black whitet">
|
||||
<style>
|
||||
/*
|
||||
Choose white text on a black background so you can add color in.
|
||||
Pick your favorite font and choose a font size.
|
||||
*/
|
||||
@import url('https://fonts.googleapis.com/css?family=Montserrat');
|
||||
html, body {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
button, input {
|
||||
padding: 1em;
|
||||
background: transparent;
|
||||
border: 1px solid white;
|
||||
border-radius: 1.5em;
|
||||
color: white;
|
||||
margin: 0.5em;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover, input:hover {
|
||||
background: white;
|
||||
color: black;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.air { padding-top: 9%; }
|
||||
.yak button { font-size: 80%; }
|
||||
.wag {
|
||||
-webkit-animation: wag 3s infinite;
|
||||
animation: wag 3s infinite;
|
||||
} @keyframes wag {
|
||||
0% {transform: rotate(0deg);}
|
||||
50% {transform: rotate(-1deg);}
|
||||
100% {transform: rotate(0deg);}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<!-- for educational sites, consider starting with a nice full screen welcome message -->
|
||||
<div class="home hold full huef center air">
|
||||
<div class="focus row">
|
||||
<p><i>how to</i></p>
|
||||
<p class="shout wag">Win at Life!</p>
|
||||
<p><i>success, fame, power.</i></p>
|
||||
<p><i>sex, ethics, & integrity.</i></p>
|
||||
<!-- just like in real life, say who you are and give a concise reason why you add value to someone's life and then make a call to action, if they want to learn more they can always scroll to learn more -->
|
||||
<div>
|
||||
<!-- a class="unit hold" href="#fullscreen"><button>WATCH TRAILER</button></a -->
|
||||
<a class="unit yak gap" href="#breathe"><button>PLAY GAME</button></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="focus center row leak">
|
||||
<!-- just like in real life, looking pretty attracts attention, so show off and look glamorous! -->
|
||||
<img class="unit blink" src="" style="min-width: 10em; width: 80%;">
|
||||
</div>
|
||||
<script>location.hash = ''</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/lib/fun.js"></script>
|
||||
</div>
|
||||
|
||||
<div id="breathe" class="hold full green">
|
||||
<style>
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Step 1: Breathe</p>
|
||||
<a class="unit yak" href="#water"><button>Yupe</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="water" class="hold full blue">
|
||||
<style>
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Step 2: Drink Water</p>
|
||||
<p></p>
|
||||
<a class="unit yak" href="#eat"><button>Next</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="eat" class="hold full red">
|
||||
<style>
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Step 3: Eat Once a Day</p>
|
||||
<p>If you do not want to be eaten, do not eat things that would not want to be eaten.</p>
|
||||
<a class="unit yak" href="#babies"><button>Got It</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="babies" class="hold full red">
|
||||
<style>
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Step 4: Babymaking* 😉</p>
|
||||
<p>Find a willing player.</p>
|
||||
<p><small> * This does not always make babies.</small></p>
|
||||
<a class="unit yak" href="#make"><button>How?</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="make" class="hold full hue">
|
||||
<style>
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Step 5: Make Dance</p>
|
||||
<p>Moving your body is how you express your thoughts.</p>
|
||||
<p>What you do with your body is what others will come to know you for. So do well.</p>
|
||||
<p>You can make art, songs, or stories; You can make science, tools, or discoveries.</p>
|
||||
<p>Who are you?</p>
|
||||
<a class="unit yak" href="#science"><button>I am a Scientist!</button></a>
|
||||
<a class="unit yak" href="#art"><button>I am an Artist!</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="science" class="hold full hue">
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Science: Knowing Games</p>
|
||||
<p>If you must win one game, it should be the game of making games.</p>
|
||||
<p>If you can make any game, then you will know how to win any game.</p>
|
||||
<a class="unit yak" href="#games"><button>Games?</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="games" class="hold full hue">
|
||||
<div class="story pad">
|
||||
<p>Games have goals and play.</p>
|
||||
<p>Play is a safe space to try new dances.</p>
|
||||
<p>Goals try to get players to do a type of dance.</p>
|
||||
<a class="unit yak" href="#swim"><button>Start</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="swim" class="hold full blue">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Audiowide');
|
||||
#hud {
|
||||
opacity: 0.4;
|
||||
font-family: 'Audiowide', cursive;
|
||||
z-index: 999999999999;
|
||||
transition: all 3s;
|
||||
}
|
||||
#hud .life {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 0px;
|
||||
padding: 0.25em 1em 0.1em;
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
transform: translateX(-50%);
|
||||
background: black;
|
||||
text-shadow: 0em -0.125em 0.75em white;
|
||||
}
|
||||
#hud .score {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 0px;
|
||||
padding: 0.1em 1em 0.25em;
|
||||
border-radius: 0 0 0.5em 0.5em;
|
||||
transform: translateX(-50%);
|
||||
background: black;
|
||||
text-shadow: 0em 0.1em 0.75em white;
|
||||
}
|
||||
#hud .down {
|
||||
bottom: -2em !important;
|
||||
}
|
||||
#hud .up {
|
||||
top: -2em !important;
|
||||
}
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p>The simplest goal is to not "die" in the game.</p>
|
||||
<p>Oh look, you've fallen into water and cannot breathe.</p>
|
||||
<p>If you do not push the button to swim to the top, you'll lose the game.</p>
|
||||
<a class="unit yak"><button>Swim</button></a>
|
||||
</div>
|
||||
<div id="hud">
|
||||
<div class="score shade up">
|
||||
SCORE: <span id="hudscore">0</span>%
|
||||
</div>
|
||||
<div class="life shade down">
|
||||
LIFE: <span id="hudlife">100</span>%
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
;(function(){
|
||||
var go, life = $('#hudlife').data();
|
||||
$(window).on('hashchange', function(){
|
||||
if(location.hash != '#swim'){ return }
|
||||
$('#hudlife').text($('#hudlife').data().is = 100);
|
||||
$('#hud .life').removeClass('down');
|
||||
go = setInterval(function(){
|
||||
if(0 >= life.is){
|
||||
location.hash = 'die';
|
||||
clearInterval(go);
|
||||
go = false;
|
||||
return;
|
||||
}
|
||||
$('#hudlife').text(life.is -= 1);
|
||||
}, 100);
|
||||
})
|
||||
$('#swim a').on('click', function(){
|
||||
$('#hudlife').text((life.is += 5) < 100? life.is : 100);
|
||||
if(100 <= life.is){
|
||||
location.hash = 'won';
|
||||
clearInterval(go);
|
||||
go = false;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="won" class="hold full hue">
|
||||
<div class="story pad">
|
||||
<p>You won your first game! 🎉</p>
|
||||
<p>See? I told you breathing is important.</p>
|
||||
<p>You also learned the most basic dance: rapid poking.</p>
|
||||
<p>The goal of the next game is to make the game you just won.</p>
|
||||
<a class="unit yak" href="#won" style="z-index: 999999;"><button>Make</button></a>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/gh/amark/gun/lib/meta.js"></script>
|
||||
<script>
|
||||
meta.edit({name: "Add", combo: ['A']});
|
||||
meta.edit({
|
||||
name: "Timer",
|
||||
combo: ['A','T'],
|
||||
on: function(){
|
||||
console.log("up");
|
||||
this.to = this.to || setInterval(this.on, 100);
|
||||
$("html, body").stop().animate({ scrollTop: $(window).scrollTop()-100 }, 100);
|
||||
p.css({top: --p.y +'%'});
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){ clearTimeout(this.to); this.to = 0 }
|
||||
});
|
||||
meta.edit({
|
||||
name: "Delete",
|
||||
combo: ['A','D'],
|
||||
on: function(){
|
||||
$(meta.tap.on).remove();
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){}
|
||||
});
|
||||
meta.edit({
|
||||
name: "Button",
|
||||
combo: ['A','B'],
|
||||
on: function(){
|
||||
$(meta.tap.on).append("<a class='unit yak'><button>Button</button></a>")
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){}
|
||||
});
|
||||
meta.edit({
|
||||
name: "Edit",
|
||||
combo: ['E'],
|
||||
on: function(){
|
||||
$('body').attr('contenteditable', 'true' == $('body').attr('contenteditable')? false : true);
|
||||
},
|
||||
use: function(){},
|
||||
up: function(){ }
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
window.requestAnimationFrame(function frame(){
|
||||
return;
|
||||
window.requestAnimationFrame(frame);
|
||||
if(location.hash != '#won'){ return }
|
||||
var p = $('#won a').offset();
|
||||
var bx = p.left, by = p.top, mx = meta.tap.x, my = meta.tap.y;
|
||||
bx = mx - bx; by = my - by;
|
||||
var d = Math.sqrt(bx*bx + by*by);
|
||||
console.log(bx, by, mx, my, d);
|
||||
if(d > 250){ return }
|
||||
$('#won a').css({position: 'fixed', left: bx + (Math.random()*100), top: by + (Math.random()*100)});
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="rules" class="hold full white blackt">
|
||||
<style>
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">Bend these Rules if it is more Moral to do so</p>
|
||||
<p></p>
|
||||
<a class="unit yak" href="#water"><button>Next</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div id="die" class="hold full red">
|
||||
<style>
|
||||
</style>
|
||||
<div class="story pad">
|
||||
<p class="loud crack">GAME OVER</p>
|
||||
<p></p>
|
||||
<a class="unit yak" href="#make"><button>Start Over</button></a>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,66 +0,0 @@
|
||||
var port = process.env.OPENSHIFT_NODEJS_PORT || process.env.VCAP_APP_PORT || process.env.PORT || process.argv[2] || 8765;
|
||||
|
||||
var Gun = require('../');
|
||||
|
||||
// have to do this before instancing gun(?)
|
||||
Gun.on('out', function(msg){
|
||||
this.to.next( msg );
|
||||
msg = JSON.stringify(msg);
|
||||
gunPeers.forEach( function(peer){ peer.send( msg ) })
|
||||
})
|
||||
|
||||
var gun = Gun({
|
||||
file: 'data.json'
|
||||
});
|
||||
|
||||
var server = require('http').createServer(function(req, res){
|
||||
var insert = "";
|
||||
if( req.url.endsWith( "gun.js" ) )
|
||||
insert = "../";
|
||||
|
||||
require('fs').createReadStream(require('path').join(__dirname, insert, req.url)).on('error',function(){ // static files!
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end(require('fs').readFileSync(require('path').join(__dirname, 'index.html'))); // or default to index
|
||||
}).pipe(res); // stream
|
||||
});
|
||||
|
||||
// do not do this to attach server... instead pull websocket provider and use that.
|
||||
// gun.wsp(server);
|
||||
|
||||
var ws = require( 'ws' ); // default websocket provider gun used...
|
||||
var WebSocketServer = ws.Server;
|
||||
|
||||
var wss = new WebSocketServer( {
|
||||
server: server, // 'ws' npm
|
||||
autoAcceptConnections : false // want to handle the request (websocket npm?)
|
||||
});
|
||||
|
||||
wss.on('connection',acceptConnection )
|
||||
|
||||
var gunPeers = []; // used as a list of connected clients.
|
||||
|
||||
function acceptConnection( connection ) {
|
||||
// connection.upgradeReq.headers['sec-websocket-protocol'] === (if present) protocol requested by client
|
||||
// connection.upgradeReq.url === url request
|
||||
console.log( "connect?", connection.upgradeReq.headers, connection.upgradeReq.url )
|
||||
gunPeers.push( connection );
|
||||
connection.on( 'error',function(error){console.log( "WebSocket Error:", error) } );
|
||||
|
||||
connection.on('message', function (msg) {
|
||||
msg = JSON.parse(msg)
|
||||
if ("forEach" in msg) msg.forEach(m => gun.on('in', JSON.parse(m)));
|
||||
else gun.on('in', msg)
|
||||
})
|
||||
|
||||
connection.on( 'close', function(reason,desc){
|
||||
// gunpeers gone.
|
||||
var i = gunPeers.findIndex( function(p){return p===connection} );
|
||||
if( i >= 0 )
|
||||
gunPeers.splice( i, 1 );
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
server.listen(port);
|
||||
|
||||
console.log('Server started on port ' + port + ' with ');
|
@ -1,21 +1,34 @@
|
||||
;(function(){
|
||||
var cluster = require('cluster');
|
||||
if(cluster.isMaster){
|
||||
return cluster.fork() && cluster.on('exit', function(){ cluster.fork() });
|
||||
}
|
||||
|
||||
var fs = require('fs');
|
||||
var config = { port: process.env.OPENSHIFT_NODEJS_PORT || process.env.VCAP_APP_PORT || process.env.PORT || process.argv[2] || 8765 };
|
||||
var Gun = require('../'); // require('gun')
|
||||
|
||||
if(process.env.HTTPS_KEY){
|
||||
config.key = fs.readFileSync(process.env.HTTPS_KEY);
|
||||
config.cert = fs.readFileSync(process.env.HTTPS_CERT);
|
||||
config.server = require('https').createServer(config, Gun.serve(__dirname));
|
||||
} else {
|
||||
config.server = require('http').createServer(Gun.serve(__dirname));
|
||||
}
|
||||
|
||||
var gun = Gun({web: config.server.listen(config.port) });
|
||||
console.log('Relay peer started on port ' + config.port + ' with /gun');
|
||||
;(function(){
|
||||
var cluster = require('cluster');
|
||||
if(cluster.isMaster){
|
||||
return cluster.fork() && cluster.on('exit',function(){ cluster.fork(); require('../lib/crashed') });
|
||||
}
|
||||
|
||||
var fs = require('fs'), env = process.env;
|
||||
var GUN = require('../'); // require('gun');
|
||||
var opt = {
|
||||
port: env.PORT || process.argv[2] || 8765,
|
||||
peers: env.PEERS && env.PEERS.split(',') || []
|
||||
};
|
||||
|
||||
if(fs.existsSync((opt.home = require('os').homedir())+'/cert.pem')){
|
||||
env.HTTPS_KEY = env.HTTPS_KEY || opt.home+'/key.pem';
|
||||
env.HTTPS_CERT = env.HTTPS_CERT || opt.home+'/cert.pem';
|
||||
}
|
||||
if(env.HTTPS_KEY){
|
||||
opt.port = 443;
|
||||
opt.key = fs.readFileSync(env.HTTPS_KEY);
|
||||
opt.cert = fs.readFileSync(env.HTTPS_CERT);
|
||||
opt.server = require('https').createServer(opt, GUN.serve(__dirname));
|
||||
require('http').createServer(function(req, res){
|
||||
res.writeHead(301, {"Location": "https://"+req.headers['host']+req.url });
|
||||
res.end();
|
||||
}).listen(80);
|
||||
} else {
|
||||
opt.server = require('http').createServer(GUN.serve(__dirname));
|
||||
}
|
||||
|
||||
var gun = GUN({web: opt.server.listen(opt.port), peers: opt.peers});
|
||||
console.log('Relay peer started on port ' + opt.port + ' with /gun');
|
||||
module.exports = gun;
|
||||
}());
|
8
examples/https.sh
Normal file
8
examples/https.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
cd ~
|
||||
git clone https://github.com/acmesh-official/acme.sh.git
|
||||
cd ~/acme.sh
|
||||
./acme.sh --install -m $EMAIL
|
||||
|
||||
bash ~/acme.sh/acme.sh --issue -d $DOMAIN -w $WEB
|
||||
bash ~/acme.sh/acme.sh --install-cert -d $DOMAIN --key-file ~/key.pem --fullchain-file ~/cert.pem --reloadcmd "service relay force-reload"
|
@ -1,28 +1,4 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>gun examples</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- h1>Examples Directory <button style="float: right;" onclick="localStorage.clear()">Clear Local Storage</button></h1 -->
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
border: none;
|
||||
b-order-top: ridge 2em skyblue;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
<a href="/todo/index.html"><iframe src="/todo/index.html"></iframe></a>
|
||||
<!-- a href="/json/index.html"><iframe src="/json/index.html"></iframe></a -->
|
||||
<a href="/chat/index.html"><iframe src="/chat/index.html"></iframe></a>
|
||||
<!-- script src="../gun.js"></script -->
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<p>This is the examples folder.
|
||||
<p>The most basic example is <a href="./basic/note.html">./basic/note.html</a>!</p>
|
||||
<p>Home page temporarily disabled.</p>
|
116
examples/infinite-scroll/ScrollWindow.js
Normal file
116
examples/infinite-scroll/ScrollWindow.js
Normal file
@ -0,0 +1,116 @@
|
||||
const DEFAULT_OPTIONS = {
|
||||
size: 20,
|
||||
stickTo: 'top',
|
||||
};
|
||||
|
||||
class ScrollWindow {
|
||||
constructor(gunNode, opts = {}) {
|
||||
this.opts = Object.assign(DEFAULT_OPTIONS, opts);
|
||||
this.elements = new Map();
|
||||
this.node = gunNode;
|
||||
this.center = this.opts.startAt;
|
||||
this.updateSubscriptions();
|
||||
}
|
||||
|
||||
updateSubscriptions() {
|
||||
this.upSubscription && this.upSubscription.off();
|
||||
this.downSubscription && this.downSubscription.off();
|
||||
|
||||
const subscribe = params => {
|
||||
this.node.get({ '.': params}).map().on((val, key, a, eve) => {
|
||||
if (params['-']) {
|
||||
this.downSubscription = eve;
|
||||
} else {
|
||||
this.upSubscription = eve;
|
||||
}
|
||||
this._addElement(key, val);
|
||||
});
|
||||
};
|
||||
|
||||
if (this.center) {
|
||||
subscribe({ '>': this.center, '<': '\uffff' });
|
||||
subscribe({'<': this.center, '>' : '', '-': true});
|
||||
} else {
|
||||
subscribe({ '<': '\uffff', '>': '', '-': this.opts.stickTo === 'top' });
|
||||
}
|
||||
}
|
||||
|
||||
_getSortedKeys() {
|
||||
this.sortedKeys = this.sortedKeys || [...this.elements.keys()].sort();
|
||||
return this.sortedKeys;
|
||||
}
|
||||
|
||||
_upOrDown(n, up) {
|
||||
this.opts.stickTo = null;
|
||||
const keys = this._getSortedKeys();
|
||||
n = n || (keys.length / 2);
|
||||
n = up ? n : -n;
|
||||
const half = Math.floor(keys.length / 2);
|
||||
const newMiddleIndex = Math.max(Math.min(half + n, keys.length - 1), 0);
|
||||
if (this.center !== keys[newMiddleIndex]) {
|
||||
this.center = keys[newMiddleIndex];
|
||||
this.updateSubscriptions();
|
||||
}
|
||||
return this.center;
|
||||
}
|
||||
|
||||
up(n) {
|
||||
return this._upOrDown(n, true);
|
||||
}
|
||||
|
||||
down(n) {
|
||||
return this._upOrDown(n, false);
|
||||
}
|
||||
|
||||
_topOrBottom(top) {
|
||||
this.opts.stickTo = top ? 'top' : 'bottom';
|
||||
this.center = null;
|
||||
this.updateSubscriptions();
|
||||
}
|
||||
|
||||
top() {
|
||||
this._topOrBottom(true);
|
||||
}
|
||||
|
||||
bottom() {
|
||||
this._topOrBottom(false);
|
||||
}
|
||||
|
||||
_addElement(key, val) {
|
||||
if (!val || this.elements.has(key)) return;
|
||||
const add = () => {
|
||||
this.elements.set(key, val);
|
||||
this.sortedKeys = [...this.elements.keys()].sort();
|
||||
const sortedElements = this.sortedKeys.map(k => this.elements.get(k));
|
||||
this.opts.onChange && this.opts.onChange(sortedElements);
|
||||
};
|
||||
const keys = this._getSortedKeys();
|
||||
if (keys.length < this.opts.size) {
|
||||
add();
|
||||
} else {
|
||||
if (this.opts.stickTo === 'top' && key > keys[0]) {
|
||||
this.elements.delete(keys[0]);
|
||||
add();
|
||||
} else if (this.opts.stickTo === 'bottom' && key < keys[keys.length - 1]) {
|
||||
this.elements.delete(keys[keys.length - 1]);
|
||||
add();
|
||||
} else if (this.center) {
|
||||
if (keys.indexOf(this.center) < (keys.length / 2)) {
|
||||
if (key < keys[keys.length - 1]) {
|
||||
this.elements.delete(keys[keys.length - 1]);
|
||||
add();
|
||||
}
|
||||
} else {
|
||||
if (key > keys[0]) {
|
||||
delete this.elements.delete(keys[0]);
|
||||
add();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getElements() {
|
||||
return this.elements;
|
||||
}
|
||||
}
|
30
examples/infinite-scroll/index.html
Normal file
30
examples/infinite-scroll/index.html
Normal file
@ -0,0 +1,30 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Infinite scroll example</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
<link rel="stylesheet" type="text/css" href="./style.css">
|
||||
<script src="/jquery.js"></script>
|
||||
<script src="/gun.js"></script>
|
||||
<script src="./ScrollWindow.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<form id="generate">
|
||||
<input type="text" id="number" placeholder="Number of posts"/>
|
||||
<button>Generate</button>
|
||||
</form>
|
||||
<div id="top-buttons">
|
||||
<button id="top">Top</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="top-sentinel" style="padding-top: 0px;"></div>
|
||||
<div id="container"></div>
|
||||
<div id="bottom-sentinel" style="padding-top: 0px;"></div>
|
||||
|
||||
<button id="bottom">Bottom</button>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
167
examples/infinite-scroll/index.js
Normal file
167
examples/infinite-scroll/index.js
Normal file
@ -0,0 +1,167 @@
|
||||
const gun = new Gun();
|
||||
|
||||
const size = 20;
|
||||
const gunNode = gun.get('posts');
|
||||
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
|
||||
let topSentinelPreviousY = 0;
|
||||
let topSentinelPreviousRatio = 0;
|
||||
let bottomSentinelPreviousY = 0;
|
||||
let bottomSentinelPreviousRatio = 0;
|
||||
let previousUpIndex = previousDownIndex = -1;
|
||||
|
||||
const render = elements => {
|
||||
const t = new Date();
|
||||
elements.reverse().forEach((data, j) => {
|
||||
var date = new Date(data.date);
|
||||
$('#date' + j).text(date.toLocaleDateString() + ' ' + date.toLocaleTimeString());
|
||||
$('#text' + j).text(data.text);
|
||||
$('#img' + j).attr('src', '');
|
||||
$('#img' + j).attr('src', _getCatImg(date.getTime()));
|
||||
$('#post' + j).css({visibility: 'visible'});
|
||||
});
|
||||
console.log('rendering took', new Date().getTime() - t.getTime(), 'ms');
|
||||
window.onRender && window.onRender(elements);
|
||||
};
|
||||
|
||||
const onChange = debounce(render, 20);
|
||||
|
||||
const scroller = new ScrollWindow(gunNode, {size, stickTo: 'top', onChange});
|
||||
|
||||
const initList = () => {
|
||||
for (var n = 0; n < size; n++) {
|
||||
var el = $("<div>").addClass('post').attr('id', 'post' + n).css({visibility: 'hidden'});
|
||||
el.append($('<b>').attr('id', 'date' + n));
|
||||
el.append($('<span>').attr('id', 'text' + n));
|
||||
el.append($('<img>').attr('id', 'img' + n).attr('height', 100).attr('width', 100));
|
||||
$('#container').append(el);
|
||||
}
|
||||
}
|
||||
|
||||
const _getCatImg = (n) => {
|
||||
const url = "https://source.unsplash.com/collection/139386/100x100/?sig=";
|
||||
return url + n % 999999;
|
||||
};
|
||||
|
||||
const getNumFromStyle = numStr => Number(numStr.substring(0, numStr.length - 2));
|
||||
|
||||
const adjustPaddings = isScrollDown => {
|
||||
const container = document.getElementById("container");
|
||||
const currentPaddingTop = getNumFromStyle(container.style.paddingTop);
|
||||
const currentPaddingBottom = getNumFromStyle(container.style.paddingBottom);
|
||||
const remPaddingsVal = 198 * (size / 2); // TODO: calculate actual element heights
|
||||
if (isScrollDown) {
|
||||
container.style.paddingTop = currentPaddingTop + remPaddingsVal + "px";
|
||||
container.style.paddingBottom = currentPaddingBottom === 0 ? "0px" : currentPaddingBottom - remPaddingsVal + "px";
|
||||
} else {
|
||||
container.style.paddingBottom = currentPaddingBottom + remPaddingsVal + "px";
|
||||
if (currentPaddingTop === 0) {
|
||||
$(window).scrollTop($('#post0').offset().top + remPaddingsVal);
|
||||
} else {
|
||||
container.style.paddingTop = currentPaddingTop - remPaddingsVal + "px";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topSentCallback = entry => {
|
||||
const container = document.getElementById("container");
|
||||
|
||||
const currentY = entry.boundingClientRect.top;
|
||||
const currentRatio = entry.intersectionRatio;
|
||||
const isIntersecting = entry.isIntersecting;
|
||||
|
||||
// conditional check for Scrolling up
|
||||
if (
|
||||
currentY > topSentinelPreviousY &&
|
||||
isIntersecting &&
|
||||
currentRatio >= topSentinelPreviousRatio &&
|
||||
scroller.center !== previousUpIndex && // stop if no new results were received
|
||||
scroller.opts.stickTo !== 'top'
|
||||
) {
|
||||
previousUpIndex = scroller.center;
|
||||
adjustPaddings(false);
|
||||
scroller.up(size / 2);
|
||||
}
|
||||
topSentinelPreviousY = currentY;
|
||||
topSentinelPreviousRatio = currentRatio;
|
||||
}
|
||||
|
||||
const botSentCallback = entry => {
|
||||
const currentY = entry.boundingClientRect.top;
|
||||
const currentRatio = entry.intersectionRatio;
|
||||
const isIntersecting = entry.isIntersecting;
|
||||
|
||||
// conditional check for Scrolling down
|
||||
if (
|
||||
currentY < bottomSentinelPreviousY &&
|
||||
currentRatio > bottomSentinelPreviousRatio &&
|
||||
isIntersecting &&
|
||||
scroller.center !== previousDownIndex && // stop if no new results were received
|
||||
scroller.opts.stickTo !== 'bottom'
|
||||
) {
|
||||
previousDownIndex = scroller.center;
|
||||
adjustPaddings(true);
|
||||
scroller.down(size / 2);
|
||||
}
|
||||
bottomSentinelPreviousY = currentY;
|
||||
bottomSentinelPreviousRatio = currentRatio;
|
||||
}
|
||||
|
||||
const initIntersectionObserver = () => {
|
||||
const options = {
|
||||
//rootMargin: '190px',
|
||||
}
|
||||
|
||||
const callback = entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.target.id === 'post0') {
|
||||
topSentCallback(entry);
|
||||
} else if (entry.target.id === `post${size - 1}`) {
|
||||
botSentCallback(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var observer = new IntersectionObserver(callback, options); // TODO: It's possible to quickly scroll past the sentinels without them firing. Top and bottom sentinels should extend to page top & bottom?
|
||||
observer.observe(document.querySelector("#post0"));
|
||||
observer.observe(document.querySelector(`#post${size - 1}`));
|
||||
}
|
||||
|
||||
initList(size);
|
||||
initIntersectionObserver();
|
||||
|
||||
$('#top').click(() => {
|
||||
scroller.top();
|
||||
$('#container').css({'padding-top': 0, 'padding-bottom': 0});
|
||||
$(document.body).animate({ scrollTop: 0 }, 500);
|
||||
});
|
||||
$('#bottom').click(() => {
|
||||
scroller.bottom();
|
||||
$('#container').css({'padding-top': 0, 'padding-bottom': 0});
|
||||
$(document.body).animate({ scrollTop: $("#container").height() }, 500);
|
||||
});
|
||||
|
||||
$('#generate').submit(e => {
|
||||
e.preventDefault();
|
||||
const day = 24 * 60 * 60 * 1000;
|
||||
const year = 365 * day;
|
||||
const n = Number($('#number').val());
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = new Date(40 * year + i * day).toISOString();
|
||||
gunNode.get(d).put({text: 'Hello world!', date: d});
|
||||
}
|
||||
});
|
77
examples/infinite-scroll/style.css
Normal file
77
examples/infinite-scroll/style.css
Normal file
@ -0,0 +1,77 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 65px;
|
||||
}
|
||||
|
||||
header {
|
||||
background: rgba(255,255,255,0.75);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
}
|
||||
|
||||
#bottom {
|
||||
position: fixed;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
border: 0;
|
||||
background-color: #efefef;
|
||||
padding: 15px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4a4f9d;
|
||||
border: 0;
|
||||
padding: 15px;
|
||||
color: white;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
input, button {
|
||||
outline: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input:focus, button:focus, button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#top-buttons, form {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#top-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin: 15px;
|
||||
padding: 15px;
|
||||
background-color: #9de1fe;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.post b {
|
||||
margin-right: 5;
|
||||
}
|
||||
|
||||
.post img {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
}
|
@ -3,27 +3,51 @@
|
||||
# README
|
||||
# This will install nodejs and npm on your system,
|
||||
# should work on most places other than Windows.
|
||||
# For it to run on boot as a server, a recent OS is needed.
|
||||
# Set any environment variables before you run this,
|
||||
# like `export RAD=false` to disable storage, or
|
||||
# pass file paths of `HTTPS_CERT` & `HTTPS_KEY`, etc.
|
||||
# Copy paste and run each line into your terminal.
|
||||
# If you are on Windows, http://nodejs.org/download/ has
|
||||
# an installer that will automatically do it for you.
|
||||
# curl -o- https://raw.githubusercontent.com/amark/gun/master/examples/install.sh | bash
|
||||
# wget -O - https://raw.githubusercontent.com/amark/gun/master/examples/install.sh | bash
|
||||
|
||||
#debian/ubuntu
|
||||
su -
|
||||
cd ~
|
||||
apt-get install sudo -y
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install curl git git-core -y
|
||||
sudo apt-get install curl git git-core systemd -y
|
||||
sudo apt-get install systemctl -y
|
||||
#fedora/openSUSE
|
||||
sudo yum check-update -y
|
||||
sudo yum install curl git git-core -y
|
||||
sudo yum install curl git git-core systemd -y
|
||||
sudo yum install systemctl -y
|
||||
|
||||
#screen -S install # You can safely CTRL+A+D to escape without stopping the process. `screen -R install` to resume. Stop all with `killall screen`. Note: May need to `sudo apt-get install screen`
|
||||
|
||||
# install nodejs
|
||||
git clone http://github.com/isaacs/nave.git
|
||||
sudo ./nave/nave.sh usemain stable
|
||||
git clone https://github.com/isaacs/nave.git
|
||||
./nave/nave.sh usemain stable
|
||||
|
||||
# If you just want nodejs and npm but not gun, stop here.
|
||||
|
||||
npm install gun
|
||||
|
||||
# to run the gun examples:
|
||||
cd ./node_modules/gun
|
||||
#npm install gun@latest
|
||||
#cd ./node_modules/gun
|
||||
mkdir node_modules
|
||||
git clone https://github.com/amark/gun.git
|
||||
cd gun
|
||||
git checkout .
|
||||
git pull
|
||||
git checkout master
|
||||
git checkout $VERSION
|
||||
git pull
|
||||
npm install .
|
||||
sudo /usr/local/bin/node ./examples/http.js 80 # change `80` to `8765` for development purposes.
|
||||
|
||||
cp ./examples/relay.service /lib/systemd/system/relay.service
|
||||
echo $PWD >> /lib/systemd/system/relay.service
|
||||
echo "fs.file-max = 999999" >> /etc/sysctl.conf
|
||||
ulimit -u unlimited
|
||||
sysctl -p /etc/sysctl.conf
|
||||
systemctl daemon-reload
|
||||
systemctl enable relay
|
||||
systemctl restart relay
|
1
examples/iris
Symbolic link
1
examples/iris
Symbolic link
@ -0,0 +1 @@
|
||||
../node_modules/iris-messenger/src
|
@ -58,7 +58,7 @@
|
||||
color: skyblue;
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
cursor: poiner;
|
||||
cursor: pointer;
|
||||
}
|
||||
ul, li {
|
||||
list-style-type: none;
|
||||
|
@ -178,21 +178,21 @@
|
||||
});
|
||||
$('#share').addClass("hide");
|
||||
} else {
|
||||
document.cookie = 'gps=' + (gps.track = (document.cookie.match(/gps\=(.*?)(\&|$|\;)/i)||[])[1] || Gun.text.random(5)); // trick with cookies!
|
||||
document.cookie = 'gps=' + (gps.track = (document.cookie.match(/gps\=(.*?)(\&|$|\;)/i)||[])[1] || String.random(5)); // trick with cookies!
|
||||
gps.ref = gun.get('gps/' + gps.track);
|
||||
gps.opt.track = function(pos){
|
||||
pos = pos.latlng;
|
||||
if(gps.follow
|
||||
|| Gun.time.is() - gps.when < 1000
|
||||
|| Gun.state() - gps.when < 1000
|
||||
|| gps.last && gps.last.lat == pos.lat && gps.last.lng == pos.lng){
|
||||
return; // throttle!
|
||||
}
|
||||
gps.when = Gun.time.is();
|
||||
gps.when = Gun.state();
|
||||
gps.ref.put(gps.last = pos);
|
||||
//$('#debug').value = JSON.stringify(gps.last);
|
||||
}
|
||||
gps.where = gps.where || Where(gps.opt);
|
||||
$('#follow').text(("where.gunDB.io/" || (location.origin + location.pathname)) + '#' + gps.track);
|
||||
$('#follow').text((location.origin + location.pathname) + '#' + gps.track);
|
||||
$('#share').removeClass("hide");
|
||||
$('#share').on('click', function(){
|
||||
$('#link').toggleClass("hide");
|
||||
|
@ -10,7 +10,7 @@ import { fromObjects, toObjects } from './asyncSerialize';
|
||||
import { subtle } from './compat';
|
||||
export function parse(text) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// need decodeURIComponent so binary strings are transfered properly
|
||||
// need decodeURIComponent so binary strings are transferred properly
|
||||
const deocodedText = unescape(text);
|
||||
const objects = JSON.parse(deocodedText);
|
||||
return fromObjects(serializers(true), objects);
|
||||
@ -19,7 +19,7 @@ export function parse(text) {
|
||||
export function stringify(value, waitForArrayBufferView = true) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const serialized = yield toObjects(serializers(waitForArrayBufferView), value);
|
||||
// need encodeURIComponent so binary strings are transfered properly
|
||||
// need encodeURIComponent so binary strings are transferred properly
|
||||
const message = JSON.stringify(serialized);
|
||||
return escape(message);
|
||||
});
|
||||
@ -147,7 +147,7 @@ const CryptoKeySerializer = {
|
||||
};
|
||||
}),
|
||||
fromObject: (cks) => __awaiter(this, void 0, void 0, function* () {
|
||||
// if we don't have access to to a real crypto implementation, just return
|
||||
// if we don't have access to a real crypto implementation, just return
|
||||
// the serialized crypto key
|
||||
if (crypto.fake) {
|
||||
const newCks = Object.assign({}, cks);
|
||||
|
18
examples/react/.gitignore
vendored
18
examples/react/.gitignore
vendored
@ -1,18 +0,0 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "gun-react-examples",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"gun": "file:../..",
|
||||
"react": "^15.5.4",
|
||||
"react-dom": "^15.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"express": "^4.15.2",
|
||||
"express-http-proxy": "^0.11.0",
|
||||
"react-scripts": "0.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"server": "PORT=8081 node ./server.js"
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
console.log("If modules not found, run `npm install` in /example folder!");
|
||||
var port = process.env.OPENSHIFT_NODEJS_PORT || process.env.VCAP_APP_PORT || process.env.PORT || process.argv[2] || 8765;
|
||||
var host = process.env.OPENSHIFT_NODEJS_HOST || process.env.VCAP_APP_HOST || process.env.HOST || 'localhost';
|
||||
|
||||
var express = require('express');
|
||||
var proxy = require('express-http-proxy');
|
||||
var http = require('http');
|
||||
var app = express();
|
||||
var server = http.createServer(app);
|
||||
|
||||
var Gun = require('gun');
|
||||
var gun = Gun({
|
||||
file: 'data.json',
|
||||
web: server
|
||||
});
|
||||
|
||||
app.use(Gun.serve);
|
||||
app.use(proxy(host + ':8765'));
|
||||
server.listen(port);
|
||||
|
||||
console.log('Server started on port ' + port + ' with /gun');
|
39
examples/react/todo.html
Normal file
39
examples/react/todo.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
<script type="text/babel">
|
||||
const gun = Gun();
|
||||
const App = () => {
|
||||
const newTodo = React.useRef()
|
||||
const [todos, setTodos] = React.useState({})
|
||||
|
||||
React.useEffect(() => {
|
||||
return gun
|
||||
.get("todos")
|
||||
.map()
|
||||
.on((todo, id) => setTodos(todos => ({...todos, [id]: todo }))).off;
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<title>TODOs</title>
|
||||
<ul>{Object.values(todos).map(({title}, i) => <li key={i}>{title}</li>)}</ul>
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault();
|
||||
gun.get("todos").set({ title: newTodo.current.value });
|
||||
newTodo.current.value = ''
|
||||
}}>
|
||||
<input ref={newTodo} placeholder="new todo"/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ReactDOM.render(<App />, document.getElementById("app"));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
26
examples/relay-sqlite-example/README.md
Normal file
26
examples/relay-sqlite-example/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# gun-relay-sqlite
|
||||
|
||||
Manager+Adapter Stable version
|
||||
|
||||
Support iOS & Android
|
||||
|
||||
# Quickly build a relay manager and Gun storage adapter with sqlite persistence.
|
||||
|
||||
```base
|
||||
yarn install
|
||||
```
|
||||
|
||||
```base
|
||||
yarn build
|
||||
```
|
||||
|
||||
```base
|
||||
npx cap sync
|
||||
```
|
||||
|
||||
```base
|
||||
npx cap open ios | android
|
||||
```
|
||||
|
||||
# Preview
|
||||

|
43
examples/relay-sqlite-example/capacitor.config.ts
Normal file
43
examples/relay-sqlite-example/capacitor.config.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
import { CapacitorHttp } from '@capacitor/core';
|
||||
import { KeyboardResize, KeyboardStyle } from '@capacitor/keyboard'
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.gun.relay',
|
||||
appName: 'Relay',
|
||||
webDir: 'dist',
|
||||
|
||||
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
Keyboard: {
|
||||
resize: KeyboardResize.None,
|
||||
resizeOnFullScreen: true,
|
||||
|
||||
},
|
||||
CapacitorSQLite: {
|
||||
migrate: true,
|
||||
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
||||
iosIsEncryption: true,
|
||||
iosKeychainPrefix: 'gundb',
|
||||
iosBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle : "Biometric login for capacitor sqlite"
|
||||
},
|
||||
androidIsEncryption: true,
|
||||
androidBiometric: {
|
||||
biometricAuth : false,
|
||||
biometricTitle : "Biometric login for capacitor sqlite",
|
||||
biometricSubTitle : "Log in using your biometric"
|
||||
},
|
||||
electronIsEncryption: true,
|
||||
electronWindowsLocation: "C:\\ProgramData\\CapacitorDatabases",
|
||||
electronMacLocation: "~/Databases/",
|
||||
electronLinuxLocation: "Databases"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
31
examples/relay-sqlite-example/index.html
Normal file
31
examples/relay-sqlite-example/index.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Gun-Relay-sqlite</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="Gun-Relay-sqlite" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
204
examples/relay-sqlite-example/package.json
Normal file
204
examples/relay-sqlite-example/package.json
Normal file
@ -0,0 +1,204 @@
|
||||
{
|
||||
"name": "gun-relay-sqlite",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run copy:sql:wasm && vite --host",
|
||||
"build:web": "npm run copy:sql:wasm && npm run build",
|
||||
"build:native": "npm run remove:sql:wasm && npm run build",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"ionic:serve:before": "npm run copy:sql:wasm",
|
||||
"copy:sql:wasm": "copyfiles -u 3 node_modules/sql.js/dist/sql-wasm.wasm public/assets",
|
||||
"remove:sql:wasm": "rimraf public/assets/sql-wasm.wasm",
|
||||
"ios:start": "npm run remove:sql:wasm && npm run build:native && npx cap sync && npx cap copy && npx cap open ios",
|
||||
"android:start": "npm run remove:sql:wasm && npm run build:native && npx cap sync && npx cap copy && npx cap open android",
|
||||
"electron:install": "cd electron && npm install && cd ..",
|
||||
"electron:prepare": "npm run remove:sql:wasm && npm run build && npx cap sync @capacitor-community/electron && npx cap copy @capacitor-community/electron",
|
||||
"electron:start": "npm run electron:prepare && cd electron && npm run electron:start",
|
||||
"clean:vite:cache": "vite clean",
|
||||
"test:e2e": "cypress run",
|
||||
"test:unit": "vitest",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/core": "^19.1.6",
|
||||
"@aparajita/capacitor-biometric-auth": "^9.0.0",
|
||||
"@aparajita/capacitor-secure-storage": "^6.0.1",
|
||||
"@capacitor-community/barcode-scanner": "^4.0.1",
|
||||
"@capacitor-community/electron": "^4.1.2",
|
||||
"@capacitor-community/media": "^8.0.0",
|
||||
"@capacitor-community/speech-recognition": "^6.0.1",
|
||||
"@capacitor-community/sqlite": "^5.2.3",
|
||||
"@capacitor/action-sheet": "^7.0.0",
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/app": "^7.0.0",
|
||||
"@capacitor/assets": "latest",
|
||||
"@capacitor/background-runner": "^2.1.0",
|
||||
"@capacitor/browser": "^7.0.0",
|
||||
"@capacitor/camera": "^7.0.0",
|
||||
"@capacitor/clipboard": "^7.0.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/device": "^7.0.0",
|
||||
"@capacitor/dialog": "^7.0.0",
|
||||
"@capacitor/filesystem": "^7.0.0",
|
||||
"@capacitor/geolocation": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/local-notifications": "^7.0.0",
|
||||
"@capacitor/network": "^7.0.0",
|
||||
"@capacitor/preferences": "^7.0.0",
|
||||
"@capacitor/push-notifications": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0",
|
||||
"@capacitor/toast": "^7.0.0",
|
||||
"@ffmpeg/core": "^0.12.10",
|
||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
"@gun-vue/composables": "^0.24.2",
|
||||
"@gun-vue/gun-es": "^0.3.1240",
|
||||
"@iconify/tailwind": "latest",
|
||||
"@ionic-native/background-mode": "^5.36.0",
|
||||
"@ionic-native/core": "^5.36.0",
|
||||
"@ionic-native/fingerprint-aio": "^5.36.0",
|
||||
"@ionic-native/media": "^5.36.0",
|
||||
"@ionic-native/qr-scanner": "^5.36.0",
|
||||
"@ionic-native/secure-storage": "^5.36.0",
|
||||
"@ionic-native/sqlite": "^5.36.0",
|
||||
"@ionic/cli": "latest",
|
||||
"@ionic/pwa-elements": "^3.2.2",
|
||||
"@ionic/storage": "^4.0.0",
|
||||
"@ionic/storage-angular": "^4.0.0",
|
||||
"@ionic/vue": "^7.0.0",
|
||||
"@ionic/vue-router": "^7.0.0",
|
||||
"@joyid/capacitor-native-passkey": "^0.0.3",
|
||||
"@peculiar/webcrypto": "^1.5.0",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"@scure/bip39": "^1.6.0",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@tresjs/core": "latest",
|
||||
"@tweenjs/tween.js": "latest",
|
||||
"@types/gun": "^0.9.6",
|
||||
"@types/text-encoding": "^0.0.40",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/webrtc": "^0.0.44",
|
||||
"@vue/language-plugin-pug": "^2.2.2",
|
||||
"@vueuse/core": "latest",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"add": "^2.0.6",
|
||||
"animate.css": "latest",
|
||||
"autopass": "^2.1.0",
|
||||
"axios": "latest",
|
||||
"b4a": "^1.6.7",
|
||||
"better-scroll": "^2.5.1",
|
||||
"btoa": "^1.2.1",
|
||||
"buffer": "^6.0.3",
|
||||
"capacitor-voice-recorder": "^7.0.5",
|
||||
"cordova-plugin-background-mode": "^0.7.3",
|
||||
"cordova-plugin-device": "^3.0.0",
|
||||
"cordova-plugin-fingerprint-aio": "^6.0.0",
|
||||
"cordova-plugin-secure-storage-echo": "^5.1.1",
|
||||
"cordova-sqlite-storage": "^7.0.0",
|
||||
"corestore": "^7.1.0",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"cryptojs": "^2.5.3",
|
||||
"elliptic": "^6.6.1",
|
||||
"ffmpeg": "^0.0.4",
|
||||
"gsap": "^3.13.0",
|
||||
"gun": "^0.2020.1240",
|
||||
"gun-avatar": "^2.2.2",
|
||||
"gun-flint": "^0.0.28",
|
||||
"ionicons": "^7.4.0",
|
||||
"jsqr": "latest",
|
||||
"localforage-cordovasqlitedriver": "^1.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"native-run": "^2.0.1",
|
||||
"node-forge": "^1.3.1",
|
||||
"nvm": "latest",
|
||||
"peerjs": "^1.5.4",
|
||||
"photoswipe": "^5.4.4",
|
||||
"pinia": "^3.0.1",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"postcss": "latest",
|
||||
"postcss-loader": "latest",
|
||||
"pug": "^3.0.3",
|
||||
"qrcode.vue": "latest",
|
||||
"qrcodejs2": "latest",
|
||||
"sass": "latest",
|
||||
"socket.io-client": "latest",
|
||||
"stats.js": "latest",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"swiper": "^11.2.6",
|
||||
"tailwind": "latest",
|
||||
"text-encoding": "^0.7.0",
|
||||
"three": "latest",
|
||||
"three-stdlib": "^2.35.16",
|
||||
"tweakpane": "latest",
|
||||
"uint8arrays": "latest",
|
||||
"unplugin-auto-import": "^19.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vite-plugin-pwa": "latest",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"vue": "latest",
|
||||
"vue-chartjs": "latest",
|
||||
"vue-count-to": "^1.0.13",
|
||||
"vue-i18n": "latest",
|
||||
"vue-image-lightbox": "^7.2.0",
|
||||
"vue-lazyload": "^3.0.0",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.8",
|
||||
"vue3-carousel": "^0.15.0",
|
||||
"vue3-photo-preview": "^0.3.0",
|
||||
"vue3-qrcode-reader": "latest",
|
||||
"vue3-touch-events": "latest",
|
||||
"webrtc-adapter": "^9.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"@iconify-json/tabler": "latest",
|
||||
"@iconify/json": "latest",
|
||||
"@tsconfig/node22": "latest",
|
||||
"@types/jsdom": "latest",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/stats.js": "latest",
|
||||
"@types/three": "latest",
|
||||
"@unocss/preset-icons": "^65.4.2",
|
||||
"@unocss/preset-uno": "^65.4.2",
|
||||
"@vitejs/plugin-legacy": "^4.0.2",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitest/eslint-plugin": "latest",
|
||||
"@vue/eslint-config-prettier": "latest",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/test-utils": "^2.3.0",
|
||||
"@vue/tsconfig": "latest",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cypress": "^13.1.0",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-oxlint": "latest",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all2": "latest",
|
||||
"oxlint": "latest",
|
||||
"prettier": "latest",
|
||||
"process": "^0.11.10",
|
||||
"rimraf": "^5.0.1",
|
||||
"tailwindcss": "latest",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.1.6",
|
||||
"unocss": "^65.4.2",
|
||||
"unplugin-vue-components": "latest",
|
||||
"vite": "^latest",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pages": "latest",
|
||||
"vite-plugin-vue-devtools": "latest",
|
||||
"vitest": "latest",
|
||||
"vue-tsc": "latest"
|
||||
},
|
||||
"description": "gun-sqlite-relay",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
131
examples/relay-sqlite-example/src/App.vue
Normal file
131
examples/relay-sqlite-example/src/App.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<ion-app>
|
||||
|
||||
|
||||
|
||||
|
||||
<ion-router-outlet />
|
||||
|
||||
|
||||
|
||||
</ion-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { IonApp, IonRouterOutlet } from '@ionic/vue';
|
||||
|
||||
|
||||
|
||||
function setupNetworkListener() {
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
window.addEventListener('online', async () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
|
||||
|
||||
|
||||
}, 500);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 保存原始 console.warn
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
|
||||
function filterGunWarnings(...args: any[]) {
|
||||
const message = args[0]?.toString() || '';
|
||||
if (message.includes('Deprecated internal utility will break in next version')) {
|
||||
return; // 忽略 Gun.js 警告
|
||||
}
|
||||
originalConsoleWarn.apply(console, args);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
console.warn = filterGunWarnings;
|
||||
await setupNetworkListener();
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('online', () => {});
|
||||
window.removeEventListener('offline', () => {});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 0px; /* 滚动条宽度 */
|
||||
background-color: transparent; /* 透明背景 */
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /* 半透明的滑块颜色 */
|
||||
border-radius: 4px; /* 圆角 */
|
||||
}
|
||||
|
||||
/* 滚动条轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* 透明轨道 */
|
||||
}
|
||||
|
||||
|
||||
.ion-page,
|
||||
ion-content {
|
||||
background: transparent !important;
|
||||
--background: transparent !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* touch-action: none;
|
||||
touch-action: pan-x pan-y;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none; */
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
638
examples/relay-sqlite-example/src/components/RelayMode.vue
Normal file
638
examples/relay-sqlite-example/src/components/RelayMode.vue
Normal file
@ -0,0 +1,638 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useNetworkStatus } from '@/composables/useNetworkStatus';
|
||||
import {
|
||||
IonIcon,
|
||||
IonToggle,
|
||||
IonModal,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
IonButtons
|
||||
} from '@ionic/vue';
|
||||
import { addCircleSharp, closeCircleSharp, searchSharp, closeSharp, refreshOutline } from 'ionicons/icons';
|
||||
import StorageService from '@/services/storageService';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
|
||||
|
||||
const appInstance = getCurrentInstance();
|
||||
const storageServ = appInstance?.appContext.config.globalProperties.$storageServ as StorageService;
|
||||
|
||||
|
||||
const {
|
||||
networkStatus,
|
||||
peersStatus,
|
||||
currentMode,
|
||||
peerStatuses,
|
||||
peersList,
|
||||
enabledPeer,
|
||||
addPeer,
|
||||
removePeer,
|
||||
enablePeer,
|
||||
disablePeer,
|
||||
peersNotes,
|
||||
savePeerNote,
|
||||
} = useNetworkStatus(storageServ);
|
||||
|
||||
|
||||
const newPeerUrl = ref('');
|
||||
const searchQuery = ref('');
|
||||
const selectedPeer = ref<string | null>(null);
|
||||
const isModalOpen = ref(false);
|
||||
const notes = ref<Record<string, string>>({});
|
||||
const isResetting = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const savedNotes = localStorage.getItem('relayNotes');
|
||||
if (savedNotes) {
|
||||
const notes = JSON.parse(savedNotes);
|
||||
for (const [peer, note] of Object.entries(notes)) {
|
||||
await savePeerNote(peer as string, note as string);
|
||||
}
|
||||
localStorage.removeItem('relayNotes'); // 迁移后删除
|
||||
console.log('Migrated relay notes to SQLite');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to migrate relay notes:', e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
watch(peersNotes, async (newNotes) => {
|
||||
for (const [peer, note] of Object.entries(newNotes)) {
|
||||
await savePeerNote(peer, note);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
|
||||
const sortedPeers = computed(() => {
|
||||
return peersList.value
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
if (a === enabledPeer.value) return -1;
|
||||
if (b === enabledPeer.value) return 1;
|
||||
return 0;
|
||||
})
|
||||
.filter((peer) => {
|
||||
const searchLower = searchQuery.value.toLowerCase();
|
||||
const peerLower = peer.toLowerCase();
|
||||
const noteLower = (peersNotes.value[peer] || '').toLowerCase();
|
||||
return peerLower.includes(searchLower) || noteLower.includes(searchLower);
|
||||
});
|
||||
});
|
||||
|
||||
const openModal = (peer: string) => {
|
||||
selectedPeer.value = peer;
|
||||
isModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
isModalOpen.value = false;
|
||||
selectedPeer.value = null;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="liquid-container" >
|
||||
<div class="status-bar" :class="['indicator', networkStatus]">
|
||||
|
||||
<div >
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="add-peer">
|
||||
<input
|
||||
v-model="newPeerUrl"
|
||||
placeholder="Enter relay URL"
|
||||
@keyup.enter="addPeer(newPeerUrl)"
|
||||
/>
|
||||
<ion-icon
|
||||
:icon="addCircleSharp"
|
||||
class="addlink"
|
||||
@click="addPeer(newPeerUrl);newPeerUrl = ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="peer-list">
|
||||
<div class="peer-list-header">
|
||||
<h3>Gun Relays</h3>
|
||||
<div class="search-container">
|
||||
<ion-icon :icon="searchSharp" class="search-icon" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search relays..."
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="peer-scroll-container">
|
||||
<div
|
||||
v-for="peer in sortedPeers"
|
||||
:key="peer"
|
||||
class="peer-item"
|
||||
@click="openModal(peer)"
|
||||
>
|
||||
<div class="peer-header">
|
||||
<div :class="['status', peerStatuses[peer]]">
|
||||
{{ peerStatuses[peer] || 'Checking' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="peer-content">
|
||||
<span class="peer-url">{{ notes[peer] || peer }}</span>
|
||||
</div>
|
||||
<div class="peer-actions">
|
||||
<ion-toggle
|
||||
@click.stop
|
||||
:checked="enabledPeer === peer"
|
||||
@ionChange="enabledPeer === peer ? disablePeer() : enablePeer(peer)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-modal :is-open="isModalOpen" @didDismiss="closeModal">
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item lines="none" v-if="selectedPeer">
|
||||
<ion-label position="stacked">Relay URL</ion-label>
|
||||
<ion-input :value="selectedPeer" readonly class="readonly-input" />
|
||||
</ion-item>
|
||||
<ion-item lines="none" v-if="selectedPeer">
|
||||
<ion-label position="stacked">Status</ion-label>
|
||||
<div :class="['status', peerStatuses[selectedPeer]]" class="status-display">
|
||||
{{ peerStatuses[selectedPeer] || 'Checking' }}
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item lines="none" v-if="selectedPeer">
|
||||
<ion-label position="stacked">Note</ion-label>
|
||||
<ion-input
|
||||
v-model="notes[selectedPeer]"
|
||||
placeholder=""
|
||||
clear-input
|
||||
style=" --padding-start: 12px;
|
||||
--padding-end: 12px;"
|
||||
/>
|
||||
</ion-item>
|
||||
<div class="modal-actions">
|
||||
<ion-button
|
||||
expand="block"
|
||||
color="danger"
|
||||
@click="removePeer(selectedPeer!); closeModal()"
|
||||
>
|
||||
Remove Relay
|
||||
</ion-button>
|
||||
<ion-button
|
||||
expand="block"
|
||||
color="medium"
|
||||
@click="closeModal"
|
||||
>
|
||||
Close
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.liquid-container {
|
||||
padding: 16px 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: transparent;
|
||||
/* background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(0, 0, 0, 0.05)); */
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(130, 130, 130, 0.15);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.status-item:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
.reset-button-container {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(102, 204, 255, 0.2), rgba(102, 204, 255, 0.4));
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
gap: 6px;
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: linear-gradient(135deg, rgba(102, 204, 255, 0.3), rgba(102, 204, 255, 0.5));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.reset-button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.reset-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reset-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rotating {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-right: 6px;
|
||||
/* color: #333; */
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 100%;
|
||||
/* padding: 4px 8px; */
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.indicator.online,
|
||||
.indicator.connected,
|
||||
.indicator.relay {
|
||||
background: linear-gradient(135deg, #88ff88, #55ccaa);
|
||||
color: #2a2a2a;
|
||||
}
|
||||
|
||||
.indicator.offline,
|
||||
.indicator.disconnected,
|
||||
.indicator.direct {
|
||||
background: linear-gradient(135deg, #ff7777, #cc5555);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.add-peer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add-peer input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
font-size: 14px;
|
||||
/* color: #333; */
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-peer input:hover,
|
||||
.add-peer input:focus {
|
||||
background: rgba(150, 150, 150, 0.25);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.addlink {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* color: #66ccff; */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.addlink:hover {
|
||||
/* color: #88ddff; */
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.peer-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.peer-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.peer-list h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
/* color: #333; */
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #777;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px 8px 36px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
font-size: 14px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:hover,
|
||||
.search-input:focus {
|
||||
background: rgba(150, 150, 150, 0.25);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.peer-scroll-container {
|
||||
border-radius: 12px;
|
||||
max-height: 539px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-track {
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, #66ccff, #88ddff);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, #55bbff, #77ccff);
|
||||
}
|
||||
|
||||
.peer-item {
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, rgba(150, 150, 150, 0.15), rgba(255, 255, 255, 0.05));
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.peer-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.peer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(4px);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: linear-gradient(135deg, #88ff88, #55ccaa);
|
||||
color: #2a2a2a;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background: linear-gradient(135deg, #ff7777, #cc5555);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status.checking {
|
||||
background: linear-gradient(135deg, #ffcc66, #ffaa33);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.peer-content {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.peer-url {
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding: 6px 10px;
|
||||
background: rgba(150, 150, 150, 0.1);
|
||||
border-radius: 8px;
|
||||
/* color: #444; */
|
||||
}
|
||||
|
||||
.peer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
ion-toggle {
|
||||
/* --background: rgba(150, 150, 150, 0.2);
|
||||
--background-checked: linear-gradient(135deg, #66ccff, #88ddff);
|
||||
--handle-background: #fff;
|
||||
--handle-background-checked: #fff; */
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
/* --handle-width: 20px;
|
||||
--handle-height: 20px; */
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
ion-modal {
|
||||
--border-radius: 12px;
|
||||
--max-width: 400px;
|
||||
--max-height: 46%;
|
||||
|
||||
--backdrop-opacity: 0.4;
|
||||
}
|
||||
|
||||
/* ion-header {
|
||||
--background: rgba(150, 150, 150, 0.1);
|
||||
} */
|
||||
|
||||
ion-toolbar {
|
||||
--background: transparent;
|
||||
--border-width: 0;
|
||||
|
||||
}
|
||||
|
||||
ion-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ion-buttons {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--background-activated: transparent;
|
||||
}
|
||||
|
||||
ion-button ion-icon {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
ion-button:hover ion-icon {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ion-padding {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--background: transparent;
|
||||
--padding-start: 0;
|
||||
--inner-padding-end: 0;
|
||||
/* margin-bottom: 16px; */
|
||||
}
|
||||
|
||||
ion-label {
|
||||
/* color: #333 !important; */
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
ion-input {
|
||||
--background: rgba(150, 150, 150, 0.15);
|
||||
--padding-start: 12px;
|
||||
--padding-end: 12px;
|
||||
--padding-top: 8px;
|
||||
--padding-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
|
||||
.readonly-input {
|
||||
--padding-start: 12px;
|
||||
--padding-end: 12px;
|
||||
|
||||
--color: #666;
|
||||
--background: rgba(150, 150, 150, 0.1);
|
||||
|
||||
}
|
||||
|
||||
.status-display {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modal-actions ion-button {
|
||||
--border-radius: 8px;
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-actions ion-button[color="danger"] {
|
||||
--background: #ff6666;
|
||||
--background-hover: #ff8888;
|
||||
--background-activated: #ff5555;
|
||||
}
|
||||
|
||||
.modal-actions ion-button[color="medium"] {
|
||||
--background: #ccc;
|
||||
--background-hover: #ddd;
|
||||
--background-activated: #bbb;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,202 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { Flint, NodeAdapter } from 'gun-flint';
|
||||
import StorageService from '../services/storageService';
|
||||
import { ISQLiteService } from '../services/sqliteService';
|
||||
import { IDbVersionService } from '../services/dbVersionService';
|
||||
|
||||
// 日志工具
|
||||
// const log = {
|
||||
// debug: (msg: string, ...args: any[]) => console.debug(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// info: (msg: string, ...args: any[]) => console.info(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// warn: (msg: string, ...args: any[]) => console.warn(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// error: (msg: string, ...args: any[]) => console.error(`[Gun-SQLite-Adapter] ${msg}`, ...args),
|
||||
// };
|
||||
|
||||
// 请求队列管理,防止重复查询
|
||||
class RequestQueue {
|
||||
private queue: Map<string, { resolve: (data: any) => void; reject: (err: any) => void }[]> = new Map();
|
||||
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
||||
private storageServ: StorageService;
|
||||
|
||||
constructor(storageServ: StorageService) {
|
||||
this.storageServ = storageServ;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handlers = this.queue.get(key) || [];
|
||||
handlers.push({ resolve, reject });
|
||||
this.queue.set(key, handlers);
|
||||
|
||||
if (!this.debounceTimers.has(key)) {
|
||||
const timer = setTimeout(async () => {
|
||||
const handlers = this.queue.get(key) || [];
|
||||
this.queue.delete(key);
|
||||
this.debounceTimers.delete(key);
|
||||
|
||||
try {
|
||||
if (!this.storageServ.db) throw new Error('Database connection not available');
|
||||
const result = await this.storageServ.query('SELECT value FROM gun_nodes WHERE key = ?', [key]);
|
||||
const data = result.values && result.values.length > 0 ? JSON.parse(result.values[0].value) : null;
|
||||
handlers.forEach(h => h.resolve(data));
|
||||
} catch (err) {
|
||||
//log.error(`Failed to get key=${key}:`, err);
|
||||
handlers.forEach(h => h.reject(err));
|
||||
}
|
||||
}, 50);
|
||||
this.debounceTimers.set(key, timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async put(soul: string, node: any): Promise<void> {
|
||||
await this.storageServ.run(
|
||||
'INSERT OR REPLACE INTO gun_nodes (key, value, timestamp) VALUES (?, ?, ?)',
|
||||
[soul, JSON.stringify(node), Date.now()]
|
||||
);
|
||||
}
|
||||
|
||||
async batchPut(nodes: Record<string, any>): Promise<void> {
|
||||
const updates: [string, string, number][] = [];
|
||||
for (const soul in nodes) {
|
||||
updates.push([soul, JSON.stringify(nodes[soul]), Date.now()]);
|
||||
}
|
||||
await this.storageServ.run(
|
||||
'INSERT OR REPLACE INTO gun_nodes (key, value, timestamp) VALUES ' + updates.map(() => '(?, ?, ?)').join(','),
|
||||
updates.flat()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 定义适配器接口
|
||||
interface GunAdapter {
|
||||
opt?: (context: any, options: any) => void;
|
||||
get: (key: string, done: (err: Error | null, node: any) => void) => void;
|
||||
put: (node: any, done: (err: Error | null) => void) => void;
|
||||
}
|
||||
|
||||
export interface IGunSQLiteAdapter {
|
||||
initialize(): Promise<void>;
|
||||
getAdapter(): GunAdapter;
|
||||
isReady: Ref<boolean>;
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
let instance: IGunSQLiteAdapter | null = null;
|
||||
|
||||
export function useGunSQLiteAdapter(
|
||||
sqliteService: ISQLiteService,
|
||||
dbVersionService: IDbVersionService,
|
||||
storageService: StorageService
|
||||
): IGunSQLiteAdapter {
|
||||
if (instance) return instance;
|
||||
|
||||
const isReady: Ref<boolean> = ref(false);
|
||||
const storageServ = storageService;
|
||||
let queue: RequestQueue | null = null;
|
||||
|
||||
async function initialize() {
|
||||
if (isReady.value) return;
|
||||
try {
|
||||
//log.info('Initializing Gun SQLite adapter...');
|
||||
// 检查 StorageService 是否已初始化数据库
|
||||
if (!storageServ.db) {
|
||||
//log.warn('StorageService database not initialized, initializing now...');
|
||||
const dbName = 'talkflowdb';
|
||||
const loadToVersion = storageServ.loadToVersion || 2;
|
||||
storageServ.db = await sqliteService.openDatabase(dbName, loadToVersion, false);
|
||||
} else {
|
||||
const isOpen = await storageServ.db.isDBOpen();
|
||||
if (!isOpen) {
|
||||
//log.warn('Database not open, reopening...');
|
||||
await storageServ.db.open();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 gun_nodes 表
|
||||
const result = await storageServ.execute(`
|
||||
CREATE TABLE IF NOT EXISTS gun_nodes (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
timestamp INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
`);
|
||||
if (result.changes && result.changes.changes >= 0) {
|
||||
//log.info('gun_nodes table created or already exists');
|
||||
} else {
|
||||
throw new Error('Failed to create gun_nodes table: no changes returned');
|
||||
}
|
||||
|
||||
queue = new RequestQueue(storageServ);
|
||||
isReady.value = true;
|
||||
//log.info('Gun SQLite adapter initialized successfully');
|
||||
} catch (err) {
|
||||
//log.error('Failed to initialize Gun SQLite adapter:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const adapterCore = {
|
||||
storageServ,
|
||||
queue,
|
||||
opt: async function (context: any, options: any) {
|
||||
// log.info('Adapter opt called:', { context, options });
|
||||
await initialize();
|
||||
return options;
|
||||
},
|
||||
get: async function (key: string, done: (err: Error | null, node: any) => void) {
|
||||
try {
|
||||
if (!isReady.value) await initialize();
|
||||
if (!queue) throw new Error('Adapter not initialized');
|
||||
const data = await queue.get(key);
|
||||
done(null, data);
|
||||
} catch (err) {
|
||||
//log.error(`Get error for key=${key}:`, err);
|
||||
done(err instanceof Error ? err : new Error('Unknown error'), null);
|
||||
}
|
||||
},
|
||||
put: async function (node: any, done: (err: Error | null) => void) {
|
||||
try {
|
||||
if (!isReady.value) await initialize();
|
||||
if (!queue) throw new Error('Adapter not initialized');
|
||||
if (typeof node !== 'object' || node === null) throw new Error('Invalid node');
|
||||
const souls = Object.keys(node).length > 1 ? Object.keys(node) : [node._?.['#'] || node._.id];
|
||||
if (!souls[0]) throw new Error('Missing soul in node');
|
||||
if (souls.length > 1) {
|
||||
await queue.batchPut(node);
|
||||
} else {
|
||||
await queue.put(souls[0], node[souls[0]] || node);
|
||||
}
|
||||
done(null);
|
||||
} catch (err) {
|
||||
//log.error('Put error:', err);
|
||||
done(err instanceof Error ? err : new Error('Unknown error'));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const adapter = new NodeAdapter(adapterCore);
|
||||
Flint.register(adapter);
|
||||
|
||||
const gunSQLiteAdapter: IGunSQLiteAdapter = {
|
||||
initialize,
|
||||
getAdapter: () => adapter,
|
||||
isReady,
|
||||
};
|
||||
|
||||
instance = gunSQLiteAdapter;
|
||||
return gunSQLiteAdapter;
|
||||
}
|
||||
|
||||
export function getGunSQLiteAdapter(
|
||||
sqliteService: ISQLiteService,
|
||||
dbVersionService: IDbVersionService,
|
||||
storageService: StorageService
|
||||
): IGunSQLiteAdapter {
|
||||
if (!instance) {
|
||||
instance = useGunSQLiteAdapter(sqliteService, dbVersionService, storageService);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
export default getGunSQLiteAdapter;
|
121
examples/relay-sqlite-example/src/composables/useNetwork.ts
Normal file
121
examples/relay-sqlite-example/src/composables/useNetwork.ts
Normal file
@ -0,0 +1,121 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import Gun, { IGunInstance } from 'gun';
|
||||
|
||||
// 模块级别的单例状态
|
||||
const isOnline = ref(navigator.onLine);
|
||||
const peersConnected = ref(false);
|
||||
const checkInterval = 60000; // 每 60 秒检查一次
|
||||
|
||||
// 单例初始化标志和变量
|
||||
let initialized = false;
|
||||
let intervalId: number | null = null;
|
||||
let currentGunInstance: IGunInstance<any> | null = null;
|
||||
let instance: ReturnType<typeof createNetwork> | null = null;
|
||||
|
||||
function createNetwork(gunInstance: IGunInstance<any>) {
|
||||
// 检查 Gun.js 对等节点,反复尝试直到成功
|
||||
async function checkPeers(): Promise<boolean> {
|
||||
const maxAttempts = 3;
|
||||
let attempt = 0;
|
||||
const retryDelay = 1000; // 每次尝试间隔 1 秒
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++;
|
||||
const result = await new Promise<boolean>((resolve) => {
|
||||
let alive = false;
|
||||
const off = gunInstance.get('~public').once((data) => {
|
||||
alive = true;
|
||||
off.off();
|
||||
resolve(true);
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (!alive) {
|
||||
resolve(false);
|
||||
}
|
||||
}, 5000); // 10 秒超时
|
||||
});
|
||||
|
||||
if (result) {
|
||||
peersConnected.value = true;
|
||||
return true;
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
}
|
||||
return false; // 理论上不会到达这里,因为 maxAttempts 是 Infinity
|
||||
}
|
||||
|
||||
async function updateNetworkStatus() {
|
||||
isOnline.value = navigator.onLine;
|
||||
peersConnected.value = await checkPeers();
|
||||
}
|
||||
|
||||
function handleOnline() {
|
||||
updateNetworkStatus();
|
||||
}
|
||||
|
||||
function handleOffline() {
|
||||
isOnline.value = false;
|
||||
peersConnected.value = false;
|
||||
}
|
||||
|
||||
function startChecking() {
|
||||
updateNetworkStatus();
|
||||
intervalId = window.setInterval(updateNetworkStatus, checkInterval);
|
||||
}
|
||||
|
||||
function stopChecking() {
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 单例初始化逻辑
|
||||
if (!initialized || currentGunInstance !== gunInstance) {
|
||||
if (initialized && currentGunInstance !== gunInstance) {
|
||||
// 如果 Gun 实例变了,清理旧的事件监听器和定时器
|
||||
stopChecking();
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
currentGunInstance = gunInstance;
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
startChecking();
|
||||
}
|
||||
|
||||
return {
|
||||
isOnline,
|
||||
peersConnected,
|
||||
updateNetworkStatus,
|
||||
checkPeers,
|
||||
};
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export function useNetwork(gunInstance: IGunInstance<any>) {
|
||||
if (!instance || currentGunInstance !== gunInstance) {
|
||||
instance = createNetwork(gunInstance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 清理函数(可选,用于测试或应用卸载时)
|
||||
export function cleanupNetwork() {
|
||||
if (instance) {
|
||||
window.removeEventListener('online', instance.updateNetworkStatus);
|
||||
window.removeEventListener('offline', instance.updateNetworkStatus);
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
instance = null;
|
||||
initialized = false;
|
||||
currentGunInstance = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,558 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useNetwork } from '@/composables/useNetwork';
|
||||
import StorageService from '@/services/storageService';
|
||||
import Gun from 'gun';
|
||||
import 'gun/sea';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 模块级别的单例状态
|
||||
const networkStatus = ref<'online' | 'offline'>('online');
|
||||
const peersStatus = ref<'connected' | 'disconnected'>('disconnected');
|
||||
const currentMode = ref<'direct' | 'relay'>('direct');
|
||||
const peerStatuses = ref<Record<string, 'connected' | 'disconnected'>>({});
|
||||
|
||||
// 单例初始化标志
|
||||
let initialized = false;
|
||||
let instance: ReturnType<typeof createNetworkStatus> | null = null;
|
||||
|
||||
function createNetworkStatus(storageService: StorageService) {
|
||||
|
||||
const { showToast } = useToast();
|
||||
const { isOnline, peersConnected, updateNetworkStatus, checkPeers } = useNetwork(gun);
|
||||
const peersNotes = ref<Record<string, string>>({});
|
||||
|
||||
const peersList = ref<string[]>([
|
||||
'https://peer.wallie.io/gun',
|
||||
'https://gun.defucc.me/gun',
|
||||
'https://talkflow.team/gun',
|
||||
'https://gun-manhattan.herokuapp.com/gun',
|
||||
'https://gundb-relay-mlccl.ondigitalocean.app/gun',
|
||||
|
||||
|
||||
]);
|
||||
|
||||
const enabledPeer = ref<string>(peersList.value[0]);
|
||||
|
||||
let gun = Gun({
|
||||
|
||||
peers: [ enabledPeer.value],
|
||||
radisk: true,
|
||||
localStorage: false,
|
||||
gunSQLiteAdapter: {
|
||||
key: 'gundb',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// 确保 storageService 已初始化
|
||||
async function ensureStorageReady() {
|
||||
try {
|
||||
if (!storageService.db || !(await storageService.db.isDBOpen())) {
|
||||
console.log('[useNetworkStatus] Initializing StorageService database...');
|
||||
await storageService.initializeDatabase();
|
||||
if (!storageService.db) {
|
||||
throw new Error('StorageService 初始化后仍无数据库连接');
|
||||
}
|
||||
console.log('[useNetworkStatus] StorageService database initialized');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to initialize StorageService:', err);
|
||||
showToast('数据库初始化失败,请重试', 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存 enabledPeer 到 SQLite
|
||||
async function saveEnabledPeer() {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
await storageService.run('UPDATE network_peers SET is_enabled = 0');
|
||||
if (enabledPeer.value) {
|
||||
await storageService.run(
|
||||
'INSERT OR REPLACE INTO network_peers (url, is_enabled, note) VALUES (?, ?, ?)',
|
||||
[enabledPeer.value, 1, peersNotes.value[enabledPeer.value] || '']
|
||||
);
|
||||
}
|
||||
console.log(`Enabled peer saved: ${enabledPeer.value}`);
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to save enabled peer:', err);
|
||||
showToast('无法保存启用节点', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存节点备注
|
||||
async function savePeerNote(peer: string, note: string) {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
await storageService.run(
|
||||
'UPDATE network_peers SET note = ? WHERE url = ?',
|
||||
[note, peer]
|
||||
);
|
||||
peersNotes.value[peer] = note;
|
||||
console.log(`Peer note saved: ${peer} -> ${note}`);
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to save peer note:', err);
|
||||
showToast('无法保存节点备注', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 从 SQLite 加载 Peer 配置和备注
|
||||
// async function loadPeers() {
|
||||
// try {
|
||||
// await ensureStorageReady();
|
||||
// const result = await storageService.query('SELECT url, is_enabled, note FROM network_peers');
|
||||
// const peers = result.values || [];
|
||||
// peersList.value = peers.map((peer: { url: string }) => peer.url);
|
||||
// peersNotes.value = peers.reduce((acc: Record<string, string>, peer: { url: string; note: string }) => {
|
||||
// acc[peer.url] = peer.note || '';
|
||||
// return acc;
|
||||
// }, {});
|
||||
// const enabled = peers.find((peer: { is_enabled: number }) => peer.is_enabled === 1);
|
||||
// if (enabled && peersList.value.includes(enabled.url)) {
|
||||
// enabledPeer.value = enabled.url;
|
||||
// } else if (peersList.value.length > 0) {
|
||||
// enabledPeer.value = peersList.value[0];
|
||||
// await saveEnabledPeer();
|
||||
// }
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// } catch (err) {
|
||||
// console.error('[useNetworkStatus] Failed to load peers:', err);
|
||||
// showToast('无法加载节点列表', 'error');
|
||||
// }
|
||||
// }
|
||||
async function loadPeers() {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
const result = await storageService.query('SELECT url, is_enabled, note FROM network_peers');
|
||||
const peers = result.values || [];
|
||||
|
||||
if (peers.length === 0) {
|
||||
// 如果 SQLite 表为空,插入 TalkFlowCore 的预设节点
|
||||
console.log('[useNetworkStatus] network_peers 表为空,插入预设节点');
|
||||
const { peersList: defaultPeersList } = getTalkFlowCore(); // 获取预设节点
|
||||
for (const peerUrl of defaultPeersList.value) {
|
||||
await storageService.run(
|
||||
'INSERT OR IGNORE INTO network_peers (url, is_enabled, note) VALUES (?, ?, ?)',
|
||||
[peerUrl, 0, '']
|
||||
);
|
||||
}
|
||||
// 重新查询以确保数据已插入
|
||||
const newResult = await storageService.query('SELECT url, is_enabled, note FROM network_peers');
|
||||
peersList.value = newResult.values.map((peer: { url: string }) => peer.url);
|
||||
} else {
|
||||
// 使用 SQLite 中的节点
|
||||
peersList.value = peers.map((peer: { url: string }) => peer.url);
|
||||
}
|
||||
|
||||
// 加载备注
|
||||
peersNotes.value = peers.reduce((acc: Record<string, string>, peer: { url: string; note: string }) => {
|
||||
acc[peer.url] = peer.note || '';
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 设置 enabledPeer
|
||||
const enabled = peers.find((peer: { is_enabled: number }) => peer.is_enabled === 1);
|
||||
if (enabled && peersList.value.includes(enabled.url)) {
|
||||
enabledPeer.value = enabled.url;
|
||||
} else if (peersList.value.length > 0) {
|
||||
enabledPeer.value = peersList.value[0];
|
||||
await saveEnabledPeer();
|
||||
}
|
||||
|
||||
// 更新 Gun 配置
|
||||
gun.opt({ peers: peersList.value });
|
||||
console.log('[useNetworkStatus] Gun 配置节点:', peersList.value);
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] 加载节点失败:', err);
|
||||
showToast('无法加载节点列表', 'error');
|
||||
}
|
||||
}
|
||||
// 更新网络和 Peer 状态
|
||||
async function updateStatus() {
|
||||
networkStatus.value = isOnline.value ? 'online' : 'offline';
|
||||
peersStatus.value = peersConnected.value ? 'connected' : 'disconnected';
|
||||
currentMode.value = peersConnected.value && enabledPeer.value ? 'relay' : 'direct';
|
||||
await updatePeerStatuses();
|
||||
}
|
||||
|
||||
// 检查单个 Peer 的状态(用于 UI 展示)
|
||||
async function checkPeerStatus(peer: string): Promise<'connected' | 'disconnected'> {
|
||||
return new Promise((resolve) => {
|
||||
const tempGun = Gun({ peers: [peer] });
|
||||
let connected = false;
|
||||
tempGun.on('hi', () => {
|
||||
connected = true;
|
||||
resolve('connected');
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (!connected) resolve('disconnected');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新所有 Peer 的状态(用于 UI 展示)
|
||||
async function updatePeerStatuses() {
|
||||
for (const peer of peersList.value) {
|
||||
const status = await checkPeerStatus(peer);
|
||||
peerStatuses.value[peer] = status;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户手动启用某个 Peer
|
||||
async function enablePeer(peer: string) {
|
||||
if (!peersList.value.includes(peer)) {
|
||||
showToast(`节点 ${peer} 不在列表中`, 'warning');
|
||||
return;
|
||||
}
|
||||
if (enabledPeer.value === peer) {
|
||||
showToast(`${peer} 已是启用状态`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
enabledPeer.value = peer;
|
||||
await saveEnabledPeer();
|
||||
gun.opt({ peers: peersList.value, priorityPeer: peer });
|
||||
showToast(`启用节点: ${peer}`, 'success');
|
||||
|
||||
const connected = await checkPeers();
|
||||
if (!connected) {
|
||||
showToast(`无法连接到 ${peer},将回退到其他节点`, 'warning');
|
||||
}
|
||||
await updateStatus();
|
||||
}
|
||||
|
||||
// 用户禁用当前启用的 Peer
|
||||
async function disablePeer() {
|
||||
if (!enabledPeer.value) {
|
||||
showToast('无启用的节点', 'info');
|
||||
return;
|
||||
}
|
||||
const oldPeer = enabledPeer.value;
|
||||
enabledPeer.value = peersList.value.length > 1 ? peersList.value.find(p => p !== oldPeer) || '' : '';
|
||||
await saveEnabledPeer();
|
||||
gun.opt({ peers: peersList.value });
|
||||
showToast(`禁用节点: ${oldPeer}`, 'success');
|
||||
await updateStatus();
|
||||
}
|
||||
|
||||
// 添加 Peer(允许任意输入)
|
||||
async function addPeer(url: string) {
|
||||
if (!url) {
|
||||
showToast('请输入节点地址', 'warning');
|
||||
return;
|
||||
}
|
||||
const trimmedUrl = url.trim();
|
||||
if (peersList.value.includes(trimmedUrl)) {
|
||||
showToast('该节点已存在', 'warning');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
console.log('[useNetworkStatus] 添加节点:', trimmedUrl);
|
||||
await storageService.run(
|
||||
'INSERT INTO network_peers (url, is_enabled, note) VALUES (?, ?, ?)',
|
||||
[trimmedUrl, 0, '']
|
||||
);
|
||||
|
||||
// 重新查询 network_peers 表,确保数据一致
|
||||
const result = await storageService.query('SELECT url FROM network_peers');
|
||||
peersList.value = result.values.map((peer: { url: string }) => peer.url);
|
||||
console.log('[useNetworkStatus] 更新 peersList:', peersList.value);
|
||||
|
||||
gun.opt({ peers: peersList.value });
|
||||
showToast(`节点已添加: ${trimmedUrl}`, 'success');
|
||||
await updatePeerStatuses();
|
||||
} catch (err: any) {
|
||||
console.error('[useNetworkStatus] Failed to add peer:', err);
|
||||
showToast(`添加节点失败: ${err.message || '未知错误'}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 移除 Peer
|
||||
async function removePeer(peer: string) {
|
||||
try {
|
||||
await ensureStorageReady();
|
||||
if (enabledPeer.value === peer) {
|
||||
await disablePeer();
|
||||
}
|
||||
await storageService.run('DELETE FROM network_peers WHERE url = ?', [peer]);
|
||||
peersList.value = peersList.value.filter(p => p !== peer);
|
||||
delete peerStatuses.value[peer];
|
||||
gun.opt({ peers: peersList.value });
|
||||
showToast(`删除节点 ${peer}`, 'success');
|
||||
await updatePeerStatuses();
|
||||
} catch (err) {
|
||||
console.error('[useNetworkStatus] Failed to remove peer:', err);
|
||||
showToast('删除节点失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理网络状态变化
|
||||
function handleOnline() {
|
||||
updateNetworkStatus();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
function handleOffline() {
|
||||
updateNetworkStatus();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
// 初始化逻辑,只执行一次
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
loadPeers();
|
||||
updateStatus();
|
||||
|
||||
// 添加网络事件监听
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
// 监听 peersList 和 enabledPeer 的变化
|
||||
watch(peersList, () => {
|
||||
updatePeerStatuses();
|
||||
});
|
||||
watch(enabledPeer, () => {
|
||||
saveEnabledPeer();
|
||||
updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
networkStatus,
|
||||
peersStatus,
|
||||
currentMode,
|
||||
peerStatuses,
|
||||
peersList,
|
||||
enabledPeer,
|
||||
addPeer,
|
||||
removePeer,
|
||||
enablePeer,
|
||||
disablePeer,
|
||||
updateStatus,
|
||||
peersNotes,
|
||||
savePeerNote,
|
||||
};
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export function useNetworkStatus(storageService: StorageService) {
|
||||
if (!instance) {
|
||||
instance = createNetworkStatus(storageService);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
// import { ref, watch } from 'vue';
|
||||
// import { useToast } from '@/composables/useToast';
|
||||
// import { useNetwork } from '@/composables/useNetwork';
|
||||
|
||||
// // 模块级别的单例状态
|
||||
// const networkStatus = ref<'online' | 'offline'>('online');
|
||||
// const peersStatus = ref<'connected' | 'disconnected'>('disconnected');
|
||||
// const currentMode = ref<'direct' | 'relay'>('direct');
|
||||
// const peerStatuses = ref<Record<string, 'connected' | 'disconnected'>>({});
|
||||
|
||||
// // 单例初始化标志
|
||||
// let initialized = false;
|
||||
// let instance: ReturnType<typeof createNetworkStatus> | null = null;
|
||||
|
||||
// // 持久化 enabledPeer 的本地存储键
|
||||
// const ENABLED_PEER_KEY = 'enabledPeer';
|
||||
|
||||
// function createNetworkStatus() {
|
||||
// const { gun, peersList, enabledPeer } = getTalkFlowCore();
|
||||
// const { showToast } = useToast();
|
||||
// const { isOnline, peersConnected, updateNetworkStatus, checkPeers } = useNetwork(gun);
|
||||
|
||||
// // 保存 enabledPeer 到 localStorage
|
||||
// function saveEnabledPeer() {
|
||||
// localStorage.setItem(ENABLED_PEER_KEY, enabledPeer.value);
|
||||
// }
|
||||
|
||||
// // 从 localStorage 加载 Peer 配置
|
||||
// function loadPeers() {
|
||||
// const savedPeers = localStorage.getItem('peers');
|
||||
// if (savedPeers) {
|
||||
// peersList.value = JSON.parse(savedPeers);
|
||||
// }
|
||||
// const savedPeer = localStorage.getItem(ENABLED_PEER_KEY);
|
||||
// if (savedPeer && peersList.value.includes(savedPeer)) {
|
||||
// enabledPeer.value = savedPeer;
|
||||
// } else if (peersList.value.length > 0) {
|
||||
// enabledPeer.value = peersList.value[0];
|
||||
// saveEnabledPeer();
|
||||
// }
|
||||
// // 初始化时使用完整的 peersList
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// }
|
||||
|
||||
// // 更新网络和 Peer 状态
|
||||
// async function updateStatus() {
|
||||
// networkStatus.value = isOnline.value ? 'online' : 'offline';
|
||||
// peersStatus.value = peersConnected.value ? 'connected' : 'disconnected';
|
||||
// currentMode.value = peersConnected.value && enabledPeer.value ? 'relay' : 'direct';
|
||||
// await updatePeerStatuses();
|
||||
// }
|
||||
|
||||
// // 检查单个 Peer 的状态(用于 UI 展示)
|
||||
// async function checkPeerStatus(peer: string): Promise<'connected' | 'disconnected'> {
|
||||
// return new Promise((resolve) => {
|
||||
// const tempGun = Gun({ peers: [peer] });
|
||||
// let connected = false;
|
||||
// tempGun.on('hi', () => {
|
||||
// connected = true;
|
||||
// resolve('connected');
|
||||
// });
|
||||
// setTimeout(() => {
|
||||
// if (!connected) resolve('disconnected');
|
||||
// }, 5000);
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 更新所有 Peer 的状态(用于 UI 展示)
|
||||
// async function updatePeerStatuses() {
|
||||
// for (const peer of peersList.value) {
|
||||
// const status = await checkPeerStatus(peer);
|
||||
// peerStatuses.value[peer] = status;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 用户手动启用某个 Peer
|
||||
// async function enablePeer(peer: string) {
|
||||
// if (!peersList.value.includes(peer)) {
|
||||
// showToast(`Peer ${peer} not in list`, 'warning');
|
||||
// return;
|
||||
// }
|
||||
// if (enabledPeer.value === peer) {
|
||||
// showToast(`${peer} is already enabled`, 'info');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// enabledPeer.value = peer;
|
||||
// saveEnabledPeer();
|
||||
// gun.opt({ peers: peersList.value, priorityPeer: peer }); // 自定义选项,优先尝试该 Peer
|
||||
// showToast(`Enabled peer: ${peer}`, 'success');
|
||||
|
||||
// // 检查连接状态
|
||||
// const connected = await checkPeers();
|
||||
// if (!connected) {
|
||||
// showToast(`Failed to connect to ${peer}, falling back to other peers`, 'warning');
|
||||
// }
|
||||
// await updateStatus();
|
||||
// }
|
||||
|
||||
// // 用户禁用当前启用的 Peer
|
||||
// function disablePeer() {
|
||||
// if (!enabledPeer.value) {
|
||||
// showToast('No peer enabled', 'info');
|
||||
// return;
|
||||
// }
|
||||
// const oldPeer = enabledPeer.value;
|
||||
// enabledPeer.value = peersList.value.length > 1 ? peersList.value.find(p => p !== oldPeer) || '' : '';
|
||||
// saveEnabledPeer();
|
||||
// gun.opt({ peers: peersList.value }); // 重置为完整列表
|
||||
// showToast(`Disabled peer: ${oldPeer}`, 'success');
|
||||
// updateStatus();
|
||||
// }
|
||||
|
||||
// // 添加 Peer
|
||||
// function addPeer(url: string) {
|
||||
// if (!url) {
|
||||
// showToast('Please enter the node URL', 'warning');
|
||||
// return;
|
||||
// }
|
||||
// if (peersList.value.includes(url)) {
|
||||
// showToast('This node already exists.', 'warning');
|
||||
// return;
|
||||
// }
|
||||
// peersList.value.push(url);
|
||||
// localStorage.setItem('peers', JSON.stringify(peersList.value));
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// showToast(`Node added: ${url}`, 'success');
|
||||
// updatePeerStatuses();
|
||||
// }
|
||||
|
||||
// // 移除 Peer
|
||||
// function removePeer(peer: string) {
|
||||
// if (enabledPeer.value === peer) {
|
||||
// disablePeer();
|
||||
// }
|
||||
// peersList.value = peersList.value.filter(p => p !== peer);
|
||||
// delete peerStatuses.value[peer];
|
||||
// localStorage.setItem('peers', JSON.stringify(peersList.value));
|
||||
// gun.opt({ peers: peersList.value });
|
||||
// showToast(`Deleted node ${peer}`, 'success');
|
||||
// updatePeerStatuses();
|
||||
// }
|
||||
|
||||
// // 处理网络状态变化
|
||||
// function handleOnline() {
|
||||
// updateNetworkStatus();
|
||||
// updateStatus();
|
||||
// }
|
||||
|
||||
// function handleOffline() {
|
||||
// updateNetworkStatus();
|
||||
// updateStatus();
|
||||
// }
|
||||
|
||||
// // 初始化逻辑,只执行一次
|
||||
// if (!initialized) {
|
||||
// initialized = true;
|
||||
// loadPeers();
|
||||
// updateStatus();
|
||||
|
||||
// // 添加网络事件监听
|
||||
// window.addEventListener('online', handleOnline);
|
||||
// window.addEventListener('offline', handleOffline);
|
||||
|
||||
// // 监听 peersList 和 enabledPeer 的变化
|
||||
// watch(peersList, () => {
|
||||
// // gun.opt({ peers: peersList.value });
|
||||
// updatePeerStatuses();
|
||||
// });
|
||||
// watch(enabledPeer, () => {
|
||||
// saveEnabledPeer();
|
||||
// updateStatus();
|
||||
// });
|
||||
// }
|
||||
|
||||
// return {
|
||||
// networkStatus,
|
||||
// peersStatus,
|
||||
// currentMode,
|
||||
// peerStatuses,
|
||||
// peersList,
|
||||
// enabledPeer,
|
||||
// addPeer,
|
||||
// removePeer,
|
||||
// enablePeer,
|
||||
// disablePeer,
|
||||
// updateStatus,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // 导出单例
|
||||
// export function useNetworkStatus() {
|
||||
// if (!instance) {
|
||||
// instance = createNetworkStatus();
|
||||
// }
|
||||
// return instance;
|
||||
// }
|
||||
|
||||
// 清理函数(用于测试或应用卸载时)
|
||||
// export function cleanupNetworkStatus() {
|
||||
// if (instance) {
|
||||
// window.removeEventListener('online', instance.updateStatus);
|
||||
// window.removeEventListener('offline', instance.updateStatus);
|
||||
// instance = null;
|
||||
// initialized = false;
|
||||
// }
|
||||
// }
|
88
examples/relay-sqlite-example/src/composables/useToast.ts
Normal file
88
examples/relay-sqlite-example/src/composables/useToast.ts
Normal file
@ -0,0 +1,88 @@
|
||||
// src/composables/useToast.ts
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Directory, Encoding, Filesystem } from '@capacitor/filesystem';
|
||||
|
||||
type ToastType = 'info' | 'success' | 'error' | 'warning';
|
||||
|
||||
interface ToastMessage {
|
||||
id: number;
|
||||
text: string;
|
||||
type: ToastType;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const messages = ref<ToastMessage[]>([]);
|
||||
let idCounter = 0;
|
||||
const isEnabled = ref(false); // 默认开启提示
|
||||
const SETTINGS_FILE = 'toast_settings.json';
|
||||
|
||||
async function loadSettings(): Promise<{ isToastEnabled: boolean }> {
|
||||
const defaultSettings = { isToastEnabled: false };
|
||||
try {
|
||||
const result = await Filesystem.readFile({
|
||||
path: SETTINGS_FILE,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
const data = typeof result.data === 'string' ? result.data : await result.data.text();
|
||||
return JSON.parse(data) || defaultSettings;
|
||||
} catch (err) {
|
||||
console.log('未找到提示设置文件,使用默认值');
|
||||
return defaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(): Promise<void> {
|
||||
try {
|
||||
await Filesystem.writeFile({
|
||||
path: SETTINGS_FILE,
|
||||
data: JSON.stringify({ isToastEnabled: isEnabled.value }),
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
console.log('提示设置已保存:', { isToastEnabled: isEnabled.value });
|
||||
} catch (err) {
|
||||
console.error('保存提示设置失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export function showToast(msg: string, msgType: ToastType = 'info', customDuration = 3000) {
|
||||
if (!isEnabled.value) return; // 如果关闭则不显示
|
||||
|
||||
const toast = {
|
||||
id: idCounter++,
|
||||
text: msg,
|
||||
type: msgType,
|
||||
duration: customDuration,
|
||||
};
|
||||
messages.value.push(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
messages.value = messages.value.filter(m => m.id !== toast.id);
|
||||
}, customDuration);
|
||||
}
|
||||
|
||||
function hideToast(id: number) {
|
||||
messages.value = messages.value.filter(m => m.id !== id);
|
||||
}
|
||||
|
||||
function toggleToast(enabled: boolean) {
|
||||
isEnabled.value = enabled;
|
||||
saveSettings(); // 保存设置
|
||||
}
|
||||
|
||||
// 初始化加载设置
|
||||
onMounted(async () => {
|
||||
const settings = await loadSettings();
|
||||
isEnabled.value = settings.isToastEnabled;
|
||||
});
|
||||
|
||||
export function useToast() {
|
||||
return {
|
||||
messages,
|
||||
isEnabled,
|
||||
showToast,
|
||||
hideToast,
|
||||
toggleToast,
|
||||
};
|
||||
}
|
97
examples/relay-sqlite-example/src/main.ts
Normal file
97
examples/relay-sqlite-example/src/main.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './src/App.vue'
|
||||
import router from './router';
|
||||
|
||||
import { IonicVue } from '@ionic/vue';
|
||||
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
import '@ionic/vue/css/core.css';
|
||||
|
||||
/* Basic CSS for apps built with Ionic */
|
||||
import '@ionic/vue/css/normalize.css';
|
||||
import '@ionic/vue/css/structure.css';
|
||||
import '@ionic/vue/css/typography.css';
|
||||
import '@ionic/vue/css/ionic.bundle.css'
|
||||
/* Optional CSS utils that can be commented out */
|
||||
import '@ionic/vue/css/padding.css';
|
||||
import '@ionic/vue/css/float-elements.css';
|
||||
import '@ionic/vue/css/text-alignment.css';
|
||||
import '@ionic/vue/css/text-transformation.css';
|
||||
import '@ionic/vue/css/flex-utils.css';
|
||||
import '@ionic/vue/css/display.css';
|
||||
|
||||
/* Theme variables */
|
||||
import './theme/variables.css';
|
||||
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { JeepSqlite } from 'jeep-sqlite/dist/components/jeep-sqlite';
|
||||
import { defineCustomElements as pwaElements} from '@ionic/pwa-elements/loader';
|
||||
import SqliteService from './services/sqliteService';
|
||||
import DbVersionService from './services/dbVersionService';
|
||||
import StorageService from './services/storageService';
|
||||
import InitializeAppService from './services/initializeAppService';
|
||||
|
||||
|
||||
|
||||
|
||||
pwaElements(window);
|
||||
customElements.define('jeep-sqlite', JeepSqlite);
|
||||
const platform = Capacitor.getPlatform();
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
.use(IonicVue)
|
||||
.use(useRouter)
|
||||
|
||||
.use(router);
|
||||
|
||||
|
||||
// Set the platform as global properties on the app
|
||||
app.config.globalProperties.$platform = platform;
|
||||
|
||||
// Define and instantiate the required services
|
||||
const sqliteServ = new SqliteService();
|
||||
const dbVersionServ = new DbVersionService();
|
||||
const storageServ = new StorageService(sqliteServ, dbVersionServ);
|
||||
// Set the services as global properties on the app
|
||||
app.config.globalProperties.$sqliteServ = sqliteServ;
|
||||
app.config.globalProperties.$dbVersionServ = dbVersionServ;
|
||||
app.config.globalProperties.$storageServ = storageServ;
|
||||
|
||||
//Define and instantiate the InitializeAppService
|
||||
const initAppServ = new InitializeAppService(sqliteServ, storageServ);
|
||||
|
||||
const mountApp = () => {
|
||||
initAppServ.initializeApp()
|
||||
.then(() => {
|
||||
router.isReady().then(() => {
|
||||
|
||||
app.mount('#app');
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('App Initialization error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (platform !== "web") {
|
||||
mountApp();
|
||||
} else {
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
const jeepEl = document.createElement("jeep-sqlite");
|
||||
document.body.appendChild(jeepEl);
|
||||
customElements.whenDefined('jeep-sqlite').then(() => {
|
||||
mountApp();
|
||||
})
|
||||
.catch ((err) => {
|
||||
console.error('jeep-sqlite creation error:', err);
|
||||
});
|
||||
});
|
||||
}
|
313
examples/relay-sqlite-example/src/pages/index.vue
Normal file
313
examples/relay-sqlite-example/src/pages/index.vue
Normal file
@ -0,0 +1,313 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
IonPage, IonHeader, IonToolbar, IonButtons, IonBackButton, IonTitle, IonContent, IonIcon,
|
||||
IonModal, IonToggle
|
||||
} from '@ionic/vue';
|
||||
import { addCircleSharp, closeCircleSharp, helpCircleOutline, closeOutline } from 'ionicons/icons';
|
||||
import RelayMode from '@/components/RelayMode.vue';
|
||||
|
||||
|
||||
const showHelpModal = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header :translucent="true" collapse="fade">
|
||||
<ion-toolbar class="liquid-toolbar">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button text="Discover" color="dark"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Network Status</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button color="dark" @click="showHelpModal = true">
|
||||
<ion-icon :icon="helpCircleOutline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content :fullscreen="true" :scroll-y="false">
|
||||
|
||||
<RelayMode/>
|
||||
|
||||
|
||||
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.addlink {
|
||||
width: 39px;
|
||||
height: 39px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Ionic Toolbar */
|
||||
.liquid-toolbar {
|
||||
--border-color: transparent;
|
||||
}
|
||||
|
||||
/* Ionic Content */
|
||||
.liquid-content {
|
||||
--background: transparent;
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
--padding-top: 0;
|
||||
--padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Original Container Styles */
|
||||
.liquid-container {
|
||||
padding: 15px 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
background: rgba(130, 130, 130, 0.1);
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.status-item:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.indicator.online,
|
||||
.indicator.connected,
|
||||
.indicator.relay {
|
||||
background: linear-gradient(45deg, #99ff99, #66ffcc);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.indicator.offline,
|
||||
.indicator.disconnected,
|
||||
.indicator.direct {
|
||||
background: linear-gradient(45deg, #ff6666, #ff9999);
|
||||
}
|
||||
|
||||
.add-peer {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add-peer input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
background: rgba(134, 134, 134, 0.25);
|
||||
font-size: 14px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.add-peer input:hover,
|
||||
.add-peer input:focus {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.peer-list {
|
||||
margin-top: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.peer-list h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.peer-scroll-container {
|
||||
border-radius: 10px;
|
||||
max-height: 390px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-track {
|
||||
background: rgba(125, 125, 125, 0.451);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(45deg, #66ccff, #99eeff);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.peer-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(45deg, #00b7ff, #66ddff);
|
||||
}
|
||||
|
||||
.peer-item {
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, rgba(127, 127, 127, 0.15), rgba(255, 255, 255, 0.05));
|
||||
border-radius: 15px;
|
||||
margin-bottom: 15px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.peer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(5px);
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: linear-gradient(45deg, #99ff99, #66ffcc);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background: linear-gradient(45deg, #ff6666, #ff9999);
|
||||
}
|
||||
|
||||
.status.checking {
|
||||
background: linear-gradient(45deg, #ffcc66, #ffdd99);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.peer-content {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.peer-url {
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
padding: 5px 10px;
|
||||
background: rgba(124, 124, 124, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.peer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
ion-toggle {
|
||||
--background: rgba(128, 128, 128, 0.2);
|
||||
--background-checked: #66ccff;
|
||||
--handle-background: #fff;
|
||||
--handle-background-checked: #fff;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
color: #ff6666;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.remove-icon:hover {
|
||||
transform: scale(1.1);
|
||||
color: #ff9999;
|
||||
}
|
||||
|
||||
|
||||
.help-modal {
|
||||
--border-radius: 16px;
|
||||
|
||||
}
|
||||
|
||||
.help-modal ion-toolbar {
|
||||
--border-width: 0;
|
||||
--background: transparent;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.help-content h2 {
|
||||
font-size: 1.25rem;
|
||||
margin: 20px 0 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.help-content h3 {
|
||||
font-size: 1.1rem;
|
||||
margin: 15px 0 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.help-content ion-list {
|
||||
background: transparent;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.help-content ion-item {
|
||||
--background: transparent;
|
||||
--padding-start: 0;
|
||||
--inner-padding-end: 0;
|
||||
}
|
||||
|
||||
.help-content ion-label {
|
||||
color: #666 !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
ion-button[color="dark"] {
|
||||
--background: #333;
|
||||
--border-radius: 12px;
|
||||
height: 44px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
ion-button[color="dark"]:hover {
|
||||
--background: #444;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
46
examples/relay-sqlite-example/src/router/index.ts
Normal file
46
examples/relay-sqlite-example/src/router/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { createRouter, createWebHistory } from '@ionic/vue-router'
|
||||
import { createAnimation } from '@ionic/vue'
|
||||
import routes from 'virtual:generated-pages'
|
||||
// import { useChatFlow } from '@/composables/TalkFlowCore'
|
||||
|
||||
// const { isLoggedIn } = useChatFlow()
|
||||
|
||||
const customRoutes = [
|
||||
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/pages/index.vue'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
...routes,
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: customRoutes,
|
||||
})
|
||||
|
||||
|
||||
// 导航守卫
|
||||
// router.beforeEach((to, from, next) => {
|
||||
// // 如果用户没有登录,且访问的不是 'i18n.vue' 页面,则重定向到 'i18n.vue'
|
||||
// if (!isLoggedIn.value && to.path !== '/') {
|
||||
// next('/'); // 重定向到 i18n 页面
|
||||
// } else {
|
||||
// next(); // 允许访问目标路由
|
||||
// }
|
||||
// });
|
||||
|
||||
// router.beforeEach(async (to, from, next) => {
|
||||
// const { isLoggedIn } = await import('@/composables/TalkFlowCore');
|
||||
// if (!isLoggedIn.value && to.path !== '/') {
|
||||
// next('/');
|
||||
// } else {
|
||||
// next();
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
export default router
|
@ -0,0 +1,19 @@
|
||||
export interface IDbVersionService {
|
||||
setDbVersion(dbName: string, version: number): void
|
||||
getDbVersion(dbName: string):number| undefined
|
||||
};
|
||||
class DbVersionService implements IDbVersionService {
|
||||
dbNameVersionDict: Map<string, number> = new Map();
|
||||
|
||||
setDbVersion(dbName: string, version: number) {
|
||||
this.dbNameVersionDict.set(dbName, version);
|
||||
console.log(`设置数据库 ${dbName} 版本为: ${version}`);
|
||||
}
|
||||
|
||||
getDbVersion(dbName: string): number | undefined {
|
||||
const version = this.dbNameVersionDict.get(dbName);
|
||||
console.log(`获取数据库 ${dbName} 版本: ${version}`);
|
||||
return version;
|
||||
}
|
||||
}
|
||||
export default DbVersionService;
|
@ -0,0 +1,9 @@
|
||||
import SQLiteService from './sqliteService';
|
||||
import DbVersionService from './dbVersionService';
|
||||
import StorageService from './storageService';
|
||||
|
||||
const sqliteServ = new SQLiteService();
|
||||
const dbVerServ = new DbVersionService();
|
||||
const storageServ = new StorageService(sqliteServ, dbVerServ);
|
||||
|
||||
export { sqliteServ, dbVerServ, storageServ };
|
@ -0,0 +1,45 @@
|
||||
import {ISQLiteService } from '../services/sqliteService';
|
||||
import {IStorageService } from '../services/storageService';
|
||||
|
||||
export interface IInitializeAppService {
|
||||
initializeApp(): Promise<boolean>
|
||||
};
|
||||
|
||||
class InitializeAppService implements IInitializeAppService {
|
||||
appInit = false;
|
||||
sqliteServ!: ISQLiteService;
|
||||
storageServ!: IStorageService;
|
||||
platform!: string;
|
||||
static platform: string;
|
||||
|
||||
constructor(sqliteService: ISQLiteService, storageService: IStorageService) {
|
||||
this.sqliteServ = sqliteService;
|
||||
this.storageServ = storageService;
|
||||
this.platform = this.sqliteServ.getPlatform();
|
||||
}
|
||||
async initializeApp(): Promise<boolean> {
|
||||
if (!this.appInit) {
|
||||
try {
|
||||
console.log('开始应用初始化');
|
||||
if (this.platform === 'web') {
|
||||
await this.sqliteServ.initWebStore();
|
||||
console.log('Web 存储初始化完成');
|
||||
}
|
||||
await this.storageServ.initializeDatabase();
|
||||
console.log('数据库初始化完成');
|
||||
// if (this.platform === 'web') {
|
||||
// await this.sqliteServ.saveToStore(this.storageServ.getDatabaseName());
|
||||
// console.log('数据库保存到 Web 存储完成');
|
||||
// }
|
||||
this.appInit = true;
|
||||
console.log('应用初始化成功');
|
||||
} catch (error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
console.error(`initializeAppError.initializeApp: ${msg}`, error);
|
||||
throw new Error(`initializeAppError.initializeApp: ${msg}`);
|
||||
}
|
||||
}
|
||||
return this.appInit;
|
||||
}
|
||||
}
|
||||
export default InitializeAppService;
|
114
examples/relay-sqlite-example/src/services/sqliteService.ts
Normal file
114
examples/relay-sqlite-example/src/services/sqliteService.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection, capSQLiteUpgradeOptions } from '@capacitor-community/sqlite';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
export interface ISQLiteService {
|
||||
getPlatform(): string
|
||||
initWebStore(): Promise<void>
|
||||
addUpgradeStatement(options: capSQLiteUpgradeOptions): Promise<void>
|
||||
openDatabase(dbName: string, loadToVersion: number, readOnly: boolean): Promise<SQLiteDBConnection>
|
||||
closeDatabase(dbName: string, readOnly: boolean): Promise<void>
|
||||
saveToStore(dbName: string): Promise<void>
|
||||
saveToLocalDisk(dbName: string): Promise<void>
|
||||
isConnection(dbName: string, readOnly: boolean): Promise<boolean>
|
||||
};
|
||||
|
||||
class SQLiteService implements ISQLiteService {
|
||||
platform = Capacitor.getPlatform();
|
||||
sqlitePlugin = CapacitorSQLite;
|
||||
sqliteConnection = new SQLiteConnection(CapacitorSQLite);
|
||||
dbNameVersionDict: Map<string, number> = new Map();
|
||||
|
||||
getPlatform(): string {
|
||||
return this.platform;
|
||||
}
|
||||
async initWebStore() : Promise<void> {
|
||||
try {
|
||||
await this.sqliteConnection.initWebStore();
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.initWebStore: ${msg}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
async addUpgradeStatement(options: capSQLiteUpgradeOptions): Promise<void> {
|
||||
try {
|
||||
await this.sqlitePlugin.addUpgradeStatement(options);
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.addUpgradeStatement: ${msg}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
async openDatabase(dbName:string, loadToVersion: number,
|
||||
readOnly: boolean): Promise<SQLiteDBConnection> {
|
||||
this.dbNameVersionDict.set(dbName, loadToVersion);
|
||||
let encrypted = false;
|
||||
const mode = encrypted ? "secret" : "no-encryption";
|
||||
try {
|
||||
let db: SQLiteDBConnection;
|
||||
const retCC = (await this.sqliteConnection.checkConnectionsConsistency()).result;
|
||||
let isConn = (await this.sqliteConnection.isConnection(dbName, readOnly)).result;
|
||||
if(retCC && isConn) {
|
||||
db = await this.sqliteConnection.retrieveConnection(dbName, readOnly);
|
||||
} else {
|
||||
db = await this.sqliteConnection
|
||||
.createConnection(dbName, encrypted, mode, loadToVersion, readOnly);
|
||||
}
|
||||
const jeepSQlEL = document.querySelector("jeep-sqlite")
|
||||
|
||||
await db.open();
|
||||
const res = await db.isDBOpen();
|
||||
return db;
|
||||
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.openDatabase: ${msg}`);
|
||||
}
|
||||
|
||||
}
|
||||
async isConnection(dbName:string, readOnly: boolean): Promise<boolean> {
|
||||
try {
|
||||
const isConn = (await this.sqliteConnection.isConnection(dbName, readOnly)).result;
|
||||
if (isConn != undefined) {
|
||||
return isConn
|
||||
} else {
|
||||
throw new Error(`sqliteService.isConnection undefined`);
|
||||
}
|
||||
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.isConnection: ${msg}`);
|
||||
}
|
||||
}
|
||||
async closeDatabase(dbName:string, readOnly: boolean):Promise<void> {
|
||||
try {
|
||||
const isConn = (await this.sqliteConnection.isConnection(dbName, readOnly)).result;
|
||||
if(isConn) {
|
||||
await this.sqliteConnection.closeConnection(dbName, readOnly);
|
||||
}
|
||||
return;
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.closeDatabase: ${msg}`);
|
||||
}
|
||||
}
|
||||
async saveToStore(dbName: string): Promise<void> {
|
||||
try {
|
||||
await this.sqliteConnection.saveToStore(dbName);
|
||||
return;
|
||||
} catch(error: any) {
|
||||
const msg = error.message ? error.message : error;
|
||||
throw new Error(`sqliteService.saveToStore: ${msg}`);
|
||||
}
|
||||
}
|
||||
async saveToLocalDisk(dbName: string): Promise<void> {
|
||||
try {
|
||||
await this.sqliteConnection.saveToLocalDisk(dbName);
|
||||
return;
|
||||
} catch(err:any) {
|
||||
const msg = err.message ? err.message : err;
|
||||
throw new Error(`sqliteService.saveToLocalDisk: ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default SQLiteService;
|
153
examples/relay-sqlite-example/src/services/storageService.ts
Normal file
153
examples/relay-sqlite-example/src/services/storageService.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { CapacitorSQLite, SQLiteDBConnection } from '@capacitor-community/sqlite';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { ISQLiteService } from './sqliteService';
|
||||
import { IDbVersionService } from './dbVersionService';
|
||||
|
||||
|
||||
export interface IStorageService {
|
||||
initializeDatabase(): Promise<void>;
|
||||
|
||||
|
||||
query(sql: string, params?: any[]): Promise<any>;
|
||||
run(sql: string, params?: any[]): Promise<any>;
|
||||
execute(sql: string): Promise<any>;
|
||||
|
||||
}
|
||||
|
||||
class StorageService implements IStorageService {
|
||||
versionUpgrades = [
|
||||
|
||||
{
|
||||
toVersion: 1,
|
||||
statements: [
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS network_peers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
url TEXT UNIQUE,
|
||||
is_enabled INTEGER DEFAULT 0,
|
||||
note TEXT DEFAULT ''
|
||||
);`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS gun_nodes (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
timestamp INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
);`,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
loadToVersion = 1;
|
||||
momentsVersion = 2;
|
||||
db!: SQLiteDBConnection;
|
||||
database: string = 'gundb';
|
||||
sqliteServ!: ISQLiteService;
|
||||
dbVerServ!: IDbVersionService;
|
||||
isInitCompleted = new BehaviorSubject(false);
|
||||
appInstance = getCurrentInstance();
|
||||
platform!: string;
|
||||
private isInitialized = false;
|
||||
|
||||
constructor(sqliteService: ISQLiteService, dbVersionService: IDbVersionService) {
|
||||
this.sqliteServ = sqliteService;
|
||||
this.dbVerServ = dbVersionService;
|
||||
this.platform = this.appInstance?.appContext.config.globalProperties.$platform || 'web';
|
||||
}
|
||||
|
||||
async initializeDatabase(): Promise<void> {
|
||||
if (this.isInitialized) {
|
||||
console.log('数据库已初始化,跳过重复调用');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始初始化数据库:', this.database);
|
||||
await this.sqliteServ.addUpgradeStatement({
|
||||
database: this.database,
|
||||
upgrade: this.versionUpgrades,
|
||||
});
|
||||
console.log('核心升级语句已添加');
|
||||
|
||||
this.db = await this.sqliteServ.openDatabase(this.database, this.loadToVersion, false);
|
||||
console.log('数据库已打开,目标版本:', this.loadToVersion);
|
||||
|
||||
const currentVersionResult = await this.db.getVersion();
|
||||
const currentVersion: number = currentVersionResult.version ?? 0;
|
||||
console.log('当前数据库版本:', currentVersion);
|
||||
|
||||
for (const upgrade of this.versionUpgrades) {
|
||||
console.log(`执行核心升级到版本 ${upgrade.toVersion}`);
|
||||
for (const stmt of upgrade.statements) {
|
||||
try {
|
||||
await this.db.execute(stmt);
|
||||
console.log('执行语句成功:', stmt);
|
||||
} catch (err) {
|
||||
console.error('执行语句失败:', stmt, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dbVerServ.setDbVersion(this.database, this.loadToVersion);
|
||||
console.log('核心数据库版本已设置为:', this.loadToVersion);
|
||||
|
||||
|
||||
|
||||
if (this.platform === 'web') {
|
||||
try {
|
||||
await this.sqliteServ.saveToStore(this.database);
|
||||
console.log('数据库已保存到 Web 存储');
|
||||
} catch (err) {
|
||||
console.warn('Web 存储保存失败(非致命错误):', err);
|
||||
}
|
||||
}
|
||||
|
||||
const tablesAfter = await this.db.query("SELECT name FROM sqlite_master WHERE type='table'");
|
||||
console.log('初始化后的表:', tablesAfter.values);
|
||||
this.isInitCompleted.next(true);
|
||||
this.isInitialized = true;
|
||||
console.log('SQLite 数据库初始化成功');
|
||||
} catch (error: any) {
|
||||
console.error(`storageService.initializeDatabase: ${error.message || error}`, error);
|
||||
throw new Error(`storageService.initializeDatabase: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
async query(sql: string, params: any[] = []): Promise<any> {
|
||||
try {
|
||||
return await this.db.query(sql, params);
|
||||
} catch (err) {
|
||||
console.error(`执行查询 ${sql} 失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async run(sql: string, params: any[] = []): Promise<any> {
|
||||
try {
|
||||
return await this.db.run(sql, params);
|
||||
} catch (err) {
|
||||
console.error(`执行语句 ${sql} 失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(sql: string): Promise<any> {
|
||||
try {
|
||||
return await this.db.execute(sql);
|
||||
} catch (err) {
|
||||
console.error(`执行语句 ${sql} 失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StorageService;
|
||||
|
||||
|
||||
|
237
examples/relay-sqlite-example/src/theme/variables.css
Normal file
237
examples/relay-sqlite-example/src/theme/variables.css
Normal file
@ -0,0 +1,237 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
--ion-background-color: #ffffff;
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #e1dfdf;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #e3e3e3;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ededed;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning #222428 **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #222428;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark #000000 **/
|
||||
--ion-color-dark: #000000;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #e1e1e1;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #e6e6e6;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #e8e8e8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/*
|
||||
* Dark Colors
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
body {
|
||||
--ion-color-primary: #428cff;
|
||||
--ion-color-primary-rgb: 66,140,255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255,255,255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80,200,255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255,255,255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106,100,255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255,255,255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47,223,117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0,0,0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
|
||||
--ion-color-warning: #ffd534;
|
||||
--ion-color-warning-rgb: 255,213,52;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0,0,0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd948;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255,73,97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255,255,255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
--ion-color-dark: #dfdfdf;
|
||||
--ion-color-dark-rgb: 244,245,248;
|
||||
--ion-color-dark-contrast: #141414;
|
||||
--ion-color-dark-contrast-rgb: 0,0,0;
|
||||
--ion-color-dark-shade: #b5b5b5;
|
||||
--ion-color-dark-tint: #dedfe0;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Dark Theme #111111
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.ios body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 0,0,0;
|
||||
|
||||
--ion-text-color: #bfcbc7;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-color-step-50: #0d0d0d;
|
||||
--ion-color-step-100: #1a1a1a;
|
||||
--ion-color-step-150: #262626;
|
||||
--ion-color-step-200: #333333;
|
||||
--ion-color-step-250: #404040;
|
||||
--ion-color-step-300: #4d4d4d;
|
||||
--ion-color-step-350: #595959;
|
||||
--ion-color-step-400: #666666;
|
||||
--ion-color-step-450: #737373;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #8c8c8c;
|
||||
--ion-color-step-600: #999999;
|
||||
--ion-color-step-650: #a6a6a6;
|
||||
--ion-color-step-700: #b3b3b3;
|
||||
--ion-color-step-750: #bfbfbf;
|
||||
--ion-color-step-800: #cccccc;
|
||||
--ion-color-step-850: #d9d9d9;
|
||||
--ion-color-step-900: #e6e6e6;
|
||||
--ion-color-step-950: #f2f2f2;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
}
|
||||
|
||||
.ios ion-modal {
|
||||
--ion-background-color: var(--ion-color-step-50);
|
||||
--ion-toolbar-background: var(--ion-color-step-100);
|
||||
--ion-toolbar-border-color: var(--ion-color-step-150);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Material Design Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.md body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 18,18,18;
|
||||
|
||||
--ion-text-color: #dfdede;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-color-step-50: #1e1e1e;
|
||||
--ion-color-step-100: #2a2a2a;
|
||||
--ion-color-step-150: #363636;
|
||||
--ion-color-step-200: #414141;
|
||||
--ion-color-step-250: #4d4d4d;
|
||||
--ion-color-step-300: #595959;
|
||||
--ion-color-step-350: #656565;
|
||||
--ion-color-step-400: #717171;
|
||||
--ion-color-step-450: #7d7d7d;
|
||||
--ion-color-step-500: #898989;
|
||||
--ion-color-step-550: #949494;
|
||||
--ion-color-step-600: #a0a0a0;
|
||||
--ion-color-step-650: #acacac;
|
||||
--ion-color-step-700: #b8b8b8;
|
||||
--ion-color-step-750: #c4c4c4;
|
||||
--ion-color-step-800: #d0d0d0;
|
||||
--ion-color-step-850: #dbdbdb;
|
||||
--ion-color-step-900: #e7e7e7;
|
||||
--ion-color-step-950: #f3f3f3;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-toolbar-background: #000000;
|
||||
|
||||
--ion-tab-bar-background: #000000;
|
||||
|
||||
--ion-card-background: #1e1e1e;
|
||||
}
|
||||
}
|
23
examples/relay-sqlite-example/tsconfig.json
Normal file
23
examples/relay-sqlite-example/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"bare-plugin": ["./bare-plugin/dist/plugin"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
9
examples/relay-sqlite-example/tsconfig.node.json
Normal file
9
examples/relay-sqlite-example/tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
73
examples/relay-sqlite-example/vite.config.ts
Normal file
73
examples/relay-sqlite-example/vite.config.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Pages from 'vite-plugin-pages'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
// import inject from "@rollup/plugin-inject";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy(),
|
||||
Components(),
|
||||
// inject({
|
||||
// Buffer: ["buffer", "Buffer"],
|
||||
// }),
|
||||
AutoImport({
|
||||
|
||||
imports: ['vue', 'vue-router'],
|
||||
|
||||
|
||||
dirs: [
|
||||
'src/composables',
|
||||
|
||||
],
|
||||
|
||||
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
|
||||
|
||||
}),
|
||||
Pages(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
maximumFileSizeToCacheInBytes: 5000 * 1024 * 1024,
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
crypto: 'crypto-browserify',
|
||||
buffer: "buffer",
|
||||
|
||||
},
|
||||
},
|
||||
define: {
|
||||
global: "window",
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: ['text-encoding'],
|
||||
output:{
|
||||
|
||||
manualChunks(id) {
|
||||
|
||||
if (id.includes('node_modules')) {
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// test: {
|
||||
// globals: true,
|
||||
// environment: 'jsdom'
|
||||
// }
|
||||
})
|
19
examples/relay.service
Normal file
19
examples/relay.service
Normal file
@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=GUN relay
|
||||
Documentation=https://gun.eco
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/usr/bin:/usr/local/bin
|
||||
LimitNOFILE=infinity
|
||||
LimitNPROC=infinity
|
||||
LimitCORE=infinity
|
||||
StartLimitBurst=999999
|
||||
StartLimitIntervalSec=999999
|
||||
Restart=always
|
||||
ExecStart=node examples/http.js 80
|
||||
# Environment=NODE_ENV=production
|
||||
WorkingDirectory=
|
1104
examples/smoothie.js
Normal file
1104
examples/smoothie.js
Normal file
File diff suppressed because it is too large
Load Diff
22
examples/start.js.html
Normal file
22
examples/start.js.html
Normal file
@ -0,0 +1,22 @@
|
||||
/*<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body></body>
|
||||
<script>// */
|
||||
;(function(){try{
|
||||
if(typeof window == "undefined"){ return }
|
||||
var url = location.hash.slice(1);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function(e){
|
||||
if(4 != xhr.readyState){ return }
|
||||
var d = document, doc = d.createElement("html");
|
||||
doc.innerHTML = xhr.responseText;
|
||||
var head = doc.getElementsByTagName('head')[0] || doc.appendChild(d.createElement('head'));
|
||||
var base = doc.getElementsByTagName('base')[0] || head.appendChild(d.createElement('base'));
|
||||
base.href = url;
|
||||
document.write(doc.outerHTML);
|
||||
};
|
||||
xhr.open('GET', url, true);
|
||||
xhr.send(null);
|
||||
}catch(e){document.write(''+e)}}());
|
||||
//</script></html>
|
153
examples/stats.html
Normal file
153
examples/stats.html
Normal file
@ -0,0 +1,153 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
font-family: helvetica;
|
||||
background-color: rgb(25,25,25);
|
||||
color: rgb(80,135,25) !important;
|
||||
text-shadow: 1px 1px 20px rgb(80,150,25);
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 0.5em;
|
||||
top: 1.75em;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 30px;
|
||||
padding:10px;
|
||||
background-color: rgb(50,50,50);
|
||||
color: rgb(250,50,50);
|
||||
}
|
||||
|
||||
.tall { height: 5em; }
|
||||
</style>
|
||||
|
||||
<div class="center"><span class="shout" id="peers">0</span> peers <span class="shout" id="time">0</span> min <span class="shout" id="nodes">0</span> nodes <span class="shout" id="hours">0</span> hours <span class="shout" id="block">0</span> block <span class="shout" id="stack">0</span> stack</div>
|
||||
|
||||
<input id="url" class="center input crack" placeholder="enter peer stats source url">
|
||||
|
||||
<div class="center row charts">
|
||||
</div>
|
||||
|
||||
<div class="model none">
|
||||
<div class="chart"><span class="label"></span><canvas class="tall row"></canvas></div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="./jquery.js"></script>
|
||||
<script src="./smoothie.js" charset="utf-8"></script>
|
||||
<script>
|
||||
if(window.location.search){url.value = window.location.search.split("?")[1]}
|
||||
var up, br = 0, bt = 0, tmp;
|
||||
var fetchData = async function(){
|
||||
// fetch the data from server
|
||||
var S = +new Date;
|
||||
var data = await (await fetch(url.value||(location.origin+'/gun/stats.radata'), {method: 'GET',mode: 'cors'})).json();
|
||||
$('#block').text(((br += (+new Date - S)/1000) / ++bt).toFixed(1));
|
||||
data.over = (data.over/1000) || 15;
|
||||
$('#stack').text((data.cpu||'').stack);
|
||||
$('#peers').text(data.peers.count);
|
||||
$('#time').text((data.peers.time / 1000 / 60).toFixed(0));
|
||||
$('#nodes').text(data.node.count);
|
||||
$('#hours').text((data.up.time / 60 / 60).toFixed(1));
|
||||
if(data.up.time === up){ console.log("up same as before") } up = data.up.time;
|
||||
|
||||
;(async function(){ try{
|
||||
Stats('peers#').line.append(+new Date, data.peers.count);
|
||||
return;
|
||||
Stats('cpu%');
|
||||
tmp = await (await fetch(new Request(location.origin+'/gun/stats.top.radata'), {method: 'GET',mode: 'cors'})).text();
|
||||
tmp = parseFloat((tmp.split('\n').filter(l => l.indexOf('node')+1)[0]||'').split(/\s+/).slice(-4)[0]||'0');
|
||||
Stats('cpu%').line.append(+new Date, tmp);
|
||||
}catch(e){console.log(e)}}());
|
||||
|
||||
Stats('memory').line.append(+new Date, data.memory.heapTotal / 1024 / 1024);
|
||||
try{ Stats('dam # in/s').line.append(+new Date, Math.round(data.dam.in.count / data.over)); }catch(e){}
|
||||
try{ Stats('dam in MB/s').line.append(+new Date, data.dam.in.done / 1024 / 1024 / data.over); }catch(e){}
|
||||
try{ Stats('dam # out/s').line.append(+new Date, Math.round(data.dam.out.count / data.over)); }catch(e){}
|
||||
try{ Stats('dam out MB/s').line.append(+new Date, data.dam.out.done / 1024 / 1024 / data.over); }catch(e){}
|
||||
|
||||
console.log('data',data);
|
||||
|
||||
//fetch keys in all, these may be dynamically changing
|
||||
//for each key, check if we already have created a time series, if not, create it and add it
|
||||
// to the chart corredsponding to the unit of measure
|
||||
$.each(data.all, function(key, arr){
|
||||
var chart = Stats(key);
|
||||
// get data and append to line
|
||||
// get the arrays inside the key
|
||||
//for each array append the data to the line
|
||||
for(var i in arr) {
|
||||
// append data [timestamp], [data]
|
||||
chart.line.append(arr[i][0], arr[i][1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
//setInterval(fetchData, 15 * 1000);
|
||||
setInterval(fetchData, 5000);
|
||||
fetchData();
|
||||
|
||||
function Stats(key, chart){
|
||||
// if we have already created, get data to append to it.
|
||||
if(chart = Stats[key]){
|
||||
return chart;
|
||||
}
|
||||
// create a new Series for this key
|
||||
// add it into the map
|
||||
chart = Stats[key] = new SmoothieChart({millisPerPixel:500, limitFPS: 16, responsive: true, minValue: 0, grid:{strokeStyle:'rgba(100%,100%,100%,0.2)'},labels:{fontSize:20}, grid: {verticalSections: 0, millisPerLine: 15000 * 4 /*, strokeStyle:'rgb(125, 0, 0)'*/}});
|
||||
chart.line = new TimeSeries();
|
||||
chart.addTimeSeries(chart.line,{ strokeStyle:'rgb('+Math.random()*255+', '+Math.random()*255+','+Math.random()*255+')', lineWidth:5 });
|
||||
chart.canvas = $('.model').find('.chart').clone(true).appendTo('.charts');
|
||||
chart.canvas.find('span').text(key);
|
||||
chart.streamTo(chart.canvas.find('canvas').get(0), 15 * 1000);
|
||||
chart.line.append(0, 0);
|
||||
// check first two characters of key to determine other charts to add this in
|
||||
// tbd later
|
||||
return chart;
|
||||
}
|
||||
;(function(){
|
||||
if('https' != (''+location).slice(0,5) && "localhost" != location.hostname){
|
||||
$('body').append("<button id='https'>click here to try generating https certs</button>");
|
||||
if(/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(location.hostname)){
|
||||
$('#https').text("Link this IP address to a Domain by adding an `A Record` to your DNS settings that point to `"+ location.hostname +"` (we recommend the `name/host` be any subdomain you want, like `relay`, but if you want the root domain itself to directly point here use `*`). Then come back here on the domain & click this button to generate HTTPS certificates.");
|
||||
return;
|
||||
}
|
||||
$('body').append("<input id='email' placeholder='email'/>");
|
||||
$('#https').on('click', function(){
|
||||
$(this).text("look at console.log for errors, if none, try https");
|
||||
var gun = GUN(location.origin + '/gun');
|
||||
if(!$('#email').val()){
|
||||
$(this).text("email necessary for certs! Type it in & click here again.");
|
||||
return;
|
||||
}
|
||||
setTimeout(function(){
|
||||
gun._.opt.mesh.say({dam: 'service', try: 'https', email: $('#email').val(), domain: location.hostname});
|
||||
setTimeout(function(){
|
||||
if(gun._.opt.mesh.near){ return }
|
||||
$('#https').text("It might have worked! try HTTPS!");
|
||||
}, 9000);
|
||||
}, 999);
|
||||
});
|
||||
}
|
||||
}());
|
||||
/*
|
||||
Notes to Self about Debugging:
|
||||
1. Read Disks can spike up to 1min, I suspect other operations are blocking it from resolving as fast as it otherwise would.
|
||||
2. JSON parsing/stringifying sometimes way slower than other times, why?
|
||||
3. Looks like RAD lex read is not optimized.
|
||||
4. got prep + got emit = non-RAD problems, compare against read disk & got differentials (should be same).
|
||||
5. Radix map/place ops could be slow?
|
||||
6. SINGLE MESSAGE PROCESS TIME occasionally is huge, should get emailed.
|
||||
7. Watch out for get/put loops times, maybe indicating (5) issues?
|
||||
*/
|
||||
</script>
|
||||
<script src="../gun.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -4,6 +4,7 @@ html, body {
|
||||
position: relative;
|
||||
line-height: 1.5;
|
||||
font-size: 18pt;
|
||||
f-ont-size: max(18pt, 2?vw);
|
||||
}
|
||||
|
||||
div, ul, ol, li, p, span, form, button, input, textarea, img {
|
||||
@ -14,6 +15,7 @@ div, ul, ol, li, p, span, form, button, input, textarea, img {
|
||||
-webkit-transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a, button, input, textarea {
|
||||
@ -21,19 +23,34 @@ a, button, input, textarea {
|
||||
border: inherit;
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
outline: none;
|
||||
}
|
||||
input:not([type=button]):not([type=submit]), textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
a:focus, button:focus, input[type=button]:focus, input[type=submit]:focus {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
ul, li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
}
|
||||
p + p {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
[contenteditable=true]:empty:before {
|
||||
content: attr(placeholder);
|
||||
}
|
||||
::placeholder, .hint {
|
||||
color: inherit;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.model, .none { display: none }
|
||||
.hide {
|
||||
@ -42,11 +59,10 @@ ul, li {
|
||||
transition: all 2s;
|
||||
}
|
||||
|
||||
.full, .page {
|
||||
.full {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.max {
|
||||
max-width: 48em;
|
||||
}
|
||||
@ -60,6 +76,25 @@ ul, li {
|
||||
min-width: 12em;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
}
|
||||
.row::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
.col {
|
||||
max-width: 24em;
|
||||
min-width: 12em;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
text-align: right;
|
||||
@ -68,12 +103,6 @@ ul, li {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.mid {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@ -85,34 +114,27 @@ ul, li {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.rim { margin: 2%; }
|
||||
.gap { padding: 3%; }
|
||||
.rim { margin: 1%; }
|
||||
.gap {
|
||||
padding: 3%;
|
||||
padding: clamp(0.5em, 3%, 1.5em);
|
||||
}
|
||||
.stack { line-height: 0; }
|
||||
.crack { margin-bottom: 1%; }
|
||||
.sit { margin-bottom: 0; }
|
||||
|
||||
.row { width: 100%; }
|
||||
.col {
|
||||
max-width: 24em;
|
||||
min-width: 12em;
|
||||
}
|
||||
|
||||
.focus {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
float: none;
|
||||
clear: both;
|
||||
}
|
||||
.unit, .symbol {
|
||||
display: inline-block;
|
||||
vertical-align: inherit;
|
||||
}
|
||||
|
||||
.leak { overflow: visible; }
|
||||
.hold { overflow: hidden; }
|
||||
|
||||
.act {
|
||||
display: block;
|
||||
/*display: block;*/
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
-webkit-transition: all 0.3s;
|
||||
@ -120,6 +142,10 @@ ul, li {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.unit, .symbol {
|
||||
display: inline-block;
|
||||
vertical-align: inherit;
|
||||
}
|
||||
.sap { border-radius: 0.1em; }
|
||||
.jot { border-bottom: 1px dashed #95B2CA; }
|
||||
|
||||
@ -131,11 +157,10 @@ ul, li {
|
||||
font-size: 6.5vmax;
|
||||
}
|
||||
|
||||
|
||||
.red { background: #ea3224; }
|
||||
.green { background: #33cc33; }
|
||||
.blue { background: #4D79D8; }
|
||||
.yellow { background: #f2b919; }
|
||||
.yellow { background: #d3a438; }
|
||||
.black { background: black; }
|
||||
.white { background: white; }
|
||||
|
||||
@ -145,7 +170,7 @@ ul, li {
|
||||
.redt { color: #ea3224; }
|
||||
.greent { color: #33cc33; }
|
||||
.bluet { color: #4D79D8; }
|
||||
.yellowt { color: #f2b919; }
|
||||
.yellowt { color: #d3a438; }
|
||||
.blackt { color: black; }
|
||||
.whitet { color: white; }
|
||||
|
||||
@ -156,13 +181,13 @@ ul, li {
|
||||
} @keyframes hue {
|
||||
0% {background-color: #4D79D8;}
|
||||
25% {background-color: #33cc33;}
|
||||
50% {background-color: #f2b919;}
|
||||
50% {background-color: #d3a438;}
|
||||
75% {background-color: #ea3224;}
|
||||
100% {background-color: #4D79D8;}
|
||||
} @-webkit-keyframes hue {
|
||||
0% {background-color: #4D79D8;}
|
||||
25% {background-color: #33cc33;}
|
||||
50% {background-color: #f2b919;}
|
||||
50% {background-color: #d3a438;}
|
||||
75% {background-color: #ea3224;}
|
||||
100% {background-color: #4D79D8;}
|
||||
}
|
||||
@ -174,13 +199,13 @@ ul, li {
|
||||
} @keyframes huet {
|
||||
0% {color: #4D79D8;}
|
||||
25% {color: #33cc33;}
|
||||
50% {color: #f2b919;}
|
||||
50% {color: #d3a438;}
|
||||
75% {color: #ea3224;}
|
||||
100% {color: #4D79D8;}
|
||||
} @-webkit-keyframes huet {
|
||||
0% {color: #4D79D8;}
|
||||
25% {color: #33cc33;}
|
||||
50% {color: #f2b919;}
|
||||
50% {color: #d3a438;}
|
||||
75% {color: #ea3224;}
|
||||
100% {color: #4D79D8;}
|
||||
}
|
||||
@ -193,13 +218,13 @@ ul, li {
|
||||
0% {background-color: #ea3224;}
|
||||
25% {background-color: #4D79D8;}
|
||||
50% {background-color: #33cc33;}
|
||||
75% {background-color: #f2b919;}
|
||||
75% {background-color: #d3a438;}
|
||||
100% {background-color: #ea3224;}
|
||||
} @-webkit-keyframes hue2 {
|
||||
0% {background-color: #ea3224;}
|
||||
25% {background-color: #4D79D8;}
|
||||
50% {background-color: #33cc33;}
|
||||
75% {background-color: #f2b919;}
|
||||
75% {background-color: #d3a438;}
|
||||
100% {background-color: #ea3224;}
|
||||
}
|
||||
|
||||
@ -211,13 +236,13 @@ ul, li {
|
||||
0% {color: #ea3224;}
|
||||
25% {color: #4D79D8;}
|
||||
50% {color: #33cc33;}
|
||||
75% {color: #f2b919;}
|
||||
75% {color: #d3a438;}
|
||||
100% {color: #ea3224;}
|
||||
} @-webkit-keyframes huet2 {
|
||||
0% {color: #ea3224;}
|
||||
25% {color: #4D79D8;}
|
||||
50% {color: #33cc33;}
|
||||
75% {color: #f2b919;}
|
||||
75% {color: #d3a438;}
|
||||
100% {color: #ea3224;}
|
||||
}
|
||||
|
||||
@ -227,13 +252,13 @@ ul, li {
|
||||
animation: hue3 900s infinite;
|
||||
} @keyframes hue3 {
|
||||
0% {background-color: #33cc33;}
|
||||
25% {background-color: #f2b919;}
|
||||
25% {background-color: #d3a438;}
|
||||
50% {background-color: #ea3224;}
|
||||
75% {background-color: #4D79D8;}
|
||||
100% {background-color: #33cc33;}
|
||||
} @-webkit-keyframes hue3 {
|
||||
0% {background-color: #33cc33;}
|
||||
25% {background-color: #f2b919;}
|
||||
25% {background-color: #d3a438;}
|
||||
50% {background-color: #ea3224;}
|
||||
75% {background-color: #4D79D8;}
|
||||
100% {background-color: #33cc33;}
|
||||
@ -245,52 +270,52 @@ ul, li {
|
||||
animation: huet3 900s infinite;
|
||||
} @keyframes huet3 {
|
||||
0% {color: #33cc33;}
|
||||
25% {color: #f2b919;}
|
||||
25% {color: #d3a438;}
|
||||
50% {color: #ea3224;}
|
||||
75% {color: #4D79D8;}
|
||||
100% {color: #33cc33;}
|
||||
} @-webkit-keyframes huet3 {
|
||||
0% {color: #33cc33;}
|
||||
25% {color: #f2b919;}
|
||||
25% {color: #d3a438;}
|
||||
50% {color: #ea3224;}
|
||||
75% {color: #4D79D8;}
|
||||
100% {color: #33cc33;}
|
||||
}
|
||||
|
||||
.hue4 {
|
||||
background: #f2b919;
|
||||
background: #d3a438;
|
||||
-webkit-animation: hue4 900s infinite;
|
||||
animation: hue4 900s infinite;
|
||||
} @keyframes hue4 {
|
||||
0% {background-color: #f2b919;}
|
||||
0% {background-color: #d3a438;}
|
||||
25% {background-color: #ea3224;}
|
||||
50% {background-color: #4D79D8;}
|
||||
75% {background-color: #33cc33;}
|
||||
100% {background-color: #f2b919;}
|
||||
100% {background-color: #d3a438;}
|
||||
} @-webkit-keyframes hue4 {
|
||||
0% {background-color: #f2b919;}
|
||||
0% {background-color: #d3a438;}
|
||||
25% {background-color: #ea3224;}
|
||||
50% {background-color: #4D79D8;}
|
||||
75% {background-color: #33cc33;}
|
||||
100% {background-color: #f2b919;}
|
||||
100% {background-color: #d3a438;}
|
||||
}
|
||||
|
||||
.huet4 {
|
||||
color: #f2b919;
|
||||
color: #d3a438;
|
||||
-webkit-animation: huet4 900s infinite;
|
||||
animation: huet4 900s infinite;
|
||||
} @keyframes huet4 {
|
||||
0% {color: #f2b919;}
|
||||
0% {color: #d3a438;}
|
||||
25% {color: #ea3224;}
|
||||
50% {color: #4D79D8;}
|
||||
75% {color: #33cc33;}
|
||||
100% {color: #f2b919;}
|
||||
100% {color: #d3a438;}
|
||||
} @-webkit-keyframes huet4 {
|
||||
0% {color: #f2b919;}
|
||||
0% {color: #d3a438;}
|
||||
25% {color: #ea3224;}
|
||||
50% {color: #4D79D8;}
|
||||
75% {color: #33cc33;}
|
||||
100% {color: #f2b919;}
|
||||
100% {color: #d3a438;}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
@ -313,4 +338,15 @@ ul, li {
|
||||
} @keyframes joy {
|
||||
0% {background-position: 0 0;}
|
||||
100% {background-position: -2800px 0;}
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
border: 0;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
@ -1,83 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Think</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="think" class="hue page">
|
||||
<link href='https://fonts.googleapis.com/css?family=Alegreya+Sans:300italic' rel='stylesheet' type='text/css'>
|
||||
<style>
|
||||
#think {
|
||||
font-size: 24pt;
|
||||
font-size: 6vmin;
|
||||
font-family: 'Alegreya Sans', sans-serif;
|
||||
color: white;
|
||||
}
|
||||
#think li {
|
||||
width: 90%;
|
||||
margin-top: 0.3em;
|
||||
border-bottom: 1px dashed white;
|
||||
}
|
||||
#think .add {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1em;
|
||||
padding: 0.5em;
|
||||
font-family: Tahoma, arial;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
#think ul, #think li {
|
||||
list-style-type: circle;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
||||
<div class="pad whitet" style="width: 75%;">
|
||||
<div style="margin-top: 2%;">
|
||||
<div class="rubric left center">Add a Thought...</div>
|
||||
<a href="#" class="right huet white act add">+</a>
|
||||
</div>
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script src="/jquery.js"></script>
|
||||
<script src="/gun.js"></script>
|
||||
<script src="/gun/nts.js"></script>
|
||||
<script>
|
||||
// Check out the interactive tutorial
|
||||
// for how to build a simplified version
|
||||
// of this example: https://scrimba.com/c/cW2Vsa
|
||||
var gun = Gun(location.origin+'/gun');
|
||||
var think = gun.get('think/' + location.hash.slice(1));
|
||||
var typing, throttle;
|
||||
$('.add').on('click', function(){
|
||||
$('<li>').attr('contenteditable', true).prependTo('ul');
|
||||
});
|
||||
$(document).on('keyup', "[contenteditable=true]", function(){
|
||||
var li = $(this), id = li.attr('id');
|
||||
if(!id){
|
||||
li.attr('id', id = Gun.text.random());
|
||||
}
|
||||
typing = id;
|
||||
clearTimeout(throttle);
|
||||
throttle = setTimeout(function(){
|
||||
think.get(id).put(li.text());
|
||||
typing = false;
|
||||
},10);
|
||||
});
|
||||
think.map().on(function(thought, id){
|
||||
var li = $('#'+id)[0] || $('<li>').attr('id', id).attr('contenteditable', true).prependTo('ul');
|
||||
if(thought){
|
||||
if(id === typing){ return }
|
||||
$(li).text(thought);
|
||||
} else {
|
||||
$(li).hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<title>Think</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<link href='https://fonts.googleapis.com/css?family=Alegreya+Sans:300italic' rel='stylesheet' type='text/css'>
|
||||
<style>
|
||||
.thought {
|
||||
font-family: 'Alegreya Sans', sans-serif;
|
||||
}
|
||||
|
||||
.thought__heading {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.thought__form-container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.thought__item {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.thought__input {
|
||||
flex: 1;
|
||||
font-family: 'Alegreya Sans', sans-serif;
|
||||
font-size: 25px;
|
||||
font-weight: 500;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.thought__add {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-family: Tahoma, arial;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
font-size: 25px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.thought__add:hover::after {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.thought__add:focus::after {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.thought__add::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.thought__list {
|
||||
list-style-type: circle;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 90px 20px;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (max-width: 567px) {
|
||||
.thought__heading {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="thought hue page">
|
||||
<div class="thought__form-container hue">
|
||||
<h2 id='title' class="thought__heading hue">Add a thought...</h2>
|
||||
<button class="thought__add say huet">
|
||||
<span aria-hidden="true">+</span>
|
||||
<span class="visually-hidden">Add thought</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="thought__list">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script src="/jquery.js"></script>
|
||||
<script src="/gun.js"></script>
|
||||
<script>
|
||||
// Check out the interactive tutorial
|
||||
// for how to build a simplified version
|
||||
// of this example: https://scrimba.com/c/cW2Vsa
|
||||
var gun = Gun(location.origin + '/gun');
|
||||
var think = gun.get('think1/' + location.hash.slice(1));
|
||||
var thoughtItemStr = function(id) { return '<li class="thought__item"><label class="visually-hidden" for="' + id + '">Thought</label><input id="' + id + '" class="thought__input huet"><li/>'}
|
||||
var typing, throttle;
|
||||
$('.thought__add').on('click', function () {
|
||||
$(thoughtItemStr('')).prependTo('.thought__list').find('.thought__input').focus();
|
||||
});
|
||||
$(document).on('keyup', '.thought__input', function () {
|
||||
var input = $(this), id = input.attr('id');
|
||||
if (!id) {
|
||||
input.attr('id', id = String.random());
|
||||
}
|
||||
typing = id;
|
||||
clearTimeout(throttle);
|
||||
throttle = setTimeout(function () {
|
||||
think.get(id).put(input.val());
|
||||
typing = false;
|
||||
}, 10);
|
||||
});
|
||||
think.map().on(function (thought, id) {
|
||||
var li = $('#' + id).parent()[0] || $(thoughtItemStr(id)).prependTo('.thought__list');
|
||||
if (thought) {
|
||||
if (id === typing) { return }
|
||||
$(li).find('.thought__input').val(thought);
|
||||
} else {
|
||||
$(li).hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
77
examples/vanilla/screen.html
Normal file
77
examples/vanilla/screen.html
Normal file
@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Think</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<video id="video" width="100%"></video>
|
||||
<center>
|
||||
<button id="record">Record</button>
|
||||
<button id="play">Play</button>
|
||||
</center>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
|
||||
<script>
|
||||
const gun = Gun(`${window.location.origin}/gun`)
|
||||
const record = { recorder: null, recording: false }
|
||||
|
||||
const video = document.querySelector('#video')
|
||||
const playButton = document.querySelector('#play')
|
||||
const recordButton = document.querySelector('#record')
|
||||
|
||||
recordButton.addEventListener('click', () => {
|
||||
console.log(record)
|
||||
if (!record.ing) {
|
||||
return record.stream()
|
||||
}
|
||||
recordButton.innerText = 'Record'
|
||||
if (record.ing.stop) { record.ing.stop() }
|
||||
record.ing = false
|
||||
}, false)
|
||||
|
||||
record.stream = () => {
|
||||
navigator.mediaDevices.getDisplayMedia({ video: true }).then(stream => {
|
||||
const chunks = [] // we have a stream, we can record it
|
||||
record.ing = new MediaRecorder(stream)
|
||||
record.ing.ondataavailable = eve => chunks.push(eve.data)
|
||||
record.ing.onstop = () => record.save(new Blob(chunks))
|
||||
record.ing.start()
|
||||
recordButton.innerText = 'End'
|
||||
}, err => { console.log(err) })
|
||||
}
|
||||
|
||||
record.save = data => {
|
||||
record.file = record.file || new FileReader()
|
||||
record.file.readAsDataURL(data)
|
||||
record.file.onloadend = () => {
|
||||
let b64 = record.file.result
|
||||
b64 = `data:video/webm${b64.slice(b64.indexOf(';'))}`
|
||||
gun.get('test').get('screen').put(b64)
|
||||
}
|
||||
}
|
||||
|
||||
playButton.addEventListener('click', () => {
|
||||
if (record.playing) {
|
||||
playButton.innerText = 'Play'
|
||||
video.pause()
|
||||
record.playing = false
|
||||
return
|
||||
}
|
||||
|
||||
playButton.innerText = 'Stop'
|
||||
record.playing = true
|
||||
|
||||
gun.get('test').get('screen').once(data => {
|
||||
if (!data) { return }
|
||||
video.src = data
|
||||
video.play()
|
||||
})
|
||||
}, false)
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,53 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Think</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Thoughts</h1>
|
||||
<h1>Thoughts</h1>
|
||||
|
||||
<form id="form">
|
||||
<input id="input">
|
||||
<button>Add</button>
|
||||
</form>
|
||||
<form id="form">
|
||||
<input id="input">
|
||||
<button>Add</button>
|
||||
</form>
|
||||
|
||||
<ul id="parentList"></ul>
|
||||
<!-- <script src="https://code.jquery.com/jquery-1.11.3.min.js"></script> -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script>
|
||||
var gun = Gun().get('thoughts');
|
||||
document.getElementById('form').addEventListener('submit', function (e) {
|
||||
e.preventDefault(); // attaches event listener and prevent default form action
|
||||
var data = document.getElementById('input').value;
|
||||
gun.set(data);
|
||||
document.getElementById('input').value = "";
|
||||
});
|
||||
gun.map().on(function (thought, id) {
|
||||
var li = document.getElementById(id) || document.getElementById('parentList').insertAdjacentHTML('beforeend', '<li id =' + id + '> ' + thought + '</li>');
|
||||
var $ = function (selector) {
|
||||
return document.querySelector(selector);
|
||||
};
|
||||
// attach the event listener to the selected li items
|
||||
var links = $('#parentList').getElementsByTagName('li');
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var link = links[i];
|
||||
// console.log(link.innerHTML);
|
||||
link.ondblclick = dynamicEvent;
|
||||
if (link.innerHTML === " null" || link.innerHTML === " " || link.innerHTML === "") {
|
||||
link.style.display = "none";
|
||||
} else {
|
||||
link.style.display = "list-item";
|
||||
};
|
||||
};
|
||||
});
|
||||
function dynamicEvent() {
|
||||
gun.get(this.id).put(null);
|
||||
this.innerHTML = document.getElementById(this.id.innerHTML);
|
||||
if (this.innerHTML === " null" || this.innerHTML === " " || this.innerHTML === "") {
|
||||
this.style.display = "none";
|
||||
} else {
|
||||
this.style.display = "list-item";
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<ul id="parentList"></ul>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
|
||||
<script>
|
||||
const gun = Gun(`${window.location.origin}/gun`).get('thoughts')
|
||||
|
||||
const parentList = document.getElementById('parentList')
|
||||
const input = document.getElementById('input')
|
||||
const form = document.getElementById('form')
|
||||
|
||||
const dynamicEvent = e => {
|
||||
const target = e.target;
|
||||
|
||||
gun.get(target.id).put(null);
|
||||
|
||||
target.innerHTML = document.getElementById(target.id);
|
||||
|
||||
if (target.innerHTML === ' null' || target.innerHTML === ' ' || target.innerHTML === '') {
|
||||
target.style.display = 'none';
|
||||
} else {
|
||||
target.style.display = 'list-item';
|
||||
}
|
||||
}
|
||||
|
||||
gun.map().on((thought, id) => {
|
||||
parentList.insertAdjacentHTML('beforeend', `<li id =${id}> ${thought}</li>`);
|
||||
|
||||
const links = parentList.getElementsByTagName('li');
|
||||
|
||||
for (const link of links) {
|
||||
if (link.innerHTML === ' null' || link.innerHTML === ' ' || link.innerHTML === '') {
|
||||
link.style.display = 'none';
|
||||
} else {
|
||||
link.style.display = 'list-item';
|
||||
}
|
||||
link.ondblclick = dynamicEvent;
|
||||
}
|
||||
})
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
gun.set(input.value);
|
||||
input.value = '';
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
58
examples/vanilla/user.html
Normal file
58
examples/vanilla/user.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<h1>User</h1>
|
||||
|
||||
<form id="sign">
|
||||
<input id="alias" placeholder="username">
|
||||
<input id="pass" type="password" placeholder="passphrase">
|
||||
<input id="signup" type="button" value="sign up">
|
||||
<input id="signin" type="button" value="sign in">
|
||||
</form>
|
||||
|
||||
<ul id="list"></ul>
|
||||
|
||||
<form id="said">
|
||||
<input id="say">
|
||||
<input id="speak" type="submit" value="speak">
|
||||
</form>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/sea.js"></script>
|
||||
|
||||
<script>
|
||||
const gun = Gun(`${window.location.origin}/gun`)
|
||||
const user = gun.user().recall({ sessionStorage: true })
|
||||
|
||||
const alias = document.querySelector('#alias')
|
||||
const pass = document.querySelector('#pass')
|
||||
const sign = document.querySelector('#sign')
|
||||
const signup = document.querySelector('#signup')
|
||||
const signin = document.querySelector('#signin')
|
||||
const said = document.querySelector('#said')
|
||||
const say = document.querySelector('#say')
|
||||
const ul = document.querySelector('#list')
|
||||
|
||||
function UI (say, id) {
|
||||
const li = document.createElement('li')
|
||||
li.setAttribute('id', id)
|
||||
li.appendChild(document.createTextNode(say))
|
||||
ul.appendChild(li)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
signup.addEventListener('click', () => user.create(alias.value, pass.value, () => user.auth(alias.value, pass.value)))
|
||||
signin.addEventListener('click', () => user.auth(alias.value, pass.value))
|
||||
|
||||
said.addEventListener('submit', e => {
|
||||
e.preventDefault()
|
||||
// if(!user.is){ return }
|
||||
user.get('said').set(say.value)
|
||||
said.value = ''
|
||||
})
|
||||
|
||||
gun.on('auth', () => {
|
||||
sign.style.display = 'none'
|
||||
user.get('said').map().on(UI)
|
||||
})
|
||||
</script>
|
83
examples/vanilla/video.html
Normal file
83
examples/vanilla/video.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<video id="video" width="100%" controls autoplay></video>
|
||||
<center>
|
||||
<input id="pass" placeholder="password">
|
||||
Record <button class="record">Camera</button> or <button class="screen">Screen</button>
|
||||
</center>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gun/sea.js"></script>
|
||||
|
||||
<script>
|
||||
const peers = [
|
||||
`${window.location.origin}/gun`,
|
||||
'https://gun-us.herokuapp.com/gun',
|
||||
'https://gun-eu.herokuapp.com/gun'
|
||||
]
|
||||
|
||||
const gun = Gun({ localStorage: true, peers })
|
||||
|
||||
const pass = document.querySelector('#pass')
|
||||
const video = document.querySelector('#video')
|
||||
const camera = document.querySelector('.record')
|
||||
const screen = document.querySelector('.screen')
|
||||
|
||||
gun.get('test').get('video').on(async data => {
|
||||
if (pass.value) { data = await SEA.decrypt(data, pass.value) }
|
||||
video.setAttribute('src', data)
|
||||
})
|
||||
|
||||
function record(type) {
|
||||
function load(media) {
|
||||
const chunks = []
|
||||
record.ing = new MediaRecorder(media)
|
||||
record.ing.ondataavailable = eve => { chunks.push(eve.data) }
|
||||
record.ing.onstop = eve => { record.save(new Blob(chunks)) }
|
||||
record.ing.start()
|
||||
}
|
||||
|
||||
function error(err) { console.log(err) }
|
||||
|
||||
if (type === 'Camera') {
|
||||
navigator.getMedia({ video: true, audio: true }, load, error)
|
||||
}
|
||||
if (type === 'Screen') {
|
||||
navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }).then(load, error)
|
||||
}
|
||||
}
|
||||
|
||||
function act(e) {
|
||||
if (record.ing) {
|
||||
if (record.ing.stop) { record.ing.stop() }
|
||||
|
||||
e.target.textContent = record.type
|
||||
record.ing = false
|
||||
return
|
||||
}
|
||||
|
||||
record(record.type = e.target.textContent)
|
||||
e.target.textContent = 'end'
|
||||
}
|
||||
|
||||
record.save = data => {
|
||||
record.file = record.file || new FileReader()
|
||||
record.file.readAsDataURL(data)
|
||||
record.file.onloadend = async () => {
|
||||
const b64 = record.file.result
|
||||
let b64formated = `data:video/webm${b64.slice(b64.indexOf(';'))}`
|
||||
|
||||
video.setAttribute('src', b64formated)
|
||||
|
||||
if (pass.value) { b64formated = await SEA.encrypt(b64formated, pass.value) }
|
||||
gun.get('test').get('video').put(b64formated)
|
||||
}
|
||||
}
|
||||
|
||||
camera.addEventListener('click', e => act(e))
|
||||
screen.addEventListener('click', e => act(e))
|
||||
|
||||
navigator.getMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia || navigator.msGetUserMedia)
|
||||
|
||||
</script>
|
@ -9,7 +9,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
This is example of simple Vue plugin. It works exatcly same as the Vue instance data property, but the name is gunData.<br>
|
||||
This is example of simple Vue plugin. It works exactly same as the Vue instance data property, but the name is gunData.<br>
|
||||
The cool part is that every property in the gunData is realtime synced via gunDB to every other page viewer!<br>
|
||||
<table>
|
||||
<tr>
|
||||
|
40
examples/wave.html
Normal file
40
examples/wave.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Wave example</title>
|
||||
</head>
|
||||
<body>
|
||||
<button id="bPiano">Piano left</button>
|
||||
<button id="bGuitar">Guitar itched</button>
|
||||
<button id="bXylo">Xylophone slow</button>
|
||||
<button id="bAll">All in one</button>
|
||||
|
||||
<script src="../lib/wave.js"></script>
|
||||
<script>
|
||||
function piano() {
|
||||
wave(':piano:zxcvbn').balance(-1).play()
|
||||
}
|
||||
function guitar() {
|
||||
wave(':guitar:zxcvbn').itch(0.5).play()
|
||||
}
|
||||
function xylo() {
|
||||
wave(':xylophone:zxcvbn').pace(100).play()
|
||||
}
|
||||
function all() {
|
||||
wave(`
|
||||
:pace 400: :itch 0: :balance 0:
|
||||
|
||||
:piano: :balance -1: zxcvbn
|
||||
|
||||
:guitar: :balance 0: :itch 0.5: zxcvbn
|
||||
|
||||
:xylophone: :itch 0: :pace 100: zxcvbn
|
||||
`).play()
|
||||
}
|
||||
document.querySelector('#bPiano').addEventListener("click", piano)
|
||||
document.querySelector('#bGuitar').addEventListener("click", guitar)
|
||||
document.querySelector('#bXylo').addEventListener("click", xylo)
|
||||
document.querySelector('#bAll').addEventListener("click", all)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
3
examples/webpack/.gitignore
vendored
Normal file
3
examples/webpack/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
package-lock.json
|
||||
node_modules
|
||||
public
|
17
examples/webpack/package.json
Normal file
17
examples/webpack/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "webpack",
|
||||
"version": "1.0.0",
|
||||
"description": "webpack build example",
|
||||
"scripts": {
|
||||
"build": "webpack --devtool source-map --config webpack.config.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"gun": "github:amark/gun",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"webpack": "^5.72.1",
|
||||
"webpack-cli": "^4.9.2"
|
||||
}
|
||||
}
|
9
examples/webpack/src/app.js
Normal file
9
examples/webpack/src/app.js
Normal file
@ -0,0 +1,9 @@
|
||||
define(function(require, exports, module) {
|
||||
|
||||
var Gun = require("gun");
|
||||
|
||||
var gun = new Gun();
|
||||
|
||||
gun.get("hello").get("world").put("from gun").on((data, key) => console.log(data, key));
|
||||
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user