forked from cory/tildefriends
Compare commits
449 Commits
Author | SHA1 | Date | |
---|---|---|---|
72369ab745 | |||
b62a05f627 | |||
03eb8e7fae | |||
a5a00b6987 | |||
542162c78a | |||
8cfe0fb7d2 | |||
49c831cb62 | |||
2c79e03094 | |||
21e6cf10b6 | |||
dc655bb359 | |||
b9987580ee | |||
cb2dfc696d | |||
7f0643f9c0 | |||
14a4117aff | |||
55fb5dce1a | |||
923d6f9835 | |||
08b5ade8ec | |||
91f41c7497 | |||
fa06282ff9 | |||
48b967f5b6 | |||
f479165aac | |||
2f83ecc1ac | |||
01efc215fd | |||
00ba74a6c4 | |||
44b5ba1a9a | |||
843e172e56 | |||
a0df336abe | |||
0db4bb06c9 | |||
ad2b49b838 | |||
ab519342e8 | |||
1f0b9012e3 | |||
1ddad6be93 | |||
cf311003c0 | |||
64249976a8 | |||
6ecb3ccd08 | |||
4867bacb72 | |||
7d029d3d7a | |||
455befc18f | |||
6e57845512 | |||
1335a6e1e5 | |||
1eab44464c | |||
c3e2da3d51 | |||
1716f71c12 | |||
b52e99c958 | |||
86531bfd7e | |||
874a22325e | |||
2380b65853 | |||
f72e8cbd91 | |||
24e418344e | |||
2b7077ca70 | |||
10d438e723 | |||
331846ee2e | |||
dc0e58afc1 | |||
18e9252998 | |||
b2e3c04036 | |||
4fd155e68a | |||
59ac0b5f20 | |||
f4979c841a | |||
74eb74deb1 | |||
9e5e7b70d4 | |||
2384fc9fa9 | |||
576e58b1e3 | |||
a0af058f5e | |||
b40457d774 | |||
2353b43514 | |||
b11d5192c2 | |||
d38c58ce1d | |||
a0f390b7dc | |||
cb12799111 | |||
86fb5c53a1 | |||
29fc728509 | |||
0fb341f378 | |||
8a1a182479 | |||
49907bc8ee | |||
21d4a9b328 | |||
d5ede43a13 | |||
b73f5011cf | |||
32ebfa78cd | |||
39c942a205 | |||
ebc4533b10 | |||
4e5f9c86a8 | |||
d89a7a5556 | |||
8ab53f2da3 | |||
4c8eab2692 | |||
08989f54d9 | |||
c78753f3ff | |||
34a87d8b3b | |||
7516524d69 | |||
549d7ffec8 | |||
ccafc23d3c | |||
709b57d84f | |||
9ef909c9a1 | |||
d7c0ffaac4 | |||
e4cd5312f1 | |||
197fca6d3b | |||
04af1f0053 | |||
93d9b1ed93 | |||
2d73116bc0 | |||
f2f6d78790 | |||
797509fc11 | |||
6920504762 | |||
9d1476a760 | |||
c1890775dc | |||
72e5fe5b8f | |||
c81ec214e2 | |||
0dcc879eb1 | |||
4f3f4295ea | |||
d02f17a8cf | |||
2f6a92168e | |||
b6a3923b27 | |||
d556cbc835 | |||
f186806117 | |||
f4f560b164 | |||
14b7f9237b | |||
f3518b3d0f | |||
7964524e0a | |||
8ab8335baa | |||
cd43bf9dfa | |||
ccebf831e7 | |||
9f2f9bd8b0 | |||
adf8c14536 | |||
606e82d718 | |||
1621f1753a | |||
196ab66e14 | |||
38ab32dad9 | |||
86046e52f0 | |||
9e7c860414 | |||
7dc8b86ee2 | |||
6ecbfe3de6 | |||
f9940fc436 | |||
58e75ee276 | |||
e7771f539d | |||
c2f62cd8e0 | |||
f4b6812675 | |||
03e4b37c04 | |||
7b3a9e0f63 | |||
067f546580 | |||
2f7697b7ec | |||
1d214f89ed | |||
0b47207949 | |||
94dd573a81 | |||
6fa4896155 | |||
28c99f9d8b | |||
88fbb5f73b | |||
402c185dd4 | |||
ae2015a604 | |||
023731fc3f | |||
99998aac8a | |||
360d0bc110 | |||
817838e522 | |||
deb3cfb4b6 | |||
af61519632 | |||
b1714cf554 | |||
f0984b19f2 | |||
eb3c9cd6f3 | |||
e677b0ac3c | |||
dd909bfe53 | |||
b13b111614 | |||
5511530926 | |||
5e1ef01bc0 | |||
a060eadab7 | |||
70db31bb8f | |||
1292775a75 | |||
0fbc84d364 | |||
0dd0b835ec | |||
d96e0a7497 | |||
625504b8eb | |||
a185ded47e | |||
5a0c77d06a | |||
e54bd316d5 | |||
f908b45cc7 | |||
d02751ee08 | |||
13ab9786f7 | |||
ba13a08e78 | |||
8c92a5ff7b | |||
37f728835b | |||
a6f1eaa09e | |||
dff8eca16e | |||
a3cca9aae6 | |||
edabd7735c | |||
461e7b7d5a | |||
06ea8d4781 | |||
62ef0bcb42 | |||
a0043ec49f | |||
305f5232e7 | |||
d467c4dd8a | |||
5b2ace80d4 | |||
1484a87cad | |||
b23bc0b16b | |||
a6c8dd846c | |||
5fff3b8161 | |||
b4236d0ec0 | |||
5e8cd12760 | |||
699438602c | |||
52aa6eed0d | |||
6cdf207dcd | |||
607e9ef71b | |||
367c489fc3 | |||
b3c9ad2fcb | |||
ee9cb63327 | |||
889773c38d | |||
b83704a218 | |||
dfe5d51d04 | |||
280dee0438 | |||
aa10ab69f6 | |||
6ef14d985d | |||
61a3226e14 | |||
f9c370212b | |||
021f3ad5bc | |||
8bdc27bf5c | |||
00eb5222f8 | |||
d06f490cc2 | |||
b087a09d37 | |||
4cedc54d2d | |||
8fe7adc50e | |||
bf5236e68b | |||
da1d686705 | |||
2ce5bc73d5 | |||
c6ae9313cc | |||
8f3883563f | |||
950273da41 | |||
391da742fd | |||
4414676076 | |||
0da45b7b40 | |||
7e02cb90f6 | |||
01ba90fdba | |||
b394140f9e | |||
4a1d136721 | |||
ab3009f771 | |||
bf340f3de4 | |||
68f5827dee | |||
b695a4ba3b | |||
d84ab2734e | |||
4f0cc793c7 | |||
b7a4ac22b2 | |||
5db9acae1d | |||
c299c1432c | |||
d8551ab732 | |||
25bc1279c2 | |||
f6d9d23456 | |||
b20d95d616 | |||
071c2f1c20 | |||
566d00f0df | |||
0550aa4e98 | |||
baf69355a5 | |||
17c0266998 | |||
14a2207064 | |||
7c721fc6eb | |||
ab03692a4c | |||
626fa4f27b | |||
15676e0f4f | |||
8709ce8ad5 | |||
bb924d79d6 | |||
8e075e33d9 | |||
b0b002104a | |||
43d1f34fa3 | |||
6db1a097aa | |||
6dae2f0749 | |||
dc87c26b99 | |||
234d597083 | |||
b74c347c7c | |||
996996e609 | |||
cd9050f61f | |||
b70b309977 | |||
ee510f3f3f | |||
8a70b8ea3e | |||
27ee73bb89 | |||
3aeb47e447 | |||
b9ceb30ecf | |||
5b6ee20b2d | |||
d062ec0dfe | |||
d5a0daa0d3 | |||
8c4ec71e26 | |||
1e5aa0ba93 | |||
0326a1f8cc | |||
8311404a09 | |||
c81111c2cf | |||
e499be12ae | |||
e785f7f10a | |||
c3da10bef6 | |||
c8c8cb305e | |||
efdd046ef8 | |||
496eefd2ee | |||
9da79b3a21 | |||
1b2b0970fb | |||
0bb45a7fa8 | |||
15a25a41aa | |||
76b6ff78cd | |||
5285b3f222 | |||
0c993c251b | |||
b3a1f17452 | |||
11929e8c68 | |||
fc9a081250 | |||
2583221117 | |||
a69e551968 | |||
8bd0027e71 | |||
84eaa3e2fd | |||
a57916b3db | |||
87e769786a | |||
05a7e941cf | |||
ed307e6b3b | |||
e8aa957209 | |||
a39f820ff1 | |||
1c621a602f | |||
7169f4a6cb | |||
e202c1a40e | |||
0f4b4da0aa | |||
1f96413bd3 | |||
9695621c91 | |||
2d67f5ead6 | |||
29d2a45abc | |||
13c8b05f9a | |||
2c1a5359c6 | |||
d8530f228e | |||
575f6c2f0a | |||
62cdc592c0 | |||
11373faf23 | |||
0473eec0a2 | |||
7fc23dc085 | |||
c741cc06b2 | |||
39dbfdec82 | |||
0e5d6056e4 | |||
d711993b3b | |||
615bf7fe43 | |||
424b9b5a2f | |||
d1e494b730 | |||
623e4b8fff | |||
b5dd1f2f86 | |||
ec83f9c747 | |||
31af27529e | |||
6302565942 | |||
cda724b2da | |||
c2e2ba2a40 | |||
b5d3f5faa7 | |||
71f3910055 | |||
70ed8c3b32 | |||
af13bfc920 | |||
e24fd92f85 | |||
7e27cefe6a | |||
450cf6424e | |||
54898d3dbb | |||
dd851a2b25 | |||
4c6b44eb30 | |||
74a3efe78d | |||
51301fc49e | |||
02dd8c3dd0 | |||
26a778c3b2 | |||
9fecbd97e8 | |||
e1383e3903 | |||
47532b8512 | |||
3c4959433a | |||
e921b4a86a | |||
b23b0ca239 | |||
191b45f054 | |||
15d0383349 | |||
d2485583fd | |||
2b94704916 | |||
85ac6c215a | |||
e83e665db9 | |||
645aafef16 | |||
152c893a6f | |||
7c130dda56 | |||
2d82dad806 | |||
e8ac5b759d | |||
4833d18968 | |||
6eafded1f6 | |||
7b440b720e | |||
e20ba7384f | |||
45231c6ede | |||
35475defb5 | |||
8741841f27 | |||
5282d19b55 | |||
d9782aa0fb | |||
9751facfb4 | |||
e0110203e7 | |||
088b44cc2c | |||
8f63bcbfbf | |||
c8029388c9 | |||
d9c4d847a1 | |||
df9d9425ec | |||
90bb3c684e | |||
9c81b6de8a | |||
6383498041 | |||
daeb88785d | |||
dcea08f73b | |||
b252b921f8 | |||
172826bf13 | |||
060f1980f5 | |||
e223d35252 | |||
99dba1a4c6 | |||
b52026c81f | |||
47b8c86426 | |||
2e55c68648 | |||
b7362dd84d | |||
01637b31e1 | |||
0e9a39608a | |||
79404e4d41 | |||
35c21fbdaf | |||
8c7bd7dc11 | |||
09ad4f0320 | |||
d96b836bef | |||
59b2ffaf95 | |||
f1b55ddd64 | |||
85acac3a30 | |||
befff5c1e5 | |||
d72ba81a67 | |||
fef88e2032 | |||
20557e8ce4 | |||
99c905e908 | |||
d7b58ee2c5 | |||
faca2d387b | |||
358d02d97f | |||
b66dac7465 | |||
f7d201859a | |||
61d2ef5469 | |||
ac994b9c62 | |||
264dcbc331 | |||
e5425c0ffb | |||
e10803de68 | |||
07b1a0e403 | |||
6ed2c702d8 | |||
5c1c33d33e | |||
70d37c88b5 | |||
1ba37d95b5 | |||
0d82198849 | |||
39927e75f2 | |||
e6fd33b969 | |||
e8fe32d5af | |||
bfc8bb864d | |||
9179746763 | |||
d0177d24cb | |||
0573008c9c | |||
9506f518c2 | |||
0f0ae9153b | |||
09c7c8ac64 | |||
5e2dfff148 | |||
958b47548d | |||
16155ef746 | |||
5755b61ea6 | |||
353847a77f | |||
bdf64edeb8 | |||
b5768dd927 | |||
3e5abf3a4d | |||
d3029639de | |||
d21d7e4add | |||
afde69b5d9 | |||
3319df3df0 | |||
1102feaac3 | |||
deede728be |
854
GNUmakefile
Normal file
854
GNUmakefile
Normal file
@ -0,0 +1,854 @@
|
|||||||
|
.ONESHELL:
|
||||||
|
.DELETE_ON_ERROR:
|
||||||
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
|
VERSION_CODE := 15
|
||||||
|
VERSION_NUMBER := 0.0.15
|
||||||
|
VERSION_NAME := Medium English breakfast tea.
|
||||||
|
|
||||||
|
PROJECT = tildefriends
|
||||||
|
BUILD_DIR ?= out
|
||||||
|
UNAME_S := $(shell uname -s)
|
||||||
|
UNAME_M := $(shell uname -m)
|
||||||
|
|
||||||
|
ANDROID_SDK ?= ~/Android/Sdk
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
||||||
|
else ifeq ($(UNAME_S),Linux)
|
||||||
|
BUILD_TYPES := debug release
|
||||||
|
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0)
|
||||||
|
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0)
|
||||||
|
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0)
|
||||||
|
else ifeq ($(UNAME_S),Haiku)
|
||||||
|
BUILD_TYPES := debug release
|
||||||
|
CFLAGS += -Dstatic_assert=_Static_assert
|
||||||
|
LDFLAGS += \
|
||||||
|
-lbsd \
|
||||||
|
-lnetwork
|
||||||
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
|
BUILD_TYPES := debug release
|
||||||
|
CFLAGS += \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
|
LDFLAGS += \
|
||||||
|
-lexecinfo \
|
||||||
|
-lc++abi
|
||||||
|
HAVE_ANDROID := 0
|
||||||
|
HAVE_LINUX_IOS := 0
|
||||||
|
HAVE_WIN := 0
|
||||||
|
else
|
||||||
|
$(error Unexpected host platform $(UNAME_S).)
|
||||||
|
endif
|
||||||
|
|
||||||
|
CFLAGS += \
|
||||||
|
-std=gnu11 \
|
||||||
|
-Wall \
|
||||||
|
-Wextra \
|
||||||
|
-Wno-unused-parameter \
|
||||||
|
-MMD \
|
||||||
|
-ffunction-sections \
|
||||||
|
-fdata-sections \
|
||||||
|
-fno-exceptions \
|
||||||
|
-g
|
||||||
|
|
||||||
|
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
||||||
|
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
|
||||||
|
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.0.10792818
|
||||||
|
ANDROID_MIN_SDK_VERSION := 24
|
||||||
|
ANDROID_TARGET_SDK_VERSION := 34
|
||||||
|
|
||||||
|
ANDROID_ARMV7A_TARGETS := \
|
||||||
|
out/androiddebug-armv7a/tildefriends \
|
||||||
|
out/androidrelease-armv7a/tildefriends
|
||||||
|
ANDROID_ARM64_TARGETS := \
|
||||||
|
out/androiddebug/tildefriends \
|
||||||
|
out/androidrelease/tildefriends
|
||||||
|
ANDROID_X86_TARGETS := \
|
||||||
|
out/androiddebug-x86/tildefriends \
|
||||||
|
out/androidrelease-x86/tildefriends
|
||||||
|
ANDROID_X86_64_TARGETS := \
|
||||||
|
out/androiddebug-x86_64/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends
|
||||||
|
ANDROID_TARGETS := \
|
||||||
|
$(ANDROID_X86_TARGETS) \
|
||||||
|
$(ANDROID_X86_64_TARGETS) \
|
||||||
|
$(ANDROID_ARMV7A_TARGETS) \
|
||||||
|
$(ANDROID_ARM64_TARGETS)
|
||||||
|
ifeq ($(HAVE_ANDROID),1)
|
||||||
|
BUILD_TYPES += \
|
||||||
|
androiddebug \
|
||||||
|
androidrelease \
|
||||||
|
androiddebug-armv7a \
|
||||||
|
androidrelease-armv7a \
|
||||||
|
androiddebug-x86 \
|
||||||
|
androidrelease-x86 \
|
||||||
|
androiddebug-x86_64 \
|
||||||
|
androidrelease-x86_64
|
||||||
|
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
|
||||||
|
endif
|
||||||
|
|
||||||
|
WINDOWS_TARGETS := \
|
||||||
|
out/windebug/tildefriends.exe \
|
||||||
|
out/winrelease/tildefriends.exe
|
||||||
|
ifeq ($(HAVE_WIN),1)
|
||||||
|
BUILD_TYPES += windebug winrelease
|
||||||
|
endif
|
||||||
|
|
||||||
|
LINUX_TARGETS := \
|
||||||
|
out/debug/tildefriends \
|
||||||
|
out/release/tildefriends
|
||||||
|
MACOS_TARGETS := \
|
||||||
|
out/macosdebug/tildefriends \
|
||||||
|
out/macosrelease/tildefriends
|
||||||
|
IOS_TARGETS := \
|
||||||
|
out/iosdebug/tildefriends \
|
||||||
|
out/iosrelease/tildefriends
|
||||||
|
IOSSIM_TARGETS := \
|
||||||
|
out/iossimdebug/tildefriends \
|
||||||
|
out/iossimrelease/tildefriends
|
||||||
|
IOS_APPS = \
|
||||||
|
out/tildefriends-iosdebug.app/tildefriends \
|
||||||
|
out/tildefriends-iosrelease.app/tildefriends
|
||||||
|
ifeq ($(HAVE_LINUX_IOS),1)
|
||||||
|
BUILD_TYPES += iosdebug iosrelease
|
||||||
|
all: $(IOS_APPS)
|
||||||
|
endif
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
all: $(IOS_APPS) \
|
||||||
|
out/tildefriends-iossimdebug.app/tildefriends \
|
||||||
|
out/tildefriends-iossimrelease.app/tildefriends
|
||||||
|
endif
|
||||||
|
|
||||||
|
DEBUG_TARGETS := \
|
||||||
|
out/debug/tildefriends \
|
||||||
|
out/windebug/tildefriends.exe \
|
||||||
|
out/iosdebug/tildefriends \
|
||||||
|
out/iossimdebug/tildefriends \
|
||||||
|
out/macosdebug/tildefriends \
|
||||||
|
out/androiddebug/tildefriends \
|
||||||
|
out/androiddebug-armv7a/tildefriends \
|
||||||
|
out/androiddebug-x86_64/tildefriends \
|
||||||
|
out/androiddebug-x86/tildefriends
|
||||||
|
RELEASE_TARGETS := \
|
||||||
|
out/release/tildefriends \
|
||||||
|
out/winrelease/tildefriends.exe \
|
||||||
|
out/iosrelease/tildefriends \
|
||||||
|
out/iossimrelease/tildefriends \
|
||||||
|
out/macosrelease/tildefriends \
|
||||||
|
out/androidrelease/tildefriends \
|
||||||
|
out/androidrelease-armv7a/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends \
|
||||||
|
out/androidrelease-x86/tildefriends
|
||||||
|
ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS)
|
||||||
|
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
||||||
|
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||||
|
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
|
||||||
|
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
|
||||||
|
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||||
|
$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
||||||
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||||
|
-fPIC \
|
||||||
|
-fdebug-compilation-dir . \
|
||||||
|
-fomit-frame-pointer \
|
||||||
|
-fno-asynchronous-unwind-tables \
|
||||||
|
-funwind-tables
|
||||||
|
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
||||||
|
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
||||||
|
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
||||||
|
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
||||||
|
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
|
||||||
|
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
|
||||||
|
$(WINDOWS_TARGETS): AS = $(CC)
|
||||||
|
$(WINDOWS_TARGETS): CFLAGS += \
|
||||||
|
-D_WIN32_WINNT=0x0A00 \
|
||||||
|
-DWINVER=0x0A00 \
|
||||||
|
-DNTDDI_VERSION=NTDDI_WIN10 \
|
||||||
|
-Ideps/openssl/mingw64/usr/local/include
|
||||||
|
$(WINDOWS_TARGETS): LDFLAGS += \
|
||||||
|
-static \
|
||||||
|
-lm \
|
||||||
|
-Ldeps/openssl/mingw64/usr/local/lib
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
$(MACOS_TARGETS): CC = xcrun clang
|
||||||
|
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
|
||||||
|
$(IOS_TARGETS): CC = xcrun --sdk iphoneos clang -isysroot $(IOS_SYSROOT) -arch arm64
|
||||||
|
$(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
|
||||||
|
$(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64
|
||||||
|
else ifeq ($(UNAME_S),Linux)
|
||||||
|
$(IOS_TARGETS): IOS_SYSROOT := deps/iPhoneOS17.0.sdk
|
||||||
|
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
|
||||||
|
endif
|
||||||
|
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
|
||||||
|
$(ANDROID_X86_TARGETS): ANDROID_NDK_TARGET_TRIPLE := i686-linux-android
|
||||||
|
$(ANDROID_ARMV7A_TARGETS): ANDROID_NDK_TARGET_TRIPLE := armv7a-linux-androideabi
|
||||||
|
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
|
||||||
|
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/$(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION)-clang
|
||||||
|
$(ANDROID_TARGETS): AS = $(CC)
|
||||||
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
|
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Ideps/openssl/android/armeabi-v7a/usr/local/include
|
||||||
|
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Ldeps/openssl/android/armeabi-v7a/usr/local/lib
|
||||||
|
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
|
||||||
|
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
||||||
|
$(ANDROID_X86_TARGETS): CFLAGS += -Ideps/openssl/android/x86/usr/local/include
|
||||||
|
$(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment
|
||||||
|
$(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
|
||||||
|
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||||
|
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||||
|
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
|
||||||
|
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
|
||||||
|
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
|
||||||
|
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
|
||||||
|
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
|
||||||
|
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
|
||||||
|
|
||||||
|
ifeq ($(UNAME_M),x86_64)
|
||||||
|
ifneq ($(UNAME_S),Haiku)
|
||||||
|
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
|
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(UNAME_M),aarch64)
|
||||||
|
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
|
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
|
endif
|
||||||
|
|
||||||
|
get_objs = \
|
||||||
|
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
|
||||||
|
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
||||||
|
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
|
||||||
|
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
||||||
|
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
||||||
|
$(foreach build_type,macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
|
||||||
|
$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_ios))))) \
|
||||||
|
$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86)))))
|
||||||
|
|
||||||
|
APP_SOURCES := $(wildcard src/*.c)
|
||||||
|
APP_SOURCES_ios := $(wildcard src/*.m)
|
||||||
|
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||||
|
$(APP_OBJS): CFLAGS += \
|
||||||
|
-Ideps/base64c/include \
|
||||||
|
-Ideps/crypt_blowfish \
|
||||||
|
-Ideps/libbacktrace \
|
||||||
|
-Ideps/libsodium \
|
||||||
|
-Ideps/libsodium/src/libsodium/include \
|
||||||
|
-Ideps/libuv/include \
|
||||||
|
-Ideps/zlib \
|
||||||
|
-Ideps/zlib/contrib/minizip \
|
||||||
|
-Ideps/picohttpparser \
|
||||||
|
-Ideps/quickjs \
|
||||||
|
-Ideps/sqlite \
|
||||||
|
-Ideps/valgrind \
|
||||||
|
-Ideps/xopt \
|
||||||
|
-Wdouble-promotion \
|
||||||
|
-Werror
|
||||||
|
ifeq ($(UNAME_M),x86_64)
|
||||||
|
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_OBJS)): CFLAGS += \
|
||||||
|
-fanalyzer
|
||||||
|
endif
|
||||||
|
|
||||||
|
BLOWFISH_SOURCES := \
|
||||||
|
deps/crypt_blowfish/crypt_blowfish.c \
|
||||||
|
deps/crypt_blowfish/crypt_gensalt.c \
|
||||||
|
deps/crypt_blowfish/wrapper.c
|
||||||
|
BLOWFISH_SOURCES_win := \
|
||||||
|
deps/crypt_blowfish/x86.S
|
||||||
|
BLOWFISH_SOURCES_x86 := \
|
||||||
|
deps/crypt_blowfish/x86.S
|
||||||
|
BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
|
||||||
|
|
||||||
|
UV_SOURCES := \
|
||||||
|
deps/libuv/src/fs-poll.c \
|
||||||
|
deps/libuv/src/idna.c \
|
||||||
|
deps/libuv/src/inet.c \
|
||||||
|
deps/libuv/src/random.c \
|
||||||
|
deps/libuv/src/strscpy.c \
|
||||||
|
deps/libuv/src/strtok.c \
|
||||||
|
deps/libuv/src/threadpool.c \
|
||||||
|
deps/libuv/src/timer.c \
|
||||||
|
deps/libuv/src/uv-common.c \
|
||||||
|
deps/libuv/src/uv-data-getter-setters.c \
|
||||||
|
deps/libuv/src/version.c
|
||||||
|
UV_SOURCES_unix := \
|
||||||
|
deps/libuv/src/unix/async.c \
|
||||||
|
deps/libuv/src/unix/core.c \
|
||||||
|
deps/libuv/src/unix/dl.c \
|
||||||
|
deps/libuv/src/unix/fs.c \
|
||||||
|
deps/libuv/src/unix/getaddrinfo.c \
|
||||||
|
deps/libuv/src/unix/getnameinfo.c \
|
||||||
|
deps/libuv/src/unix/loop-watcher.c \
|
||||||
|
deps/libuv/src/unix/loop.c \
|
||||||
|
deps/libuv/src/unix/pipe.c \
|
||||||
|
deps/libuv/src/unix/poll.c \
|
||||||
|
deps/libuv/src/unix/process.c \
|
||||||
|
deps/libuv/src/unix/random-devurandom.c \
|
||||||
|
deps/libuv/src/unix/random-getrandom.c \
|
||||||
|
deps/libuv/src/unix/signal.c \
|
||||||
|
deps/libuv/src/unix/stream.c \
|
||||||
|
deps/libuv/src/unix/tcp.c \
|
||||||
|
deps/libuv/src/unix/thread.c \
|
||||||
|
deps/libuv/src/unix/tty.c \
|
||||||
|
deps/libuv/src/unix/udp.c
|
||||||
|
ifeq ($(UNAME_S),Linux)
|
||||||
|
UV_SOURCES_unix += \
|
||||||
|
deps/libuv/src/unix/linux.c \
|
||||||
|
deps/libuv/src/unix/procfs-exepath.c \
|
||||||
|
deps/libuv/src/unix/proctitle.c \
|
||||||
|
deps/libuv/src/unix/random-sysctl-linux.c
|
||||||
|
else ifeq ($(UNAME_S),Haiku)
|
||||||
|
UV_SOURCES_unix += \
|
||||||
|
deps/libuv/src/unix/bsd-ifaddrs.c \
|
||||||
|
deps/libuv/src/unix/haiku.c \
|
||||||
|
deps/libuv/src/unix/no-fsevents.c \
|
||||||
|
deps/libuv/src/unix/no-proctitle.c \
|
||||||
|
deps/libuv/src/unix/posix-hrtime.c \
|
||||||
|
deps/libuv/src/unix/posix-poll.c
|
||||||
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
|
UV_SOURCES_unix += \
|
||||||
|
deps/libuv/src/unix/bsd-ifaddrs.c \
|
||||||
|
deps/libuv/src/unix/kqueue.c \
|
||||||
|
deps/libuv/src/unix/no-proctitle.c \
|
||||||
|
deps/libuv/src/unix/openbsd.c \
|
||||||
|
deps/libuv/src/unix/posix-hrtime.c \
|
||||||
|
deps/libuv/src/unix/random-getentropy.c
|
||||||
|
endif
|
||||||
|
UV_SOURCES_android := \
|
||||||
|
deps/libuv/src/unix/random-getentropy.c
|
||||||
|
UV_SOURCES_win := \
|
||||||
|
deps/libuv/src/win/async.c \
|
||||||
|
deps/libuv/src/win/core.c \
|
||||||
|
deps/libuv/src/win/detect-wakeup.c \
|
||||||
|
deps/libuv/src/win/dl.c \
|
||||||
|
deps/libuv/src/win/error.c \
|
||||||
|
deps/libuv/src/win/fs-event.c \
|
||||||
|
deps/libuv/src/win/fs.c \
|
||||||
|
deps/libuv/src/win/getaddrinfo.c \
|
||||||
|
deps/libuv/src/win/getnameinfo.c \
|
||||||
|
deps/libuv/src/win/handle.c \
|
||||||
|
deps/libuv/src/win/loop-watcher.c \
|
||||||
|
deps/libuv/src/win/pipe.c \
|
||||||
|
deps/libuv/src/win/poll.c \
|
||||||
|
deps/libuv/src/win/process-stdio.c \
|
||||||
|
deps/libuv/src/win/process.c \
|
||||||
|
deps/libuv/src/win/signal.c \
|
||||||
|
deps/libuv/src/win/snprintf.c \
|
||||||
|
deps/libuv/src/win/stream.c \
|
||||||
|
deps/libuv/src/win/tcp.c \
|
||||||
|
deps/libuv/src/win/thread.c \
|
||||||
|
deps/libuv/src/win/tty.c \
|
||||||
|
deps/libuv/src/win/udp.c \
|
||||||
|
deps/libuv/src/win/util.c \
|
||||||
|
deps/libuv/src/win/winapi.c \
|
||||||
|
deps/libuv/src/win/winsock.c
|
||||||
|
UV_SOURCES_macos := \
|
||||||
|
deps/libuv/src/unix/async.c \
|
||||||
|
deps/libuv/src/unix/bsd-ifaddrs.c \
|
||||||
|
deps/libuv/src/unix/core.c \
|
||||||
|
deps/libuv/src/unix/darwin.c \
|
||||||
|
deps/libuv/src/unix/darwin-proctitle.c \
|
||||||
|
deps/libuv/src/unix/dl.c \
|
||||||
|
deps/libuv/src/unix/fs.c \
|
||||||
|
deps/libuv/src/unix/fsevents.c \
|
||||||
|
deps/libuv/src/unix/getaddrinfo.c \
|
||||||
|
deps/libuv/src/unix/getnameinfo.c \
|
||||||
|
deps/libuv/src/unix/kqueue.c \
|
||||||
|
deps/libuv/src/unix/loop-watcher.c \
|
||||||
|
deps/libuv/src/unix/loop.c \
|
||||||
|
deps/libuv/src/unix/pipe.c \
|
||||||
|
deps/libuv/src/unix/poll.c \
|
||||||
|
deps/libuv/src/unix/process.c \
|
||||||
|
deps/libuv/src/unix/proctitle.c \
|
||||||
|
deps/libuv/src/unix/random-devurandom.c \
|
||||||
|
deps/libuv/src/unix/random-getentropy.c \
|
||||||
|
deps/libuv/src/unix/signal.c \
|
||||||
|
deps/libuv/src/unix/stream.c \
|
||||||
|
deps/libuv/src/unix/tcp.c \
|
||||||
|
deps/libuv/src/unix/thread.c \
|
||||||
|
deps/libuv/src/unix/tty.c \
|
||||||
|
deps/libuv/src/unix/udp.c
|
||||||
|
UV_OBJS := $(call get_objs,UV_SOURCES)
|
||||||
|
$(UV_OBJS): CFLAGS += \
|
||||||
|
-Ideps/libuv/include \
|
||||||
|
-Ideps/libuv/src \
|
||||||
|
-Wno-dangling-pointer \
|
||||||
|
-Wno-incompatible-pointer-types \
|
||||||
|
-Wno-maybe-uninitialized \
|
||||||
|
-Wno-sign-compare \
|
||||||
|
-Wno-unused-but-set-parameter \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-result \
|
||||||
|
-Wno-unused-variable
|
||||||
|
ifeq ($(UNAME_S),Linux)
|
||||||
|
$(UV_OBJS): CFLAGS += \
|
||||||
|
-D_GNU_SOURCE
|
||||||
|
else ifeq ($(UNAME_S),Haiku)
|
||||||
|
$(UV_OBJS): CFLAGS += \
|
||||||
|
-D_BSD_SOURCE \
|
||||||
|
-Wno-format-truncation
|
||||||
|
endif
|
||||||
|
|
||||||
|
SODIUM_SOURCES := \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis128l/aead_aegis128l.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis128l/aegis128l_soft.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis256/aead_aegis256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis256/aegis256_soft.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/softaes/softaes.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/sandy2x/curve25519_sandy2x.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_verify/verify.c \
|
||||||
|
deps/libsodium/src/libsodium/randombytes/randombytes.c \
|
||||||
|
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/core.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/codecs.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/runtime.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/utils.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/version.c
|
||||||
|
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
||||||
|
$(SODIUM_OBJS): CFLAGS += \
|
||||||
|
-DCONFIGURED=1 \
|
||||||
|
-DMINIMAL=1 \
|
||||||
|
-DHAVE_ALLOCA \
|
||||||
|
-DHAVE_CPUID_V \
|
||||||
|
-DHAVE_GCC_MEMORY_FENCES \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-unused-variable \
|
||||||
|
-Wno-type-limits \
|
||||||
|
-Wno-unknown-pragmas \
|
||||||
|
-Wno-attributes \
|
||||||
|
-Ideps/libsodium/builds/msvc \
|
||||||
|
-Ideps/libsodium/src/libsodium/include/sodium
|
||||||
|
ifneq ($(UNAME_S),OpenBSD)
|
||||||
|
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
|
||||||
|
-DHAVE_ALLOCA_H
|
||||||
|
endif
|
||||||
|
|
||||||
|
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||||
|
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||||
|
$(SQLITE_OBJS): CFLAGS += \
|
||||||
|
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
||||||
|
-DSQLITE_DEFAULT_MEMSTATUS=0 \
|
||||||
|
-DSQLITE_DQS=0 \
|
||||||
|
-DSQLITE_ENABLE_MEMSYS5 \
|
||||||
|
-DSQLITE_ENABLE_FTS5 \
|
||||||
|
-DSQLITE_ENABLE_JSON1 \
|
||||||
|
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
|
||||||
|
-DSQLITE_MAX_ATTACHED=1 \
|
||||||
|
-DSQLITE_MAX_COLUMN=100 \
|
||||||
|
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||||
|
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||||
|
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||||
|
-DSQLITE_MAX_LENGTH=5242880 \
|
||||||
|
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
||||||
|
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||||
|
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
||||||
|
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||||
|
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||||
|
-DSQLITE_OMIT_DEPRECATED \
|
||||||
|
-DSQLITE_OMIT_DESERIALIZE \
|
||||||
|
-DSQLITE_OMIT_LOAD_EXTENSION \
|
||||||
|
-DSQLITE_OMIT_TCL_VARIABLE \
|
||||||
|
-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
|
||||||
|
-DSQLITE_SECURE_DELETE \
|
||||||
|
-DSQLITE_THREADSAFE=2 \
|
||||||
|
-DSQLITE_UNTESTABLE \
|
||||||
|
-DSQLITE_USE_ALLOCA \
|
||||||
|
-DHAVE_ISNAN \
|
||||||
|
-DHAVE_GETHOSTUUID=0 \
|
||||||
|
-Wno-implicit-fallthrough \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-unused-variable
|
||||||
|
|
||||||
|
XOPT_SOURCES := deps/xopt/xopt.c
|
||||||
|
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
||||||
|
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
||||||
|
-DHAVE_SNPRINTF \
|
||||||
|
-DHAVE_VSNPRINTF \
|
||||||
|
-DHAVE_VASNPRINTF \
|
||||||
|
-DHAVE_VASPRINTF \
|
||||||
|
-Dvsnprintf=rpl_vsnprintf
|
||||||
|
$(XOPT_OBJS): CFLAGS += \
|
||||||
|
-Wno-implicit-const-int-float-conversion \
|
||||||
|
-Wno-pointer-to-int-cast
|
||||||
|
|
||||||
|
QUICKJS_SOURCES := \
|
||||||
|
deps/quickjs/cutils.c \
|
||||||
|
deps/quickjs/libbf.c \
|
||||||
|
deps/quickjs/libregexp.c \
|
||||||
|
deps/quickjs/libunicode.c \
|
||||||
|
deps/quickjs/quickjs.c
|
||||||
|
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += \
|
||||||
|
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
||||||
|
-DCONFIG_BIGNUM \
|
||||||
|
-D_GNU_SOURCE \
|
||||||
|
-Wno-enum-conversion \
|
||||||
|
-Wno-implicit-const-int-float-conversion \
|
||||||
|
-Wno-implicit-fallthrough \
|
||||||
|
-Wno-sign-compare \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-variable
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Haiku)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
|
||||||
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
|
||||||
|
endif
|
||||||
|
|
||||||
|
LIBBACKTRACE_SOURCES := \
|
||||||
|
deps/libbacktrace/atomic.c \
|
||||||
|
deps/libbacktrace/backtrace.c \
|
||||||
|
deps/libbacktrace/dwarf.c \
|
||||||
|
deps/libbacktrace/fileline.c \
|
||||||
|
deps/libbacktrace/print.c \
|
||||||
|
deps/libbacktrace/simple.c \
|
||||||
|
deps/libbacktrace/sort.c \
|
||||||
|
deps/libbacktrace/state.c
|
||||||
|
LIBBACKTRACE_SOURCES_unix := \
|
||||||
|
deps/libbacktrace/elf.c \
|
||||||
|
deps/libbacktrace/mmap.c \
|
||||||
|
deps/libbacktrace/mmapio.c \
|
||||||
|
deps/libbacktrace/posix.c
|
||||||
|
LIBBACKTRACE_SOURCES_win := \
|
||||||
|
deps/libbacktrace/alloc.c \
|
||||||
|
deps/libbacktrace/pecoff.c \
|
||||||
|
deps/libbacktrace/posix.c \
|
||||||
|
deps/libbacktrace/read.c
|
||||||
|
LIBBACKTRACE_SOURCES_macos := \
|
||||||
|
deps/libbacktrace/dwarf.c \
|
||||||
|
deps/libbacktrace/macho.c \
|
||||||
|
deps/libbacktrace/mmap.c \
|
||||||
|
deps/libbacktrace/mmapio.c \
|
||||||
|
deps/libbacktrace/posix.c
|
||||||
|
LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
|
||||||
|
$(LIBBACKTRACE_OBJS): CFLAGS += \
|
||||||
|
-Ideps/libbacktrace_config \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-maybe-initialized \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-DBACKTRACE_ELF_SIZE=64
|
||||||
|
|
||||||
|
PICOHTTPPARSER_SOURCES := \
|
||||||
|
deps/picohttpparser/picohttpparser.c
|
||||||
|
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
||||||
|
|
||||||
|
MINIUNZIP_SOURCES := \
|
||||||
|
deps/zlib/contrib/minizip/unzip.c \
|
||||||
|
deps/zlib/contrib/minizip/ioapi.c \
|
||||||
|
deps/zlib/adler32.c \
|
||||||
|
deps/zlib/crc32.c \
|
||||||
|
deps/zlib/inffast.c \
|
||||||
|
deps/zlib/inflate.c \
|
||||||
|
deps/zlib/inftrees.c \
|
||||||
|
deps/zlib/zutil.c
|
||||||
|
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
|
||||||
|
$(MINIUNZIP_OBJS): CFLAGS += \
|
||||||
|
-Ideps/zlib \
|
||||||
|
-Wno-maybe-uninitialized
|
||||||
|
|
||||||
|
LDFLAGS += \
|
||||||
|
-pthread \
|
||||||
|
-lm
|
||||||
|
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto
|
||||||
|
ifneq ($(UNAME_S),Haiku)
|
||||||
|
ifneq ($(UNAME_S),OpenBSD)
|
||||||
|
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
|
-ldl
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
$(WINDOWS_TARGETS): LDFLAGS += \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto \
|
||||||
|
-lcrypt32 \
|
||||||
|
-ldbghelp \
|
||||||
|
-liphlpapi \
|
||||||
|
-lkernel32 \
|
||||||
|
-lole32 \
|
||||||
|
-luserenv \
|
||||||
|
-luuid \
|
||||||
|
-lws2_32 \
|
||||||
|
-lwsock32
|
||||||
|
$(ANDROID_TARGETS): LDFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
-ldl \
|
||||||
|
-llog \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto
|
||||||
|
$(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
|
$(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
|
-framework Foundation \
|
||||||
|
-framework CoreFoundation \
|
||||||
|
-framework UIKit \
|
||||||
|
-framework WebKit
|
||||||
|
|
||||||
|
unix: debug release
|
||||||
|
win: windebug winrelease
|
||||||
|
all: $(BUILD_TYPES)
|
||||||
|
.PHONY: all win unix
|
||||||
|
|
||||||
|
ALL_APP_OBJS := \
|
||||||
|
$(APP_OBJS) \
|
||||||
|
$(BLOWFISH_OBJS) \
|
||||||
|
$(LIBBACKTRACE_OBJS) \
|
||||||
|
$(MINIUNZIP_OBJS) \
|
||||||
|
$(PICOHTTPPARSER_OBJS) \
|
||||||
|
$(QUICKJS_OBJS) \
|
||||||
|
$(SODIUM_OBJS) \
|
||||||
|
$(SQLITE_OBJS) \
|
||||||
|
$(UV_OBJS) \
|
||||||
|
$(XOPT_OBJS)
|
||||||
|
|
||||||
|
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||||
|
-include $(DEPS)
|
||||||
|
|
||||||
|
define build_rules
|
||||||
|
$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
||||||
|
.PHONY: $(1)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
||||||
|
@echo [link] $$@
|
||||||
|
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.c
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo [c] $$@
|
||||||
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.m
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo [m] $$@
|
||||||
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.S
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo [as] $$@
|
||||||
|
@$$(AS) -c $$< -o $$@
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||||
|
|
||||||
|
src/version.h : $(firstword $(MAKEFILE_LIST))
|
||||||
|
@echo [version] $@
|
||||||
|
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
|
||||||
|
@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
|
||||||
|
|
||||||
|
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
||||||
|
@echo [android_version] $@
|
||||||
|
@sed -i \
|
||||||
|
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
||||||
|
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
||||||
|
-e 's/android:minSdkVersion="[[:digit:]]*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \
|
||||||
|
-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
|
||||||
|
$@
|
||||||
|
|
||||||
|
# Android support.
|
||||||
|
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo [aapt2] $@
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
||||||
|
|
||||||
|
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo [aapt2] $@
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
||||||
|
|
||||||
|
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
||||||
|
|
||||||
|
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||||
|
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||||
|
|
||||||
|
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||||
|
@echo [javac] $(CLASS_FILES)
|
||||||
|
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
||||||
|
|
||||||
|
out/apk/classes.dex: $(CLASS_FILES)
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo [d8] $@
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||||
|
|
||||||
|
PACKAGE_DIRS := \
|
||||||
|
apps/ \
|
||||||
|
core/ \
|
||||||
|
deps/codemirror/ \
|
||||||
|
deps/lit/
|
||||||
|
|
||||||
|
RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
||||||
|
|
||||||
|
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
|
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
|
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
|
||||||
|
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
|
||||||
|
out/apk/TildeFriends-arm-%.unsigned.apk:
|
||||||
|
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||||
|
@echo [aapt] $@
|
||||||
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||||
|
@cp out/apk/res.apk $@
|
||||||
|
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
||||||
|
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||||
|
@zip -u $@ -q -9 $(RAW_FILES)
|
||||||
|
|
||||||
|
out/apk/TildeFriends-x86-%.unsigned.apk:
|
||||||
|
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||||
|
@echo [aapt] $@
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||||
|
@cp out/apk/res.apk $@
|
||||||
|
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
||||||
|
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||||
|
@zip -u $@ -q -9 $(RAW_FILES)
|
||||||
|
|
||||||
|
out/%.apk: out/apk/%.unsigned.apk
|
||||||
|
@echo [apksigner] $(notdir $@)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
||||||
|
|
||||||
|
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk
|
||||||
|
.PHONY: release-apk
|
||||||
|
|
||||||
|
releaseapkgo: out/TildeFriends-arm-release.apk
|
||||||
|
@adb install -r $<
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
||||||
|
.PHONY: releaseapkgo
|
||||||
|
|
||||||
|
# iOS Support
|
||||||
|
out/%.app/Info.plist: src/ios/Info.plist
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@cp -v $< $@
|
||||||
|
out/%.app/tildefriends.png: src/ios/tildefriends.png
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@cp -v $< $@
|
||||||
|
|
||||||
|
out/%/data.zip: $(RAW_FILES)
|
||||||
|
@zip -u $@ -q -9 $(RAW_FILES)
|
||||||
|
|
||||||
|
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@cp -v $< $@
|
||||||
|
ifeq ($(HAVE_LINUX_IOS),1)
|
||||||
|
@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
|
||||||
|
endif
|
||||||
|
.SECONDARY:
|
||||||
|
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
||||||
|
@echo [ipa] $@
|
||||||
|
@rm -rf $@.tmp $@
|
||||||
|
@mkdir -p $@.tmp/Payload/tildefriends.app/
|
||||||
|
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
|
||||||
|
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
||||||
|
@rm -rf $@.tmp/
|
||||||
|
|
||||||
|
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
||||||
|
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
||||||
|
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
||||||
|
iosrelease-app: out/tildefriends-iosrelease.app/tildefriends
|
||||||
|
|
||||||
|
iosdebug-ipa: out/tildefriends-debug.ipa
|
||||||
|
iosrelease-ipa: out/tildefriends-release.ipa
|
||||||
|
.PHONY: iossimdebug-app iossimrelease-app iosdebug-app iosrelease-app
|
||||||
|
|
||||||
|
ios%go: out/tildefriends-ios%.app/tildefriends
|
||||||
|
ideviceinstaller -i $(realpath $(dir $<))
|
||||||
|
|
||||||
|
iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends
|
||||||
|
xcrun simctl install booted out/tildefriends-iossimdebug.app/
|
||||||
|
xcrun simctl launch booted com.unprompted.tildefriends
|
||||||
|
.PHONY: iossimdebuggo
|
||||||
|
|
||||||
|
apklog:
|
||||||
|
@adb logcat *:S tildefriends
|
||||||
|
.PHONY: apklog
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR)
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
dist: release-apk iosrelease-ipa
|
||||||
|
@echo "[export] $$(svn info --show-item url)"
|
||||||
|
@rm -rf tildefriends-$(VERSION_NUMBER)
|
||||||
|
@svn export -q . tildefriends-$(VERSION_NUMBER)
|
||||||
|
@echo "tildefriends-$(VERSION_NUMBER): $(VERSION_NAME)" > tildefriends-$(VERSION_NUMBER)/VERSION
|
||||||
|
@echo "[tar] tildefriends-$(VERSION_NUMBER).tar.xz"
|
||||||
|
@tar \
|
||||||
|
--exclude=apps/gg* \
|
||||||
|
--exclude=apps/welcome* \
|
||||||
|
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
||||||
|
--exclude=deps/libsodium/builds/msvc/vs* \
|
||||||
|
--exclude=deps/libsodium/builds/msvc/build \
|
||||||
|
--exclude=deps/libsodium/builds/msvc/properties \
|
||||||
|
--exclude=deps/libsodium/configure \
|
||||||
|
--exclude=deps/libsodium/test \
|
||||||
|
--exclude=deps/libuv/docs \
|
||||||
|
--exclude=deps/libuv/test \
|
||||||
|
--exclude=deps/openssl \
|
||||||
|
--exclude=deps/speedscope/*.map \
|
||||||
|
--exclude=deps/sqlite/shell.c \
|
||||||
|
--exclude=deps/zlib/contrib/vstudio \
|
||||||
|
--exclude=deps/zlib/doc \
|
||||||
|
-caf tildefriends-$(VERSION_NUMBER).tar.xz tildefriends-$(VERSION_NUMBER)
|
||||||
|
@rm -rf tildefriends-$(VERSION_NUMBER)
|
||||||
|
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
||||||
|
@cp out/TildeFriends-x86-release.apk TildeFriends-x86-$(VERSION_NUMBER).apk
|
||||||
|
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
||||||
|
@cp out/TildeFriends-arm-release.apk TildeFriends-arm-$(VERSION_NUMBER).apk
|
||||||
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
||||||
|
@cp out/tildefriends-release.ipa TildeFriends-$(VERSION_NUMBER).ipa
|
||||||
|
.PHONY: dist
|
||||||
|
|
||||||
|
dist-test: dist
|
||||||
|
@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
|
@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release
|
||||||
|
@docker build tildefriends-$(VERSION_NUMBER)/
|
||||||
|
@rm -rf tildefriends-$(VERSION_NUMBER)
|
||||||
|
.PHONY: dist-test
|
526
Makefile
526
Makefile
@ -1,526 +0,0 @@
|
|||||||
.ONESHELL:
|
|
||||||
.DELETE_ON_ERROR:
|
|
||||||
MAKEFLAGS += --warn-undefined-variables
|
|
||||||
MAKEFLAGS += --no-builtin-rules
|
|
||||||
|
|
||||||
VERSION_CODE := 9
|
|
||||||
VERSION_NUMBER := 0.0.9
|
|
||||||
VERSION_NAME := Failure is the only opportunity to begin again.
|
|
||||||
|
|
||||||
PROJECT = tildefriends
|
|
||||||
BUILD_DIR ?= out
|
|
||||||
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
|
|
||||||
UNAME_M := $(shell uname -m)
|
|
||||||
|
|
||||||
CFLAGS += \
|
|
||||||
-Wall \
|
|
||||||
-Wextra \
|
|
||||||
-Wno-unused-parameter \
|
|
||||||
-Wno-cast-function-type \
|
|
||||||
-MMD \
|
|
||||||
-ffunction-sections \
|
|
||||||
-fdata-sections \
|
|
||||||
-fno-exceptions \
|
|
||||||
-g
|
|
||||||
LDFLAGS += -Wl,--gc-sections
|
|
||||||
|
|
||||||
ANDROID_SDK ?= ~/Android/Sdk
|
|
||||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
|
|
||||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
|
|
||||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
|
|
||||||
ANDROID_NDK_API_VERSION := 31
|
|
||||||
ANDROID_MIN_SDK_VERSION := 26
|
|
||||||
|
|
||||||
ANDROID_ARM64_TARGETS := \
|
|
||||||
out/androiddebug/tildefriends \
|
|
||||||
out/androidrelease/tildefriends
|
|
||||||
ANDROID_X86_64_TARGETS := \
|
|
||||||
out/androiddebug-x86_64/tildefriends \
|
|
||||||
out/androidrelease-x86_64/tildefriends
|
|
||||||
ANDROID_TARGETS := \
|
|
||||||
$(ANDROID_X86_64_TARGETS) \
|
|
||||||
$(ANDROID_ARM64_TARGETS)
|
|
||||||
|
|
||||||
DEBUG_TARGETS := \
|
|
||||||
out/debug/tildefriends \
|
|
||||||
out/windebug/tildefriends \
|
|
||||||
out/androiddebug/tildefriends \
|
|
||||||
out/androiddebug-x86_64/tildefriends
|
|
||||||
RELEASE_TARGETS := \
|
|
||||||
out/release/tildefriends \
|
|
||||||
out/winrelease/tildefriends \
|
|
||||||
out/androidrelease/tildefriends \
|
|
||||||
out/androidrelease-x86_64/tildefriends
|
|
||||||
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
|
||||||
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
|
||||||
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(DEBUG_TARGETS) $(RELEASE_TARGETS))
|
|
||||||
|
|
||||||
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
|
||||||
$(NONANDROID_TARGETS): LDFLAGS += -rdynamic
|
|
||||||
$(ANDROID_TARGETS): CFLAGS += \
|
|
||||||
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
|
||||||
-fPIC \
|
|
||||||
-fdebug-compilation-dir . \
|
|
||||||
-fomit-frame-pointer \
|
|
||||||
-fno-asynchronous-unwind-tables
|
|
||||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
|
||||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
|
||||||
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
|
||||||
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
|
||||||
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
|
|
||||||
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
|
|
||||||
windebug winrelease: AS = $(CC)
|
|
||||||
windebug winrelease: CFLAGS += \
|
|
||||||
-D_WIN32_WINNT=0x0A00 \
|
|
||||||
-DWINVER=0x0A00 \
|
|
||||||
-DNTDDI_VERSION=NTDDI_WIN10 \
|
|
||||||
-Ideps/openssl/mingw64/include
|
|
||||||
windebug winrelease: LDFLAGS += \
|
|
||||||
-static \
|
|
||||||
-lm \
|
|
||||||
-Ldeps/openssl/mingw64/lib
|
|
||||||
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
|
|
||||||
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
|
|
||||||
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
|
|
||||||
$(ANDROID_TARGETS): AS = $(CC)
|
|
||||||
$(ANDROID_TARGETS): CFLAGS += \
|
|
||||||
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
|
|
||||||
-Wno-unknown-warning-option
|
|
||||||
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
|
|
||||||
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
|
||||||
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
|
||||||
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
|
||||||
|
|
||||||
ifeq ($(UNAME_M),x86_64)
|
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
|
||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
|
||||||
endif
|
|
||||||
|
|
||||||
get_objs = \
|
|
||||||
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
|
|
||||||
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
|
||||||
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
|
|
||||||
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
|
||||||
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
|
|
||||||
|
|
||||||
APP_SOURCES := $(wildcard src/*.c)
|
|
||||||
APP_OBJS := $(call get_objs,APP_SOURCES)
|
|
||||||
$(APP_OBJS): CFLAGS += \
|
|
||||||
-Ideps/base64c/include \
|
|
||||||
-Ideps/crypt_blowfish \
|
|
||||||
-Ideps/libbacktrace \
|
|
||||||
-Ideps/libsodium \
|
|
||||||
-Ideps/libsodium/src/libsodium/include \
|
|
||||||
-Ideps/libuv/include \
|
|
||||||
-Ideps/zlib \
|
|
||||||
-Ideps/zlib/contrib/minizip \
|
|
||||||
-Ideps/picohttpparser \
|
|
||||||
-Ideps/quickjs \
|
|
||||||
-Ideps/sqlite \
|
|
||||||
-Ideps/valgrind \
|
|
||||||
-Ideps/xopt \
|
|
||||||
-Wdouble-promotion \
|
|
||||||
-Werror
|
|
||||||
|
|
||||||
BLOWFISH_SOURCES := \
|
|
||||||
deps/crypt_blowfish/crypt_blowfish.c \
|
|
||||||
deps/crypt_blowfish/crypt_gensalt.c \
|
|
||||||
deps/crypt_blowfish/wrapper.c
|
|
||||||
BLOWFISH_SOURCES_win = \
|
|
||||||
deps/crypt_blowfish/x86.S
|
|
||||||
BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
|
|
||||||
|
|
||||||
UV_SOURCES := \
|
|
||||||
deps/libuv/src/fs-poll.c \
|
|
||||||
deps/libuv/src/idna.c \
|
|
||||||
deps/libuv/src/inet.c \
|
|
||||||
deps/libuv/src/random.c \
|
|
||||||
deps/libuv/src/strscpy.c \
|
|
||||||
deps/libuv/src/strtok.c \
|
|
||||||
deps/libuv/src/threadpool.c \
|
|
||||||
deps/libuv/src/timer.c \
|
|
||||||
deps/libuv/src/uv-common.c \
|
|
||||||
deps/libuv/src/uv-data-getter-setters.c \
|
|
||||||
deps/libuv/src/version.c
|
|
||||||
UV_SOURCES_unix := \
|
|
||||||
deps/libuv/src/unix/async.c \
|
|
||||||
deps/libuv/src/unix/core.c \
|
|
||||||
deps/libuv/src/unix/dl.c \
|
|
||||||
deps/libuv/src/unix/fs.c \
|
|
||||||
deps/libuv/src/unix/getaddrinfo.c \
|
|
||||||
deps/libuv/src/unix/getnameinfo.c \
|
|
||||||
deps/libuv/src/unix/linux.c \
|
|
||||||
deps/libuv/src/unix/loop-watcher.c \
|
|
||||||
deps/libuv/src/unix/loop.c \
|
|
||||||
deps/libuv/src/unix/pipe.c \
|
|
||||||
deps/libuv/src/unix/poll.c \
|
|
||||||
deps/libuv/src/unix/process.c \
|
|
||||||
deps/libuv/src/unix/procfs-exepath.c \
|
|
||||||
deps/libuv/src/unix/proctitle.c \
|
|
||||||
deps/libuv/src/unix/random-devurandom.c \
|
|
||||||
deps/libuv/src/unix/random-getrandom.c \
|
|
||||||
deps/libuv/src/unix/random-sysctl-linux.c \
|
|
||||||
deps/libuv/src/unix/signal.c \
|
|
||||||
deps/libuv/src/unix/stream.c \
|
|
||||||
deps/libuv/src/unix/tcp.c \
|
|
||||||
deps/libuv/src/unix/thread.c \
|
|
||||||
deps/libuv/src/unix/tty.c \
|
|
||||||
deps/libuv/src/unix/udp.c
|
|
||||||
UV_SOURCES_android := \
|
|
||||||
deps/libuv/src/unix/random-getentropy.c
|
|
||||||
UV_SOURCES_win := \
|
|
||||||
deps/libuv/src/win/async.c \
|
|
||||||
deps/libuv/src/win/core.c \
|
|
||||||
deps/libuv/src/win/detect-wakeup.c \
|
|
||||||
deps/libuv/src/win/dl.c \
|
|
||||||
deps/libuv/src/win/error.c \
|
|
||||||
deps/libuv/src/win/fs-event.c \
|
|
||||||
deps/libuv/src/win/fs.c \
|
|
||||||
deps/libuv/src/win/getaddrinfo.c \
|
|
||||||
deps/libuv/src/win/getnameinfo.c \
|
|
||||||
deps/libuv/src/win/handle.c \
|
|
||||||
deps/libuv/src/win/loop-watcher.c \
|
|
||||||
deps/libuv/src/win/pipe.c \
|
|
||||||
deps/libuv/src/win/poll.c \
|
|
||||||
deps/libuv/src/win/process-stdio.c \
|
|
||||||
deps/libuv/src/win/process.c \
|
|
||||||
deps/libuv/src/win/signal.c \
|
|
||||||
deps/libuv/src/win/snprintf.c \
|
|
||||||
deps/libuv/src/win/stream.c \
|
|
||||||
deps/libuv/src/win/tcp.c \
|
|
||||||
deps/libuv/src/win/thread.c \
|
|
||||||
deps/libuv/src/win/tty.c \
|
|
||||||
deps/libuv/src/win/udp.c \
|
|
||||||
deps/libuv/src/win/util.c \
|
|
||||||
deps/libuv/src/win/winapi.c \
|
|
||||||
deps/libuv/src/win/winsock.c
|
|
||||||
UV_OBJS := $(call get_objs,UV_SOURCES)
|
|
||||||
$(UV_OBJS): CFLAGS += \
|
|
||||||
-Ideps/libuv/include \
|
|
||||||
-Ideps/libuv/src \
|
|
||||||
-Wno-dangling-pointer \
|
|
||||||
-Wno-incompatible-pointer-types \
|
|
||||||
-Wno-maybe-uninitialized \
|
|
||||||
-Wno-sign-compare \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-unused-result \
|
|
||||||
-Wno-unused-variable \
|
|
||||||
-D_GNU_SOURCE
|
|
||||||
|
|
||||||
SODIUM_SOURCES := \
|
|
||||||
deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_verify/sodium/verify.c \
|
|
||||||
deps/libsodium/src/libsodium/randombytes/randombytes.c \
|
|
||||||
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/core.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/codecs.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/runtime.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/utils.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/version.c
|
|
||||||
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
|
||||||
$(SODIUM_OBJS): CFLAGS += \
|
|
||||||
-DCONFIGURED=1 \
|
|
||||||
-DMINIMAL=1 \
|
|
||||||
-Wno-unused-function \
|
|
||||||
-Wno-unused-variable \
|
|
||||||
-Wno-type-limits \
|
|
||||||
-Wno-unknown-pragmas \
|
|
||||||
-Ideps/libsodium/builds/msvc \
|
|
||||||
-Ideps/libsodium/src/libsodium/include/sodium
|
|
||||||
|
|
||||||
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
|
||||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
|
||||||
$(SQLITE_OBJS): CFLAGS += \
|
|
||||||
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
|
||||||
-DSQLITE_DEFAULT_MEMSTATUS=0 \
|
|
||||||
-DSQLITE_DQS=0 \
|
|
||||||
-DSQLITE_ENABLE_MEMSYS5 \
|
|
||||||
-DSQLITE_ENABLE_FTS5 \
|
|
||||||
-DSQLITE_ENABLE_JSON1 \
|
|
||||||
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
|
|
||||||
-DSQLITE_MAX_ATTACHED=0 \
|
|
||||||
-DSQLITE_MAX_COLUMN=100 \
|
|
||||||
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
|
||||||
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
|
||||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
|
||||||
-DSQLITE_MAX_LENGTH=5242880 \
|
|
||||||
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
|
||||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
|
||||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
|
||||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
|
||||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
|
||||||
-DSQLITE_OMIT_DEPRECATED \
|
|
||||||
-DSQLITE_OMIT_DESERIALIZE \
|
|
||||||
-DSQLITE_OMIT_LOAD_EXTENSION \
|
|
||||||
-DSQLITE_OMIT_TCL_VARIABLE \
|
|
||||||
-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
|
|
||||||
-DSQLITE_SECURE_DELETE \
|
|
||||||
-DSQLITE_THREADSAFE=0 \
|
|
||||||
-DSQLITE_UNTESTABLE \
|
|
||||||
-DSQLITE_USE_ALLOCA \
|
|
||||||
-DHAVE_ISNAN \
|
|
||||||
-Wno-implicit-fallthrough \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-unused-function \
|
|
||||||
-Wno-unused-variable
|
|
||||||
|
|
||||||
XOPT_SOURCES := deps/xopt/xopt.c
|
|
||||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
|
||||||
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
|
||||||
-DHAVE_SNPRINTF \
|
|
||||||
-DHAVE_VSNPRINTF \
|
|
||||||
-DHAVE_VASNPRINTF \
|
|
||||||
-DHAVE_VASPRINTF \
|
|
||||||
-Dvsnprintf=rpl_vsnprintf
|
|
||||||
$(XOPT_OBJS): CFLAGS += \
|
|
||||||
-Wno-implicit-const-int-float-conversion
|
|
||||||
|
|
||||||
QUICKJS_SOURCES := \
|
|
||||||
deps/quickjs/cutils.c \
|
|
||||||
deps/quickjs/libbf.c \
|
|
||||||
deps/quickjs/libregexp.c \
|
|
||||||
deps/quickjs/libunicode.c \
|
|
||||||
deps/quickjs/quickjs.c
|
|
||||||
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
|
||||||
$(QUICKJS_OBJS): CFLAGS += \
|
|
||||||
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
|
||||||
-DCONFIG_BIGNUM \
|
|
||||||
-D_GNU_SOURCE \
|
|
||||||
-Wno-enum-conversion \
|
|
||||||
-Wno-implicit-const-int-float-conversion \
|
|
||||||
-Wno-implicit-fallthrough \
|
|
||||||
-Wno-sign-compare \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-unused-variable
|
|
||||||
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
|
||||||
|
|
||||||
LIBBACKTRACE_SOURCES := \
|
|
||||||
deps/libbacktrace/atomic.c \
|
|
||||||
deps/libbacktrace/backtrace.c \
|
|
||||||
deps/libbacktrace/dwarf.c \
|
|
||||||
deps/libbacktrace/fileline.c \
|
|
||||||
deps/libbacktrace/print.c \
|
|
||||||
deps/libbacktrace/simple.c \
|
|
||||||
deps/libbacktrace/sort.c \
|
|
||||||
deps/libbacktrace/state.c
|
|
||||||
LIBBACKTRACE_SOURCES_unix := \
|
|
||||||
deps/libbacktrace/elf.c \
|
|
||||||
deps/libbacktrace/mmap.c \
|
|
||||||
deps/libbacktrace/mmapio.c \
|
|
||||||
deps/libbacktrace/posix.c
|
|
||||||
LIBBACKTRACE_SOURCES_win := \
|
|
||||||
deps/libbacktrace/alloc.c \
|
|
||||||
deps/libbacktrace/pecoff.c \
|
|
||||||
deps/libbacktrace/posix.c \
|
|
||||||
deps/libbacktrace/read.c
|
|
||||||
LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
|
|
||||||
$(LIBBACKTRACE_OBJS): CFLAGS += \
|
|
||||||
-Ideps/libbacktrace_config \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-maybe-initialized \
|
|
||||||
-Wno-unused-function \
|
|
||||||
-DBACKTRACE_ELF_SIZE=64
|
|
||||||
|
|
||||||
PICOHTTPPARSER_SOURCES := \
|
|
||||||
deps/picohttpparser/picohttpparser.c
|
|
||||||
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
|
||||||
|
|
||||||
MINIUNZIP_SOURCES := \
|
|
||||||
deps/zlib/contrib/minizip/unzip.c \
|
|
||||||
deps/zlib/contrib/minizip/ioapi.c \
|
|
||||||
deps/zlib/adler32.c \
|
|
||||||
deps/zlib/crc32.c \
|
|
||||||
deps/zlib/inffast.c \
|
|
||||||
deps/zlib/inflate.c \
|
|
||||||
deps/zlib/inftrees.c \
|
|
||||||
deps/zlib/zutil.c
|
|
||||||
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
|
|
||||||
$(MINIUNZIP_OBJS): CFLAGS += \
|
|
||||||
-Ideps/zlib \
|
|
||||||
-Wno-maybe-uninitialized
|
|
||||||
|
|
||||||
LDFLAGS += \
|
|
||||||
-pthread \
|
|
||||||
-lm
|
|
||||||
debug release: LDFLAGS += \
|
|
||||||
-ldl \
|
|
||||||
-lssl \
|
|
||||||
-lcrypto
|
|
||||||
windebug winrelease: LDFLAGS += \
|
|
||||||
-lssl \
|
|
||||||
-lcrypto \
|
|
||||||
-lcrypt32 \
|
|
||||||
-ldbghelp \
|
|
||||||
-liphlpapi \
|
|
||||||
-lkernel32 \
|
|
||||||
-lole32 \
|
|
||||||
-luserenv \
|
|
||||||
-luuid \
|
|
||||||
-lws2_32 \
|
|
||||||
-lwsock32
|
|
||||||
$(ANDROID_TARGETS): LDFLAGS += \
|
|
||||||
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
|
|
||||||
-ldl \
|
|
||||||
-llog \
|
|
||||||
-lssl \
|
|
||||||
-lcrypto
|
|
||||||
|
|
||||||
unix: debug release
|
|
||||||
win: windebug winrelease
|
|
||||||
all: $(BUILD_TYPES) out/TildeFriends-debug.apk out/TildeFriends-release.apk
|
|
||||||
.PHONY: all win unix
|
|
||||||
|
|
||||||
ALL_APP_OBJS := \
|
|
||||||
$(APP_OBJS) \
|
|
||||||
$(BLOWFISH_OBJS) \
|
|
||||||
$(LIBBACKTRACE_OBJS) \
|
|
||||||
$(MINIUNZIP_OBJS) \
|
|
||||||
$(PICOHTTPPARSER_OBJS) \
|
|
||||||
$(QUICKJS_OBJS) \
|
|
||||||
$(SODIUM_OBJS) \
|
|
||||||
$(SQLITE_OBJS) \
|
|
||||||
$(UV_OBJS) \
|
|
||||||
$(XOPT_OBJS)
|
|
||||||
|
|
||||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
|
||||||
-include $(DEPS)
|
|
||||||
|
|
||||||
define build_rules
|
|
||||||
$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
|
||||||
.PHONY: $(1)
|
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
|
||||||
@echo [link] $$@
|
|
||||||
@$$(CC) -o $$@ -Wl,-Map,$$@.map $$^ $$(LDFLAGS)
|
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.c
|
|
||||||
@mkdir -p $$(dir $$@)
|
|
||||||
@echo [c] $$@
|
|
||||||
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.S
|
|
||||||
@mkdir -p $$(dir $$@)
|
|
||||||
@echo [as] $$@
|
|
||||||
@$$(AS) -c $$< -o $$@
|
|
||||||
endef
|
|
||||||
|
|
||||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
|
||||||
|
|
||||||
src/version.h : $(firstword $(MAKEFILE_LIST))
|
|
||||||
@echo [version] $@
|
|
||||||
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"\n#define VERSION_NAME \"$(VERSION_NAME)\"\n" > $@
|
|
||||||
|
|
||||||
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
|
||||||
@echo [android_version] $@
|
|
||||||
@sed -i \
|
|
||||||
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
|
||||||
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
|
||||||
-e 's/android:minSdkVersion=".*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \
|
|
||||||
$@
|
|
||||||
|
|
||||||
# Android support.
|
|
||||||
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
|
||||||
@mkdir -p $(dir $@)
|
|
||||||
@echo [aapt2] $@
|
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
|
||||||
|
|
||||||
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
|
||||||
@mkdir -p $(dir $@)
|
|
||||||
@echo [aapt2] $@
|
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
|
||||||
|
|
||||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
|
||||||
@mkdir -p $(dir $@)
|
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
|
||||||
|
|
||||||
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
|
||||||
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
|
||||||
|
|
||||||
$(CLASS_FILES) &: $(JAVA_FILES)
|
|
||||||
@echo [javac] $(CLASS_FILES)
|
|
||||||
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
|
||||||
|
|
||||||
out/apk/classes.dex: $(CLASS_FILES)
|
|
||||||
@mkdir -p $(dir $@)
|
|
||||||
@echo [d8] $@
|
|
||||||
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
|
||||||
|
|
||||||
PACKAGE_DIRS := \
|
|
||||||
apps/ \
|
|
||||||
core/ \
|
|
||||||
deps/codemirror/ \
|
|
||||||
deps/lit/ \
|
|
||||||
deps/split/ \
|
|
||||||
deps/smoothie/
|
|
||||||
|
|
||||||
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
|
|
||||||
|
|
||||||
out/apk/TildeFriends-debug.unsigned.apk: BUILD_TYPE := debug
|
|
||||||
out/apk/TildeFriends-release.unsigned.apk: BUILD_TYPE := release
|
|
||||||
|
|
||||||
out/apk/TildeFriends-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
|
||||||
out/apk/TildeFriends-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
|
||||||
|
|
||||||
out/%.unsigned.apk:
|
|
||||||
@mkdir -p $(dir $@) out/apk$(BUILD_TYPE)/bin/aarch64/ out/apk$(BUILD_TYPE)/bin/x86_64/
|
|
||||||
@echo [aapt] $@
|
|
||||||
@cp out/android$(BUILD_TYPE)/tildefriends out/apk$(BUILD_TYPE)/bin/aarch64/
|
|
||||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk$(BUILD_TYPE)/bin/x86_64/
|
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/aarch64/tildefriends
|
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/x86_64/tildefriends
|
|
||||||
@cp out/apk/res.apk $@
|
|
||||||
@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
|
|
||||||
@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
|
||||||
@zip -u $@ -q -9 -x '*.map' -r $(PACKAGE_DIRS) $(RAW_FILES)
|
|
||||||
|
|
||||||
out/%.apk: out/apk/%.unsigned.apk
|
|
||||||
@echo [apksigner] $(notdir $@)
|
|
||||||
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
|
||||||
|
|
||||||
apk: out/TildeFriends-release.apk
|
|
||||||
.PHONY: apk
|
|
||||||
|
|
||||||
apkgo: out/TildeFriends-release.apk
|
|
||||||
@adb install $<
|
|
||||||
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
|
||||||
.PHONY: apkgo
|
|
||||||
|
|
||||||
apklog:
|
|
||||||
@adb logcat *:S tildefriends
|
|
||||||
.PHONY: apklog
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf $(BUILD_DIR)
|
|
||||||
.PHONY: clean
|
|
@ -15,8 +15,9 @@ Scuttlebutt, as well as a platform for writing and running web applications.
|
|||||||
are kept up to date in the tree.
|
are kept up to date in the tree.
|
||||||
2. To build, run `make debug` or `make release`. An executable will be
|
2. To build, run `make debug` or `make release`. An executable will be
|
||||||
generated in a subdirectory of `out/`.
|
generated in a subdirectory of `out/`.
|
||||||
3. `make windebug` or `make winrelease` will generate a windows executable
|
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
||||||
which might work.
|
the right dependencies in the right places. `make windebug winrelease
|
||||||
|
iosdebug-ipa iosrelease-ipa release-apk`.
|
||||||
4. To build in docker, `docker build .`.
|
4. To build in docker, `docker build .`.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
@ -9,6 +9,7 @@ tfrpc.register(function global_settings_set(key, value) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
try {
|
||||||
let data = {
|
let data = {
|
||||||
users: {},
|
users: {},
|
||||||
granted: await core.allPermissionsGranted(),
|
granted: await core.allPermissionsGranted(),
|
||||||
@ -18,5 +19,8 @@ async function main() {
|
|||||||
data.users[user] = await core.permissionsForUser(user);
|
data.users[user] = await core.permissionsForUser(user);
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
||||||
|
} catch {
|
||||||
|
await app.setDocument('<span style="color: #f00">Only an administrator can modify these settings.</span>');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
main();
|
main();
|
@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html style="width: 100%">
|
||||||
<head>
|
<head>
|
||||||
<script>const g_data = $data;</script>
|
<script>const g_data = $data;</script>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body style="color: #fff; width: 100%">
|
||||||
<h1>Tilde Friends Administration</h1>
|
<h1>Tilde Friends Administration</h1>
|
||||||
</body>
|
</body>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
|
@ -25,29 +25,37 @@ window.addEventListener('load', function() {
|
|||||||
function input_template(key, description) {
|
function input_template(key, description) {
|
||||||
if (description.type === 'boolean') {
|
if (description.type === 'boolean') {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em">
|
||||||
<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
|
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
||||||
<span>${description.description}</span>
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (description.type === 'textarea') {
|
} else if (description.type === 'textarea') {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em"">
|
||||||
<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div style="width: 100%; padding: 0; margin: 0">
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
<div style="width: 90%; padding: 0 margin: 0">
|
||||||
<span>${description.description}</span>
|
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
|
||||||
|
</div>
|
||||||
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em">
|
||||||
<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||||
<span>${description.description}</span>
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -67,12 +75,13 @@ window.addEventListener('load', function() {
|
|||||||
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
const page_template = (data) =>
|
const page_template = (data) =>
|
||||||
html`<div>
|
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||||
<h2>Global Settings</h2>
|
<h2>Global Settings</h2>
|
||||||
<div style="display: grid">
|
<div>
|
||||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
||||||
</div>
|
</div>
|
||||||
${users_template(data.users)}
|
${users_template(data.users)}
|
||||||
</div>`;
|
</div>
|
||||||
|
`;
|
||||||
render(page_template(g_data), document.body);
|
render(page_template(g_data), document.body);
|
||||||
});
|
});
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📜"
|
"emoji": "📜",
|
||||||
|
"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
356
apps/api/docs.js
Normal file
356
apps/api/docs.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
export const docs = {};
|
||||||
|
|
||||||
|
docs.global = `# Tilde Friends API Documentation
|
||||||
|
|
||||||
|
Welcome to the Tilde Friends API documentation.
|
||||||
|
|
||||||
|
* [App Globals](#App_Globals)
|
||||||
|
* [Database Interface](#Database)
|
||||||
|
* [Remote Procedure Calls](#tfrpc)
|
||||||
|
|
||||||
|
<a id="App_Globals"></a>
|
||||||
|
## <span style="color: #aaf">App Globals</span>
|
||||||
|
The following are functions and values exposed to all apps in their \`app.js\` or \`handler.js\`. Most
|
||||||
|
of these are asynchronous, returning a \`Promise\` that will be resolved when the call completes, unless
|
||||||
|
noted otherwise.
|
||||||
|
|
||||||
|
This is all a work in progess. These are liable to change without warning. Feedback is welcome.
|
||||||
|
|
||||||
|
The exposed functions in this API balance multiple competing needs:
|
||||||
|
* The surface area of the exposed API ought to be fairly minimal. If something can be implemented entirely app-side, that is
|
||||||
|
generally preferred over building it into the core.
|
||||||
|
* Everything is built on this API. Ideally the admin app, the SSB app, and the editor all use standard API exposed to all
|
||||||
|
apps, with appropriate permission guards in place making it so that only trusted apps do potentially destructive operations.
|
||||||
|
There will be some things here that aren't necessarily general use to support what's required.
|
||||||
|
|
||||||
|
If you are looking at the [Tilde Friends source code](https://www.tildefriends.net/~cory/releases/),
|
||||||
|
the vast majority of these are implemented in \`src/*.js.c\` files, and exposed to apps via \`core/core.js\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['core.user.credentials.session.name'] = `
|
||||||
|
*String* The name of the authenticated user.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.setDocument()'] = `
|
||||||
|
Set the contents of the client <iframe/>.
|
||||||
|
### Parameters
|
||||||
|
* *String* **html** The HTML contents.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['core.apps()'] = `
|
||||||
|
Gets a list of apps owned by the current user.
|
||||||
|
### Returns
|
||||||
|
*Array* An array of string names of the apps owned by the current user.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['core.url'] = `
|
||||||
|
The url by which the running app is being invoked.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.localStorageSet()'] = `
|
||||||
|
Set a value in browser local storage.
|
||||||
|
### Parameters
|
||||||
|
*String* **key** The localStorage key to set.
|
||||||
|
*String* **value** The localStorage value to set.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.localStorageGet()'] = `
|
||||||
|
Gets a value from browser local storage.
|
||||||
|
### Parameters
|
||||||
|
*String* **key** The key with which the value was set.
|
||||||
|
### Returns
|
||||||
|
*String* The value, or undefined.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.print()'] = `
|
||||||
|
Log information for debugging purposes to the server and to the connected browser console.
|
||||||
|
### Parameters
|
||||||
|
* ... Any args to print.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.createIdentity()'] = `
|
||||||
|
Create a new SSB identity.
|
||||||
|
### Returns
|
||||||
|
*String* The created identity public key.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.getIdentities()'] = `
|
||||||
|
Get all SSB identities owned by the current user.
|
||||||
|
### Returns
|
||||||
|
*Array* An array of public key strings.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.sqlAsync()'] = `
|
||||||
|
Run an SQL query against the sqlite database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **query** The sqlite query.
|
||||||
|
* *Array* **args** The query arguments to bind.
|
||||||
|
* *Function* **callback** Callback called for each row result.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.appendMessageWithIdentity()'] = `
|
||||||
|
Signs and stores a message in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **id** The public key of an SSB identity owned by the authenticated user.
|
||||||
|
* *Object* **message** The unsigned message.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.storeMessage()'] = `
|
||||||
|
Verifies and stores a signed message in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *Object* **message** The valid, signed message to store.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.blobStore()'] = `
|
||||||
|
Store a blob in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *String*/*Uint8Array* **blob** The blob contents to store
|
||||||
|
### Returns
|
||||||
|
*String* The stored blob ID.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.blobGet()'] = `
|
||||||
|
Fetches a blob from the database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **blob_id** The blob identifier to fetch (\`&....sha256\`).
|
||||||
|
### Returns
|
||||||
|
*ArrayBuffer* The blob data.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['print()'] = `
|
||||||
|
Log debug information both to the server's console and to the visiting user's browser console when possible.
|
||||||
|
### Parameters
|
||||||
|
* **...** Whatever you want to log. Will be joined with spaces.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database()'] = `
|
||||||
|
Returns a database instance that is specific to the authenticated user and the given key.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['my_shared_database()'] = `
|
||||||
|
Returns a database instance that is specific to the authenticated user and the given key.
|
||||||
|
### Parameters
|
||||||
|
* *String* **package_name** The database package name.
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['shared_database()'] = `
|
||||||
|
Returns a database instance that is shared between all users of the app, determined by its owner and app name.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['base64Decode()'] = `
|
||||||
|
Decode a base64 string to bytes.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* value The base64-encoded string.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The decoded bytes.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['base64Encode()'] = `
|
||||||
|
Encode bytes to a base64 string.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* The bytes to encode.
|
||||||
|
### Returns
|
||||||
|
*String* The base64-encoded string.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['utf8Decode()'] = `
|
||||||
|
Decode UTF-8 bytes to a string.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **value** The value to decode.
|
||||||
|
### Returns
|
||||||
|
*String* The value as a string.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['utf8Encode()'] = `
|
||||||
|
Encodes a string to UTF-8 bytes.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* **value** The value to encode.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The encoded \`value\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['setTimeout()'] = `
|
||||||
|
Call a function after some delay.
|
||||||
|
### Parameters
|
||||||
|
* *Function* **callback** The function to call.
|
||||||
|
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['parseHttpRequest()'] = `
|
||||||
|
Parses an HTTP request.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
|
||||||
|
indicate when and where it is complete.
|
||||||
|
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
|
||||||
|
### Returns
|
||||||
|
* *Integer* **-2** if the request is incomplete.
|
||||||
|
* *Integer* **-1** if the request could not be parsed.
|
||||||
|
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['parseHttpResponse()'] = `
|
||||||
|
Parses an HTTP response.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
|
||||||
|
indicate when and where it is complete.
|
||||||
|
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
|
||||||
|
### Returns
|
||||||
|
* *Integer* **-2** if the response is incomplete.
|
||||||
|
* *Integer* **-1** if the response could not be parsed.
|
||||||
|
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['sha1Digest()'] =`
|
||||||
|
Calculates a SHA1 digest.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* **value** The value for which to calculate the digest.
|
||||||
|
### Returns
|
||||||
|
*String* The SHA1 digest of UTF-8 encoded \`value\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['maskBytes()'] = `
|
||||||
|
Masks bytes for WebSocket communication.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **bytes** The byte array of data to mask.
|
||||||
|
* *Uint32* **mask** The mask to apply.
|
||||||
|
### Returns
|
||||||
|
*Uint32Array* The masked bytes.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['exit()'] = `
|
||||||
|
Exits the app. But why would you want to do that?
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Integer* **exit_code** System exit code.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['version()'] = `
|
||||||
|
Gets version information for the running server.
|
||||||
|
### Returns
|
||||||
|
*Object* Keys are things like \`name\` and \`number\` for the server itself and \`libuv\` and \`openssl\` for
|
||||||
|
dependencies. Values are *String* version numbers.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['platform()'] = `
|
||||||
|
Gets the host operating system platform of the running server.
|
||||||
|
### Returns
|
||||||
|
*String* The platform, one of \`windows\`, \`android\`, \`linux\`, or \`other\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['getFile()'] = `
|
||||||
|
Gets a file from the running app.
|
||||||
|
### Parameters
|
||||||
|
* *String* **name** Name of the file to retrieve.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The contents of a file from the app with the given name, or *undefined*.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs.database = `
|
||||||
|
# <span style="color: #aaf">Database</span>
|
||||||
|
Local-only storage is provided by a \`Database\` type representing a key-value store.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.get()'] = `
|
||||||
|
Gets a value from the database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
### Returns
|
||||||
|
*String* The value from the database or undefined if not found.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.getAll()'] = `
|
||||||
|
Gets all keys from the database.
|
||||||
|
### Returns
|
||||||
|
*Array* An array of *String* key names for all keys in the given database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.getLike()'] = `
|
||||||
|
Gets all keys and values from the database matching a pattern.
|
||||||
|
### Parameters
|
||||||
|
* *String* **pattern** An sqlite \`LIKE\` pattern to match keys against.
|
||||||
|
### Returns
|
||||||
|
*Object* An object whose keys are the database keys and values are the database values that match the given pattern.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.set()'] = `
|
||||||
|
Sets a value in the database, creating a new entry or replacing an existing entry.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
* *String* **value** The value.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.exchange()'] = `
|
||||||
|
Performs an atomic compare and exchange operation, setting a value in the database only if its current value matches what is expected.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
* *String* **expected** The expected value.
|
||||||
|
* *String* **value** The new value.
|
||||||
|
### Returns
|
||||||
|
*Boolean* true if the value is now the given value.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.remove()'] = `
|
||||||
|
Removes an entry from the database if it exists.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs.tfrpc = `
|
||||||
|
# <span style="color: #aaf" id="tfrpc">tfrpc</span>
|
||||||
|
\`tfrpc.js\` is a small helper script that is available to be used to facilitate communication between parts of an application.
|
||||||
|
|
||||||
|
\`tfrpc.js\` can be used to asynchronously make calls between the app code running in a sandboxed iframe in the browser
|
||||||
|
and the app process on the server.
|
||||||
|
|
||||||
|
From \`app.js\`:
|
||||||
|
\`\`\`
|
||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
|
||||||
|
From script running in the browser:
|
||||||
|
\`\`\`
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Either side can register or call functions, though they must be registered before they can be called. Arguments and return
|
||||||
|
values are ultimately serialized by means that attempt to preserve most JSON-serializable values as well as functions themselves.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['tfrpc.register()'] = `
|
||||||
|
Register a function, allowing it to be called remotely.
|
||||||
|
### Parameters
|
||||||
|
* *Function* **function** The function to register. Its name will be how it will be called.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['tfrpc.rpc.*()'] = `
|
||||||
|
Call a remote function.
|
||||||
|
### Parameters
|
||||||
|
* **...** Parameters to pass to the function.
|
||||||
|
### Returns
|
||||||
|
The return value of the called function.
|
||||||
|
`;
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💻"
|
"emoji": "💻",
|
||||||
|
"previous": "&33ngNe0YrH3JScss6krlCwddZcXl8C5szonp7DYy4qA=.sha256"
|
||||||
}
|
}
|
@ -8,9 +8,42 @@ async function fetch_info(apps) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetch_shared_apps() {
|
||||||
|
let messages = {};
|
||||||
|
await ssb.sqlAsync(`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages_fts('"application/tildefriends"')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
ORDER BY timestamp
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
function(row) {
|
||||||
|
let content = JSON.parse(row.content);
|
||||||
|
for (let mention of content.mentions) {
|
||||||
|
if (mention?.type === 'application/tildefriends') {
|
||||||
|
messages[JSON.stringify([row.author, mention.name])] = {
|
||||||
|
message: row,
|
||||||
|
blob: mention.link,
|
||||||
|
name: mention.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let result = {};
|
||||||
|
for (let app of Object.values(messages).sort((x, y) => y.message.timestamp - x.message.timestamp)) {
|
||||||
|
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
|
||||||
|
if (app_object) {
|
||||||
|
app_object.blob_id = app.blob;
|
||||||
|
result[app.name] = app_object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
var apps = await fetch_info(await core.apps());
|
var apps = await fetch_info(await core.apps());
|
||||||
var core_apps = await fetch_info(await core.apps('core'));
|
var core_apps = await fetch_info(await core.apps('core'));
|
||||||
|
let shared_apps = await fetch_shared_apps();
|
||||||
var doc = `<!DOCTYPE html>
|
var doc = `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@ -40,6 +73,8 @@ async function main() {
|
|||||||
<body style="background: #888">
|
<body style="background: #888">
|
||||||
<h1 id="apps_title">Apps</h1>
|
<h1 id="apps_title">Apps</h1>
|
||||||
<div id="apps" class="container"></div>
|
<div id="apps" class="container"></div>
|
||||||
|
<h1>Shared Apps</h1>
|
||||||
|
<div id="shared_apps" class="container"></div>
|
||||||
<h1>Core Apps</h1>
|
<h1>Core Apps</h1>
|
||||||
<div id="core_apps" class="container"></div>
|
<div id="core_apps" class="container"></div>
|
||||||
</body>
|
</body>
|
||||||
@ -50,18 +85,19 @@ async function main() {
|
|||||||
let div = list.appendChild(document.createElement('div'));
|
let div = list.appendChild(document.createElement('div'));
|
||||||
div.classList.add('app');
|
div.classList.add('app');
|
||||||
|
|
||||||
|
let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
|
||||||
let icon_a = document.createElement('a');
|
let icon_a = document.createElement('a');
|
||||||
let icon = document.createElement('div');
|
let icon = document.createElement('div');
|
||||||
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
||||||
icon.style.fontSize = 'xxx-large';
|
icon.style.fontSize = 'xxx-large';
|
||||||
icon_a.appendChild(icon);
|
icon_a.appendChild(icon);
|
||||||
icon_a.href = '/~' + name + '/' + app + '/';
|
icon_a.href = href;
|
||||||
icon_a.target = '_top';
|
icon_a.target = '_top';
|
||||||
div.appendChild(icon_a);
|
div.appendChild(icon_a);
|
||||||
|
|
||||||
let a = document.createElement('a');
|
let a = document.createElement('a');
|
||||||
a.appendChild(document.createTextNode(app));
|
a.appendChild(document.createTextNode(app));
|
||||||
a.href = '/~' + name + '/' + app + '/';
|
a.href = href;
|
||||||
a.target = '_top';
|
a.target = '_top';
|
||||||
div.appendChild(a);
|
div.appendChild(a);
|
||||||
}
|
}
|
||||||
@ -69,6 +105,7 @@ async function main() {
|
|||||||
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
|
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
|
||||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||||
|
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
|
||||||
</script>
|
</script>
|
||||||
</html>`;
|
</html>`;
|
||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app",
|
|
||||||
"emoji": "🛍"
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
async function get_apps() {
|
|
||||||
let results = {};
|
|
||||||
await ssb.sqlAsync(`
|
|
||||||
SELECT messages.*
|
|
||||||
FROM messages_fts('"application/tildefriends"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
||||||
ORDER BY timestamp
|
|
||||||
`,
|
|
||||||
[],
|
|
||||||
function(row) {
|
|
||||||
let content = JSON.parse(row.content);
|
|
||||||
for (let mention of content.mentions) {
|
|
||||||
if (mention?.type === 'application/tildefriends') {
|
|
||||||
results[JSON.stringify([row.author, mention.name])] = {
|
|
||||||
message: row,
|
|
||||||
blob: mention.link,
|
|
||||||
name: mention.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function render_app(app) {
|
|
||||||
return `
|
|
||||||
<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
|
|
||||||
<a href="/~cory/ssb/#${app.message.author}">@</a>
|
|
||||||
<a href="/~cory/ssb/#${app.message.id}">%</a>
|
|
||||||
<a href="/${app.blob}/">${app.name}</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let apps = await get_apps();
|
|
||||||
app.setDocument(`
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<base target="_top">
|
|
||||||
<style>
|
|
||||||
a:link { color: #bbf; }
|
|
||||||
a:visited { color: #ddd; }
|
|
||||||
a:hover { color: #ddf; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body style="color: #fff">
|
|
||||||
<h1>${apps.length} apps</h1>
|
|
||||||
${apps.map(render_app).join('\n')}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
5
apps/blog.json
Normal file
5
apps/blog.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🪵",
|
||||||
|
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
|
||||||
|
}
|
8
apps/blog/app.js
Normal file
8
apps/blog/app.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as blog from './blog.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
await app.setDocument(blog.render_html(blogs));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
189
apps/blog/blog.js
Normal file
189
apps/blog/blog.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import * as commonmark from './commonmark.min.js';
|
||||||
|
|
||||||
|
function escape(text) {
|
||||||
|
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttribute(text) {
|
||||||
|
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_blog_message(id) {
|
||||||
|
let message;
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
'SELECT author, timestamp, content FROM messages WHERE id = ?',
|
||||||
|
[id],
|
||||||
|
function(row) {
|
||||||
|
let content = JSON.parse(row.content);
|
||||||
|
message = {
|
||||||
|
author: row.author,
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
blog: content?.blog,
|
||||||
|
title: content?.title,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (message) {
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT json_extract(content, '$.name') AS name
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ?
|
||||||
|
AND json_extract(content, '$.type') = 'about'
|
||||||
|
AND json_extract(content, '$.about') = author
|
||||||
|
AND name IS NOT NULL
|
||||||
|
ORDER BY sequence DESC LIMIT 1
|
||||||
|
`,
|
||||||
|
[message.author],
|
||||||
|
function(row) {
|
||||||
|
message.name = row.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdown(md) {
|
||||||
|
let reader = new commonmark.Parser({safe: true});
|
||||||
|
let writer = new commonmark.HtmlRenderer();
|
||||||
|
let parsed = reader.parse(md || '');
|
||||||
|
let walker = parsed.walker();
|
||||||
|
let event, node;
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
node = event.node;
|
||||||
|
if (event.entering) {
|
||||||
|
if (node.destination?.startsWith('&')) {
|
||||||
|
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
|
} else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) {
|
||||||
|
node.destination = '/~core/ssb/#' + escape(node.destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function render_blog_post_html(blog_post) {
|
||||||
|
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><a href="./">🪵Tilde Friends Blog</a></h1>
|
||||||
|
<div>
|
||||||
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||||
|
<div>${markdown(blob)}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_blog_post(blog_post) {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<h2><a href="/~${core.app.owner}/${core.app.name}/${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
|
||||||
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||||
|
<div>${markdown(blog_post.summary)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render_html(blogs) {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>🪵Tilde Friends Blog</title>
|
||||||
|
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
||||||
|
<h1>🪵Tilde Friends Blog</h1>
|
||||||
|
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
||||||
|
</div>
|
||||||
|
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_blog_post_atom(blog_post) {
|
||||||
|
return `<entry>
|
||||||
|
<title>${escape(blog_post.title)}</title>
|
||||||
|
<link href="/~cory/ssb/#${blog_post.id}" />
|
||||||
|
<id>${blog_post.id}</id>
|
||||||
|
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
|
||||||
|
<summary>${escape(blog_post.summary)}</summary>
|
||||||
|
<author>
|
||||||
|
<name>${escape(blog_post.name)}</name>
|
||||||
|
<feed>${escape(blog_post.author)}</feed>
|
||||||
|
</author>
|
||||||
|
</entry>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render_atom(blogs) {
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>🪵Tilde Blog</title>
|
||||||
|
<subtitle>A subtitle.</subtitle>
|
||||||
|
<link href="${core.url}/atom" rel="self"/>
|
||||||
|
<link href="${core.url}"/>
|
||||||
|
<id>${core.url}</id>
|
||||||
|
<updated>${new Date().toString()}</updated>
|
||||||
|
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
|
||||||
|
</feed>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_posts() {
|
||||||
|
let blogs = [];
|
||||||
|
let ids = await ssb.getIdentities();
|
||||||
|
await ssb.sqlAsync(`
|
||||||
|
WITH
|
||||||
|
blogs AS (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
messages.id,
|
||||||
|
json_extract(messages.content, '$.title') AS title,
|
||||||
|
json_extract(messages.content, '$.summary') AS summary,
|
||||||
|
json_extract(messages.content, '$.blog') AS blog,
|
||||||
|
messages.timestamp
|
||||||
|
FROM messages_fts('blog')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'blog'),
|
||||||
|
public AS (
|
||||||
|
SELECT author FROM (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||||
|
json_extract(messages.content, '$.publicWebHosting') AS is_public
|
||||||
|
FROM messages_fts('about')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
|
||||||
|
WHERE author_rank = 1 AND is_public),
|
||||||
|
names AS (
|
||||||
|
SELECT author, name FROM (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||||
|
json_extract(messages.content, '$.name') AS name
|
||||||
|
FROM messages_fts('about')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'about' AND
|
||||||
|
json_extract(messages.content, '$.about') = messages.author AND
|
||||||
|
name IS NOT NULL)
|
||||||
|
WHERE author_rank = 1)
|
||||||
|
SELECT blogs.*, names.name FROM blogs
|
||||||
|
JOIN json_each(?) AS self ON self.value = blogs.author
|
||||||
|
JOIN public ON public.author = blogs.author
|
||||||
|
LEFT OUTER JOIN names ON names.author = blogs.author
|
||||||
|
ORDER BY blogs.timestamp DESC LIMIT 20
|
||||||
|
`, [JSON.stringify(ids)], function(row) {
|
||||||
|
blogs.push(row);
|
||||||
|
});
|
||||||
|
return blogs;
|
||||||
|
}
|
1
apps/blog/commonmark.min.js
vendored
Normal file
1
apps/blog/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
31
apps/blog/handler.js
Normal file
31
apps/blog/handler.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import * as blog from './blog.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
|
||||||
|
let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path;
|
||||||
|
let message = await blog.get_blog_message(id);
|
||||||
|
if (message) {
|
||||||
|
respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'});
|
||||||
|
} else {
|
||||||
|
respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'});
|
||||||
|
}
|
||||||
|
} else if (request.path == 'atom') {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
|
||||||
|
} else {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
for (let blog_post of blogs) {
|
||||||
|
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
||||||
|
if (request.path === title) {
|
||||||
|
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(function(error) {
|
||||||
|
respond({data: `<!DOCTYPE html>
|
||||||
|
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
|
||||||
|
});
|
120
apps/blog/lit-all.min.js
vendored
Normal file
120
apps/blog/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/blog/lit-all.min.js.map
Normal file
1
apps/blog/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app",
|
|
||||||
"emoji": "📚"
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
@ -1,16 +0,0 @@
|
|||||||
# Tilde Friends Developer's Guide
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
A Tilde Friends application runs on the server. To make an interesting
|
|
||||||
application that interacts with the client, it's necessary to understand
|
|
||||||
how the parts work together.
|
|
||||||
|
|
||||||
## Hello, world!
|
|
||||||
|
|
||||||
A simple starting point. Presents `Hello, world!` in the browser when
|
|
||||||
visited.
|
|
||||||
|
|
||||||
**app.js**:
|
|
||||||
```
|
|
||||||
app.setDocument('<h1>Hello, world!</h1>');
|
|
||||||
```
|
|
@ -1,12 +0,0 @@
|
|||||||
# Tilde Friends Documentation
|
|
||||||
|
|
||||||
Tilde Friends is a participating member of a greater social
|
|
||||||
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
|
|
||||||
adding a way to safely and securely write, share,
|
|
||||||
and run code in the form of server-side web applications.
|
|
||||||
|
|
||||||
- [Tilde Friends Vision](#vision)
|
|
||||||
- [Secure Scuttlebutt from Scratch](#ssb)
|
|
||||||
- [Structure](#structure)
|
|
||||||
- [Guide](#guide)
|
|
||||||
- [TODO](#todo)
|
|
@ -1,41 +0,0 @@
|
|||||||
# Secure Scuttlebutt from Scratch
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
This aims to be the missing reference for those who wish to create a Secure
|
|
||||||
Scuttlebutt client from scratch.
|
|
||||||
|
|
||||||
## Discovery
|
|
||||||
A good way to get started is to participate in local network discovery with a known working
|
|
||||||
client on the same network. The
|
|
||||||
[Scuttlebutt Programming Guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#local-network)
|
|
||||||
is a good start, here, with a few things to note:
|
|
||||||
|
|
||||||
1. Some clients advertise multiple addresses separated by semicolons (`;`).
|
|
||||||
2. Some clients advertise alternative protocols than `shs` and use hostnames instead of
|
|
||||||
IPv4 addresses.
|
|
||||||
|
|
||||||
So be prepared to accept variations.
|
|
||||||
|
|
||||||
There also an undocumented "new" style of discovery message.
|
|
||||||
|
|
||||||
## Secret Handshake, Box Stream, and RPC Protocol
|
|
||||||
Now that two clients are aware of eachother, they need to complete a secret handshake.
|
|
||||||
The [programming guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake)
|
|
||||||
is once again a good reference.
|
|
||||||
|
|
||||||
The box stream and RPC protocol can both be implemented from the
|
|
||||||
[same documentation](https://ssbc.github.io/scuttlebutt-protocol-guide/#box-stream)
|
|
||||||
without surprises.
|
|
||||||
|
|
||||||
## Synchronizing Data
|
|
||||||
|
|
||||||
... `ebt.replicate` or `createHistoryStream` ...
|
|
||||||
|
|
||||||
## Rooms
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## References
|
|
||||||
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
|
|
||||||
* [https://dev.planetary.social/](https://dev.planetary.social/)
|
|
||||||
* [https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints](https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints)
|
|
@ -1,65 +0,0 @@
|
|||||||
# Tilde Friends Structure
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
Tilde Friends is a mostly-self-contained executable written in C.
|
|
||||||
|
|
||||||
In combines the following key components:
|
|
||||||
- A Secure Scuttlebutt (SSB) client/server. This talks with other SSB
|
|
||||||
instances, storing messages and blobs for anyone visible to local
|
|
||||||
users as they are encountered and sharing anything published locally
|
|
||||||
as appropriate.
|
|
||||||
- An sqlite database. This is where the SSB instance stores its data.
|
|
||||||
The general schema involves a `messages` table, storing mostly JSON,
|
|
||||||
a `blobs` table storing arbitrary blob data, and a `properties` table,
|
|
||||||
storing arbitrary state gleaned from `messages` and `blobs`, generally
|
|
||||||
updated on demand and incrementally.
|
|
||||||
- A QuickJS runtime. The core process runs stock scripts and has access
|
|
||||||
and permission to use all resources. All other processes, which
|
|
||||||
includes everything which runs untrusted code created by Tilde Friends
|
|
||||||
users, are strictly sandboxed in ways similar to how web browsers run
|
|
||||||
untrusted code. All attempts to access potentially sensitive resources
|
|
||||||
are mediated through the core process.
|
|
||||||
|
|
||||||
When run with no arguments, it starts a web server on
|
|
||||||
[http://localhost:12345/](http://localhost:12345/) and an SSB node.
|
|
||||||
|
|
||||||
## Web Interface
|
|
||||||
The Tilde Friends web server provides access to Tilde Friends applications,
|
|
||||||
which are arbitrary user-defined web applications.
|
|
||||||
|
|
||||||
At the top left, in addition to some basic navigation links, is an `edit`
|
|
||||||
link. Anyone can view, modify, and run in-place the code to any Tilde
|
|
||||||
Friends application by using the in-browser editor.
|
|
||||||
|
|
||||||
At the top right, one can `login` (to save work in their own space)
|
|
||||||
or `logout` (proceeding as a guest).
|
|
||||||
|
|
||||||
The rest of the page is an iframe belonging to the application.
|
|
||||||
|
|
||||||
## Special Paths
|
|
||||||
|
|
||||||
- `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user`
|
|
||||||
is a username of a Tilde Friends account, and `app` is an arbitrary name
|
|
||||||
of an application saved by the given user.
|
|
||||||
- `/~user/app/file` - A raw file in an app.
|
|
||||||
- `/&blobid.ed25519` - A raw blob. Content-Type is inferred for at least
|
|
||||||
a few common image types.
|
|
||||||
|
|
||||||
## Communication Channels
|
|
||||||
Web Browser <-> Core <-> Sandbox
|
|
||||||
|
|
||||||
Visiting an application path delivers stock HTML and JavaScript which
|
|
||||||
establishes a WebSocket connection back to the server.
|
|
||||||
|
|
||||||
At this point, a new sandbox process is started in Tilde Friends, much
|
|
||||||
as a new sandboxed process might be started for a new tab in a web
|
|
||||||
browser. This process has a custom RPC connection to the core process
|
|
||||||
which holds the WebSocket connection to the browser.
|
|
||||||
|
|
||||||
The custom RPC communication between the sandbox process and the core
|
|
||||||
process facilitates passing and calling functions remotely. Calling a
|
|
||||||
function in another process returns a `Promise`.
|
|
||||||
|
|
||||||
An application will typically call `app.setDocument()` at startup to
|
|
||||||
populate the app's iframe in the web browser with its own client web
|
|
||||||
application resources.
|
|
@ -1,63 +0,0 @@
|
|||||||
# Tilde Friends TODO
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
## MVP3
|
|
||||||
- Sync status (problem feeds, messages/seconds stats, ...)
|
|
||||||
- app: wiki
|
|
||||||
- app: public blog
|
|
||||||
- Content-Disposition: download
|
|
||||||
- remove SSB credentials
|
|
||||||
- export SSB credentials
|
|
||||||
- initial: better empty news screen
|
|
||||||
- initial: remembered wrong user across login/logout
|
|
||||||
- initial: bad experience when following nobody
|
|
||||||
- make a cool independent app
|
|
||||||
- indicate when workspace differs from installed
|
|
||||||
- / => Something good.
|
|
||||||
- update docs
|
|
||||||
- audit + document API exposed to apps
|
|
||||||
- fix weird HTTP warnings
|
|
||||||
- channels
|
|
||||||
- placeholder/missing images
|
|
||||||
- no denial of service
|
|
||||||
- package standalone executable
|
|
||||||
- editor without app iframe
|
|
||||||
- sequence_before_author -> flags
|
|
||||||
- linkify ssb: links
|
|
||||||
- perfect rooms support
|
|
||||||
- connections 2.0
|
|
||||||
- make a better connections API
|
|
||||||
|
|
||||||
## Maybe Done
|
|
||||||
- blob_wants 2.0
|
|
||||||
- image downsample
|
|
||||||
- app: todo
|
|
||||||
- app: build archive
|
|
||||||
- update README
|
|
||||||
- administrators config
|
|
||||||
- apps name characters
|
|
||||||
- initial: can't switch to account when there is only one
|
|
||||||
- get tarball under 5MB
|
|
||||||
- rooms
|
|
||||||
- initial: doesn't refresh when create identity
|
|
||||||
- tf account timeout why
|
|
||||||
- ssb don't overflow boxes
|
|
||||||
- jwt for session tokens
|
|
||||||
- linkify https://...
|
|
||||||
- emoji reaction picker
|
|
||||||
- expose loads of stats
|
|
||||||
- confirm posting all new messages
|
|
||||||
- multiple identities per user, in database
|
|
||||||
- auto-populate data on initial launch
|
|
||||||
- make the docker image good / test it / use it
|
|
||||||
- leaking imports / exports
|
|
||||||
- file upload widget
|
|
||||||
- keep working on good error feedback
|
|
||||||
- build for windows
|
|
||||||
- installable apps (bring back an app message?)
|
|
||||||
- sqlStream => sqlExec or something
|
|
||||||
- !ssb from child process?
|
|
||||||
|
|
||||||
## Done
|
|
||||||
- update LICENSE
|
|
||||||
- logging to browser
|
|
@ -1,62 +0,0 @@
|
|||||||
# Tilde Friends Vision
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
Tilde Friends is a tool for making and sharing.
|
|
||||||
|
|
||||||
It is both a peer-to-peer social network client, participating in Secure
|
|
||||||
Scuttlebutt, and an environment for creating and running web applications.
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
This is a thing that I wanted to exist and wanted to work on. No other reason.
|
|
||||||
There is not a business model. I believe it is interesting and unique.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
1. Make it **easy and fun** to run all sorts of web applications.
|
|
||||||
|
|
||||||
2. Provide **security** that is easy to understand and protects your data.
|
|
||||||
|
|
||||||
3. Make **creating and sharing** web applications accessible to anyone with a
|
|
||||||
browser.
|
|
||||||
|
|
||||||
## Ways to Use Tilde Friends
|
|
||||||
1. **Social Network User**: This is a social network first. You are just here,
|
|
||||||
because your friends are. Or you like how we limit your message length or
|
|
||||||
short videos or whatever the trend is. If you are ambitious, you click links
|
|
||||||
and see interactive experiences (apps) that you wouldn't see elsewhere.
|
|
||||||
|
|
||||||
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
|
|
||||||
lists, wiki pages, ..., and you interact with them as though they were
|
|
||||||
cloud-hosted by a megacorporation. They just work, and you don't think twice.
|
|
||||||
|
|
||||||
3. **Group leader**: You host or use a small public instance, installing apps for
|
|
||||||
a group of friends to use as web visitors.
|
|
||||||
|
|
||||||
4. **Developer**: You like to write code and make or improve apps for fun or to
|
|
||||||
solve problems. When you encounter a Tilde Friends app on a strange server,
|
|
||||||
you know you can trivially modify it or download it to your own instance.
|
|
||||||
|
|
||||||
## Future Goals / Endgame
|
|
||||||
1. Mobile apps. This can run on your old phone. Maybe you won't be hosting
|
|
||||||
the web interface publicly, but you can sync, install and edit apps, and
|
|
||||||
otherwise get the full experience from a tiny touch screen.
|
|
||||||
|
|
||||||
2. The universal application runtime. The web browser is the universal
|
|
||||||
platform, but even for the simplest application that you might want to host
|
|
||||||
for your friends, cloud hosting, containers, and complicated dependencies might
|
|
||||||
all enter the mix. Tilde Friends, though it is yet another thing to host,
|
|
||||||
includes everything you need out of the box to run a vast variety of interesting
|
|
||||||
apps.
|
|
||||||
|
|
||||||
Tilde Friends will be built out, gradually providing safe access to host
|
|
||||||
resources and client resources the same way web browsers extended access to
|
|
||||||
resources like GPU, persistent storage, cameras, ... over the years.
|
|
||||||
|
|
||||||
Not much effort has been put forward yet to having a robust, long-lasting API,
|
|
||||||
but since the client side longevity is already handled by web browsers, it
|
|
||||||
seems possible that the server-side API can be managed in a similar way.
|
|
||||||
|
|
||||||
3. An awesome development environment. Right now it runs JavaScript from the
|
|
||||||
first embeddable text editor I could poorly configure enough to edit code,
|
|
||||||
but it could incorporate a debugger, source control integration a la ssb-git,
|
|
||||||
merge tools, and transpiling from all sorts of different languages.
|
|
@ -1,73 +1,163 @@
|
|||||||
var g_following_cache = {};
|
let g_about_cache = {};
|
||||||
var g_following_deep_cache = {};
|
|
||||||
var g_about_cache = {};
|
|
||||||
|
|
||||||
async function following(db, id) {
|
async function query(sql, args) {
|
||||||
if (g_following_cache[id]) {
|
let result = [];
|
||||||
return g_following_cache[id];
|
await ssb.sqlAsync(sql, args, function(row) {
|
||||||
}
|
result.push(row);
|
||||||
var o = await db.get(id + ":following");
|
|
||||||
const k_version = 5;
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {users: [], sequence: 0, version: k_version};
|
|
||||||
}
|
|
||||||
f.users = new Set(f.users);
|
|
||||||
await ssb.sqlAsync(
|
|
||||||
"SELECT "+
|
|
||||||
" sequence, "+
|
|
||||||
" json_extract(content, '$.contact') AS contact, "+
|
|
||||||
" json_extract(content, '$.following') AS following "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" author = ?1 AND "+
|
|
||||||
" sequence > ?2 AND "+
|
|
||||||
" json_extract(content, '$.type') = 'contact' "+
|
|
||||||
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
|
|
||||||
"ORDER BY sequence",
|
|
||||||
[id, f.sequence],
|
|
||||||
function(row) {
|
|
||||||
if (row.following) {
|
|
||||||
f.users.add(row.contact);
|
|
||||||
} else {
|
|
||||||
f.users.delete(row.contact);
|
|
||||||
}
|
|
||||||
f.sequence = row.sequence;
|
|
||||||
});
|
});
|
||||||
var as_set = f.users;
|
return result;
|
||||||
f.users = Array.from(f.users).sort();
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":following", j);
|
|
||||||
}
|
|
||||||
f.users = as_set;
|
|
||||||
g_following_cache[id] = f.users;
|
|
||||||
return f.users;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function followingDeep(db, seed_ids, depth) {
|
async function contacts_internal(id, last_row_id, following, max_row_id) {
|
||||||
if (depth <= 0) {
|
let result = Object.assign({}, following[id] || {});
|
||||||
return seed_ids;
|
result.following = result.following || {};
|
||||||
|
result.blocking = result.blocking || {};
|
||||||
|
let contacts = await query(
|
||||||
|
`
|
||||||
|
SELECT content FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
rowid > ? AND
|
||||||
|
rowid <= ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact'
|
||||||
|
ORDER BY sequence
|
||||||
|
`,
|
||||||
|
[id, last_row_id, max_row_id]);
|
||||||
|
for (let row of contacts) {
|
||||||
|
let contact = JSON.parse(row.content);
|
||||||
|
if (contact.following === true) {
|
||||||
|
result.following[contact.contact] = true;
|
||||||
|
} else if (contact.following === false) {
|
||||||
|
delete result.following[contact.contact];
|
||||||
|
} else if (contact.blocking === true) {
|
||||||
|
result.blocking[contact.contact] = true;
|
||||||
|
} else if (contact.blocking === false) {
|
||||||
|
delete result.blocking[contact.contact];
|
||||||
}
|
}
|
||||||
var key = JSON.stringify([seed_ids, depth]);
|
|
||||||
if (g_following_deep_cache[key]) {
|
|
||||||
return g_following_deep_cache[key];
|
|
||||||
}
|
}
|
||||||
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
|
following[id] = result;
|
||||||
var ids = [].concat(...f);
|
return result;
|
||||||
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
|
}
|
||||||
x = [...new Set([].concat(...x, ...seed_ids))].sort();
|
|
||||||
g_following_deep_cache[key] = x;
|
async function contact(id, last_row_id, following, max_row_id) {
|
||||||
return x;
|
return await contacts_internal(id, last_row_id, following, max_row_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
||||||
|
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id)));
|
||||||
|
let result = {};
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
let id = ids[i];
|
||||||
|
let contact = contacts[i];
|
||||||
|
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
||||||
|
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
|
||||||
|
let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
|
||||||
|
result[id] = [id, ...found, ...deeper];
|
||||||
|
}
|
||||||
|
return [...new Set(Object.values(result).flat())];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function following_deep(ids, depth, blocking) {
|
||||||
|
let db = await database('cache');
|
||||||
|
const k_cache_version = 5;
|
||||||
|
let cache = await db.get('following');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
following: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = (await query(`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`, []))[0].max_row_id;
|
||||||
|
let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
let store = JSON.stringify(cache);
|
||||||
|
await db.set('following', store);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch_about(db, ids, users) {
|
||||||
|
const k_cache_version = 1;
|
||||||
|
let cache = await db.get('about');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
about: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = 0;
|
||||||
|
await ssb.sqlAsync(`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
function(row) {
|
||||||
|
max_row_id = row.max_row_id;
|
||||||
|
});
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
if (ids.indexOf(id) == -1) {
|
||||||
|
delete cache.about[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let abouts = [];
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?1) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid > ?3 AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?2) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
ORDER BY messages.author, messages.sequence
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(ids.filter(id => cache.about[id])),
|
||||||
|
JSON.stringify(ids.filter(id => !cache.about[id])),
|
||||||
|
cache.last_row_id,
|
||||||
|
max_row_id,
|
||||||
|
]);
|
||||||
|
for (let about of abouts) {
|
||||||
|
let content = JSON.parse(about.content);
|
||||||
|
if (content.about === about.author) {
|
||||||
|
delete content.type;
|
||||||
|
delete content.about;
|
||||||
|
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
await db.set('about', JSON.stringify(cache));
|
||||||
|
users = users || {};
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
users[id] = Object.assign(users[id] || {}, cache.about[id]);
|
||||||
|
}
|
||||||
|
return Object.assign({}, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAbout(db, id) {
|
async function getAbout(db, id) {
|
||||||
if (g_about_cache[id]) {
|
if (g_about_cache[id]) {
|
||||||
return g_about_cache[id];
|
return g_about_cache[id];
|
||||||
}
|
}
|
||||||
var o = await db.get(id + ":about");
|
let o = await db.get(id + ":about");
|
||||||
const k_version = 4;
|
const k_version = 4;
|
||||||
var f = o ? JSON.parse(o) : o;
|
let f = o ? JSON.parse(o) : o;
|
||||||
if (!f || f.version != k_version) {
|
if (!f || f.version != k_version) {
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
f = {about: {}, sequence: 0, version: k_version};
|
||||||
}
|
}
|
||||||
@ -87,7 +177,7 @@ async function getAbout(db, id) {
|
|||||||
function(row) {
|
function(row) {
|
||||||
f.sequence = row.sequence;
|
f.sequence = row.sequence;
|
||||||
if (row.content) {
|
if (row.content) {
|
||||||
var about = {};
|
let about = {};
|
||||||
try {
|
try {
|
||||||
about = JSON.parse(row.content);
|
about = JSON.parse(row.content);
|
||||||
} catch {
|
} catch {
|
||||||
@ -97,7 +187,7 @@ async function getAbout(db, id) {
|
|||||||
f.about = Object.assign(f.about, about);
|
f.about = Object.assign(f.about, about);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var j = JSON.stringify(f);
|
let j = JSON.stringify(f);
|
||||||
if (o != j) {
|
if (o != j) {
|
||||||
await db.set(id + ":about", j);
|
await db.set(id + ":about", j);
|
||||||
}
|
}
|
||||||
@ -108,7 +198,7 @@ async function getAbout(db, id) {
|
|||||||
async function getSize(db, id) {
|
async function getSize(db, id) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
||||||
[id],
|
[id],
|
||||||
function (row) {
|
function (row) {
|
||||||
size += row.size;
|
size += row.size;
|
||||||
@ -116,6 +206,25 @@ async function getSize(db, id) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getSizes(ids) {
|
||||||
|
let sizes = {};
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
author,
|
||||||
|
(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS ids ON author = ids.value
|
||||||
|
GROUP BY author
|
||||||
|
`,
|
||||||
|
[JSON.stringify(ids)],
|
||||||
|
function (row) {
|
||||||
|
sizes[row.author] = row.size;
|
||||||
|
});
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
function niceSize(bytes) {
|
function niceSize(bytes) {
|
||||||
let value = bytes;
|
let value = bytes;
|
||||||
let unit = 'B';
|
let unit = 'B';
|
||||||
@ -131,27 +240,28 @@ function niceSize(bytes) {
|
|||||||
return Math.round(value * 10) / 10 + ' ' + unit;
|
return Math.round(value * 10) / 10 + ' ' + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildTree(db, root, indent, depth) {
|
function escape(value) {
|
||||||
var f = await following(db, root);
|
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||||
var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
|
|
||||||
if (depth > 0) {
|
|
||||||
for (let next of f) {
|
|
||||||
result += await buildTree(db, next, indent + ' ', depth - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
||||||
var db = await database('ssb');
|
let db = await database('ssb');
|
||||||
var whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
var tree = '';
|
let tree = '';
|
||||||
for (let id of whoami) {
|
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
|
||||||
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
|
let following = await following_deep(whoami, 2, {});
|
||||||
tree += await buildTree(db, id, '', 2);
|
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`);
|
||||||
|
let [about, sizes] = await Promise.all([
|
||||||
|
fetch_about(db, following, {}),
|
||||||
|
getSizes(following),
|
||||||
|
]);
|
||||||
|
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
||||||
|
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0)));
|
||||||
|
for (let id of following) {
|
||||||
|
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
||||||
}
|
}
|
||||||
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
|
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
5
apps/gg.json
Normal file
5
apps/gg.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🗺",
|
||||||
|
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
|
||||||
|
}
|
80
apps/gg/app.js
Normal file
80
apps/gg/app.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
import * as strava from './strava.js';
|
||||||
|
|
||||||
|
let g_database;
|
||||||
|
let g_shared_database;
|
||||||
|
|
||||||
|
tfrpc.register(async function createIdentity() {
|
||||||
|
return ssb.createIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
|
print('APPEND', JSON.stringify(message));
|
||||||
|
return ssb.appendMessageWithIdentity(id, message);
|
||||||
|
});
|
||||||
|
tfrpc.register(function url() {
|
||||||
|
return core.url;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getUser() {
|
||||||
|
return core.user;
|
||||||
|
});
|
||||||
|
tfrpc.register(function getIdentities() {
|
||||||
|
return ssb.getIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseGet(key) {
|
||||||
|
return g_database ? g_database.get(key) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseSet(key, value) {
|
||||||
|
return g_database ? g_database.set(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseRemove(key, value) {
|
||||||
|
return g_database ? g_database.remove(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function sharedDatabaseGet(key) {
|
||||||
|
return g_shared_database ? g_shared_database.get(key) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function sharedDatabaseSet(key, value) {
|
||||||
|
return g_shared_database ? g_shared_database.set(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function sharedDatabaseRemove(key, value) {
|
||||||
|
return g_shared_database ? g_shared_database.remove(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (typeof(blob) == 'string') {
|
||||||
|
blob = utf8Encode(blob);
|
||||||
|
}
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(strava.refresh_token);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
g_shared_database = await shared_database('state');
|
||||||
|
if (core.user.credentials?.session?.name) {
|
||||||
|
g_database = await database('state');
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempt;
|
||||||
|
if (core.user.credentials?.session?.name) {
|
||||||
|
let shared_db = await shared_database('state');
|
||||||
|
attempt = await shared_db.get(core.user.credentials.session.name);
|
||||||
|
}
|
||||||
|
app.setDocument(utf8Decode(getFile('index.html')).replace('${data}', JSON.stringify({
|
||||||
|
attempt: attempt,
|
||||||
|
state: core.user?.credentials?.session?.name,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
1
apps/gg/emojis.json
Normal file
1
apps/gg/emojis.json
Normal file
File diff suppressed because one or more lines are too long
81
apps/gg/gpx.js
Normal file
81
apps/gg/gpx.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
function xml_parse(xml) {
|
||||||
|
let result;
|
||||||
|
let path = [];
|
||||||
|
let tag_begin;
|
||||||
|
let text_begin;
|
||||||
|
for (let i = 0; i < xml.length; i++) {
|
||||||
|
let c = xml.charAt(i);
|
||||||
|
if (!tag_begin && c == '<') {
|
||||||
|
if (i > text_begin && path.length) {
|
||||||
|
let value = xml.substring(text_begin, i);
|
||||||
|
if (!/^\s*$/.test(value)) {
|
||||||
|
path[path.length - 1].value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag_begin = i + 1;
|
||||||
|
} else if (tag_begin && c == '>') {
|
||||||
|
let tag = xml.substring(tag_begin, i).trim();
|
||||||
|
if (tag.startsWith('?') && tag.endsWith('?')) {
|
||||||
|
/* Ignore directives. */
|
||||||
|
} else if (tag.startsWith('/')) {
|
||||||
|
path.pop();
|
||||||
|
} else {
|
||||||
|
let parts = tag.split(' ');
|
||||||
|
let attributes = {};
|
||||||
|
for (let j = 1; j < parts.length; j++) {
|
||||||
|
let eq = parts[j].indexOf('=');
|
||||||
|
let value = parts[j].substring(eq + 1);
|
||||||
|
if (value.startsWith('"') && value.endsWith('"')) {
|
||||||
|
value = value.substring(1, value.length - 1);
|
||||||
|
}
|
||||||
|
attributes[parts[j].substring(0, eq)] = value;
|
||||||
|
}
|
||||||
|
let next = {name: parts[0], children: [], attributes: attributes};
|
||||||
|
if (path.length) {
|
||||||
|
path[path.length - 1].children.push(next);
|
||||||
|
} else {
|
||||||
|
result = next;
|
||||||
|
}
|
||||||
|
if (!tag.endsWith('/')) {
|
||||||
|
path.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag_begin = undefined;
|
||||||
|
text_begin = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function* xml_each(node, name) {
|
||||||
|
for (let child of node.children) {
|
||||||
|
if (child.name == name) {
|
||||||
|
yield child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gpx_parse(xml) {
|
||||||
|
let result = {segments: []};
|
||||||
|
let tree = xml_parse(xml);
|
||||||
|
if (tree?.name == 'gpx') {
|
||||||
|
for (let trk of xml_each(tree, 'trk')) {
|
||||||
|
for (let trkseg of xml_each(trk, 'trkseg')) {
|
||||||
|
let segment = [];
|
||||||
|
for (let trkpt of xml_each(trkseg, 'trkpt')) {
|
||||||
|
segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)});
|
||||||
|
}
|
||||||
|
result.segments.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let metadata of xml_each(tree, 'metadata')) {
|
||||||
|
for (let link of xml_each(metadata, 'link')) {
|
||||||
|
result.link = link.attributes.href;
|
||||||
|
}
|
||||||
|
for (let time of xml_each(metadata, 'time')) {
|
||||||
|
result.time = time.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
21
apps/gg/handler.js
Normal file
21
apps/gg/handler.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as strava from './strava.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
print('handler running');
|
||||||
|
let r = await strava.authorization_code(request.query.code);
|
||||||
|
print('state =', request.query.state);
|
||||||
|
print('body = ', r.body);
|
||||||
|
if (request.query.state && r.body) {
|
||||||
|
let shared_db = await shared_database('state');
|
||||||
|
await shared_db.set(request.query.state, utf8Decode(r.body));
|
||||||
|
}
|
||||||
|
await respond({
|
||||||
|
data: r.body,
|
||||||
|
content_type: 'text/plain',
|
||||||
|
headers: {
|
||||||
|
Location: 'https://tildefriends.net/~cory/gg/',
|
||||||
|
},
|
||||||
|
status_code: 307,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
main();
|
14
apps/gg/index.html
Normal file
14
apps/gg/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html style="width: 100%; height: 100%; margin: 0; padding: 0">
|
||||||
|
<head>
|
||||||
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
|
<script>
|
||||||
|
let g_data = ${data};
|
||||||
|
</script>
|
||||||
|
<script src="script.js" type="module"></script>
|
||||||
|
<script src="leaflet.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="color: #fff; display: flex; flex-flow: column; height: 100%; width: 100%; margin: 0; padding: 0">
|
||||||
|
<gg-app style="width: 100%; height: 100%" id="ggapp"></gg-app>
|
||||||
|
</body>
|
||||||
|
</html>
|
661
apps/gg/leaflet.css
Normal file
661
apps/gg/leaflet.css
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
6
apps/gg/leaflet.js
Normal file
6
apps/gg/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
1
apps/gg/leaflet.js.map
Normal file
1
apps/gg/leaflet.js.map
Normal file
File diff suppressed because one or more lines are too long
120
apps/gg/lit-all.min.js
vendored
Normal file
120
apps/gg/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/gg/lit-all.min.js.map
Normal file
1
apps/gg/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
158
apps/gg/polyline.js
Normal file
158
apps/gg/polyline.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* Based off of [the offical Google document](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)
|
||||||
|
*
|
||||||
|
* Some parts from [this implementation](http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.js)
|
||||||
|
* by [Mark McClure](http://facstaff.unca.edu/mcmcclur/)
|
||||||
|
*
|
||||||
|
* @module polyline
|
||||||
|
*/
|
||||||
|
|
||||||
|
var polyline = {};
|
||||||
|
|
||||||
|
function py2_round(value) {
|
||||||
|
// Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values
|
||||||
|
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(current, previous, factor) {
|
||||||
|
current = py2_round(current * factor);
|
||||||
|
previous = py2_round(previous * factor);
|
||||||
|
var coordinate = (current - previous) * 2;
|
||||||
|
if (coordinate < 0) {
|
||||||
|
coordinate = -coordinate - 1
|
||||||
|
}
|
||||||
|
var output = '';
|
||||||
|
while (coordinate >= 0x20) {
|
||||||
|
output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
|
||||||
|
coordinate /= 32;
|
||||||
|
}
|
||||||
|
output += String.fromCharCode((coordinate | 0) + 63);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes to a [latitude, longitude] coordinates array.
|
||||||
|
*
|
||||||
|
* This is adapted from the implementation in Project-OSRM.
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @param {Number} precision
|
||||||
|
* @returns {Array}
|
||||||
|
*
|
||||||
|
* @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
|
||||||
|
*/
|
||||||
|
polyline.decode = function(str, precision) {
|
||||||
|
var index = 0,
|
||||||
|
lat = 0,
|
||||||
|
lng = 0,
|
||||||
|
coordinates = [],
|
||||||
|
shift = 0,
|
||||||
|
result = 0,
|
||||||
|
byte = null,
|
||||||
|
latitude_change,
|
||||||
|
longitude_change,
|
||||||
|
factor = Math.pow(10, Number.isInteger(precision) ? precision : 5);
|
||||||
|
|
||||||
|
// Coordinates have variable length when encoded, so just keep
|
||||||
|
// track of whether we've hit the end of the string. In each
|
||||||
|
// loop iteration, a single coordinate is decoded.
|
||||||
|
while (index < str.length) {
|
||||||
|
|
||||||
|
// Reset shift, result, and byte
|
||||||
|
byte = null;
|
||||||
|
shift = 1;
|
||||||
|
result = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte = str.charCodeAt(index++) - 63;
|
||||||
|
result += (byte & 0x1f) * shift;
|
||||||
|
shift *= 32;
|
||||||
|
} while (byte >= 0x20);
|
||||||
|
|
||||||
|
latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
||||||
|
|
||||||
|
shift = 1;
|
||||||
|
result = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte = str.charCodeAt(index++) - 63;
|
||||||
|
result += (byte & 0x1f) * shift;
|
||||||
|
shift *= 32;
|
||||||
|
} while (byte >= 0x20);
|
||||||
|
|
||||||
|
longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
||||||
|
|
||||||
|
lat += latitude_change;
|
||||||
|
lng += longitude_change;
|
||||||
|
|
||||||
|
coordinates.push([lat / factor, lng / factor]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the given [latitude, longitude] coordinates array.
|
||||||
|
*
|
||||||
|
* @param {Array.<Array.<Number>>} coordinates
|
||||||
|
* @param {Number} precision
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
polyline.encode = function(coordinates, precision) {
|
||||||
|
if (!coordinates.length) { return ''; }
|
||||||
|
|
||||||
|
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
|
||||||
|
output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor);
|
||||||
|
|
||||||
|
for (var i = 1; i < coordinates.length; i++) {
|
||||||
|
var a = coordinates[i], b = coordinates[i - 1];
|
||||||
|
output += encode(a[0], b[0], factor);
|
||||||
|
output += encode(a[1], b[1], factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
function flipped(coords) {
|
||||||
|
var flipped = [];
|
||||||
|
for (var i = 0; i < coords.length; i++) {
|
||||||
|
var coord = coords[i].slice();
|
||||||
|
flipped.push([coord[1], coord[0]]);
|
||||||
|
}
|
||||||
|
return flipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a GeoJSON LineString feature/geometry.
|
||||||
|
*
|
||||||
|
* @param {Object} geojson
|
||||||
|
* @param {Number} precision
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
polyline.fromGeoJSON = function(geojson, precision) {
|
||||||
|
if (geojson && geojson.type === 'Feature') {
|
||||||
|
geojson = geojson.geometry;
|
||||||
|
}
|
||||||
|
if (!geojson || geojson.type !== 'LineString') {
|
||||||
|
throw new Error('Input must be a GeoJSON LineString');
|
||||||
|
}
|
||||||
|
return polyline.encode(flipped(geojson.coordinates), precision);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes to a GeoJSON LineString geometry.
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @param {Number} precision
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
polyline.toGeoJSON = function(str, precision) {
|
||||||
|
var coords = polyline.decode(str, precision);
|
||||||
|
return {
|
||||||
|
type: 'LineString',
|
||||||
|
coordinates: flipped(coords)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let polyline_decode = polyline.decode;
|
||||||
|
export { polyline_decode as decode };
|
807
apps/gg/script.js
Normal file
807
apps/gg/script.js
Normal file
@ -0,0 +1,807 @@
|
|||||||
|
import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import * as polyline from './polyline.js';
|
||||||
|
import {gpx_parse} from './gpx.js';
|
||||||
|
|
||||||
|
const k_client_id = '28276';
|
||||||
|
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
|
||||||
|
|
||||||
|
const k_color_snow = [128, 128, 255, 255];
|
||||||
|
const k_color_ice = [160, 160, 255, 255];
|
||||||
|
const k_color_water = [0, 0, 255, 255];
|
||||||
|
const k_color_dirt = [128, 129, 130, 255];
|
||||||
|
const k_color_pavement = [32, 32, 32, 255];
|
||||||
|
const k_color_grass = [0, 255, 0, 255];
|
||||||
|
const k_color_default = [128, 128, 128, 255];
|
||||||
|
|
||||||
|
const k_store = {
|
||||||
|
'🦞': 15,
|
||||||
|
'🛶': 10,
|
||||||
|
'🏠': 10,
|
||||||
|
'⛰': 10,
|
||||||
|
'🐠': 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const k_marker_snap = {x: 5, y: 4};
|
||||||
|
|
||||||
|
class GgAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
user: {type: Object},
|
||||||
|
strava: {type: Object},
|
||||||
|
activities: {type: Array},
|
||||||
|
activity: {type: Object},
|
||||||
|
world: {type: Object},
|
||||||
|
whoami: {type: String},
|
||||||
|
status: {type: Object},
|
||||||
|
tab: {type: String},
|
||||||
|
url: {type: String},
|
||||||
|
currency: {type: Number},
|
||||||
|
to_build: {type: String},
|
||||||
|
emoji_of_the_day: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.activities = [];
|
||||||
|
this.activity = {};
|
||||||
|
this.loaded_activities = [];
|
||||||
|
this.placed_emojis = [];
|
||||||
|
this.strava = {};
|
||||||
|
this.min_lat = Number.MAX_VALUE;
|
||||||
|
this.min_lon = Number.MAX_VALUE;
|
||||||
|
this.max_lat = -Number.MAX_VALUE;
|
||||||
|
this.max_lon = -Number.MAX_VALUE;
|
||||||
|
this.focus = undefined;
|
||||||
|
this.status = undefined;
|
||||||
|
this.tab = 'map';
|
||||||
|
this.load().catch(function(e) {
|
||||||
|
console.log('load error', e);
|
||||||
|
});
|
||||||
|
this.to_build = '🏠';
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
console.log('load');
|
||||||
|
let emojis = await (await fetch('emojis.json')).json();
|
||||||
|
emojis = Object.values(emojis).map(x => Object.values(x)).flat();
|
||||||
|
let today = new Date();
|
||||||
|
let date_index = today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
|
||||||
|
this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length];
|
||||||
|
this.user = await tfrpc.rpc.getUser();
|
||||||
|
this.url = (await tfrpc.rpc.url()).split('?')[0];
|
||||||
|
try {
|
||||||
|
await this.update_credentials();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('update_credentials failed', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.update_activities();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('update_activities failed', e);
|
||||||
|
}
|
||||||
|
await this.acquire_ssb_identity();
|
||||||
|
if (this.whoami && this.activities?.length) {
|
||||||
|
await this.sync_activities();
|
||||||
|
}
|
||||||
|
await this.get_activities_from_ssb();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b?permalink_comment_id=3591045#gistcomment-3591045 */
|
||||||
|
async promise_all(promises, max_concurrent) {
|
||||||
|
let index = 0;
|
||||||
|
let results = [];
|
||||||
|
async function exec_thread() {
|
||||||
|
while (index < promises.length) {
|
||||||
|
const current = index++;
|
||||||
|
results[current] = await promises[current];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const threads = [];
|
||||||
|
for (let thread = 0; thread < max_concurrent; thread++) {
|
||||||
|
threads.push(exec_thread());
|
||||||
|
}
|
||||||
|
await Promise.all(threads);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_activities_from_ssb() {
|
||||||
|
this.status = {text: 'loading activities'};
|
||||||
|
this.loaded_activities = [];
|
||||||
|
let rows = await tfrpc.rpc.query(`
|
||||||
|
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
|
||||||
|
FROM messages_fts('"gg-activity"')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid,
|
||||||
|
json_each(messages.content, '$.mentions') as mention
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
|
||||||
|
json_extract(mention.value, '$.name') = 'activity_data'
|
||||||
|
ORDER BY messages.timestamp DESC
|
||||||
|
`, []);
|
||||||
|
this.status = {text: 'loading activity data'};
|
||||||
|
let authors = rows.map(x => x.author);
|
||||||
|
let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8);
|
||||||
|
this.status = {text: 'processing activity data'};
|
||||||
|
for (let [index, blob] of blobs.entries()) {
|
||||||
|
let activity;
|
||||||
|
try {
|
||||||
|
activity = JSON.parse(blob);
|
||||||
|
} catch {
|
||||||
|
activity = gpx_parse(blob);
|
||||||
|
}
|
||||||
|
if (activity) {
|
||||||
|
activity.author = authors[index];
|
||||||
|
this.loaded_activities.push(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.status = {text: 'calculating balance'};
|
||||||
|
rows = await tfrpc.rpc.query(`
|
||||||
|
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
|
||||||
|
`, [this.whoami]);
|
||||||
|
let currency = rows[0].currency;
|
||||||
|
rows = await tfrpc.rpc.query(`
|
||||||
|
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
|
||||||
|
`, [this.whoami]);
|
||||||
|
let spent = rows[0].cost;
|
||||||
|
this.currency = currency - spent;
|
||||||
|
this.status = {text: 'getting placed emojis'};
|
||||||
|
rows = await tfrpc.rpc.query(`
|
||||||
|
SELECT messages.content
|
||||||
|
FROM messages_fts('"gg-place"')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'gg-place'
|
||||||
|
ORDER BY messages.timestamp
|
||||||
|
`);
|
||||||
|
for (let row of rows) {
|
||||||
|
console.log(row.content);
|
||||||
|
let content = JSON.parse(row.content);
|
||||||
|
this.placed_emojis.push({
|
||||||
|
position: content.position,
|
||||||
|
emoji: content.emoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(this.placed_emojis);
|
||||||
|
this.status = undefined;
|
||||||
|
this.update_map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync_activities() {
|
||||||
|
let ids = this.activities.map(x => `https://www.strava.com/activities/${x.id}`);
|
||||||
|
let missing = await tfrpc.rpc.query(`
|
||||||
|
WITH my_activities AS (
|
||||||
|
SELECT json_extract(mention.value, '$.link') AS url
|
||||||
|
FROM messages, json_each(messages.content, '$.mentions') AS mention
|
||||||
|
WHERE
|
||||||
|
author = ? AND
|
||||||
|
json_extract(messages.content, '$.type') = 'gg-activity' AND
|
||||||
|
json_extract(mention.value, '$.name') = 'activity_url')
|
||||||
|
SELECT from_strava.value FROM json_each(?) AS from_strava
|
||||||
|
LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
|
||||||
|
WHERE my_activities.url IS NULL
|
||||||
|
`, [this.whoami, JSON.stringify(ids)]);
|
||||||
|
console.log('missing = ', missing);
|
||||||
|
for (let [index, row] of missing.entries()) {
|
||||||
|
this.status = {text: 'syncing from strava', value: index, max: missing.length};
|
||||||
|
let url = row.value;
|
||||||
|
let id = url.match(/.*\/(\d+)/)[1];
|
||||||
|
let response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.strava.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let activity = await response.json();
|
||||||
|
let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
|
||||||
|
let message = {
|
||||||
|
type: 'gg-activity',
|
||||||
|
mentions: [
|
||||||
|
{
|
||||||
|
link: url,
|
||||||
|
name: 'activity_url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: blob_id,
|
||||||
|
name: 'activity_data',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
|
}
|
||||||
|
this.status = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquire_ssb_identity() {
|
||||||
|
let user = await tfrpc.rpc.getUser();
|
||||||
|
if (!user?.credentials?.session?.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ids = await tfrpc.rpc.getIdentities();
|
||||||
|
let players = ids.length ? (await tfrpc.rpc.query(`
|
||||||
|
SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
|
||||||
|
WHERE
|
||||||
|
json_extract(messages.content, '$.type') = 'gg-player' AND
|
||||||
|
json_extract(messages.content, '$.active')
|
||||||
|
ORDER BY timestamp DESC limit 1
|
||||||
|
`, [JSON.stringify(ids)])).map(row => row.author) : [];
|
||||||
|
if (!players.length) {
|
||||||
|
this.whoami = await tfrpc.rpc.createIdentity();
|
||||||
|
if (this.whoami) {
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, {
|
||||||
|
type: 'gg-player',
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
players.sort();
|
||||||
|
this.whoami = players[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update_credentials() {
|
||||||
|
let name = this.user?.credentials?.session?.name;
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shared = await tfrpc.rpc.sharedDatabaseGet(name);
|
||||||
|
if (shared) {
|
||||||
|
await tfrpc.rpc.databaseSet('strava', shared);
|
||||||
|
await tfrpc.rpc.sharedDatabaseRemove(name);
|
||||||
|
}
|
||||||
|
this.strava = JSON.parse(await tfrpc.rpc.databaseGet('strava') || '{}');
|
||||||
|
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
|
||||||
|
console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at);
|
||||||
|
let x = await tfrpc.rpc.refresh_token(this.strava);
|
||||||
|
if (x) {
|
||||||
|
this.strava = x;
|
||||||
|
await tfrpc.rpc.databaseSet('strava', JSON.stringify(x));
|
||||||
|
} else {
|
||||||
|
this.strava = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update_activities() {
|
||||||
|
if (this?.strava?.access_token) {
|
||||||
|
let response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.strava.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.activities = await response.json();
|
||||||
|
this.activities.sort((a, b) => (a.id - b.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color_to_emoji(color) {
|
||||||
|
const k_map = [
|
||||||
|
[k_color_snow, '⬜'],
|
||||||
|
[k_color_ice, '🟦'],
|
||||||
|
[k_color_water, '🟦'],
|
||||||
|
[k_color_dirt, '🟫'],
|
||||||
|
[k_color_pavement, '⬛'],
|
||||||
|
[k_color_grass, '🟩'],
|
||||||
|
[k_color_default, '🟧'],
|
||||||
|
];
|
||||||
|
for (let m of k_map) {
|
||||||
|
if (m[0][0] == color[0] &&
|
||||||
|
m[0][1] == color[1] &&
|
||||||
|
m[0][2] == color[2] &&
|
||||||
|
m[0][3] == color[3]) {
|
||||||
|
return m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity_bounds(activity) {
|
||||||
|
let min_lat = Number.MAX_VALUE;
|
||||||
|
let min_lon = Number.MAX_VALUE;
|
||||||
|
let max_lat = -Number.MAX_VALUE;
|
||||||
|
let max_lon = -Number.MAX_VALUE;
|
||||||
|
if (activity?.map?.polyline) {
|
||||||
|
for (let pt of polyline.decode(activity.map.polyline)) {
|
||||||
|
min_lat = Math.min(min_lat, pt[0]);
|
||||||
|
min_lon = Math.min(min_lon, pt[1]);
|
||||||
|
max_lat = Math.max(max_lat, pt[0]);
|
||||||
|
max_lon = Math.max(max_lon, pt[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activity?.segments) {
|
||||||
|
for (let segment of activity.segments) {
|
||||||
|
for (let pt of segment) {
|
||||||
|
min_lat = Math.min(min_lat, pt.lat);
|
||||||
|
min_lon = Math.min(min_lon, pt.lon);
|
||||||
|
max_lat = Math.max(max_lat, pt.lat);
|
||||||
|
max_lon = Math.max(max_lon, pt.lon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
min: {
|
||||||
|
lat: min_lat,
|
||||||
|
lng: min_lon,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
lat: max_lat,
|
||||||
|
lng: max_lon,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
on_click(event) {
|
||||||
|
let popup = L.popup()
|
||||||
|
.setLatLng(event.latlng)
|
||||||
|
.setContent(`
|
||||||
|
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
|
||||||
|
`)
|
||||||
|
.openOn(this.leaflet);
|
||||||
|
}
|
||||||
|
|
||||||
|
async build() {
|
||||||
|
if (this.popup) {
|
||||||
|
this.popup.remove();
|
||||||
|
}
|
||||||
|
if (!this.marker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let latlng = this.marker.getLatLng();
|
||||||
|
|
||||||
|
let cost = k_store[this.to_build];
|
||||||
|
if (cost > this.currency) {
|
||||||
|
alert('Insufficient funds.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let message = {
|
||||||
|
type: 'gg-place',
|
||||||
|
position: {lat: latlng.lat, lng: latlng.lng},
|
||||||
|
emoji: this.to_build,
|
||||||
|
cost: cost,
|
||||||
|
};
|
||||||
|
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
|
this.marker.remove();
|
||||||
|
this.placed_emojis.push({
|
||||||
|
position: {lat: latlng.lat, lng: latlng.lng},
|
||||||
|
emoji: this.to_build,
|
||||||
|
});
|
||||||
|
this.currency -= cost;
|
||||||
|
return this.update_map();
|
||||||
|
}
|
||||||
|
|
||||||
|
on_marker_click(event) {
|
||||||
|
this.popup = L.popup()
|
||||||
|
.setLatLng(event.latlng)
|
||||||
|
.setContent(`
|
||||||
|
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
|
||||||
|
`)
|
||||||
|
.openOn(this.leaflet);
|
||||||
|
}
|
||||||
|
|
||||||
|
snap_to_grid(latlng, fudge, zoom) {
|
||||||
|
let position = this.leaflet.options.crs.latLngToPoint(latlng, zoom ?? this.leaflet.getZoom());
|
||||||
|
position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
|
||||||
|
position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
|
||||||
|
position = this.leaflet.options.crs.pointToLatLng(position, zoom ?? this.leaflet.getZoom());
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_marker_move(event) {
|
||||||
|
if (!this.no_snap && this.marker) {
|
||||||
|
this.no_snap = true;
|
||||||
|
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
||||||
|
this.no_snap = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_zoom(event) {
|
||||||
|
if (this.marker) {
|
||||||
|
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_mouse_down(event) {
|
||||||
|
if (this.marker) {
|
||||||
|
this.marker.remove();
|
||||||
|
this.marker = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.to_build) {
|
||||||
|
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet);
|
||||||
|
this.marker.on({click: this.on_marker_click.bind(this)});
|
||||||
|
this.marker.on({drag: this.on_marker_move.bind(this)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update_map() {
|
||||||
|
let map = this.shadowRoot.getElementById('map');
|
||||||
|
if (!map || !this.loaded_activities.length) {
|
||||||
|
this.leaflet = undefined;
|
||||||
|
this.grid_layer = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.leaflet) {
|
||||||
|
this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
|
||||||
|
this.leaflet.on({contextmenu: this.on_click.bind(this)});
|
||||||
|
this.leaflet.on({click: this.on_mouse_down.bind(this)});
|
||||||
|
this.leaflet.on({zoom: this.on_zoom.bind(this)});
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
let grid_layer = L.GridLayer.extend({
|
||||||
|
createTile: function(coords) {
|
||||||
|
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
||||||
|
var size = this.getTileSize();
|
||||||
|
tile.width = size.x;
|
||||||
|
tile.height = size.y;
|
||||||
|
var context = tile.getContext('2d');
|
||||||
|
context.font = '10pt sans';
|
||||||
|
let bounds = this._tileCoordsToBounds(coords);
|
||||||
|
let degrees = 360.0 / (2 ** coords.z);
|
||||||
|
let ul = bounds.getNorthWest();
|
||||||
|
let lr = bounds.getSouthEast();
|
||||||
|
|
||||||
|
let mini = document.createElement('canvas');
|
||||||
|
mini.width = Math.floor(size.x / 16.0);
|
||||||
|
mini.height = Math.floor(size.y / 16.0);
|
||||||
|
let mini_context = mini.getContext('2d');
|
||||||
|
let image_data = context.getImageData(0, 0, mini.width, mini.height);
|
||||||
|
for (let activity of self.loaded_activities) {
|
||||||
|
self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity);
|
||||||
|
}
|
||||||
|
context.textAlign = 'left';
|
||||||
|
context.textBaseline = 'bottom';
|
||||||
|
for (let x = 0; x < mini.width; x++) {
|
||||||
|
for (let y = 0; y < mini.height; y++) {
|
||||||
|
let start = (y * mini.width + x) * 4;
|
||||||
|
let pixel = self.color_to_emoji(image_data.data.slice(start, start + 4));
|
||||||
|
if (pixel) {
|
||||||
|
//context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height);
|
||||||
|
context.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height + mini.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let placed of self.placed_emojis) {
|
||||||
|
let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position, undefined, coords.z), coords.z);
|
||||||
|
let tile_x = Math.floor(position.x / size.x);
|
||||||
|
let tile_y = Math.floor(position.y / size.y);
|
||||||
|
position.x = position.x - tile_x * size.x;
|
||||||
|
position.y = position.y - tile_y * size.y;
|
||||||
|
if (tile_x == coords.x && tile_y == coords.y) {
|
||||||
|
//context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height);
|
||||||
|
context.fillText(placed.emoji, position.x, position.y + mini.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (this.grid_layer) {
|
||||||
|
this.grid_layer.redraw();
|
||||||
|
} else {
|
||||||
|
this.grid_layer = new grid_layer();
|
||||||
|
this.grid_layer.addTo(this.leaflet);
|
||||||
|
}
|
||||||
|
for (let activity of this.loaded_activities) {
|
||||||
|
let bounds = this.activity_bounds(activity);
|
||||||
|
this.min_lat = Math.min(this.min_lat, bounds.min.lat);
|
||||||
|
this.min_lon = Math.min(this.min_lon, bounds.min.lng);
|
||||||
|
this.max_lat = Math.max(this.max_lat, bounds.max.lat);
|
||||||
|
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
|
||||||
|
}
|
||||||
|
if (this.focus) {
|
||||||
|
this.leaflet.fitBounds([
|
||||||
|
this.focus.min,
|
||||||
|
this.focus.max,
|
||||||
|
]);
|
||||||
|
this.focus = undefined;
|
||||||
|
} else {
|
||||||
|
this.leaflet.fitBounds([
|
||||||
|
[this.min_lat, this.min_lon],
|
||||||
|
[this.max_lat, this.max_lon],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity_to_color(activity) {
|
||||||
|
let color = [0, 0, 0, 255];
|
||||||
|
switch (activity.sport_type) {
|
||||||
|
/* Implies snow. */
|
||||||
|
case 'AlpineSki':
|
||||||
|
case 'BackcountrySki':
|
||||||
|
case 'NordicSki':
|
||||||
|
case 'Snowshoe':
|
||||||
|
case 'Snowboard':
|
||||||
|
color = k_color_snow;
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Implies ice. */
|
||||||
|
case 'IceSkate':
|
||||||
|
case 'InlineSkate':
|
||||||
|
color = k_color_ice;
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Implies water. */
|
||||||
|
case 'Canoeing':
|
||||||
|
case 'Kayaking':
|
||||||
|
case 'Kitesurf':
|
||||||
|
case 'Rowing':
|
||||||
|
case 'Sail':
|
||||||
|
case 'StandUpPaddling':
|
||||||
|
case 'Surfing':
|
||||||
|
case 'Swim':
|
||||||
|
case 'Windsurf':
|
||||||
|
color = k_color_water;
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Implies dirt. */
|
||||||
|
case 'EMountainBikeRide':
|
||||||
|
case 'Hike':
|
||||||
|
case 'MountainBikeRide':
|
||||||
|
case 'RockClimbing':
|
||||||
|
case 'TrailRun':
|
||||||
|
color = k_color_dirt;
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Implies pavement. */
|
||||||
|
case 'EBikeRide':
|
||||||
|
case 'GravelRide':
|
||||||
|
case 'Handcycle':
|
||||||
|
case 'Ride':
|
||||||
|
case 'RollerSki':
|
||||||
|
case 'Run':
|
||||||
|
case 'Skateboard':
|
||||||
|
case 'Badminton':
|
||||||
|
case 'Tennis':
|
||||||
|
case 'Velomobile':
|
||||||
|
case 'Walk':
|
||||||
|
case 'Wheelchair':
|
||||||
|
color = k_color_pavement;
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Grass, maybe? */
|
||||||
|
case 'Golf':
|
||||||
|
case 'Soccer':
|
||||||
|
case 'Squash':
|
||||||
|
color = k_color_grass;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Crossfit,
|
||||||
|
// Elliptical
|
||||||
|
// HighIntensityIntervalTraining
|
||||||
|
// Pickleball
|
||||||
|
// Pilates
|
||||||
|
// Racquetball
|
||||||
|
// StairStepper
|
||||||
|
// TableTennis,
|
||||||
|
// VirtualRide
|
||||||
|
// VirtualRow
|
||||||
|
// VirtualRun
|
||||||
|
// WeightTraining
|
||||||
|
// Workout
|
||||||
|
// Yoga
|
||||||
|
default:
|
||||||
|
color = k_color_default;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
line(image_data, x0, y0, x1, y1, value) {
|
||||||
|
/* <3 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm */
|
||||||
|
let dx = Math.abs(x1 - x0);
|
||||||
|
let sx = x0 < x1 ? 1 : -1;
|
||||||
|
let dy = -Math.abs(y1 - y0);
|
||||||
|
let sy = y0 < y1 ? 1 : -1;
|
||||||
|
let error = dx + dy;
|
||||||
|
while (true) {
|
||||||
|
if (x0 >= 0 && y0 >= 0 && x0 < image_data.width && y0 < image_data.height) {
|
||||||
|
let base = (y0 * image_data.width + x0) * 4;
|
||||||
|
image_data.data[base + 0] = value[0];
|
||||||
|
image_data.data[base + 1] = value[1];
|
||||||
|
image_data.data[base + 2] = value[2];
|
||||||
|
image_data.data[base + 3] = value[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x0 == x1 && y0 == y1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let e2 = 2 * error;
|
||||||
|
if (e2 >= dy) {
|
||||||
|
if (x0 == x1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
error += dy;
|
||||||
|
x0 = Math.round(x0 + sx);
|
||||||
|
}
|
||||||
|
if (e2 <= dx) {
|
||||||
|
if (y0 == y1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
error += dx;
|
||||||
|
y0 = Math.round(y0 + sy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_activity_to_tile(image_data, width, height, ul, lr, activity) {
|
||||||
|
let color = this.activity_to_color(activity);
|
||||||
|
if (activity?.map?.polyline) {
|
||||||
|
let last;
|
||||||
|
for (let pt of polyline.decode(activity.map.polyline)) {
|
||||||
|
let px = [
|
||||||
|
Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)),
|
||||||
|
Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)),
|
||||||
|
];
|
||||||
|
if (last) {
|
||||||
|
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
||||||
|
}
|
||||||
|
last = px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activity?.segments) {
|
||||||
|
for (let segment of activity.segments) {
|
||||||
|
let last;
|
||||||
|
for (let pt of segment) {
|
||||||
|
let px = [
|
||||||
|
Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)),
|
||||||
|
Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)),
|
||||||
|
];
|
||||||
|
if (last) {
|
||||||
|
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
||||||
|
}
|
||||||
|
last = px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_upload(event) {
|
||||||
|
try {
|
||||||
|
let file = event.srcElement.files[0];
|
||||||
|
let xml = await file.text();
|
||||||
|
let gpx = gpx_parse(xml);
|
||||||
|
let blob_id = await tfrpc.rpc.store_blob(xml);
|
||||||
|
console.log('blob_id = ', blob_id);
|
||||||
|
console.log(gpx);
|
||||||
|
let message = {
|
||||||
|
type: 'gg-activity',
|
||||||
|
mentions: [
|
||||||
|
{
|
||||||
|
link: `https://${gpx.link}/activity/${gpx.time}`,
|
||||||
|
name: 'activity_url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: blob_id,
|
||||||
|
name: 'activity_data',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
console.log('id =', this.whoami, 'message = ', message);
|
||||||
|
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
|
console.log('appended message', id);
|
||||||
|
alert('Activity uploaded.');
|
||||||
|
await this.get_activities_from_ssb();
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Error: ${JSON.stringify(e, null, 2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upload() {
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.onchange = (event) => this.on_upload(event);
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
this.update_map();
|
||||||
|
}
|
||||||
|
|
||||||
|
focus_map(activity) {
|
||||||
|
let bounds = this.activity_bounds(activity);
|
||||||
|
if (bounds.min.lat < bounds.max.lat &&
|
||||||
|
bounds.min.lng < bounds.max.lng) {
|
||||||
|
this.tab = 'map';
|
||||||
|
this.focus = bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_news() {
|
||||||
|
return html`
|
||||||
|
<ul>
|
||||||
|
${this.loaded_activities.map(x => html`
|
||||||
|
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>${x.author} ${x.name ?? x.time}</li>
|
||||||
|
`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_store_item(item) {
|
||||||
|
let [emoji, cost] = item;
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<input type="button" value="${emoji}" @click=${() => this.to_build = emoji}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_store() {
|
||||||
|
let store = Object.assign({}, k_store);
|
||||||
|
store[this.emoji_of_the_day] = 5;
|
||||||
|
return html`
|
||||||
|
<h2>Store</h2>
|
||||||
|
<div><b>Your balance:</b> ${this.currency}</div>
|
||||||
|
${Object.entries(store).map(this.render_store_item.bind(this))}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let header;
|
||||||
|
if (!this.user?.credentials?.session?.name) {
|
||||||
|
header = html`<div style="flex: 1 0">Please <a target="_top" href="/login?return=${this.url}">login</a> to Tilde Friends, first.</div>`;
|
||||||
|
} else if (!this.strava?.access_token) {
|
||||||
|
let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
|
||||||
|
header = html`
|
||||||
|
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
||||||
|
<div style="flex: 1 1">Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>
|
||||||
|
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
||||||
|
<input type="button" value="📁" @click=${this.upload}></input>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
header = html`
|
||||||
|
<div>
|
||||||
|
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
||||||
|
<h1>Welcome, ${this.user.credentials.session.name}</h1>
|
||||||
|
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
||||||
|
<input type="button" value="📁" @click=${this.upload}></input>
|
||||||
|
</div>
|
||||||
|
<h3 ?hidden=${!this.status?.text}>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let navigation = html`
|
||||||
|
<style>
|
||||||
|
#navigation input[type="button"] {
|
||||||
|
min-width: 3em;
|
||||||
|
min-height: 3em;
|
||||||
|
flex: 1 0;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="navigation" style="display: flex; flex-direction: row">
|
||||||
|
<input type="button" id="button_map" @click=${() => this.tab = 'map'} value="🗺️Map"></input>
|
||||||
|
<input type="button" id="button_news" @click=${() => this.tab = 'news'} value="🏃News"></input>
|
||||||
|
<input type="button" id="button_friends" @click=${() => this.tab = 'friends'} value="👫Friends"></input>
|
||||||
|
<input type="button" id="button_store" @click=${() => this.tab = 'store'} value="🏗️Store"></input>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
switch (this.tab) {
|
||||||
|
case 'map':
|
||||||
|
content = html`<div id="map" style="width: 100%; height: 100%"></div>`;
|
||||||
|
break;
|
||||||
|
case 'news':
|
||||||
|
content = this.render_news();
|
||||||
|
break;
|
||||||
|
case 'friends':
|
||||||
|
content = html`<div>Friends</div>`;
|
||||||
|
break;
|
||||||
|
case 'store':
|
||||||
|
content = this.render_store();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.build-icon::before {
|
||||||
|
content: '📍';
|
||||||
|
border: 2px solid red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="leaflet.css"/>
|
||||||
|
<div style="width: 100%; height: 100%; display: flex; flex-direction: column">
|
||||||
|
${header}
|
||||||
|
<div style="flex: 1 0; overflow: scroll">${content}</div>
|
||||||
|
${navigation}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('gg-app', GgAppElement);
|
20
apps/gg/strava.js
Normal file
20
apps/gg/strava.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const k_client_id = '28276';
|
||||||
|
const k_client_secret = '3123f1f5afe132d9731111066d1d17bdb22ef27e';
|
||||||
|
const k_access_token = 'f753e77764c26252bd2d80e7c5cc17ace51a8864';
|
||||||
|
const k_refresh_token = 'f58d8e1b5a3ec3bf96e681589d5014f9a294f5a4';
|
||||||
|
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
|
||||||
|
|
||||||
|
export async function refresh_token(token) {
|
||||||
|
let r = await fetch('https://www.strava.com/api/v3/oauth/token', {
|
||||||
|
method: 'POST',
|
||||||
|
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&refresh_token=${token.refresh_token}&grant_type=refresh_token`,
|
||||||
|
});
|
||||||
|
return r?.body ? JSON.parse(utf8Decode(r.body)) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authorization_code(code) {
|
||||||
|
return await fetch('https://www.strava.com/api/v3/oauth/token', {
|
||||||
|
method: 'POST',
|
||||||
|
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&code=${code}&grant_type=authorization_code`,
|
||||||
|
});
|
||||||
|
}
|
5
apps/identity.json
Normal file
5
apps/identity.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🪪",
|
||||||
|
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
||||||
|
}
|
87
apps/identity/app.js
Normal file
87
apps/identity/app.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
tfrpc.register(async function get_private_key(id) {
|
||||||
|
return bip39Words(await ssb.getPrivateKey(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function create_id(id) {
|
||||||
|
return await ssb.createIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function add_id(id) {
|
||||||
|
return await ssb.addIdentity(bip39Bytes(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function delete_id(id) {
|
||||||
|
return await ssb.deleteIdentity(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function reload() {
|
||||||
|
await main();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let ids = await ssb.getIdentities();
|
||||||
|
await app.setDocument(`<body style="color: #fff">
|
||||||
|
<script>const handler = {};</script>
|
||||||
|
<script type="module">
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
handler.export_id = async function export_id(event) {
|
||||||
|
let id = event.srcElement.dataset.id;
|
||||||
|
let element = document.createElement('textarea');
|
||||||
|
element.value = await tfrpc.rpc.get_private_key(id);
|
||||||
|
element.style = 'width: 100%; read-only: true';
|
||||||
|
element.readOnly = true;
|
||||||
|
event.srcElement.parentElement.appendChild(element);
|
||||||
|
event.srcElement.onclick = event => handler.hide_id(event, element);
|
||||||
|
}
|
||||||
|
handler.add_id = async function add_id(event) {
|
||||||
|
let id = document.getElementById('add_id').value;
|
||||||
|
try {
|
||||||
|
let new_id = await tfrpc.rpc.add_id(id);
|
||||||
|
alert('Successfully imported: ' + new_id);
|
||||||
|
await tfrpc.rpc.reload();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Error importing identity: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.create_id = async function create_id(event) {
|
||||||
|
try {
|
||||||
|
let id = await tfrpc.rpc.create_id();
|
||||||
|
alert('Successfully created: ' + id);
|
||||||
|
await tfrpc.rpc.reload();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Error creating identity: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.hide_id = function hide_id(event, element) {
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
event.srcElement.onclick = handler.export_id;
|
||||||
|
}
|
||||||
|
handler.delete_id = async function delete_id(event) {
|
||||||
|
let id = event.srcElement.dataset.id;
|
||||||
|
try {
|
||||||
|
if (prompt('Are you sure you want to delete "' + id + '"? It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') {
|
||||||
|
if (await tfrpc.rpc.delete_id(id)) {
|
||||||
|
alert('Successfully deleted ID: ' + id);
|
||||||
|
}
|
||||||
|
await tfrpc.rpc.reload();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Error deleting ID: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<h1>SSB Identity Management</h1>
|
||||||
|
<h2>Create a new identity</h2>
|
||||||
|
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
|
||||||
|
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
||||||
|
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
||||||
|
<h2>Identities</h2>
|
||||||
|
<ul>`+
|
||||||
|
ids.map(id => `<li>
|
||||||
|
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
||||||
|
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
||||||
|
${id}
|
||||||
|
</li>`).join('\n')+
|
||||||
|
` </ul>
|
||||||
|
</body>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
5
apps/issues.json
Normal file
5
apps/issues.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🦟",
|
||||||
|
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
||||||
|
}
|
105
apps/issues/app.js
Normal file
105
apps/issues/app.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
let g_database;
|
||||||
|
let g_hash;
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageGet(key) {
|
||||||
|
return app.localStorageGet(key);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function localStorageSet(key, value) {
|
||||||
|
return app.localStorageSet(key, value);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseGet(key) {
|
||||||
|
return g_database ? g_database.get(key) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseSet(key, value) {
|
||||||
|
return g_database ? g_database.set(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createIdentity() {
|
||||||
|
return ssb.createIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getIdentities() {
|
||||||
|
return ssb.getIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getAllIdentities() {
|
||||||
|
return ssb.getAllIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getBroadcasts() {
|
||||||
|
return ssb.getBroadcasts();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getConnections() {
|
||||||
|
return ssb.connections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getStoredConnections() {
|
||||||
|
return ssb.storedConnections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function forgetStoredConnection(connection) {
|
||||||
|
return ssb.forgetStoredConnection(connection);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createTunnel(portal, target) {
|
||||||
|
return ssb.createTunnel(portal, target);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function connect(token) {
|
||||||
|
await ssb.connect(token);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function closeConnection(id) {
|
||||||
|
await ssb.closeConnection(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
|
return ssb.appendMessageWithIdentity(id, message);
|
||||||
|
});
|
||||||
|
core.register('message', async function message_handler(message) {
|
||||||
|
if (message.event == 'hashChange') {
|
||||||
|
g_hash = message.hash;
|
||||||
|
await tfrpc.rpc.hashChanged(message.hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tfrpc.register(function getHash(id, message) {
|
||||||
|
return g_hash;
|
||||||
|
});
|
||||||
|
tfrpc.register(function setHash(hash) {
|
||||||
|
return app.setHash(hash);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('message', async function(id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_message(message) {
|
||||||
|
return await ssb.storeMessage(message);
|
||||||
|
});
|
||||||
|
tfrpc.register(function apps() {
|
||||||
|
return core.apps();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('broadcasts', async function() {
|
||||||
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
|
});
|
||||||
|
|
||||||
|
core.register('onConnectionsChanged', async function() {
|
||||||
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (typeof(database) !== 'undefined') {
|
||||||
|
g_database = await database('ssb');
|
||||||
|
}
|
||||||
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
}
|
||||||
|
main();
|
91
apps/issues/commonmark-linkify.js
Normal file
91
apps/issues/commonmark-linkify.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
function textNode(text) {
|
||||||
|
const node = new commonmark.Node("text", undefined);
|
||||||
|
node.literal = text;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkNode(text, url) {
|
||||||
|
const urlNode = new commonmark.Node("link", undefined);
|
||||||
|
urlNode.destination = url;
|
||||||
|
urlNode.appendChild(textNode(text));
|
||||||
|
|
||||||
|
return urlNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMatches(text, regexp) {
|
||||||
|
// Regexp must be sticky.
|
||||||
|
regexp = new RegExp(regexp, "gm");
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let match = regexp.exec(text);
|
||||||
|
while (match) {
|
||||||
|
const matchText = match[0];
|
||||||
|
|
||||||
|
if (match.index > i) {
|
||||||
|
result.push([text.substring(i, match.index), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push([matchText, true]);
|
||||||
|
i = match.index + matchText.length;
|
||||||
|
|
||||||
|
match = regexp.exec(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < text.length) {
|
||||||
|
result.push([text.substring(i, text.length), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
|
||||||
|
|
||||||
|
function splitURLs(textNodes) {
|
||||||
|
const text = textNodes.map(n => n.literal).join("");
|
||||||
|
const parts = splitMatches(text, urlRegexp);
|
||||||
|
|
||||||
|
return parts.map(part => {
|
||||||
|
if (part[1]) {
|
||||||
|
return linkNode(part[0], part[0]);
|
||||||
|
} else {
|
||||||
|
return textNode(part[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transform(parsed) {
|
||||||
|
const walker = parsed.walker();
|
||||||
|
let event;
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
const node = event.node;
|
||||||
|
if (event.entering && node.type === "text") {
|
||||||
|
nodes.push(node);
|
||||||
|
} else {
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
nodes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
1
apps/issues/commonmark.min.js
vendored
Normal file
1
apps/issues/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
apps/issues/index.html
Normal file
14
apps/issues/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html style="color: #fff">
|
||||||
|
<head>
|
||||||
|
<title>Tilde Friends</title>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<tf-issues-app/>
|
||||||
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
|
<script src="commonmark.min.js"></script>
|
||||||
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
|
<script src="script.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
120
apps/issues/lit-all.min.js
vendored
Normal file
120
apps/issues/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/issues/lit-all.min.js.map
Normal file
1
apps/issues/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
260
apps/issues/script.js
Normal file
260
apps/issues/script.js
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import * as tfutils from './tf-utils.js';
|
||||||
|
|
||||||
|
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
||||||
|
|
||||||
|
class TfIdPickerElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
ids: {type: Array},
|
||||||
|
selected: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.selected = await tfrpc.rpc.localStorageGet('whoami');
|
||||||
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
changed(event) {
|
||||||
|
this.selected = event.srcElement.value;
|
||||||
|
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.ids) {
|
||||||
|
return html`
|
||||||
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
|
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return html`<div>Loading...</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-id-picker', TfIdPickerElement);
|
||||||
|
|
||||||
|
class TfComposeElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
value: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
input() {
|
||||||
|
let input = this.renderRoot.getElementById('input');
|
||||||
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
|
if (input && preview) {
|
||||||
|
preview.innerHTML = tfutils.markdown(input.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.dispatchEvent(new CustomEvent('tf-submit', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
value: this.renderRoot.getElementById('input').value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
this.renderRoot.getElementById('input').value = '';
|
||||||
|
this.input();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea>
|
||||||
|
<div id="preview" style="flex: 1 1"></div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Submit" @click=${this.submit}></input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-compose', TfComposeElement);
|
||||||
|
|
||||||
|
class TfIssuesAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
issues: {type: Array},
|
||||||
|
selected: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.issues = [];
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
let issues = {};
|
||||||
|
let messages = await tfrpc.rpc.query(`
|
||||||
|
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
|
||||||
|
messages.id = messages_refs.message
|
||||||
|
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
||||||
|
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
|
||||||
|
issues.id = messages_refs.ref JOIN messages ON
|
||||||
|
messages.id = messages_refs.message
|
||||||
|
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
||||||
|
SELECT * FROM issues
|
||||||
|
UNION
|
||||||
|
SELECT * FROM edits ORDER BY timestamp
|
||||||
|
`, [k_project]);
|
||||||
|
for (let message of messages) {
|
||||||
|
let content = JSON.parse(message.content);
|
||||||
|
switch (content.type) {
|
||||||
|
case 'issue':
|
||||||
|
issues[message.id] = {
|
||||||
|
id: message.id,
|
||||||
|
author: message.author,
|
||||||
|
text: content.text,
|
||||||
|
updates: [],
|
||||||
|
created: message.timestamp,
|
||||||
|
open: true,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'issue-edit':
|
||||||
|
case 'post':
|
||||||
|
for (let issue of (content.issues || [])) {
|
||||||
|
if (issues[issue.link]) {
|
||||||
|
if (issue.open !== undefined) {
|
||||||
|
issues[issue.link].open = issue.open;
|
||||||
|
message.open = issue.open;
|
||||||
|
}
|
||||||
|
issues[issue.link].updates.push(message);
|
||||||
|
issues[issue.link].updated = message.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.issues = Object.values(issues).sort((x, y) => (y.open - x.open) || (y.created - x.created));
|
||||||
|
if (this.selected) {
|
||||||
|
for (let issue of this.issues) {
|
||||||
|
if (issue.id == this.selected.id) {
|
||||||
|
this.selected = issue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_issue_table_row(issue) {
|
||||||
|
return html`
|
||||||
|
<tr>
|
||||||
|
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
||||||
|
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td>
|
||||||
|
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}>
|
||||||
|
${issue.text.split('\n')?.[0]}
|
||||||
|
</td>
|
||||||
|
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_update(update) {
|
||||||
|
let content = JSON.parse(update.content);
|
||||||
|
let message;
|
||||||
|
if (content.text) {
|
||||||
|
message = unsafeHTML(tfutils.markdown(content.text));
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div style="border-left: 2px solid #fff; padding-left: 8px">
|
||||||
|
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
||||||
|
<div>${update.author}</div>
|
||||||
|
<div>${message}</div>
|
||||||
|
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set_open(id, open) {
|
||||||
|
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) {
|
||||||
|
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||||
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
|
type: 'issue-edit',
|
||||||
|
issues: [
|
||||||
|
{
|
||||||
|
link: id,
|
||||||
|
open: open,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create_issue(event) {
|
||||||
|
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||||
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
|
type: 'issue',
|
||||||
|
project: k_project,
|
||||||
|
text: event.detail.value,
|
||||||
|
});
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async reply_to_issue(event) {
|
||||||
|
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||||
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
|
type: 'post',
|
||||||
|
text: event.detail.value,
|
||||||
|
root: this.selected.id,
|
||||||
|
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
|
||||||
|
issues: [
|
||||||
|
{
|
||||||
|
link: this.selected.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let header = html`
|
||||||
|
<h1>Tilde Friends Issues</h1>
|
||||||
|
<tf-id-picker id="picker"></tf-id-picker>
|
||||||
|
`;
|
||||||
|
if (this.selected) {
|
||||||
|
return html`
|
||||||
|
${header}
|
||||||
|
<div>
|
||||||
|
<input type="button" value="Back" @click=${() => this.selected = undefined}></input>
|
||||||
|
${this.selected.open ?
|
||||||
|
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` :
|
||||||
|
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`}
|
||||||
|
</div>
|
||||||
|
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
||||||
|
<div>${this.selected.author}</div>
|
||||||
|
<div>${this.selected.id}</div>
|
||||||
|
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
||||||
|
${this.selected.updates.map(x => this.render_update(x))}
|
||||||
|
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
${header}
|
||||||
|
<h2>New Issue</h2>
|
||||||
|
<tf-compose @tf-submit=${this.create_issue}></tf-compose>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Author</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Date</th>
|
||||||
|
</tr>
|
||||||
|
${this.issues.map(x => this.render_issue_table_row(x))}
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-issues-app', TfIssuesAppElement);
|
91
apps/issues/tf-utils.js
Normal file
91
apps/issues/tf-utils.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
|
||||||
|
function image(node, entering) {
|
||||||
|
if (node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</video>');
|
||||||
|
}
|
||||||
|
} else if (node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</audio>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entering) {
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
||||||
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
|
this.lit('<img src="" alt="');
|
||||||
|
} else {
|
||||||
|
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
if (node.title) {
|
||||||
|
this.lit('" title="' + this.esc(node.title));
|
||||||
|
}
|
||||||
|
this.lit('" />');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdown(md) {
|
||||||
|
var reader = new commonmark.Parser({safe: true});
|
||||||
|
var writer = new commonmark.HtmlRenderer();
|
||||||
|
writer.image = image;
|
||||||
|
var parsed = reader.parse(md || '');
|
||||||
|
parsed = linkify.transform(parsed);
|
||||||
|
var walker = parsed.walker();
|
||||||
|
var event, node;
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
node = event.node;
|
||||||
|
if (event.entering) {
|
||||||
|
if (node.type == 'link') {
|
||||||
|
if (node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')) {
|
||||||
|
node.destination = '#' + node.destination;
|
||||||
|
} else if (node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')) {
|
||||||
|
node.destination = '#' + node.destination;
|
||||||
|
} else if (node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')) {
|
||||||
|
node.destination = '/' + node.destination + '/view';
|
||||||
|
}
|
||||||
|
} else if (node.type == 'image') {
|
||||||
|
if (node.destination.startsWith('&')) {
|
||||||
|
node.destination = '/' + node.destination + '/view';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function human_readable_size(bytes) {
|
||||||
|
let v = bytes;
|
||||||
|
let u = 'B';
|
||||||
|
for (let unit of ['kB', 'MB', 'GB']) {
|
||||||
|
if (v > 1024) {
|
||||||
|
v /= 1024;
|
||||||
|
u = unit;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${Math.round(v * 10) / 10} ${u}`;
|
||||||
|
}
|
5
apps/journal.json
Normal file
5
apps/journal.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📝",
|
||||||
|
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
||||||
|
}
|
173
apps/journal/app.js
Normal file
173
apps/journal/app.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
let g_hash;
|
||||||
|
let g_collection_notifies = {};
|
||||||
|
|
||||||
|
tfrpc.register(async function getOwnerIdentities() {
|
||||||
|
return ssb.getOwnerIdentities();
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function getIdentities() {
|
||||||
|
return ssb.getIdentities();
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageGet(key) {
|
||||||
|
return app.localStorageGet(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageSet(key, value) {
|
||||||
|
return app.localStorageSet(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function following(ids, depth) {
|
||||||
|
return ssb.following(ids, depth);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
|
return ssb.appendMessageWithIdentity(id, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
let g_new_message_resolve;
|
||||||
|
let g_new_message_promise = new Promise(function(resolve, reject) {
|
||||||
|
g_new_message_resolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
function new_message() {
|
||||||
|
return g_new_message_promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssb.addEventListener('message', function(id) {
|
||||||
|
let resolve = g_new_message_resolve;
|
||||||
|
g_new_message_promise = new Promise(function(resolve, reject) {
|
||||||
|
g_new_message_resolve = resolve;
|
||||||
|
});
|
||||||
|
if (resolve) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
core.register('message', async function message_handler(message) {
|
||||||
|
if (message.event == 'hashChange') {
|
||||||
|
print('hash change', message.hash);
|
||||||
|
g_hash = message.hash;
|
||||||
|
await tfrpc.rpc.hash_changed(message.hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(function set_hash(hash) {
|
||||||
|
if (g_hash != hash) {
|
||||||
|
return app.setHash(hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(function get_hash(id, message) {
|
||||||
|
return g_hash;
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function process_message(whoami, collection, message, kind, parent) {
|
||||||
|
let content = JSON.parse(message.content);
|
||||||
|
if (typeof content == 'string') {
|
||||||
|
let x;
|
||||||
|
for (let id of whoami) {
|
||||||
|
x = await ssb.privateMessageDecrypt(id, content);
|
||||||
|
if (x) {
|
||||||
|
content = JSON.parse(x);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!x) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (content.type !== kind ||
|
||||||
|
(parent && content.parent !== parent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (content?.key) {
|
||||||
|
if (content?.tombstone) {
|
||||||
|
delete collection[content.key];
|
||||||
|
} else {
|
||||||
|
collection[content.key] = Object.assign(collection[content.key] || {}, content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
||||||
|
let whoami = await ssb.getIdentities();
|
||||||
|
data = data ?? {};
|
||||||
|
let rowid = 0;
|
||||||
|
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
||||||
|
rowid = row.rowid;
|
||||||
|
});
|
||||||
|
while (true) {
|
||||||
|
if (rowid == max_rowid) {
|
||||||
|
await new_message();
|
||||||
|
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
||||||
|
rowid = row.rowid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified = false;
|
||||||
|
let rows = [];
|
||||||
|
await ssb.sqlAsync(`
|
||||||
|
SELECT messages.id, author, content, timestamp
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||||
|
WHERE
|
||||||
|
messages.rowid > ?2 AND
|
||||||
|
messages.rowid <= ?3 AND
|
||||||
|
((json_extract(messages.content, '$.type') = ?4 AND
|
||||||
|
(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR
|
||||||
|
content LIKE '"%')
|
||||||
|
`,
|
||||||
|
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
||||||
|
function(row) {
|
||||||
|
rows.push(row);
|
||||||
|
});
|
||||||
|
max_rowid = rowid;
|
||||||
|
for (let row of rows) {
|
||||||
|
if (await process_message(whoami, data, row, kind, parent)) {
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modified) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [rowid, data];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
1
apps/journal/commonmark.min.js
vendored
Normal file
1
apps/journal/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
apps/journal/index.html
Normal file
14
apps/journal/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body style="color: #fff">
|
||||||
|
<tf-journal-app></tf-journal-app>
|
||||||
|
<script src="commonmark.min.js"></script>
|
||||||
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
|
<script src="tf-journal-app.js" type="module"></script>
|
||||||
|
<script src="tf-journal-entry.js" type="module"></script>
|
||||||
|
<script src="tf-id-picker.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
120
apps/journal/lit-all.min.js
vendored
Normal file
120
apps/journal/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/journal/lit-all.min.js.map
Normal file
1
apps/journal/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
36
apps/journal/tf-id-picker.js
Normal file
36
apps/journal/tf-id-picker.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Provide a list of IDs, and this lets the user pick one.
|
||||||
|
*/
|
||||||
|
class TfIdentityPickerElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
ids: {type: Array},
|
||||||
|
selected: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.ids = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
changed(event) {
|
||||||
|
this.selected = event.srcElement.value;
|
||||||
|
this.dispatchEvent(new Event('change', {
|
||||||
|
srcElement: this,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
|
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
75
apps/journal/tf-journal-app.js
Normal file
75
apps/journal/tf-journal-app.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import {LitElement, html, keyed, live} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfJournalAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
ids: {type: Array},
|
||||||
|
owner_ids: {type: Array},
|
||||||
|
whoami: {type: String},
|
||||||
|
journals: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.ids = [];
|
||||||
|
this.owner_ids = [];
|
||||||
|
this.journals = {};
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.ids = await tfrpc.rpc.getIdentities();
|
||||||
|
this.whoami = await tfrpc.rpc.localStorageGet('journal_whoami');
|
||||||
|
await this.read_journals();
|
||||||
|
}
|
||||||
|
|
||||||
|
async read_journals() {
|
||||||
|
let max_rowid;
|
||||||
|
let journals;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals);
|
||||||
|
this.journals = Object.assign({}, journals);
|
||||||
|
console.log('JOURNALS', this.journals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_whoami_changed(event) {
|
||||||
|
let new_id = event.srcElement.selected;
|
||||||
|
await tfrpc.rpc.localStorageSet('journal_whoami', new_id);
|
||||||
|
this.whoami = new_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_journal_publish(event) {
|
||||||
|
let key = event.detail.key;
|
||||||
|
let text = event.detail.text;
|
||||||
|
let message = {
|
||||||
|
type: 'journal-entry',
|
||||||
|
key: key,
|
||||||
|
text: text,
|
||||||
|
};
|
||||||
|
message.recps = [this.whoami];
|
||||||
|
print(message);
|
||||||
|
message = await tfrpc.rpc.encrypt(this.whoami, message.recps, JSON.stringify(message));
|
||||||
|
print(message);
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
console.log('RENDER APP', this.journals);
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed}></tf-id-picker>
|
||||||
|
</div>
|
||||||
|
<tf-journal-entry
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.journals=${this.journals}
|
||||||
|
@publish=${this.on_journal_publish}></tf-journal-entry>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-journal-app', TfJournalAppElement);
|
84
apps/journal/tf-journal-entry.js
Normal file
84
apps/journal/tf-journal-entry.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import {LitElement, html, unsafeHTML, range} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfJournalEntryElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
key: {type: String},
|
||||||
|
journals: {type: Object},
|
||||||
|
text: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.journals = {};
|
||||||
|
this.key = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown(md) {
|
||||||
|
var reader = new commonmark.Parser({safe: true});
|
||||||
|
var writer = new commonmark.HtmlRenderer();
|
||||||
|
var parsed = reader.parse(md || '');
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
on_discard(event) {
|
||||||
|
this.text = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_publish() {
|
||||||
|
console.log('publish', this.text);
|
||||||
|
this.dispatchEvent(new CustomEvent('publish', {
|
||||||
|
bubbles: true,
|
||||||
|
detail: {
|
||||||
|
key: this.shadowRoot.getElementById('date_picker').value,
|
||||||
|
text: this.text,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
back_dates(count) {
|
||||||
|
let now = new Date();
|
||||||
|
let result = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
let next = new Date(now);
|
||||||
|
next.setDate(now.getDate() - i);
|
||||||
|
result.push(next.toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_edit(event) {
|
||||||
|
this.text = event.srcElement.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_date_change(event) {
|
||||||
|
this.key = event.srcElement.value;
|
||||||
|
this.text = this.journals[this.key]?.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
||||||
|
return html`
|
||||||
|
<select id="date_picker" @change=${this.on_date_change}>
|
||||||
|
${this.back_dates(10).map(x => html`
|
||||||
|
<option value=${x}>${x}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
|
<div style="display: inline-flex; flex-direction: row">
|
||||||
|
<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</button>
|
||||||
|
<button @click=${this.on_discard}>Discard</button>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<textarea
|
||||||
|
style="flex: 1 1; min-height: 10em"
|
||||||
|
@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea>
|
||||||
|
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-journal-entry', TfJournalEntryElement);
|
50
apps/sneaker/lit-all.min.js
vendored
50
apps/sneaker/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -118,17 +118,23 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);
|
zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);
|
||||||
|
|
||||||
let blobs = await tfrpc.rpc.query(
|
let blobs = await tfrpc.rpc.query(
|
||||||
`SELECT blobs.id
|
`SELECT messages_refs.ref AS id
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN messages_refs ON messages.id = messages_refs.message
|
JOIN messages_refs ON messages.id = messages_refs.message
|
||||||
JOIN blobs ON messages_refs.ref = blobs.id
|
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
||||||
WHERE messages.author = ?`,
|
|
||||||
[id]);
|
[id]);
|
||||||
let blobs_done = 0;
|
let blobs_done = 0;
|
||||||
for (let row of blobs) {
|
for (let row of blobs) {
|
||||||
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
||||||
let blob = await tfrpc.rpc.get_blob(row.id);
|
let blob;
|
||||||
|
try {
|
||||||
|
blob = await tfrpc.rpc.get_blob(row.id);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed to get ${row.id}: ${e.message}`);
|
||||||
|
}
|
||||||
|
if (blob) {
|
||||||
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
||||||
|
}
|
||||||
blobs_done++;
|
blobs_done++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🐌"
|
"emoji": "🐌",
|
||||||
|
"previous": "&h+PXCrnUHtHHfKyUaLW+Y1dP/JpWwG9cbRNjxOCVqw0=.sha256"
|
||||||
}
|
}
|
@ -18,12 +18,21 @@ tfrpc.register(async function databaseSet(key, value) {
|
|||||||
tfrpc.register(async function createIdentity() {
|
tfrpc.register(async function createIdentity() {
|
||||||
return ssb.createIdentity();
|
return ssb.createIdentity();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function getServerIdentity() {
|
||||||
|
return ssb.getServerIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function setServerFollowingMe(id, following) {
|
||||||
|
return ssb.setServerFollowingMe(id, following);
|
||||||
|
});
|
||||||
tfrpc.register(async function getIdentities() {
|
tfrpc.register(async function getIdentities() {
|
||||||
return ssb.getIdentities();
|
return ssb.getIdentities();
|
||||||
});
|
});
|
||||||
tfrpc.register(async function getAllIdentities() {
|
tfrpc.register(async function getAllIdentities() {
|
||||||
return ssb.getAllIdentities();
|
return ssb.getAllIdentities();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function following(ids, depth) {
|
||||||
|
return ssb.following(ids, depth);
|
||||||
|
});
|
||||||
tfrpc.register(async function getBroadcasts() {
|
tfrpc.register(async function getBroadcasts() {
|
||||||
return ssb.getBroadcasts();
|
return ssb.getBroadcasts();
|
||||||
});
|
});
|
||||||
@ -88,6 +97,9 @@ tfrpc.register(function apps() {
|
|||||||
tfrpc.register(async function try_decrypt(id, content) {
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
return await ssb.privateMessageDecrypt(id, content);
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
|
});
|
||||||
ssb.addEventListener('broadcasts', async function() {
|
ssb.addEventListener('broadcasts', async function() {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
@ -64,7 +64,7 @@ export function picker(callback, anchor) {
|
|||||||
while (list.firstChild) {
|
while (list.firstChild) {
|
||||||
list.removeChild(list.firstChild);
|
list.removeChild(list.firstChild);
|
||||||
}
|
}
|
||||||
let search = input.value;
|
let search = input.value.toLowerCase();
|
||||||
let any_at_all = false;
|
let any_at_all = false;
|
||||||
for (let row of Object.entries(json)) {
|
for (let row of Object.entries(json)) {
|
||||||
let header = document.createElement('div');
|
let header = document.createElement('div');
|
||||||
@ -74,7 +74,7 @@ export function picker(callback, anchor) {
|
|||||||
for (let entry of Object.entries(row[1])) {
|
for (let entry of Object.entries(row[1])) {
|
||||||
if (search &&
|
if (search &&
|
||||||
search.length &&
|
search.length &&
|
||||||
entry[0].indexOf(search) == -1) {
|
entry[0].toLowerCase().indexOf(search) == -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let emoji = document.createElement('span');
|
let emoji = document.createElement('span');
|
||||||
|
1
apps/ssb/filesaver.min.js.map
Normal file
1
apps/ssb/filesaver.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -3,15 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top">
|
||||||
<link rel="stylesheet" href="tribute.css" />
|
<link rel="stylesheet" href="tribute.css"/>
|
||||||
<style>
|
<style>
|
||||||
.tribute-container {
|
.tribute-container {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="background-color: #223a5e">
|
||||||
<tf-app/>
|
<tf-app class="w3-deep-purple"/>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
<script src="filesaver.min.js"></script>
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
|
50
apps/ssb/lit-all.min.js
vendored
50
apps/ssb/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -13,4 +13,5 @@ import * as tf_tab_news from './tf-tab-news.js';
|
|||||||
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
||||||
import * as tf_tab_search from './tf-tab-search.js';
|
import * as tf_tab_search from './tf-tab-search.js';
|
||||||
import * as tf_tab_connections from './tf-tab-connections.js';
|
import * as tf_tab_connections from './tf-tab-connections.js';
|
||||||
|
import * as tf_tab_query from './tf-tab-query.js';
|
||||||
import * as tf_tag from './tf-tag.js';
|
import * as tf_tag from './tf-tag.js';
|
@ -68,83 +68,13 @@ class TfElement extends LitElement {
|
|||||||
this.tab = 'connections';
|
this.tab = 'connections';
|
||||||
} else if (this.hash === '#mentions') {
|
} else if (this.hash === '#mentions') {
|
||||||
this.tab = 'mentions';
|
this.tab = 'mentions';
|
||||||
|
} else if (this.hash.startsWith('#sql=')) {
|
||||||
|
this.tab = 'query';
|
||||||
} else {
|
} else {
|
||||||
this.tab = 'news';
|
this.tab = 'news';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async contacts_internal(id, last_row_id, following, max_row_id) {
|
|
||||||
let result = Object.assign({}, following[id] || {});
|
|
||||||
result.following = result.following || {};
|
|
||||||
result.blocking = result.blocking || {};
|
|
||||||
let contacts = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT content FROM messages
|
|
||||||
WHERE author = ? AND
|
|
||||||
rowid > ? AND
|
|
||||||
rowid <= ? AND
|
|
||||||
json_extract(content, '$.type') = 'contact'
|
|
||||||
ORDER BY sequence
|
|
||||||
`,
|
|
||||||
[id, last_row_id, max_row_id]);
|
|
||||||
for (let row of contacts) {
|
|
||||||
let contact = JSON.parse(row.content);
|
|
||||||
if (contact.following === true) {
|
|
||||||
result.following[contact.contact] = true;
|
|
||||||
} else if (contact.following === false) {
|
|
||||||
delete result.following[contact.contact];
|
|
||||||
} else if (contact.blocking === true) {
|
|
||||||
result.blocking[contact.contact] = true;
|
|
||||||
} else if (contact.blocking === false) {
|
|
||||||
delete result.blocking[contact.contact];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
following[id] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async contact(id, last_row_id, following, max_row_id) {
|
|
||||||
return await this.contacts_internal(id, last_row_id, following, max_row_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
|
||||||
let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id)));
|
|
||||||
let result = {};
|
|
||||||
for (let i = 0; i < ids.length; i++) {
|
|
||||||
let id = ids[i];
|
|
||||||
let contact = contacts[i];
|
|
||||||
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
|
||||||
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
|
|
||||||
let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
|
|
||||||
result[id] = [id, ...found, ...deeper];
|
|
||||||
}
|
|
||||||
return [...new Set(Object.values(result).flat())];
|
|
||||||
}
|
|
||||||
|
|
||||||
async following_deep(ids, depth, blocking) {
|
|
||||||
const k_cache_version = 5;
|
|
||||||
let cache = await tfrpc.rpc.databaseGet('following');
|
|
||||||
cache = cache ? JSON.parse(cache) : {};
|
|
||||||
if (cache.version !== k_cache_version) {
|
|
||||||
cache = {
|
|
||||||
version: k_cache_version,
|
|
||||||
following: {},
|
|
||||||
last_row_id: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let max_row_id = (await tfrpc.rpc.query(`
|
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
|
||||||
`, []))[0].max_row_id;
|
|
||||||
let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
|
||||||
cache.last_row_id = max_row_id;
|
|
||||||
let store = JSON.stringify(cache);
|
|
||||||
/* 2023-02-20: Exceeding message size. */
|
|
||||||
//if (store.length < 512 * 1024) {
|
|
||||||
await tfrpc.rpc.databaseSet('following', store);
|
|
||||||
//}
|
|
||||||
return [result, cache.following];
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetch_about(ids, users) {
|
async fetch_about(ids, users) {
|
||||||
const k_cache_version = 1;
|
const k_cache_version = 1;
|
||||||
let cache = await tfrpc.rpc.databaseGet('about');
|
let cache = await tfrpc.rpc.databaseGet('about');
|
||||||
@ -226,6 +156,7 @@ class TfElement extends LitElement {
|
|||||||
]);
|
]);
|
||||||
if (messages && messages.length) {
|
if (messages && messages.length) {
|
||||||
this.unread = [...this.unread, ...messages];
|
this.unread = [...this.unread, ...messages];
|
||||||
|
this.unread = this.unread.slice(this.unread.length - 1024);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,13 +176,18 @@ class TfElement extends LitElement {
|
|||||||
if (confirm("Are you sure you want to create a new identity?")) {
|
if (confirm("Are you sure you want to create a new identity?")) {
|
||||||
await tfrpc.rpc.createIdentity();
|
await tfrpc.rpc.createIdentity();
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
if (this.ids && !this.whoami) {
|
||||||
|
this.whoami = this.ids[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_id_picker() {
|
render_id_picker() {
|
||||||
return html`
|
return html`
|
||||||
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
|
<div style="display: flex; gap: 8px">
|
||||||
<button @click=${this.create_identity}>Create Identity</button>
|
<tf-id-picker id="picker" style="flex: 1 1 auto" selected=${this.whoami} .ids=${this.ids} .users=${this.users} @change=${this._handle_whoami_changed}></tf-id-picker>
|
||||||
|
<button class="w3-button w3-dark-grey w3-border" style="flex: 0 0 auto" @click=${this.create_identity} id="create_identity">Create Identity</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,9 +214,21 @@ class TfElement extends LitElement {
|
|||||||
async load() {
|
async load() {
|
||||||
let whoami = this.whoami;
|
let whoami = this.whoami;
|
||||||
let tags = this.load_recent_tags();
|
let tags = this.load_recent_tags();
|
||||||
let [following, users] = await this.following_deep([whoami], 2, {});
|
let following = await tfrpc.rpc.following([whoami], 2);
|
||||||
users = await this.fetch_about(following.sort(), users);
|
let users = {};
|
||||||
this.following = following;
|
let by_count = [];
|
||||||
|
for (let [id, v] of Object.entries(following)) {
|
||||||
|
users[id] = {
|
||||||
|
following: v.of,
|
||||||
|
blocking: v.ob,
|
||||||
|
followed: v.if,
|
||||||
|
blocked: v.ib,
|
||||||
|
};
|
||||||
|
by_count.push({count: v.of, id: id});
|
||||||
|
}
|
||||||
|
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
||||||
|
users = await this.fetch_about(Object.keys(following).sort(), users);
|
||||||
|
this.following = Object.keys(following);
|
||||||
this.users = users;
|
this.users = users;
|
||||||
await tags;
|
await tags;
|
||||||
console.log(`load finished ${whoami} => ${this.whoami}`);
|
console.log(`load finished ${whoami} => ${this.whoami}`);
|
||||||
@ -293,7 +241,7 @@ class TfElement extends LitElement {
|
|||||||
let users = this.users;
|
let users = this.users;
|
||||||
if (this.tab === 'news') {
|
if (this.tab === 'news') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
return html`
|
return html`
|
||||||
@ -307,6 +255,10 @@ class TfElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
||||||
`;
|
`;
|
||||||
|
} else if (this.tab === 'query') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,6 +270,8 @@ class TfElement extends LitElement {
|
|||||||
await tfrpc.rpc.setHash('#connections');
|
await tfrpc.rpc.setHash('#connections');
|
||||||
} else if (tab === 'mentions') {
|
} else if (tab === 'mentions') {
|
||||||
await tfrpc.rpc.setHash('#mentions');
|
await tfrpc.rpc.setHash('#mentions');
|
||||||
|
} else if (tab === 'query') {
|
||||||
|
await tfrpc.rpc.setHash('#sql=');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,19 +279,25 @@ class TfElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||||
console.log(`starting loading ${this.whoami} ${this.loaded}`);
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.load().finally(function() {
|
this.load().finally(function() {
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const k_tabs = {
|
||||||
|
'📰': 'news',
|
||||||
|
'📡': 'connections',
|
||||||
|
'@': 'mentions',
|
||||||
|
'🔍': 'search',
|
||||||
|
'👩💻': 'query',
|
||||||
|
};
|
||||||
|
|
||||||
let tabs = html`
|
let tabs = html`
|
||||||
<div>
|
<div class="w3-bar w3-black">
|
||||||
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
|
${Object.entries(k_tabs).map(([k, v]) => html`
|
||||||
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
|
<button title=${v} class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab == v ? 'w3-red' : 'w3-black'}" @click=${() => self.set_tab(v)}>${k}</button>
|
||||||
<input type="button" class="tab" value="Mentions" ?disabled=${self.tab == 'mentions'} @click=${() => self.set_tab('mentions')}></input>
|
`)}
|
||||||
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
let contents =
|
let contents =
|
||||||
|
@ -176,7 +176,7 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
async submit() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
@ -195,14 +195,25 @@ class TfComposeElement extends LitElement {
|
|||||||
message.contentWarning = draft.content_warning;
|
message.contentWarning = draft.content_warning;
|
||||||
}
|
}
|
||||||
console.log('Would post:', message);
|
console.log('Would post:', message);
|
||||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
if (draft.encrypt_to) {
|
||||||
|
let to = new Set(draft.encrypt_to);
|
||||||
|
to.add(this.whoami);
|
||||||
|
to = [...to];
|
||||||
|
message.recps = to;
|
||||||
|
console.log('message is now', message);
|
||||||
|
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
|
||||||
|
console.log('encrypted as', message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||||
edit.value = '';
|
edit.value = '';
|
||||||
self.change();
|
self.change();
|
||||||
self.notify(undefined);
|
self.notify(undefined);
|
||||||
self.requestUpdate();
|
self.requestUpdate();
|
||||||
}).catch(function(error) {
|
|
||||||
alert(error.message);
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
@ -226,12 +237,47 @@ class TfComposeElement extends LitElement {
|
|||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async autocomplete(text, callback) {
|
||||||
|
this.last_autocomplete = text;
|
||||||
|
let results = [];
|
||||||
|
try {
|
||||||
|
let rows = await tfrpc.rpc.query(`
|
||||||
|
SELECT messages.content FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE messages.content LIKE ?
|
||||||
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
|
`, ['"' + text.replace('"', '""') + '"', `%%`]);
|
||||||
|
for (let row of rows) {
|
||||||
|
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
||||||
|
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
||||||
|
results.push({key: match[1], value: match[2]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (this.last_autocomplete === text) {
|
||||||
|
callback(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
|
collection: [
|
||||||
|
{
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||||
selectTemplate: function(item) {
|
selectTemplate: function(item) {
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
return `[@${item.original.key}](${item.original.value})`;
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: '&',
|
||||||
|
values: this.autocomplete,
|
||||||
|
selectTemplate: function(item) {
|
||||||
|
return ``;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
tribute.attach(this.renderRoot.getElementById('edit'));
|
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||||
}
|
}
|
||||||
@ -244,6 +290,16 @@ class TfComposeElement extends LitElement {
|
|||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.value);
|
||||||
this.last_updated_text = edit.value;
|
this.last_updated_text = edit.value;
|
||||||
}
|
}
|
||||||
|
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||||
|
if (encrypt) {
|
||||||
|
let tribute = new Tribute({
|
||||||
|
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||||
|
selectTemplate: function(item) {
|
||||||
|
return item.original.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tribute.attach(encrypt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_mention(id) {
|
remove_mention(id) {
|
||||||
@ -258,7 +314,7 @@ class TfComposeElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<div style="align-self: center; margin: 0.5em">
|
<div style="align-self: center; margin: 0.5em">
|
||||||
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
|
<button class="w3-button w3-dark-grey" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}>🚮</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
<h3>${mention.name}</h3>
|
<h3>${mention.name}</h3>
|
||||||
@ -301,12 +357,12 @@ class TfComposeElement extends LitElement {
|
|||||||
|
|
||||||
if (this.apps) {
|
if (this.apps) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div class="w3-card-4 w3-margin w3-padding">
|
||||||
<select id="select">
|
<select id="select" class="w3-select w3-dark-grey">
|
||||||
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
||||||
</select>
|
</select>
|
||||||
<input type="button" value="Attach" @click=${attach_selected_app}></input>
|
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>Attach</button>
|
||||||
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
|
<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -318,9 +374,9 @@ class TfComposeElement extends LitElement {
|
|||||||
self.apps = await tfrpc.rpc.apps();
|
self.apps = await tfrpc.rpc.apps();
|
||||||
}
|
}
|
||||||
if (!this.apps) {
|
if (!this.apps) {
|
||||||
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
|
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>Attach App</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
|
return html`<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Discard App</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,15 +392,17 @@ class TfComposeElement extends LitElement {
|
|||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
if (draft.content_warning !== undefined) {
|
if (draft.content_warning !== undefined) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div class="w3-container w3-padding">
|
||||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
|
<p>
|
||||||
|
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
</p>
|
||||||
|
<input type="text" class="w3-input w3-border w3-dark-grey" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
|
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -354,28 +412,78 @@ class TfComposeElement extends LitElement {
|
|||||||
return this.drafts[this.branch || ''] || {};
|
return this.drafts[this.branch || ''] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_encrypt(event) {
|
||||||
|
let input = event.srcElement;
|
||||||
|
let matches = input.value.match(/@.*?\.ed25519/g);
|
||||||
|
if (matches) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
let to = [...new Set(matches.concat(draft.encrypt_to))];
|
||||||
|
this.set_encrypt(to);
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_encrypt() {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
if (draft.encrypt_to === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
|
<label for="encrypt_to">🔐 To:</label>
|
||||||
|
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
${draft.encrypt_to.map(x => html`
|
||||||
|
<li>
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
|
||||||
|
</li>`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_encrypt(encrypt) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
draft.encrypt_to = encrypt;
|
||||||
|
this.notify(draft);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let draft = self.get_draft();
|
let draft = self.get_draft();
|
||||||
let content_warning =
|
let content_warning =
|
||||||
draft.content_warning !== undefined ?
|
draft.content_warning !== undefined ?
|
||||||
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
|
html`<div class="w3-panel w3-round-xlarge w3-blue">
|
||||||
|
<p id="content_warning_preview">${draft.content_warning}</p>
|
||||||
|
</div>` :
|
||||||
undefined;
|
undefined;
|
||||||
|
let encrypt = draft.encrypt_to !== undefined ?
|
||||||
|
undefined :
|
||||||
|
html`<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt([])}>🔐</button>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<div class="w3-card-4 w3-blue-grey w3-padding" style="box-sizing: border-box">
|
||||||
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
|
${this.render_encrypt()}
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
|
||||||
|
<div style="flex: 1 0 50%">
|
||||||
|
<p><textarea class="w3-input w3-dark-grey w3-border" style="resize: vertical" placeholder="Write a post here." id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste}>${draft.text}</textarea></p>
|
||||||
|
</div>
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
${content_warning}
|
${content_warning}
|
||||||
<div id="preview"></div>
|
<div id="preview"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
||||||
${this.render_content_warning()}
|
|
||||||
${this.render_attach_app()}
|
${this.render_attach_app()}
|
||||||
<input type="button" value="Submit" @click=${this.submit}></input>
|
${this.render_content_warning()}
|
||||||
<input type="button" value="Attach" @click=${this.attach}></input>
|
<button class="w3-button w3-dark-grey" id="submit" @click=${this.submit}>Submit</button>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.attach}>Attach</button>
|
||||||
${this.render_attach_app_button()}
|
${this.render_attach_app_button()}
|
||||||
<input type="button" value="Discard" @click=${this.discard}></input>
|
${encrypt}
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.discard}>Discard</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Provide a list of IDs, and this lets the user pick one.
|
** Provide a list of IDs, and this lets the user pick one.
|
||||||
@ -9,13 +10,16 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
return {
|
return {
|
||||||
ids: {type: Array},
|
ids: {type: Array},
|
||||||
selected: {type: String},
|
selected: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
|
||||||
this.ids = [];
|
this.ids = [];
|
||||||
|
this.users = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
@ -27,8 +31,8 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
<select class="w3-select w3-dark-grey w3-padding w3-border" @change=${this.changed} style="max-width: 100%; overflow: hidden">
|
||||||
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${this.users[id]?.name ? (this.users[id]?.name + ' - ') : undefined}<small>${id}</small></option>`)}
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,9 @@ class TfMessageElement extends LitElement {
|
|||||||
message: {type: Object},
|
message: {type: Object},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
raw: {type: Boolean},
|
format: {type: String},
|
||||||
blog_data: {type: String},
|
blog_data: {type: String},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
decrypted: {type: Object},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,13 +26,14 @@ class TfMessageElement extends LitElement {
|
|||||||
this.message = {};
|
this.message = {};
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.raw = false;
|
this.format = 'message';
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
this.decrypted = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show_reply() {
|
show_reply() {
|
||||||
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
|
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
|
||||||
|
encrypt_to: this.message?.decrypted?.recps,
|
||||||
|
}}});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,17 +213,17 @@ class TfMessageElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
if (this.message.child_messages?.length) {
|
if (this.message.child_messages?.length) {
|
||||||
if (!this.expanded[this.message.id]) {
|
if (!this.expanded[this.message.id]) {
|
||||||
return html`<input type="button" value=${this.total_child_messages(this.message) + ' More'} @click=${() => self.set_expanded(true)}></input>`;
|
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(true)}>+ ${this.total_child_messages(this.message) + ' More'}</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<input type="button" value="Collapse" @click=${() => self.set_expanded(false)}></input>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`;
|
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(false)}>Collapse</button>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_channels() {
|
render_channels() {
|
||||||
let content = this.message?.content;
|
let content = this.message?.content;
|
||||||
if (this.decrypted?.type == 'post') {
|
if (this?.messsage?.decrypted?.type == 'post') {
|
||||||
content = this.decrypted;
|
content = this.message.decrypted;
|
||||||
}
|
}
|
||||||
let channels = [];
|
let channels = [];
|
||||||
if (typeof content.channel === 'string') {
|
if (typeof content.channel === 'string') {
|
||||||
@ -240,45 +240,57 @@ class TfMessageElement extends LitElement {
|
|||||||
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
|
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async try_decrypt(content) {
|
|
||||||
let result = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
|
||||||
if (result) {
|
|
||||||
this.decrypted = JSON.parse(result);
|
|
||||||
} else {
|
|
||||||
this.decrypted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let content = this.message?.content;
|
let content = this.message?.content;
|
||||||
if (this.decrypted?.type == 'post') {
|
if (this.message?.decrypted?.type == 'post') {
|
||||||
content = this.decrypted;
|
content = this.message.decrypted;
|
||||||
}
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
let raw_button = this.raw ?
|
let raw_button;
|
||||||
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
|
switch (this.format) {
|
||||||
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
|
case 'raw':
|
||||||
|
if (content?.type == 'post' || content?.type == 'blog') {
|
||||||
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`;
|
||||||
|
} else {
|
||||||
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
||||||
|
break;
|
||||||
|
case 'decrypted':
|
||||||
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (this.message.decrypted) {
|
||||||
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'decrypted'}>Decrypted</button>`;
|
||||||
|
} else {
|
||||||
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
function small_frame(inner) {
|
function small_frame(inner) {
|
||||||
|
let body;
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
|
<div class="w3-card-4" style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
|
||||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||||
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
||||||
${raw_button}
|
${raw_button}
|
||||||
${self.raw ? self.render_raw() : inner}
|
${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
${self.render_votes()}
|
${self.render_votes()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (this.message?.type === 'contact_group') {
|
if (this.message?.type === 'contact_group') {
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||||
${this.message.messages.map(x =>
|
${this.message.messages.map(x =>
|
||||||
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
||||||
)}
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (this.message.placeholder) {
|
} else if (this.message.placeholder) {
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
||||||
<div>${this.render_votes()}</div>
|
<div>${this.render_votes()}</div>
|
||||||
${(this.message.child_messages || []).map(x => html`
|
${(this.message.child_messages || []).map(x => html`
|
||||||
@ -339,14 +351,23 @@ class TfMessageElement extends LitElement {
|
|||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}></tf-compose>
|
@tf-discard=${this.discard_reply}></tf-compose>
|
||||||
` : html`
|
` : html`
|
||||||
<input type="button" value="Reply" @click=${this.show_reply}></input>
|
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
||||||
`;
|
`;
|
||||||
let self = this;
|
let self = this;
|
||||||
let body = this.raw ?
|
let body;
|
||||||
this.render_raw() :
|
switch (this.format) {
|
||||||
unsafeHTML(tfutils.markdown(content.text));
|
case 'raw':
|
||||||
|
body = this.render_raw();
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</code>`;
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
body = unsafeHTML(tfutils.markdown(content.text));
|
||||||
|
break;
|
||||||
|
}
|
||||||
let content_warning = html`
|
let content_warning = html`
|
||||||
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
<div class="w3-panel w3-round-xlarge w3-blue" style="cursor: pointer" @click=${x => this.toggle_expanded(':cw')}><p>${content.contentWarning}</p></div>
|
||||||
`;
|
`;
|
||||||
let content_html =
|
let content_html =
|
||||||
html`
|
html`
|
||||||
@ -363,8 +384,8 @@ class TfMessageElement extends LitElement {
|
|||||||
` :
|
` :
|
||||||
content_warning :
|
content_warning :
|
||||||
content_html;
|
content_html;
|
||||||
let is_encrypted = this.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
||||||
let style_background = this.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -380,7 +401,7 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
${is_encrypted}
|
${is_encrypted}
|
||||||
@ -390,10 +411,44 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload}
|
||||||
${this.render_votes()}
|
${this.render_votes()}
|
||||||
<div>
|
<p>
|
||||||
${reply}
|
${reply}
|
||||||
<input type="button" value="React" @click=${this.react}></input>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
||||||
|
</p>
|
||||||
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (content.type === 'issue') {
|
||||||
|
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
||||||
|
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
${is_encrypted}
|
||||||
|
<span style="flex: 1"></span>
|
||||||
|
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
|
<span>${raw_button}</span>
|
||||||
|
</div>
|
||||||
|
${content.text}
|
||||||
|
${this.render_votes()}
|
||||||
|
<p>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
||||||
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -406,9 +461,16 @@ class TfMessageElement extends LitElement {
|
|||||||
this.expanded[(this.message.id || '') + ':blog'] ?
|
this.expanded[(this.message.id || '') + ':blog'] ?
|
||||||
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
||||||
undefined;
|
undefined;
|
||||||
let body = this.raw ?
|
let body;
|
||||||
this.render_raw() :
|
switch (this.format) {
|
||||||
html`
|
case 'raw':
|
||||||
|
body = this.render_raw();
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
body = content.summary;
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
body = html`
|
||||||
<div
|
<div
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||||
@click=${x => self.toggle_expanded(':blog')}>
|
@click=${x => self.toggle_expanded(':blog')}>
|
||||||
@ -420,6 +482,19 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload}
|
||||||
`;
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
||||||
|
<tf-compose
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
root=${this.message.content.root || this.message.id}
|
||||||
|
branch=${this.message.id}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-discard=${this.discard_reply}></tf-compose>
|
||||||
|
` : html`
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
||||||
|
`;
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -435,7 +510,7 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
@ -445,7 +520,12 @@ class TfMessageElement extends LitElement {
|
|||||||
|
|
||||||
<div>${body}</div>
|
<div>${body}</div>
|
||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
|
<div>
|
||||||
|
${reply}
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
||||||
|
</div>
|
||||||
${this.render_votes()}
|
${this.render_votes()}
|
||||||
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'pub') {
|
} else if (content.type === 'pub') {
|
||||||
@ -468,11 +548,12 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
} else if (typeof(this.message.content) == 'string') {
|
} else if (typeof(this.message.content) == 'string') {
|
||||||
if (this.decrypted) {
|
if (this.message?.decrypted) {
|
||||||
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.decrypted, null, 2)}</pre>`);
|
if (this.format == 'decrypted') {
|
||||||
} else if (this.decrypted === undefined) {
|
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`);
|
||||||
this.try_decrypt(content);
|
} else {
|
||||||
return small_frame(html`<span>🔐</span>`);
|
return small_frame(html`<span>🔓</span><div>${this.message.decrypted.type}</div>`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<span>🔒</span>`);
|
return small_frame(html`<span>🔒</span>`);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,9 @@ class TfProfileElement extends LitElement {
|
|||||||
id: {type: String},
|
id: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
size: {type: Number},
|
size: {type: Number},
|
||||||
|
server_follows_me: {type: Boolean},
|
||||||
|
following: {type: Boolean},
|
||||||
|
blocking: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +27,51 @@ class TfProfileElement extends LitElement {
|
|||||||
this.id = null;
|
this.id = null;
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.size = 0;
|
this.size = 0;
|
||||||
|
this.server_follows_me = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
if (this.whoami !== this._follow_whoami) {
|
||||||
|
this._follow_whoami = this.whoami;
|
||||||
|
this.following = undefined;
|
||||||
|
this.blocking = undefined;
|
||||||
|
|
||||||
|
let result = await tfrpc.rpc.query(`
|
||||||
|
SELECT json_extract(content, '$.following') AS following
|
||||||
|
FROM messages WHERE author = ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
|
json_extract(content, '$.contact') = ? AND
|
||||||
|
following IS NOT NULL
|
||||||
|
ORDER BY sequence DESC LIMIT 1
|
||||||
|
`, [this.whoami, this.id]);
|
||||||
|
this.following = result?.[0]?.following ?? false;
|
||||||
|
result = await tfrpc.rpc.query(`
|
||||||
|
SELECT json_extract(content, '$.blocking') AS blocking
|
||||||
|
FROM messages WHERE author = ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
|
json_extract(content, '$.contact') = ? AND
|
||||||
|
blocking IS NOT NULL
|
||||||
|
ORDER BY sequence DESC LIMIT 1
|
||||||
|
`, [this.whoami, this.id]);
|
||||||
|
this.blocking = result?.[0]?.blocking ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initial_load() {
|
||||||
|
this.server_follows_me = undefined;
|
||||||
|
let server_id = await tfrpc.rpc.getServerIdentity();
|
||||||
|
let followed = await tfrpc.rpc.query(`
|
||||||
|
SELECT json_extract(content, '$.following') AS following
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
|
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
||||||
|
`, [server_id, this.whoami]);
|
||||||
|
let is_followed = false;
|
||||||
|
for (let row of followed) {
|
||||||
|
is_followed = row.following != 0;
|
||||||
|
}
|
||||||
|
this.server_follows_me = is_followed;
|
||||||
}
|
}
|
||||||
|
|
||||||
modify(change) {
|
modify(change) {
|
||||||
@ -58,6 +106,7 @@ class TfProfileElement extends LitElement {
|
|||||||
name: original.name,
|
name: original.name,
|
||||||
description: original.description,
|
description: original.description,
|
||||||
image: original.image,
|
image: original.image,
|
||||||
|
publicWebHosting: original.publicWebHosting,
|
||||||
};
|
};
|
||||||
console.log(this.editing);
|
console.log(this.editing);
|
||||||
}
|
}
|
||||||
@ -103,7 +152,24 @@ class TfProfileElement extends LitElement {
|
|||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async server_follow_me(follow) {
|
||||||
|
try {
|
||||||
|
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.initial_load();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.id == this.whoami && this.editing && this.server_follows_me === undefined) {
|
||||||
|
this.initial_load();
|
||||||
|
}
|
||||||
|
this.load();
|
||||||
let self = this;
|
let self = this;
|
||||||
let profile = this.users[this.id] || {};
|
let profile = this.users[this.id] || {};
|
||||||
tfrpc.rpc.query(
|
tfrpc.rpc.query(
|
||||||
@ -116,50 +182,59 @@ class TfProfileElement extends LitElement {
|
|||||||
let block;
|
let block;
|
||||||
if (this.id === this.whoami) {
|
if (this.id === this.whoami) {
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
|
let server_follow;
|
||||||
|
if (this.server_follows_me === true) {
|
||||||
|
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(false)}>Server, Stop Following Me</button>`;
|
||||||
|
} else if (this.server_follows_me === false) {
|
||||||
|
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(true)}>Server, Follow Me</button>`;
|
||||||
|
}
|
||||||
edit = html`
|
edit = html`
|
||||||
<input type="button" value="Save Profile" @click=${this.save_edits}></input>
|
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>Save Profile</button>
|
||||||
<input type="button" value="Discard" @click=${this.discard_edits}></input>
|
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>Discard</button>
|
||||||
|
${server_follow}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
|
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami &&
|
||||||
this.users[this.whoami]?.following) {
|
this.following !== undefined) {
|
||||||
follow =
|
follow =
|
||||||
this.users[this.whoami].following[this.id] ?
|
this.following ?
|
||||||
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
|
html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>Unfollow</button>` :
|
||||||
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
|
html`<button class="w3-button w3-dark-grey" @click=${this.follow}>Follow</button>`;
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami &&
|
||||||
this.users[this.whoami]?.blocking) {
|
this.blocking !== undefined) {
|
||||||
block =
|
block =
|
||||||
this.users[this.whoami].blocking[this.id] ?
|
this.blocking ?
|
||||||
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
|
html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>Unblock</button>` :
|
||||||
html`<input type="button" value="Block" @click=${this.block}></input>`;
|
html`<button class="w3-button w3-dark-grey" @click=${this.block}>Block</button>`;
|
||||||
}
|
}
|
||||||
let edit_profile = this.editing ? html`
|
let edit_profile = this.editing ? html`
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
||||||
|
<div class="w3-container">
|
||||||
<div>
|
<div>
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
|
<input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
<input type="checkbox" id="public_web_hosting" value=${this.editing.public_web_hosting} @input=${event => this.editing = Object.assign({}, this.editing, {publicWebHosting: event.srcElement.checked})}></input>
|
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${event => self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked})}></input>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
|
||||||
</div>` : null;
|
</div>` : null;
|
||||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
||||||
image = this.editing?.image ?? image;
|
image = this.editing?.image ?? image;
|
||||||
let description = this.editing?.description ?? profile.description;
|
let description = this.editing?.description ?? profile.description;
|
||||||
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
||||||
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||||
${edit_profile}
|
${edit_profile}
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
@ -167,10 +242,10 @@ class TfProfileElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Following ${Object.keys(profile.following || {}).length} identities.
|
Following ${profile.following} identities.
|
||||||
Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities.
|
Followed by ${profile.followed} identities.
|
||||||
Blocking ${Object.keys(profile.blocking || {}).length} identities.
|
Blocking ${profile.blocking} identities.
|
||||||
Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities.
|
Blocked by ${profile.blocked} identities.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
${edit}
|
${edit}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {css} from './lit-all.min.js';
|
import {css} from './lit-all.min.js';
|
||||||
|
|
||||||
export let styles = css`
|
const tf = css`
|
||||||
a:link {
|
a:link {
|
||||||
color: #bbf;
|
color: #bbf;
|
||||||
}
|
}
|
||||||
@ -45,4 +45,259 @@ div.img_caption {
|
|||||||
div.img_caption::after {
|
div.img_caption::after {
|
||||||
content: ' ±';
|
content: ' ±';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #444;
|
||||||
|
padding-left: 3px;
|
||||||
|
padding-right: 3px;
|
||||||
|
border: 1px dotted #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
background-color: #607d8b;
|
||||||
|
border-left: 4px solid #fff;
|
||||||
|
padding: 8px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const w3 = css`
|
||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export let styles = [tf, w3];
|
@ -1,5 +1,6 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabConnectionsElement extends LitElement {
|
class TfTabConnectionsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@ -12,6 +13,8 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
let self = this;
|
||||||
@ -42,11 +45,8 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
||||||
if (peers.length) {
|
if (peers.length) {
|
||||||
return html`
|
let connections = this.connections.map(x => x.id);
|
||||||
<ul>
|
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`;
|
||||||
${peers.map(x => html`${self.render_room_peer(x)}`)}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +58,8 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
|
<button class="w3-button w3-dark-grey" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}>Connect</button>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
render_broadcast(connection) {
|
render_broadcast(connection) {
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(connection)} value="Connect"></input>
|
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(connection)}>Connect</button>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
${this.render_connection_summary(connection)}
|
${this.render_connection_summary(connection)}
|
||||||
</li>
|
</li>
|
||||||
@ -79,34 +79,41 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
|
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_connection(connection) {
|
||||||
|
return html`
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</button>
|
||||||
|
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
||||||
|
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
|
||||||
|
<ul>
|
||||||
|
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
|
${this.render_room_peers(connection.id)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
|
<div class="w3-container">
|
||||||
<h2>New Connection</h2>
|
<h2>New Connection</h2>
|
||||||
<div style="display: flex; flex-direction: column">
|
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
|
||||||
<textarea id="code"></textarea>
|
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}>Connect</button>
|
||||||
</div>
|
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} value="Connect"></input>
|
|
||||||
<h2>Broadcasts</h2>
|
<h2>Broadcasts</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections.map(x => html`
|
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
|
||||||
<li>
|
<li>${this.render_connection(x)}</li>
|
||||||
<input type="button" @click=${() => tfrpc.rpc.closeConnection(x)} value="Close"></input>
|
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
|
||||||
${self.render_room_peers(x)}
|
|
||||||
</li>
|
|
||||||
`)}
|
`)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Stored Connections (WIP)</h2>
|
<h2>Stored Connections (WIP)</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.stored_connections.map(x => html`
|
${this.stored_connections.map(x => html`
|
||||||
<li>
|
<li>
|
||||||
<input type="button" @click=${() => self.forget_stored_connection(x)} value="Forget"></input>
|
<button class="w3-button w3-dark-grey" @click=${() => self.forget_stored_connection(x)}>Forget</button>
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(x)} value="Connect"></input>
|
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(x)}>Connect</button>
|
||||||
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||||
</li>
|
</li>
|
||||||
`)}
|
`)}
|
||||||
@ -115,6 +122,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
<ul>
|
<ul>
|
||||||
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,12 +65,15 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
this.hash.substring(1),
|
this.hash.substring(1),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
return await tfrpc.rpc.query(
|
let promises = [];
|
||||||
|
const k_following_limit = 256;
|
||||||
|
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
||||||
|
promises.push(tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.*
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ?
|
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
||||||
ORDER BY messages.timestamp DESC)
|
ORDER BY messages.timestamp DESC)
|
||||||
SELECT messages.*
|
SELECT messages.*
|
||||||
FROM news
|
FROM news
|
||||||
@ -85,9 +88,16 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
SELECT news.* FROM news
|
SELECT news.* FROM news
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(this.following),
|
JSON.stringify(this.following.slice(i, i + k_following_limit)),
|
||||||
this.start_time,
|
this.start_time,
|
||||||
]);
|
/*
|
||||||
|
** Don't show messages more than a day into the future to prevent
|
||||||
|
** messages with far-future timestamps from staying at the top forever.
|
||||||
|
*/
|
||||||
|
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
return [].concat(...(await Promise.all(promises)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,19 +129,51 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
this.start_time,
|
this.start_time,
|
||||||
last_start_time,
|
last_start_time,
|
||||||
]);
|
]);
|
||||||
this.messages = [...more, ...this.messages];
|
this.messages = await this.decrypt([...more, ...this.messages]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(messages) {
|
||||||
|
console.log('decrypt');
|
||||||
|
let result = [];
|
||||||
|
for (let message of messages) {
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = JSON.parse(message?.content);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
if (typeof(content) === 'string') {
|
||||||
|
let decrypted;
|
||||||
|
try {
|
||||||
|
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
if (decrypted) {
|
||||||
|
try {
|
||||||
|
message.decrypted = JSON.parse(decrypted);
|
||||||
|
} catch {
|
||||||
|
message.decrypted = decrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(message);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add_messages(messages) {
|
||||||
|
this.messages = await this.decrypt([...messages, ...this.messages]);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.messages ||
|
if (!this.messages ||
|
||||||
this._messages_hash !== this.hash ||
|
this._messages_hash !== this.hash ||
|
||||||
this._messages_following !== this.following) {
|
this._messages_following !== this.following) {
|
||||||
console.log(`loading messages for ${this.whoami}`);
|
console.log(`loading messages for ${this.whoami} (following ${this.following.length})`);
|
||||||
let self = this;
|
let self = this;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this._messages_hash = this.hash;
|
this._messages_hash = this.hash;
|
||||||
this._messages_following = this.following;
|
this._messages_following = this.following;
|
||||||
this.fetch_messages().then(function(messages) {
|
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
|
||||||
self.messages = messages;
|
self.messages = messages;
|
||||||
console.log(`loading mesages done for ${self.whoami}`);
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
@ -141,7 +183,9 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
let more;
|
let more;
|
||||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||||
more = html`
|
more = html`
|
||||||
<input type="button" value="Load More" @click=${this.load_more}></input>
|
<p>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.load_more}>Load More</button>
|
||||||
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
|
@ -48,7 +48,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let news = this.shadowRoot?.getElementById('news');
|
let news = this.shadowRoot?.getElementById('news');
|
||||||
if (news) {
|
if (news) {
|
||||||
console.log('injecting messages', news.messages);
|
console.log('injecting messages', news.messages);
|
||||||
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
|
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x]))));
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,7 +66,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
counts[type] = (counts[type] || 0) + 1;
|
counts[type] = (counts[type] || 0) + 1;
|
||||||
}
|
}
|
||||||
return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
return '↻ Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
draft(event) {
|
draft(event) {
|
||||||
@ -106,10 +106,11 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let profile = this.hash.startsWith('#@') ?
|
let profile = this.hash.startsWith('#@') ?
|
||||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
||||||
return html`
|
return html`
|
||||||
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
|
<p class="w3-bar">
|
||||||
<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
|
<button class="w3-bar-item w3-button w3-dark-grey" @click=${this.show_more}>${this.new_messages_text()}</button>
|
||||||
|
</p>
|
||||||
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
||||||
<div><tf-compose whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
||||||
${profile}
|
${profile}
|
||||||
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
|
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
|
||||||
`;
|
`;
|
||||||
|
114
apps/ssb/tf-tab-query.js
Normal file
114
apps/ssb/tf-tab-query.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabQueryElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
following: {type: Array},
|
||||||
|
query: {type: String},
|
||||||
|
expanded: {type: Object},
|
||||||
|
results: {type: Array},
|
||||||
|
error: {type: Object},
|
||||||
|
duration: {type: Number},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.following = [];
|
||||||
|
this.expanded = {};
|
||||||
|
this.duration = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
console.log('Searching...', this.whoami, query);
|
||||||
|
this.results = [];
|
||||||
|
this.error = undefined;
|
||||||
|
this.duration = undefined;
|
||||||
|
let search = this.renderRoot.getElementById('search');
|
||||||
|
if (search) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
||||||
|
let start_time = new Date();
|
||||||
|
try {
|
||||||
|
this.results = await tfrpc.rpc.query(query, [])
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
let end_time = new Date();
|
||||||
|
this.duration = (end_time - start_time).valueOf();
|
||||||
|
console.log('Done.');
|
||||||
|
search = this.renderRoot.getElementById('search');
|
||||||
|
if (search) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search_keydown(event) {
|
||||||
|
if (event.keyCode == 13 && event.ctrlKey) {
|
||||||
|
this.query = this.renderRoot.getElementById('search').value;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_expand(event) {
|
||||||
|
if (event.detail.expanded) {
|
||||||
|
let expand = {};
|
||||||
|
expand[event.detail.id] = true;
|
||||||
|
this.expanded = Object.assign({}, this.expanded, expand);
|
||||||
|
} else {
|
||||||
|
delete this.expanded[event.detail.id];
|
||||||
|
this.expanded = Object.assign({}, this.expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_results() {
|
||||||
|
if (!this.results?.length) {
|
||||||
|
return html`<div>No results.</div>`;
|
||||||
|
} else {
|
||||||
|
let keys = Object.keys(this.results[0]).sort();
|
||||||
|
return html`<table style="width: 100%; max-width: 100%">
|
||||||
|
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr>
|
||||||
|
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</tr>`)}
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_error() {
|
||||||
|
if (this.error) {
|
||||||
|
return html`<h2 style="color: red">${this.error.message}</h2>
|
||||||
|
<pre style="color: red">${this.error.stack}</pre>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.query !== this.last_query) {
|
||||||
|
this.last_query = this.query;
|
||||||
|
this.search(this.query);
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||||
|
<textarea id="search" rows=8 class="w3-input w3-dark-grey" style="flex: 1; resize: vertical" @keydown=${this.search_keydown}>${this.query}</textarea>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Execute</button>
|
||||||
|
</div>
|
||||||
|
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
|
||||||
|
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
||||||
|
${this.render_error()}
|
||||||
|
${this.render_results()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-query', TfTabQueryElement);
|
@ -75,9 +75,9 @@ class TfTabSearchElement extends LitElement {
|
|||||||
}
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||||
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
<input type="text" class="w3-input w3-dark-grey" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||||
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
|
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||||
</div>
|
</div>
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
||||||
`;
|
`;
|
||||||
|
@ -46,14 +46,14 @@ function image(node, entering) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function markdown(md) {
|
export function markdown(md) {
|
||||||
var reader = new commonmark.Parser({safe: true});
|
let reader = new commonmark.Parser({safe: true});
|
||||||
var writer = new commonmark.HtmlRenderer();
|
let writer = new commonmark.HtmlRenderer();
|
||||||
writer.image = image;
|
writer.image = image;
|
||||||
var parsed = reader.parse(md || '');
|
let parsed = reader.parse(md || '');
|
||||||
parsed = linkify.transform(parsed);
|
parsed = linkify.transform(parsed);
|
||||||
parsed = hashtagify.transform(parsed);
|
parsed = hashtagify.transform(parsed);
|
||||||
var walker = parsed.walker();
|
let walker = parsed.walker();
|
||||||
var event, node;
|
let event, node;
|
||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
|
5
apps/welcome.json
Normal file
5
apps/welcome.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "👋",
|
||||||
|
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
|
||||||
|
}
|
5
apps/welcome/app.js
Normal file
5
apps/welcome/app.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
async function main() {
|
||||||
|
await app.setDocument(utf8Decode(getFile('index.html')));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
6
apps/welcome/brands.min.css
vendored
Normal file
6
apps/welcome/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
apps/welcome/fa-brands-400.ttf
Normal file
BIN
apps/welcome/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
apps/welcome/fa-brands-400.woff2
Normal file
BIN
apps/welcome/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
apps/welcome/fa-regular-400.ttf
Normal file
BIN
apps/welcome/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
apps/welcome/fa-regular-400.woff2
Normal file
BIN
apps/welcome/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
apps/welcome/fa-solid-900.ttf
Normal file
BIN
apps/welcome/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
apps/welcome/fa-solid-900.woff2
Normal file
BIN
apps/welcome/fa-solid-900.woff2
Normal file
Binary file not shown.
9
apps/welcome/fontawesome.min.css
vendored
Normal file
9
apps/welcome/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
173
apps/welcome/index.html
Normal file
173
apps/welcome/index.html
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="w3.css">
|
||||||
|
<link rel="stylesheet" href="fontawesome.min.css">
|
||||||
|
<link rel="stylesheet" href="regular.min.css">
|
||||||
|
<link rel="stylesheet" href="solid.min.css">
|
||||||
|
<link rel="stylesheet" href="brands.min.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body,h1,h2,h3,h4,h5 {font-family: "Poppins", sans-serif}
|
||||||
|
body {font-size: 16px;}
|
||||||
|
img {margin-bottom: -8px;}
|
||||||
|
.mySlides {display: none;}
|
||||||
|
</style>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body class="w3-content w3-black" style="max-width:1500px;">
|
||||||
|
<!-- The App Section -->
|
||||||
|
<div class="w3-padding-64 w3-white">
|
||||||
|
<div class="w3-row-padding">
|
||||||
|
<div class="w3-col l8 m6 w3-padding-32">
|
||||||
|
<h1 class="w3-jumbo">
|
||||||
|
<b>😎 Tilde Friends</b>
|
||||||
|
</h1>
|
||||||
|
<h1 class="w3-xxlarge w3-text-green"><b>Make apps and friends from the comfort of your web browser.</b></h1>
|
||||||
|
<p>Tilde Friends is a platform for building, running, and sharing web applications.</p>
|
||||||
|
<p>Available for lots of devices:
|
||||||
|
<i class="fa-brands fa-linux w3-xlarge"></i>
|
||||||
|
<i class="fa-brands fa-android w3-xlarge"></i>
|
||||||
|
<i class="fa-brands fa-apple w3-xlarge"></i>
|
||||||
|
<i class="fa fa-mobile-screen w3-xlarge"></i>
|
||||||
|
<i class="fa-brands fa-windows w3-xlarge"></i>
|
||||||
|
</p>
|
||||||
|
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/releases/"><i class="fa fa-download"></i> Download</a>
|
||||||
|
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/apps/"><i class="fa fa-link"></i> Try It</a>
|
||||||
|
</div>
|
||||||
|
<div class="w3-col l4 m6">
|
||||||
|
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SSB Section -->
|
||||||
|
<div class="w3-light-grey">
|
||||||
|
<div class="w3-row-padding w3-padding-64 ">
|
||||||
|
<div class="w3-col l4 m6 s4">
|
||||||
|
<a href="https://scuttlebutt.nz/"><img class="w3-image w3-round-large" src="ssb.png" alt="Secure Scuttlebutt"></a>
|
||||||
|
</div>
|
||||||
|
<div class="w3-col l8 m6" style="height: auto">
|
||||||
|
<h1 class="w3-jumbo"><b>Built for Sharing</b></h1>
|
||||||
|
<p>
|
||||||
|
Tilde Friends participates in the <a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed social network.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Share apps with friends. Discover new apps made by enemies. Post pictures of your coffee. Or just lurk.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The social network integration provides tools for connecting with other people world-wide
|
||||||
|
while still allowing apps and everything to operate offline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor Section -->
|
||||||
|
<div class="w3-container w3-padding-64 w3-light-blue">
|
||||||
|
<div class="w3-row-padding">
|
||||||
|
<div class="w3-col l8 m6">
|
||||||
|
<h1 class="w3-jumbo"><b>Edit Anything</b></h1>
|
||||||
|
<i class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray" style="padding: 32px"></i>
|
||||||
|
<p>
|
||||||
|
See that <code><b>edit</b></code> link near the top left corner of this page? It's there for
|
||||||
|
every Tilde Friends app, so you can modify and see your changes right away.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It's kind of like a wiki, but for code!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sandbox Section -->
|
||||||
|
<div class="w3-padding-64 w3-grey">
|
||||||
|
<div class="w3-row-padding">
|
||||||
|
<div class="w3-col">
|
||||||
|
<h1 class="w3-jumbo" style="text-align: right"><b>Sandbox Security</b></h1>
|
||||||
|
<i class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow" style="padding: 32px"></i>
|
||||||
|
<p>
|
||||||
|
Tilde Friends tries to make sure apps can be trusted using similar techniques to how web
|
||||||
|
browsers and operating systems do it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is all a work in progress, and it varies by platform, so don't give it all your
|
||||||
|
innermost secrets yet, but do kick its tires and
|
||||||
|
<a href="mailto:cory@tildefriends.net">share</a> any surprises you find.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Technlology Section -->
|
||||||
|
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
|
||||||
|
<h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
|
||||||
|
<p>Tilde Friends is built using boring, trusted tech.</p>
|
||||||
|
<p>Though of course for building Tilde Friends apps, you are free to use whatever fits.</p>
|
||||||
|
|
||||||
|
<div class="w3-row" style="margin-top:64px">
|
||||||
|
<a href="https://en.wikipedia.org/wiki/C_(programming_language)" class="w3-col s3">
|
||||||
|
<i class="fa fa-c w3-text-blue w3-jumbo"></i>
|
||||||
|
<p>C</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://bellard.org/quickjs/" class="w3-col s3">
|
||||||
|
<i class="fa-brands fa-js w3-text-orange w3-jumbo"></i>
|
||||||
|
<p>QuickJS</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.sqlite.org/" class="w3-col s3">
|
||||||
|
<i class="fa fa-database w3-text-red w3-jumbo"></i>
|
||||||
|
<p>SQLite</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/libuv/libuv" class="w3-col s3">
|
||||||
|
<i class="fa fa-bolt w3-text-yellow w3-jumbo"></i>
|
||||||
|
<p>libuv</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w3-row" style="margin-top:64px">
|
||||||
|
<a href="https://www.zlib.net/" class="w3-col s3">
|
||||||
|
<i class="fa fa-file-zipper w3-text-cyan w3-jumbo"></i>
|
||||||
|
<p>zlib</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://doc.libsodium.org/" class="w3-col s3">
|
||||||
|
<i class="fa fa-lock w3-text-purple w3-jumbo"></i>
|
||||||
|
<p>libsodium</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.openssl.org/" class="w3-col s3">
|
||||||
|
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
|
||||||
|
<p>OpenSSL </p>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ianlancetaylor/libbacktrace" class="w3-col s3">
|
||||||
|
<i class="fa fa-burst w3-text-pink w3-jumbo"></i>
|
||||||
|
<p>libbacktrace</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w3-row" style="margin-top:64px">
|
||||||
|
<a href="https://codemirror.net/5/" class="w3-col s3">
|
||||||
|
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
|
||||||
|
<p>CodeMirror</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/jlfwong/speedscope/" class="w3-col s3">
|
||||||
|
<i class="fa fa-microscope w3-text-orange w3-jumbo"></i>
|
||||||
|
<p>Speedscope</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/lit/lit/" class="w3-col s3">
|
||||||
|
<i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
|
||||||
|
<p>Lit</p>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.gnu.org/software/make/" class="w3-col s3">
|
||||||
|
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
|
||||||
|
<p>GNU Make</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
|
||||||
|
<p class="w3-medium">This page and Tilde Friends itself was made by Cory mostly in coffee shops and a local pizza place.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
apps/welcome/regular.min.css
vendored
Normal file
6
apps/welcome/regular.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(fa-regular-400.woff2) format("woff2"),url(fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}
|
6
apps/welcome/solid.min.css
vendored
Normal file
6
apps/welcome/solid.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(fa-solid-900.woff2) format("woff2"),url(fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
BIN
apps/welcome/ssb.png
Normal file
BIN
apps/welcome/ssb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user