Compare commits
496 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
fc3dd84122 | |||
9239441d73 | |||
b984811851 | |||
1c52446331 | |||
b6dffa8e66 | |||
315d650d27 | |||
07c121044a | |||
f3169afcf5 | |||
c371fc2a8e | |||
6889e11fd1 | |||
fb73fd0afc | |||
6fcebd7a08 | |||
15ea62a546 | |||
b0cd58f5aa | |||
7fe8f66fd3 | |||
68ca99e9d9 | |||
a2542c658b | |||
eb203c7e62 | |||
6ef466f3ed | |||
5074246462 | |||
73bbcebddb | |||
18128303b6 | |||
c4a2d790a3 | |||
c1ec150696 | |||
f4b856df15 | |||
85b87553dd | |||
5decdf3afa | |||
a4acee4939 | |||
d06aea2831 | |||
ae0a8b0a33 | |||
f0452704a1 | |||
b8b1f1ba80 | |||
caf7478da4 | |||
0e40ba78a4 | |||
d1eac6c9eb | |||
8f5201b2bc | |||
6022001d66 | |||
f018c367ed | |||
48c47f097a | |||
39ac215b5a | |||
7d562ce85c | |||
51b317233a | |||
87ce715011 | |||
ef5afc1e23 | |||
486212f22a | |||
0e8867dd6e | |||
ca28b5ca82 | |||
19e26c1759 | |||
790f6643a4 | |||
2158ad3c0b | |||
d904d8922f | |||
da50792500 | |||
b4629acc48 | |||
0cf4118330 | |||
dd61a6ecc3 | |||
8e6f1284e1 | |||
813d3cd492 | |||
f421606e21 | |||
1ccb9183b4 | |||
7d9b627f37 | |||
3038138909 | |||
2ca08d21e4 | |||
478e96fc5f | |||
e237c7ea1d | |||
bf9ff088fd | |||
e073ebedd1 | |||
10d4ae7dcc | |||
5b8bdbb3e4 | |||
c807e21c6b | |||
cc92d0e316 | |||
09c396d5a3 | |||
bc5bbca951 | |||
ed4faedcd7 | |||
251556ebed | |||
1324afb459 | |||
1119804fc2 | |||
cdf6440197 | |||
8727fe00af | |||
7da7890bb6 | |||
706bd2c51f | |||
acabec940e | |||
470b998b61 | |||
80fad05f23 | |||
07a912fb9a | |||
e9d83262c4 | |||
74323c22f9 | |||
2614e89b1b | |||
e092fe1399 | |||
9cbe895cb8 | |||
b0b0f74e83 | |||
d9eaa92c37 | |||
566d07117e | |||
2bffdb1168 | |||
1359b48c9f | |||
a69fb5eeac | |||
38e313350e | |||
5052dc04f2 | |||
9ef3a3aca0 | |||
7b91a2ec37 | |||
2926f855a1 | |||
639419db60 | |||
54747c127c | |||
791c3dd787 | |||
b00d75ab7c | |||
956ea0df56 | |||
30014040e7 | |||
ab055c3394 | |||
1e37eeea05 | |||
84aec0278d | |||
06642f58c5 | |||
e6d44b32f4 | |||
1f3f6e2b92 | |||
8f2d3e3bcd | |||
2df2fc5792 | |||
20b0337e0a | |||
e86b9dae48 | |||
71de897419 | |||
3edfaf9137 | |||
19c1784864 | |||
0d9fac7363 | |||
2fb91fccc0 | |||
24e1ab12ab | |||
10ea885d8d | |||
ec65faa12d | |||
53692a1ea8 | |||
ebef51b4ea | |||
a94d6f9271 | |||
3d2c88c201 | |||
bdeee7fc0e | |||
33a037e0ea | |||
2dc2d9ebf6 | |||
9748f0ed8b | |||
d6be2f7d54 | |||
63615747a7 | |||
fbb657a85c | |||
bdac0c7879 | |||
54dde76a8a | |||
2bbe22bc7a | |||
ad8532f7ac | |||
602941104e | |||
d38b41687c | |||
08125cd1e8 | |||
2ce2097a3f | |||
a5da17e1b1 | |||
2b0962f087 | |||
37173cce4c | |||
37edbd9824 | |||
a32bb02223 | |||
2ab1b84432 | |||
52ae19220c | |||
10bfa65a4e | |||
2a3b1a1e33 | |||
f74f4f6da9 | |||
12a8b7a058 | |||
400f07660f | |||
d532795b7f | |||
6064ed6a3a | |||
2c1a43df2e | |||
bf72782c9f | |||
63dcab30c3 | |||
50e48af7c4 | |||
9127a18ff0 | |||
61ff466908 | |||
1c10768aa4 | |||
992b123853 | |||
f736756b20 | |||
28d73f5b37 | |||
262b0e5e52 | |||
1e3807bcb9 | |||
2ed3295f77 | |||
8c9d687d50 | |||
b8b694864e | |||
961109635b | |||
86bc46a11e | |||
a6a6fe75ec | |||
f55f863867 | |||
4ce988d00b | |||
1548a8a852 | |||
a9551b057b | |||
88c7d91858 | |||
53cb80ebf7 | |||
1f67343d75 | |||
4bea8bb6ba | |||
8e1461b3f1 | |||
90b513d070 | |||
8a2d3d4669 | |||
1741403206 | |||
980db880cc | |||
507a62539d | |||
6b5d73ed5c | |||
1f77df7a90 | |||
fa87462405 | |||
a5f9f927e6 | |||
b35d74ce36 | |||
ac60be14a5 | |||
beda047eb0 | |||
f6742bebf3 | |||
7f334ad783 | |||
ffda896308 | |||
b2fbe9dfac | |||
6d6c41bffa | |||
e04d137af5 | |||
ec52e62908 | |||
6104af0d70 | |||
0ca05e297d | |||
e0dcec074c | |||
a8cecb5c64 | |||
582ee0e4d7 | |||
0ba54c2b7b | |||
3c288f7f68 |
@ -2,8 +2,10 @@ FROM bitnami/minideb:bullseye AS build
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libssl-dev
|
||||
gcc \
|
||||
libc6-dev \
|
||||
libssl-dev \
|
||||
make
|
||||
|
||||
COPY . /app
|
||||
RUN make -C /app -j $(nproc) release
|
||||
|
839
GNUmakefile
Normal file
839
GNUmakefile
Normal file
@ -0,0 +1,839 @@
|
||||
.ONESHELL:
|
||||
.DELETE_ON_ERROR:
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
VERSION_CODE := 13
|
||||
VERSION_NUMBER := 0.0.13
|
||||
VERSION_NAME := Served on grilled naan or gluten free sweet potato flatbread.
|
||||
|
||||
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 += \
|
||||
-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 += -Os
|
||||
$(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
|
||||
|
||||
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
|
||||
$(SODIUM_OBJS_unix): CFLAGS += \
|
||||
-DHAVE_ALLOCA_H
|
||||
|
||||
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
|
||||
|
||||
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_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/gg% apps/welcome% %.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 -x '*.map' --exclude=apps/gg* --exclude=apps/welcome* -r $(PACKAGE_DIRS) $(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/
|
||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends
|
||||
@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 -x '*.map' --exclude=apps/gg* --exclude=apps/welcome* -r $(PACKAGE_DIRS) $(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
|
369
Makefile
369
Makefile
@ -1,369 +0,0 @@
|
||||
.ONESHELL:
|
||||
.DELETE_ON_ERROR:
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
PROJECT = tildefriends
|
||||
BUILD_DIR ?= out
|
||||
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease
|
||||
UNAME_M := $(shell uname -m)
|
||||
|
||||
CFLAGS += \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-cast-function-type \
|
||||
-MMD \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-fno-omit-frame-pointer \
|
||||
-g
|
||||
LDFLAGS += -Wl,--gc-sections
|
||||
|
||||
NDK_PATH := /usr/lib/android-sdk/ndk-bundle
|
||||
NDK_API_VERSION := 30
|
||||
NDK_TARGET_TRIPLE := aarch64-linux-android
|
||||
|
||||
debug windebug androiddebug: CFLAGS += -Og
|
||||
debug release androidrelease: LDFLAGS += -rdynamic
|
||||
release winrelease: CFLAGS += -DNDEBUG -O3
|
||||
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
|
||||
androiddebug androidrelease: CC = $(NDK_PATH)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
|
||||
androiddebug androidrelease: AS = $(CC)
|
||||
androiddebug androidrelease: CFLAGS += \
|
||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
||||
-Ideps/openssl/android/arm64-v8a/usr/local/include \
|
||||
-Wno-unknown-warning-option
|
||||
androiddebug androidrelease: LDFLAGS += \
|
||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
||||
-Ldeps/openssl/android/arm64-v8a/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,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
||||
$(foreach build_type,androiddebug androidrelease,$(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/picohttpparser \
|
||||
-Ideps/quickjs \
|
||||
-Ideps/sqlite \
|
||||
-Ideps/valgrind \
|
||||
-Ideps/xopt \
|
||||
-Werror
|
||||
|
||||
BASE64C_SOURCES := deps/base64c/src/base64c.c
|
||||
BASE64C_OBJS := $(call get_objs,BASE64C_SOURCES)
|
||||
$(BASE64C_OBJS): CFLAGS += \
|
||||
-Wno-sign-compare
|
||||
|
||||
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/epoll.c \
|
||||
deps/libuv/src/unix/fs.c \
|
||||
deps/libuv/src/unix/getaddrinfo.c \
|
||||
deps/libuv/src/unix/getnameinfo.c \
|
||||
deps/libuv/src/unix/linux-core.c \
|
||||
deps/libuv/src/unix/linux-inotify.c \
|
||||
deps/libuv/src/unix/linux-syscalls.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/pthread-fixes.c \
|
||||
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-unused-but-set-variable \
|
||||
-Wno-incompatible-pointer-types \
|
||||
-Wno-sign-compare \
|
||||
-Wno-unused-variable \
|
||||
-Wno-dangling-pointer \
|
||||
-Wno-maybe-uninitialized \
|
||||
-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/runtime.c \
|
||||
deps/libsodium/src/libsodium/sodium/utils.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/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_ENABLE_FTS5 \
|
||||
-DSQLITE_ENABLE_JSON1 \
|
||||
-DSQLITE_MAX_LENGTH=5242880 \
|
||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||
-DSQLITE_MAX_COLUMN=100 \
|
||||
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||
-DSQLITE_MAX_ATTACHED=0 \
|
||||
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
||||
-DSQLITE_SECURE_DELETE \
|
||||
-Wno-implicit-fallthrough \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-unused-function
|
||||
|
||||
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
|
||||
|
||||
QUICKJS_SOURCES := \
|
||||
deps/quickjs/cutils.c \
|
||||
deps/quickjs/libbf.c \
|
||||
deps/quickjs/libregexp.c \
|
||||
deps/quickjs/libunicode.c \
|
||||
deps/quickjs/quickjs-libc.c \
|
||||
deps/quickjs/quickjs.c
|
||||
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
||||
$(QUICKJS_OBJS): CFLAGS += \
|
||||
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
||||
-DCONFIG_BIGNUM \
|
||||
-DDUMP_LEAKS \
|
||||
-D_GNU_SOURCE \
|
||||
-Wno-sign-compare \
|
||||
-Wno-implicit-fallthrough \
|
||||
-Wno-unused-variable \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-enum-conversion
|
||||
|
||||
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)
|
||||
# $(PICOHTTPPARSER_OBJS): CFLAGS +=
|
||||
|
||||
LDFLAGS += \
|
||||
-pthread \
|
||||
-lm
|
||||
debug release: LDFLAGS += \
|
||||
-ldl \
|
||||
-lssl \
|
||||
-lcrypto
|
||||
windebug winrelease: LDFLAGS += \
|
||||
-lwsock32 \
|
||||
-lws2_32 \
|
||||
-lkernel32 \
|
||||
-liphlpapi \
|
||||
-luserenv \
|
||||
-lssl \
|
||||
-lcrypto \
|
||||
-lws2_32 \
|
||||
-lcrypt32
|
||||
androiddebug androidrelease: LDFLAGS += \
|
||||
-ldl \
|
||||
-lssl \
|
||||
-lcrypto
|
||||
|
||||
unix: debug release
|
||||
win: windebug winrelease
|
||||
all: $(BUILD_TYPES)
|
||||
.PHONY: all win unix
|
||||
|
||||
ALL_APP_OBJS := \
|
||||
$(APP_OBJS) \
|
||||
$(BASE64C_OBJS) \
|
||||
$(BLOWFISH_OBJS) \
|
||||
$(LIBBACKTRACE_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: %.S
|
||||
@mkdir -p $$(dir $$@)
|
||||
@echo [as] $$@
|
||||
@$$(AS) -c $$< -o $$@
|
||||
endef
|
||||
|
||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||
|
||||
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.
|
||||
2. To build, run `make debug` or `make release`. An executable will be
|
||||
generated in a subdirectory of `out/`.
|
||||
3. `make windebug` or `make winrelease` will generate a windows executable
|
||||
which might work.
|
||||
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
||||
the right dependencies in the right places. `make windebug winrelease
|
||||
iosdebug-ipa iosrelease-ipa apk`.
|
||||
4. To build in docker, `docker build .`.
|
||||
|
||||
## Running
|
||||
@ -28,7 +29,7 @@ privileges. Further administration can be done at
|
||||
<http://localhost:12345/~core/admin/`>.
|
||||
|
||||
## Documentation
|
||||
There are the very beginnings of developer documentation in `apps/cory/docs/`
|
||||
There are the very beginnings of developer documentation in `apps/docs/`
|
||||
that can be read in-place or at <http://localhost:12345/~core/docs/>.
|
||||
|
||||
## License
|
||||
|
4
apps/admin.json
Normal file
4
apps/admin.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🎛"
|
||||
}
|
26
apps/admin/app.js
Normal file
26
apps/admin/app.js
Normal file
@ -0,0 +1,26 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
tfrpc.register(function delete_user(user) {
|
||||
return core.deleteUser(user);
|
||||
});
|
||||
|
||||
tfrpc.register(function global_settings_set(key, value) {
|
||||
return core.globalSettingsSet(key, value);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
let data = {
|
||||
users: {},
|
||||
granted: await core.allPermissionsGranted(),
|
||||
settings: await core.globalSettingsDescriptions(),
|
||||
};
|
||||
for (let user of await core.users()) {
|
||||
data.users[user] = await core.permissionsForUser(user);
|
||||
}
|
||||
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();
|
10
apps/admin/index.html
Normal file
10
apps/admin/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="width: 100%">
|
||||
<head>
|
||||
<script>const g_data = $data;</script>
|
||||
</head>
|
||||
<body style="color: #fff; width: 100%">
|
||||
<h1>Tilde Friends Administration</h1>
|
||||
</body>
|
||||
<script type="module" src="script.js"></script>
|
||||
</html>
|
87
apps/admin/script.js
Normal file
87
apps/admin/script.js
Normal file
@ -0,0 +1,87 @@
|
||||
import {html, render} from './lit.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
function delete_user(user) {
|
||||
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||
tfrpc.rpc.delete_user(user).then(function() {
|
||||
alert(`User "${user}" deleted successfully.`);
|
||||
}).catch(function(error) {
|
||||
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function global_settings_set(key, value) {
|
||||
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
||||
alert(`Set "${key}" to "${value}".`);
|
||||
}).catch(function(error) {
|
||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
const permission_template = (permission) =>
|
||||
html` <code>${permission}</code>`;
|
||||
function input_template(key, description) {
|
||||
if (description.type === 'boolean') {
|
||||
return html`
|
||||
<div style="margin-top: 1em">
|
||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||
<div>
|
||||
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
||||
<div>${description.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (description.type === 'textarea') {
|
||||
return html`
|
||||
<div style="margin-top: 1em"">
|
||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||
<div style="width: 100%; padding: 0; margin: 0">
|
||||
<div style="width: 90%; padding: 0 margin: 0">
|
||||
<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>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<div style="margin-top: 1em">
|
||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||
<div>
|
||||
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||
<div>${description.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
const user_template = (user, permissions) => html`
|
||||
<li>
|
||||
<button @click=${(e) => delete_user(user)}>
|
||||
Delete
|
||||
</button>
|
||||
${user}:
|
||||
${permissions.map(x => permission_template(x))}
|
||||
</li>
|
||||
`;
|
||||
const users_template = (users) =>
|
||||
html`<h2>Users</h2>
|
||||
<ul>
|
||||
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
||||
</ul>`;
|
||||
const page_template = (data) =>
|
||||
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||
<h2>Global Settings</h2>
|
||||
<div>
|
||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
||||
</div>
|
||||
${users_template(data.users)}
|
||||
</div>
|
||||
`;
|
||||
render(page_template(g_data), document.body);
|
||||
});
|
5
apps/api.json
Normal file
5
apps/api.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📜",
|
||||
"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
|
||||
}
|
77
apps/api/app.js
Normal file
77
apps/api/app.js
Normal file
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.
|
||||
`;
|
4
apps/apps.json
Normal file
4
apps/apps.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💻"
|
||||
}
|
77
apps/apps/app.js
Normal file
77
apps/apps/app.js
Normal file
@ -0,0 +1,77 @@
|
||||
async function fetch_info(apps) {
|
||||
let result = {};
|
||||
for (let [key, value] of Object.entries(apps)) {
|
||||
let blob = await ssb.blobGet(value);
|
||||
blob = blob ? utf8Decode(blob) : '{}';
|
||||
result[key] = JSON.parse(blob);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
var apps = await fetch_info(await core.apps());
|
||||
var core_apps = await fetch_info(await core.apps('core'));
|
||||
var doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 64px);
|
||||
justify-content: space-around;
|
||||
}
|
||||
.app {
|
||||
height: 96px;
|
||||
width: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.app > a {
|
||||
text-decoration: none;
|
||||
max-width: 64px;
|
||||
text-overflow: ellipsis ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background: #888">
|
||||
<h1 id="apps_title">Apps</h1>
|
||||
<div id="apps" class="container"></div>
|
||||
<h1>Core Apps</h1>
|
||||
<div id="core_apps" class="container"></div>
|
||||
</body>
|
||||
<script>
|
||||
function populate_apps(id, name, apps) {
|
||||
var list = document.getElementById(id);
|
||||
for (let app of Object.keys(apps).sort()) {
|
||||
let div = list.appendChild(document.createElement('div'));
|
||||
div.classList.add('app');
|
||||
|
||||
let icon_a = document.createElement('a');
|
||||
let icon = document.createElement('div');
|
||||
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
||||
icon.style.fontSize = 'xxx-large';
|
||||
icon_a.appendChild(icon);
|
||||
icon_a.href = '/~' + name + '/' + app + '/';
|
||||
icon_a.target = '_top';
|
||||
div.appendChild(icon_a);
|
||||
|
||||
let a = document.createElement('a');
|
||||
a.appendChild(document.createTextNode(app));
|
||||
a.href = '/~' + name + '/' + app + '/';
|
||||
a.target = '_top';
|
||||
div.appendChild(a);
|
||||
}
|
||||
}
|
||||
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('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||
</script>
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
main();
|
4
apps/appstore.json
Normal file
4
apps/appstore.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🛍"
|
||||
}
|
55
apps/appstore/app.js
Normal file
55
apps/appstore/app.js
Normal file
@ -0,0 +1,55 @@
|
||||
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();
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&uhGJsy5+qBgOgEgMqCTDasK+C+GWGptHKfPiAsD5eGA=.sha256","index.html":"&D3JwdPXy/QsLXkmwNDrBFXdzxfqO1/JGxfqEArnS5v4=.sha256","lit.min.js":"&3FfrVflmGr0n4lvN0GriN1Qz1lEw31SbZxRSJrcXR28=.sha256","script.js":"&TZ2ymD6cFVUjQleGcDslt8apjp7k3xLlfv2F8rQVM4I=.sha256"}}
|
@ -1,22 +0,0 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
tfrpc.register(function delete_user(user) {
|
||||
return core.deleteUser(user);
|
||||
});
|
||||
|
||||
tfrpc.register(function global_settings_set(key, value) {
|
||||
return core.globalSettingsSet(key, value);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
let data = {
|
||||
users: {},
|
||||
granted: await core.allPermissionsGranted(),
|
||||
settings: await core.globalSettingsDescriptions(),
|
||||
};
|
||||
for (let user of await core.users()) {
|
||||
data.users[user] = await core.permissionsForUser(user);
|
||||
}
|
||||
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
||||
}
|
||||
main();
|
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>const g_data = $data;</script>
|
||||
</head>
|
||||
<body style="color: #fff">
|
||||
<h1>Tilde Friends Administration</h1>
|
||||
</body>
|
||||
<script type="module" src="script.js"></script>
|
||||
</html>
|
@ -1,78 +0,0 @@
|
||||
import {html, render} from './lit.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
function delete_user(user) {
|
||||
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||
tfrpc.rpc.delete_user(user).then(function() {
|
||||
alert(`User "${user}" deleted successfully.`);
|
||||
}).catch(function(error) {
|
||||
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function global_settings_set(key, value) {
|
||||
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
||||
alert(`Set "${key}" to "${value}".`);
|
||||
}).catch(function(error) {
|
||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
const permission_template = (permission) =>
|
||||
html` <code>${permission}</code>`;
|
||||
function input_template(key, description) {
|
||||
if (description.type === 'boolean') {
|
||||
return html`
|
||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
||||
<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
|
||||
<div style="grid-column: 3">
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
|
||||
<span>${description.description}</span>
|
||||
</div>
|
||||
`;
|
||||
} else if (description.type === 'textarea') {
|
||||
return html`
|
||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
||||
<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
|
||||
<div style="grid-column: 3">
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
||||
<span>${description.description}</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
||||
<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
|
||||
<div style="grid-column: 3">
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
||||
<span>${description.description}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
const user_template = (user, permissions) => html`
|
||||
<li>
|
||||
<button @click=${(e) => delete_user(user)}>
|
||||
Delete
|
||||
</button>
|
||||
${user}:
|
||||
${permissions.map(x => permission_template(x))}
|
||||
</li>
|
||||
`;
|
||||
const users_template = (users) =>
|
||||
html`<h2>Users</h2>
|
||||
<ul>
|
||||
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
||||
</ul>`;
|
||||
const page_template = (data) =>
|
||||
html`<div>
|
||||
<h2>Global Settings</h2>
|
||||
<div style="display: grid">
|
||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
||||
</div>
|
||||
${users_template(data.users)}
|
||||
</div>`;
|
||||
render(page_template(g_data), document.body);
|
||||
});
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&p35JmopfHf8hFh3Y9x6LrIxiUwaJZ5Nabzi2sVXpKoo=.sha256"}}
|
@ -1,11 +0,0 @@
|
||||
var global = Function('return this')();
|
||||
function treeify(o) {
|
||||
if (typeof(o) == 'object') {
|
||||
return Object.fromEntries(Object.keys(o).map(x => [x, treeify(o[x])]));
|
||||
} else if (typeof(o) == 'function') {
|
||||
return 'function';
|
||||
} else if (typeof(o) == 'string' || typeof(o) == 'number') {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
app.setDocument(`<pre style="color:#fff">${JSON.stringify(treeify(global), null, 2)}</pre>`);
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&qEJDfZ43KazIxiZl8OCKb2uaDOsPkxnIohEzQ1LLFpg=.sha256"}}
|
@ -1,31 +0,0 @@
|
||||
async function main() {
|
||||
var apps = await core.apps();
|
||||
var core_apps = await core.apps('core');
|
||||
var doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<h1>Apps</h1>
|
||||
<ul id="apps"></ul>
|
||||
<h1>Core Apps</h1>
|
||||
<ul id="core_apps"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_apps(id, name, apps) {
|
||||
var list = document.getElementById(id);
|
||||
for (let app of Object.keys(apps).sort()) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
var a = document.createElement('a');
|
||||
a.innerText = app;
|
||||
a.href = '/~' + name + '/' + app + '/';
|
||||
a.target = '_top';
|
||||
li.appendChild(a);
|
||||
}
|
||||
}
|
||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||
</script>
|
||||
</html>`
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
main();
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&V5o5IM9/OUyIsVkjkMW/X0i/tflQOSVJuJBmHdMT9aM=.sha256"}}
|
@ -1,70 +0,0 @@
|
||||
async function database_list() {
|
||||
var dbs = await databases();
|
||||
var doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<h1>Databases</h1>
|
||||
<ul id="dbs"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_dbs(id, dbs) {
|
||||
var list = document.getElementById(id);
|
||||
for (let db of dbs) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
var a = document.createElement('a');
|
||||
a.innerText = db;
|
||||
a.href = './#' + db;
|
||||
a.target = '_top';
|
||||
li.appendChild(a);
|
||||
}
|
||||
}
|
||||
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
||||
</script>
|
||||
</html>`
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
async function key_list(db) {
|
||||
let keys = await db.getAll();
|
||||
let object = {};
|
||||
for (let key of keys) {
|
||||
object[key] = await db.get(key);
|
||||
}
|
||||
let doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<a href="#" target="_top">back</a>
|
||||
<h1>Keys</h1>
|
||||
<ul id="keys"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_dbs(id, keys) {
|
||||
var list = document.getElementById(id);
|
||||
for (let [key, value] of Object.entries(keys)) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
li.innerText = key + ' = ' + value;
|
||||
}
|
||||
}
|
||||
populate_dbs('keys', ${JSON.stringify(object)});
|
||||
</script>
|
||||
</html>`
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
core.register('message', async function(message) {
|
||||
if (message.event == 'hashChange') {
|
||||
let hash = message.hash.substring(1);
|
||||
if (hash.startsWith(':shared:')) {
|
||||
let parts = hash.split(':');
|
||||
let packageName = parts[3];
|
||||
let key = parts.slice(4).join(':');
|
||||
key_list(await my_shared_database(packageName, key));
|
||||
} else if (hash.length) {
|
||||
key_list(await database(hash.split(':').slice(1).join(':')));
|
||||
} else {
|
||||
database_list();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
database_list();
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&WEvJYebSMi5d2eXgUwJJmvR/Q4slFg3zHYB8Q2mXJII=.sha256","index.md":"&79+ntX4sRvg+MboV5nMFz01BSicxsWIQRx719VHS8uk=.sha256","todo.md":"&hQABwP24zFFhdHagRMF3Am7rV2yH19e+0xJ4wnZ4kfM=.sha256","structure.md":"&jph8x/fMXKOd4I0ZiUVb0ZLTfPQ7gBWoxJPrvtX6vtw=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256","ssb.md":"&JH1JfoTaCcUifCpnAwhImKBACI0PHoLhoOw1WAnWpLw=.sha256","vision.md":"&v2wu2MGlhNvaALQQ9rGna7ZeEQWSghFgQcDfD5xEyE0=.sha256"}}
|
@ -1,17 +0,0 @@
|
||||
# ID Refactor
|
||||
[Back to index](#index)
|
||||
|
||||
## Goals
|
||||
- no way to get private key in javascript
|
||||
- ssb.c syncs/broadcasts/... efficiently for everybody
|
||||
|
||||
## Schema
|
||||
- separate table to discourage leakage
|
||||
- `CREATE TABLE identities (user TEXT, public TEXT, secret TEXT);`
|
||||
|
||||
## API
|
||||
- `ssb.createIdentity()` -> `id`
|
||||
- `ssb.getIdentities()` => `[id, ...]`
|
||||
- `ssb.deleteIdentity(id)`
|
||||
- `ssb.post(id, ...)`
|
||||
- `ssb.appendMessage(id, ...)`
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&3d9ABFgRwQvWsYbFv/rzimtnLDnVrWlGtdw7serFIGw=.sha256"}}
|
@ -1,159 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
var g_following_cache = {};
|
||||
var g_following_deep_cache = {};
|
||||
var g_about_cache = {};
|
||||
|
||||
async function following(db, id) {
|
||||
if (g_following_cache[id]) {
|
||||
return g_following_cache[id];
|
||||
}
|
||||
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.sqlStream(
|
||||
"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;
|
||||
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) {
|
||||
if (depth <= 0) {
|
||||
return seed_ids;
|
||||
}
|
||||
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])));
|
||||
var ids = [].concat(...f);
|
||||
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;
|
||||
return x;
|
||||
}
|
||||
|
||||
async function getAbout(db, id) {
|
||||
if (g_about_cache[id]) {
|
||||
return g_about_cache[id];
|
||||
}
|
||||
var o = await db.get(id + ":about");
|
||||
const k_version = 4;
|
||||
var f = o ? JSON.parse(o) : o;
|
||||
if (!f || f.version != k_version) {
|
||||
f = {about: {}, sequence: 0, version: k_version};
|
||||
}
|
||||
await ssb.sqlStream(
|
||||
"SELECT "+
|
||||
" sequence, "+
|
||||
" content "+
|
||||
"FROM messages "+
|
||||
"WHERE "+
|
||||
" author = ?1 AND "+
|
||||
" sequence > ?2 AND "+
|
||||
" json_extract(content, '$.type') = 'about' AND "+
|
||||
" json_extract(content, '$.about') = ?1 "+
|
||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
||||
"ORDER BY sequence",
|
||||
[id, f.sequence],
|
||||
function(row) {
|
||||
f.sequence = row.sequence;
|
||||
if (row.content) {
|
||||
var about = {};
|
||||
try {
|
||||
about = JSON.parse(row.content);
|
||||
} catch {
|
||||
}
|
||||
delete about.about;
|
||||
delete about.type;
|
||||
f.about = Object.assign(f.about, about);
|
||||
}
|
||||
});
|
||||
var j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":about", j);
|
||||
}
|
||||
g_about_cache[id] = f.about;
|
||||
return f.about;
|
||||
}
|
||||
|
||||
async function getSize(db, id) {
|
||||
let size = 0;
|
||||
await ssb.sqlStream(
|
||||
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
||||
[id],
|
||||
function (row) {
|
||||
size += row.size;
|
||||
});
|
||||
return size;
|
||||
}
|
||||
|
||||
function niceSize(bytes) {
|
||||
let value = bytes;
|
||||
let unit = 'B';
|
||||
const k_units = ['kB', 'MB', 'GB', 'TB'];
|
||||
for (let u of k_units) {
|
||||
if (value >= 1024) {
|
||||
value /= 1024;
|
||||
unit = u;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Math.round(value * 10) / 10 + ' ' + unit;
|
||||
}
|
||||
|
||||
async function buildTree(db, root, indent, depth) {
|
||||
var f = await following(db, root);
|
||||
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() {
|
||||
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
||||
var db = await database('ssb');
|
||||
var whoami = await ssb.getIdentities();
|
||||
var tree = '';
|
||||
for (let id of whoami) {
|
||||
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
|
||||
tree += await buildTree(db, id, '', 2);
|
||||
}
|
||||
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
|
||||
}
|
||||
|
||||
main();
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&1HWTkyCc1doft6dyKF5FDxtRAErNeY25CBrfZbKPpyo=.sha256","lit-all.min.js":"&XKgdRySJuiZeZvchNFGjVWn0XOVhQFmG7/HTWYQ8s68=.sha256","index.html":"&TxhFekB9ov7tf/fmkAg7x5797i27oLidhgxEfDKC0T0=.sha256","script.js":"&G8puK9Q4MngHy3D4ppcKyT49WKbHD2OCeUcAw2ghTDE=.sha256","lit-all.min.js.map":"&lA9iFp1YbqSndxXZuwtgmrj7NDMkN71nJITbtjWL3VA=.sha256","tf-id-picker.js":"&maN8DUFrmRxW5nsVyOAMk5k1ekcz/pfzvSS99ac3jo8=.sha256","tf-app.js":"&F0fyawIO410YFidrzFjlHeY++sZy6ledf6CAXB+45U4=.sha256","tf-message.js":"&HToh+7UCoanBzlr/TEsy/JG4OS2IBU1tMuzjuNmUkAo=.sha256","tf-user.js":"&bXTedgBudTQLXEBPY9R8OLfQ/ZLpo8YRU9Oq/wuGG3Y=.sha256","tf-utils.js":"&lYNeL7cVlDgcqrfkoRIe69DHZeqSZMiHhZIieblHbU0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&7HZLHf5NB5hE6FW0hiXNvM17ekGBn5BBle1bvnjVjyo=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&tOkUocccQWBzkNzSEf9VMltkTSHcUALYSPYVWmJMoBc=.sha256","tf-styles.js":"&LFeL/vWgrv4N8q/mBrQAnhbaOI+dXNJYvH9bn1bXSqQ=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-tab-search.js":"&ESt2vMG19sH5j6ungKua/ZuvIGslyuWyb3juXdOCecg=.sha256","tf-tab-news.js":"&fY+thANurOKU2/RhDt411ZtkxW0nV24+hLEf00Z1sTY=.sha256","tf-tab-connections.js":"&ywqBz3w63R6naH09kZ+01A0SfmtuSfk8QPBXWsli0yg=.sha256","tf-news.js":"&Zn+vxLUqVJbo/q6RcW8ezvbdilzllvXhZRyXk8kYwL0=.sha256","tribute.css":"&9FogMzZHKXCfGb7mlh7z+/wiNZzBsOB/tKoh6MfYJno=.sha256","tribute.esm.js":"&P1wKqCfYULpR/ahSB98JP8xaxfikuZwwtT6I/SAo7/Y=.sha256","commonmark-hashtag.js":"&fudY0YdvcMjVCSZ0oiCqUt0+bVT0a06j5TcjWaCDO8E=.sha256"}}
|
@ -1,99 +0,0 @@
|
||||
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.sqlStream(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(function apps() {
|
||||
return core.apps();
|
||||
});
|
||||
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();
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="color: #fff">
|
||||
<head>
|
||||
<title>Tilde Friends</title>
|
||||
<base target="_top">
|
||||
<link rel="stylesheet" href="tribute.css" />
|
||||
<style>
|
||||
.tribute-container {
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<tf-app/>
|
||||
<script>window.litDisableBundleWarning = true;</script>
|
||||
<script src="commonmark.min.js"></script>
|
||||
<script src="commonmark-linkify.js" type="module"></script>
|
||||
<script src="commonmark-hashtag.js" type="module"></script>
|
||||
<script src="script.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
132
apps/cory/ssb/lit-all.min.js
vendored
132
apps/cory/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
@ -1,13 +0,0 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
import * as tf_id_picker from './tf-id-picker.js';
|
||||
import * as tf_app from './tf-app.js';
|
||||
import * as tf_message from './tf-message.js';
|
||||
import * as tf_user from './tf-user.js';
|
||||
import * as tf_compose from './tf-compose.js';
|
||||
import * as tf_news from './tf-news.js';
|
||||
import * as tf_profile from './tf-profile.js';
|
||||
import * as tf_tab_news from './tf-tab-news.js';
|
||||
import * as tf_tab_search from './tf-tab-search.js';
|
||||
import * as tf_tab_connections from './tf-tab-connections.js';
|
@ -1,331 +0,0 @@
|
||||
import {LitElement, html, css, guard, until} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
hash: {type: String},
|
||||
unread: {type: Array},
|
||||
tab: {type: String},
|
||||
broadcasts: {type: Array},
|
||||
connections: {type: Array},
|
||||
loading: {type: Boolean},
|
||||
loaded: {type: Boolean},
|
||||
following: {type: Array},
|
||||
users: {type: Object},
|
||||
ids: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.hash = '#';
|
||||
this.unread = [];
|
||||
this.tab = 'news';
|
||||
this.broadcasts = [];
|
||||
this.connections = [];
|
||||
this.following = [];
|
||||
this.users = {};
|
||||
this.loaded = false;
|
||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
|
||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
|
||||
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
||||
tfrpc.register(function hashChanged(hash) {
|
||||
self.set_hash(hash);
|
||||
});
|
||||
tfrpc.register(async function notifyNewMessage(id) {
|
||||
await self.fetch_new_message(id);
|
||||
});
|
||||
tfrpc.register(function set(name, value) {
|
||||
if (name === 'broadcasts') {
|
||||
self.broadcasts = value;
|
||||
} else if (name === 'connections') {
|
||||
self.connections = value;
|
||||
}
|
||||
});
|
||||
this.initial_load();
|
||||
}
|
||||
|
||||
async initial_load() {
|
||||
let whoami = await tfrpc.rpc.localStorageGet('whoami');
|
||||
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
set_hash(hash) {
|
||||
this.hash = hash || '#';
|
||||
if (this.hash.startsWith('#q=')) {
|
||||
this.tab = 'search';
|
||||
} else if (this.hash === '#connections') {
|
||||
this.tab = 'connections';
|
||||
} else {
|
||||
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;
|
||||
await tfrpc.rpc.databaseSet('following', JSON.stringify(cache));
|
||||
return [result, cache.following];
|
||||
}
|
||||
|
||||
async fetch_about(ids, users) {
|
||||
const k_cache_version = 1;
|
||||
let cache = await tfrpc.rpc.databaseGet('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 = (await tfrpc.rpc.query(`
|
||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||
`, []))[0].max_row_id;
|
||||
for (let id of Object.keys(cache.about)) {
|
||||
if (ids.indexOf(id) == -1) {
|
||||
delete cache.about[id];
|
||||
}
|
||||
}
|
||||
|
||||
let abouts = await tfrpc.rpc.query(
|
||||
`
|
||||
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 tfrpc.rpc.databaseSet('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 fetch_new_message(id) {
|
||||
let messages = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.*
|
||||
FROM messages
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.id = ?
|
||||
`,
|
||||
[
|
||||
JSON.stringify(this.following),
|
||||
id,
|
||||
]);
|
||||
if (messages && messages.length) {
|
||||
this.unread = [...this.unread, ...messages];
|
||||
}
|
||||
}
|
||||
|
||||
async _handle_whoami_changed(event) {
|
||||
let old_id = this.whoami;
|
||||
let new_id = event.srcElement.selected;
|
||||
console.log('received', new_id);
|
||||
if (this.whoami !== new_id) {
|
||||
console.log(event);
|
||||
this.whoami = new_id;
|
||||
console.log(`whoami ${old_id} => ${new_id}`);
|
||||
await tfrpc.rpc.localStorageSet('whoami', new_id);
|
||||
}
|
||||
}
|
||||
|
||||
async create_identity() {
|
||||
if (confirm("Are you sure you want to create a new identity?")) {
|
||||
await tfrpc.rpc.createIdentity();
|
||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||
}
|
||||
}
|
||||
|
||||
render_id_picker() {
|
||||
return html`
|
||||
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
|
||||
<button @click=${this.create_identity}>Create Identity</button>
|
||||
`;
|
||||
}
|
||||
|
||||
async load() {
|
||||
let whoami = this.whoami;
|
||||
let [following, users] = await this.following_deep([whoami], 2, {});
|
||||
users = await this.fetch_about(following.sort(), users);
|
||||
this.following = following;
|
||||
this.users = users;
|
||||
console.log(`load finished ${whoami} => ${this.whoami}`);
|
||||
this.whoami = whoami;
|
||||
this.loaded = whoami;
|
||||
}
|
||||
|
||||
render_tab() {
|
||||
let following = this.following;
|
||||
let users = this.users;
|
||||
if (this.tab === 'news') {
|
||||
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>
|
||||
`;
|
||||
} else if (this.tab === 'connections') {
|
||||
return html`
|
||||
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
||||
`;
|
||||
} else if (this.tab === 'search') {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
add_fake_news() {
|
||||
this.unread = [{
|
||||
author: this.whoami,
|
||||
placeholder: true,
|
||||
id: '%fake_id',
|
||||
text: 'text',
|
||||
content: 'hello',
|
||||
}, ...this.unread];
|
||||
}
|
||||
|
||||
async set_tab(tab) {
|
||||
this.tab = tab;
|
||||
if (tab === 'news') {
|
||||
await tfrpc.rpc.setHash('#');
|
||||
} else if (tab === 'connections') {
|
||||
await tfrpc.rpc.setHash('#connections');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
|
||||
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||
console.log(`starting loading ${this.whoami} ${this.loaded}`);
|
||||
this.loading = true;
|
||||
this.load().finally(function() {
|
||||
self.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
let tabs = html`
|
||||
<div>
|
||||
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
|
||||
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
|
||||
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
|
||||
</div>
|
||||
`;
|
||||
let contents =
|
||||
!this.loaded ?
|
||||
this.loading ?
|
||||
html`<div>Loading...</div>` :
|
||||
html`<div>Select or create an identity.</div>` :
|
||||
this.render_tab();
|
||||
return html`
|
||||
${this.render_id_picker()}
|
||||
${tabs}
|
||||
<!-- <input type="button" value="Fake News" @click=${this.add_fake_news}></input> -->
|
||||
${contents}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-app', TfElement);
|
@ -1,300 +0,0 @@
|
||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||
import * as tfutils from './tf-utils.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
import Tribute from './tribute.esm.js';
|
||||
|
||||
class TfComposeElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
users: {type: Object},
|
||||
root: {type: String},
|
||||
branch: {type: String},
|
||||
mentions: {type: Object},
|
||||
apps: {type: Object},
|
||||
drafts: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.users = {};
|
||||
this.root = undefined;
|
||||
this.branch = undefined;
|
||||
this.mentions = {};
|
||||
this.apps = undefined;
|
||||
this.drafts = {};
|
||||
}
|
||||
|
||||
process_text(text) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
/* Update mentions. */
|
||||
for (let match of text.matchAll(/\[([^\[]+)]\(([@&%][^\)]+)/g)) {
|
||||
let name = match[1];
|
||||
let link = match[2];
|
||||
let balance = 0;
|
||||
let bracket_end = match.index + match[1].length + '[]'.length - 1;
|
||||
for (let i = bracket_end; i >= 0; i--) {
|
||||
if (text.charAt(i) == ']') {
|
||||
balance++;
|
||||
} else if (text.charAt(i) == '[') {
|
||||
balance--;
|
||||
}
|
||||
if (balance <= 0) {
|
||||
name = text.substring(i + 1, bracket_end);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.mentions[link]) {
|
||||
this.mentions[link] = {
|
||||
link: link,
|
||||
}
|
||||
}
|
||||
this.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
||||
this.mentions = Object.assign({}, this.mentions);
|
||||
console.log(this.mentions);
|
||||
}
|
||||
return tfutils.markdown(text);
|
||||
}
|
||||
|
||||
input(event) {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let preview = this.renderRoot.getElementById('preview');
|
||||
preview.innerHTML = this.process_text(edit.value);
|
||||
}
|
||||
|
||||
notify(draft) {
|
||||
this.dispatchEvent(new CustomEvent('tf-draft', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
id: this.branch,
|
||||
draft: draft
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
change(event) {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
this.notify(edit.value);
|
||||
}
|
||||
|
||||
convert_to_format(buffer, type, mime_type) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let img = new Image();
|
||||
img.onload = function() {
|
||||
let canvas = document.createElement('canvas');
|
||||
let width_scale = Math.min(img.width, 1024) / img.width;
|
||||
let height_scale = Math.min(img.height, 1024) / img.height;
|
||||
let scale = Math.min(width_scale, height_scale);
|
||||
canvas.width = img.width * scale;
|
||||
canvas.height = img.height * scale;
|
||||
let context = canvas.getContext('2d');
|
||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
let data_url = canvas.toDataURL(mime_type);
|
||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
||||
resolve(result);
|
||||
}
|
||||
img.onerror = function(event) {
|
||||
reject(new Error('Failed to load image.'));
|
||||
};
|
||||
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
||||
let original = `data:${type};base64,${btoa(raw)}`;
|
||||
img.src = original;
|
||||
});
|
||||
}
|
||||
|
||||
async add_file(file) {
|
||||
try {
|
||||
let self = this;
|
||||
let buffer = await file.arrayBuffer();
|
||||
let type = file.type;
|
||||
if (type.startsWith('image/')) {
|
||||
let best_buffer;
|
||||
let best_type;
|
||||
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||
let test_buffer = await self.convert_to_format(buffer, file.type, format);
|
||||
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||
best_buffer = test_buffer;
|
||||
best_type = format;
|
||||
}
|
||||
}
|
||||
buffer = best_buffer;
|
||||
type = best_type;
|
||||
} else {
|
||||
buffer = Array.from(new Uint8Array(buffer));
|
||||
}
|
||||
let id = await tfrpc.rpc.store_blob(buffer);
|
||||
let name = type.split('/')[0] + ':' + file.name;
|
||||
self.mentions[id] = {
|
||||
link: id,
|
||||
name: name,
|
||||
type: type,
|
||||
size: buffer.length ?? buffer.byteLength,
|
||||
};
|
||||
self.mentions = Object.assign({}, self.mentions);
|
||||
let edit = self.renderRoot.getElementById('edit');
|
||||
edit.value += `\n`;
|
||||
self.change();
|
||||
} catch(e) {
|
||||
alert(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
paste(event) {
|
||||
let self = this;
|
||||
for (let item of event.clipboardData.items) {
|
||||
if (item.type?.startsWith('image/')) {
|
||||
let file = item.getAsFile();
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
self.add_file(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submit() {
|
||||
let self = this;
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let message = {
|
||||
type: 'post',
|
||||
text: edit.value,
|
||||
};
|
||||
if (this.root || this.branch) {
|
||||
message.root = this.root;
|
||||
message.branch = this.branch;
|
||||
}
|
||||
if (Object.values(this.mentions).length) {
|
||||
message.mentions = Object.values(this.mentions);
|
||||
}
|
||||
console.log('Would post:', message);
|
||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||
edit.value = '';
|
||||
self.mentions = {};
|
||||
self.change();
|
||||
self.notify(undefined);
|
||||
}).catch(function(error) {
|
||||
alert(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
discard() {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
edit.value = '';
|
||||
this.change();
|
||||
let preview = this.renderRoot.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
this.notify(undefined);
|
||||
}
|
||||
|
||||
attach() {
|
||||
let self = this;
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.onchange = function(event) {
|
||||
let file = event.target.files[0];
|
||||
self.add_file(file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
let tribute = new Tribute({
|
||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||
selectTemplate: function(item) {
|
||||
return `[@${item.original.key}](${item.original.value})`;
|
||||
},
|
||||
});
|
||||
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||
}
|
||||
|
||||
remove_mention(id) {
|
||||
delete this.mentions[id];
|
||||
this.mentions = Object.assign({}, this.mentions);
|
||||
}
|
||||
|
||||
render_mention(mention) {
|
||||
let self = this;
|
||||
return html`
|
||||
<div>
|
||||
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
|
||||
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render_attach_app() {
|
||||
let self = this;
|
||||
|
||||
async function attach_selected_app() {
|
||||
let name = self.renderRoot.getElementById('select').value;
|
||||
let id = self.apps[name];
|
||||
let mentions = {};
|
||||
mentions[id] = {
|
||||
name: name,
|
||||
link: id,
|
||||
type: 'application/tildefriends',
|
||||
};
|
||||
if (name && id) {
|
||||
let app = JSON.parse(await tfrpc.rpc.get_blob(id));
|
||||
for (let entry of Object.entries(app.files)) {
|
||||
mentions[entry[1]] = {
|
||||
name: entry[0],
|
||||
link: entry[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
this.mentions = Object.assign(this.mentions || {}, mentions);
|
||||
this.apps = null;
|
||||
}
|
||||
|
||||
if (this.apps) {
|
||||
return html`
|
||||
<div>
|
||||
<select id="select">
|
||||
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
||||
</select>
|
||||
<input type="button" value="Attach" @click=${attach_selected_app}></input>
|
||||
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
render_attach_app_button() {
|
||||
async function attach_app() {
|
||||
this.apps = await tfrpc.rpc.apps();
|
||||
}
|
||||
if (!this.apps) {
|
||||
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
|
||||
} else {
|
||||
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
let result = html`
|
||||
<div style="display: flex; flex-direction: row; width: 100%">
|
||||
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${this.drafts[this.branch || '']}</textarea>
|
||||
<div id="preview" style="flex: 1 0 50%"></div>
|
||||
</div>
|
||||
${Object.values(this.mentions).map(x => self.render_mention(x))}
|
||||
${this.render_attach_app()}
|
||||
<input type="button" value="Submit" @click=${this.submit}></input>
|
||||
<input type="button" value="Attach" @click=${this.attach}></input>
|
||||
${this.render_attach_app_button()}
|
||||
<input type="button" value="Discard" @click=${this.discard}></input>
|
||||
`;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-compose', TfComposeElement);
|
@ -1,57 +0,0 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TfConnectionsElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
broadcasts: {type: Array},
|
||||
identities: {type: Array},
|
||||
connections: {type: Array},
|
||||
users: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.broadcasts = [];
|
||||
this.identities = [];
|
||||
this.connections = [];
|
||||
this.users = {};
|
||||
tfrpc.rpc.getAllIdentities().then(function(identities) {
|
||||
self.identities = identities || [];
|
||||
});
|
||||
}
|
||||
|
||||
_emit_change() {
|
||||
let changed_event = new Event('change', {
|
||||
srcElement: this,
|
||||
});
|
||||
this.dispatchEvent(changed_event);
|
||||
}
|
||||
|
||||
changed(event) {
|
||||
this.selected = event.srcElement.value;
|
||||
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
||||
this._emit_change();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h2>Broadcasts</h2>
|
||||
<ul>
|
||||
${this.broadcasts.map(x => html`<li><tf-user id=${x.pubkey} .users=${this.users}></tf-user></li>`)}
|
||||
</ul>
|
||||
<h2>Connections</h2>
|
||||
<ul>
|
||||
${this.connections.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||
</ul>
|
||||
<h2>Local Accounts</h2>
|
||||
<ul>
|
||||
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-connections', TfConnectionsElement);
|
@ -1,37 +0,0 @@
|
||||
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();
|
||||
let self = this;
|
||||
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);
|
@ -1,426 +0,0 @@
|
||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import * as tfutils from './tf-utils.js';
|
||||
import * as emojis from './emojis.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfMessageElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
message: {type: Object},
|
||||
users: {type: Object},
|
||||
drafts: {type: Object},
|
||||
raw: {type: Boolean},
|
||||
blog_data: {type: String},
|
||||
expanded: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.whoami = null;
|
||||
this.message = {};
|
||||
this.users = {};
|
||||
this.drafts = {};
|
||||
this.raw = false;
|
||||
this.expanded = {};
|
||||
}
|
||||
|
||||
show_reply() {
|
||||
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
discard_reply() {
|
||||
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
|
||||
}
|
||||
|
||||
render_votes() {
|
||||
function normalize_expression(expression) {
|
||||
if (expression === 'Like' || !expression) {
|
||||
return '👍';
|
||||
} else if (expression === 'Unlike') {
|
||||
return '👎';
|
||||
} else if (expression === 'heart') {
|
||||
return '❤️';
|
||||
} else {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
return html`<div>${(this.message.votes || []).map(
|
||||
vote => html`
|
||||
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
|
||||
${normalize_expression(vote.content.vote.expression)}
|
||||
</span>
|
||||
`)}</div>`;
|
||||
}
|
||||
|
||||
render_raw() {
|
||||
let raw = {
|
||||
id: this.message?.id,
|
||||
previous: this.message?.previous,
|
||||
author: this.message?.author,
|
||||
sequence: this.message?.sequence,
|
||||
timestamp: this.message?.timestamp,
|
||||
hash: this.message?.hash,
|
||||
content: this.message?.content,
|
||||
signature: this.message?.signature,
|
||||
}
|
||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
|
||||
}
|
||||
|
||||
vote(emoji) {
|
||||
let reaction = emoji.emoji;
|
||||
let message = this.message.id;
|
||||
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
||||
tfrpc.rpc.appendMessage(
|
||||
this.whoami,
|
||||
{
|
||||
type: 'vote',
|
||||
vote: {
|
||||
link: message,
|
||||
value: 1,
|
||||
expression: reaction,
|
||||
},
|
||||
}).catch(function(error) {
|
||||
alert(error?.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
react(event) {
|
||||
emojis.picker(x => this.vote(x));
|
||||
}
|
||||
|
||||
show_image(link) {
|
||||
let div = document.createElement('div');
|
||||
div.style.left = 0;
|
||||
div.style.top = 0;
|
||||
div.style.width = '100%';
|
||||
div.style.height = '100%';
|
||||
div.style.position = 'fixed';
|
||||
div.style.background = '#000';
|
||||
div.style.zIndex = 100;
|
||||
div.style.display = 'grid';
|
||||
let img = document.createElement('img');
|
||||
img.src = link;
|
||||
img.style.maxWidth = '100%';
|
||||
img.style.maxHeight = '100%';
|
||||
img.style.display = 'block';
|
||||
img.style.margin = 'auto';
|
||||
img.style.objectFit = 'contain';
|
||||
img.style.width = '100%';
|
||||
div.appendChild(img);
|
||||
function image_close(event) {
|
||||
document.body.removeChild(div);
|
||||
window.removeEventListener('keydown', image_close);
|
||||
}
|
||||
div.onclick = image_close;
|
||||
window.addEventListener('keydown', image_close);
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
body_click(event) {
|
||||
if (event.srcElement.tagName == 'IMG') {
|
||||
this.show_image(event.srcElement.src);
|
||||
}
|
||||
}
|
||||
|
||||
render_mention(mention) {
|
||||
if (!mention?.link || typeof(mention.link) != 'string') {
|
||||
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
||||
} else if (mention?.link?.startsWith('&') &&
|
||||
mention?.type?.startsWith('image/')) {
|
||||
return html`
|
||||
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
|
||||
`;
|
||||
} else if (mention.link?.startsWith('&') &&
|
||||
mention.name?.startsWith('audio:')) {
|
||||
return html`
|
||||
<audio controls style="height: 32px">
|
||||
<source src=${'/' + mention.link + '/view'}></source>
|
||||
</audio>
|
||||
`;
|
||||
} else if (mention.link?.startsWith('&') &&
|
||||
mention.name?.startsWith('video:')) {
|
||||
return html`
|
||||
<video controls style="max-height: 240px">
|
||||
<source src=${'/' + mention.link + '/view'}></source>
|
||||
</video>
|
||||
`;
|
||||
} else if (mention.link?.startsWith('&') &&
|
||||
mention?.type === 'application/tildefriends') {
|
||||
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
||||
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
||||
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
|
||||
} else if (mention.link?.startsWith('#')) {
|
||||
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
|
||||
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
|
||||
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
||||
} else {
|
||||
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
render_mentions() {
|
||||
let mentions = this.message?.content?.mentions || [];
|
||||
mentions = mentions.filter(x =>
|
||||
x.name?.startsWith('audio:') ||
|
||||
x.name?.startsWith('video:') ||
|
||||
this.message?.content?.text?.indexOf(x.link) === -1);
|
||||
if (mentions.length) {
|
||||
let self = this;
|
||||
return html`
|
||||
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
|
||||
<legend>Mentions</legend>
|
||||
${mentions.map(x => self.render_mention(x))}
|
||||
</fieldset>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
total_child_messages(message) {
|
||||
if (!message.child_messages) {
|
||||
return 0;
|
||||
}
|
||||
let total = message.child_messages.length;
|
||||
for (let m of message.child_messages)
|
||||
{
|
||||
total += this.total_child_messages(m);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
set_expanded(expanded, tag) {
|
||||
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
|
||||
}
|
||||
|
||||
toggle_expanded(tag) {
|
||||
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
|
||||
}
|
||||
|
||||
render_children() {
|
||||
let self = this;
|
||||
if (this.message.child_messages?.length) {
|
||||
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>`;
|
||||
} 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>`)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let content = this.message?.content;
|
||||
let self = this;
|
||||
let raw_button = this.raw ?
|
||||
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
|
||||
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
|
||||
function small_frame(inner) {
|
||||
return html`
|
||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block">
|
||||
<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>
|
||||
${raw_button}
|
||||
${self.raw ? self.render_raw() : inner}
|
||||
${self.render_votes()}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (this.message.placeholder) {
|
||||
return html`
|
||||
<div 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)
|
||||
<div>${this.render_votes()}</div>
|
||||
${(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>
|
||||
`)}
|
||||
</div>`;
|
||||
} else if (typeof(content?.type === 'string')) {
|
||||
if (content.type == 'about') {
|
||||
let name;
|
||||
let image;
|
||||
let description;
|
||||
if (content.name !== undefined) {
|
||||
name = html`<div><b>Name:</b> ${content.name}</div>`;
|
||||
}
|
||||
if (content.image !== undefined) {
|
||||
image = html`
|
||||
<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
||||
`;
|
||||
}
|
||||
if (content.description !== undefined) {
|
||||
description = html`
|
||||
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
||||
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
let update = content.about == this.message.author ?
|
||||
html`<div style="font-weight: bold">Updated profile.</div>` :
|
||||
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
|
||||
return small_frame(html`
|
||||
${update}
|
||||
${name}
|
||||
${image}
|
||||
${description}
|
||||
`);
|
||||
} else if (content.type == 'contact') {
|
||||
return small_frame(html`
|
||||
<div>
|
||||
is
|
||||
${
|
||||
content.blocking === true ? 'blocking' :
|
||||
content.blocking === false ? 'no longer blocking' :
|
||||
content.following === true ? 'following' :
|
||||
content.following === false ? 'no longer following' :
|
||||
'?'
|
||||
}
|
||||
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
||||
</div>
|
||||
`);
|
||||
} else if (content.type == 'post') {
|
||||
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`
|
||||
<input type="button" value="Reply" @click=${this.show_reply}></input>
|
||||
`;
|
||||
let self = this;
|
||||
let body = this.raw ?
|
||||
this.render_raw() :
|
||||
unsafeHTML(tfutils.markdown(content.text));
|
||||
let content_warning = html`
|
||||
<div style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
||||
`;
|
||||
let content_html =
|
||||
html`
|
||||
<div @click=${this.body_click}>${body}</div>
|
||||
${this.render_mentions()}
|
||||
`;
|
||||
let payload =
|
||||
content.contentWarning ?
|
||||
self.expanded[(this.message.id || '') + ':cw'] ?
|
||||
html`
|
||||
${content_warning}
|
||||
${content_html}
|
||||
` :
|
||||
content_warning :
|
||||
content_html;
|
||||
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 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">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
<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>
|
||||
${payload}
|
||||
${this.render_votes()}
|
||||
<div>
|
||||
${reply}
|
||||
<input type="button" value="React" @click=${this.react}></input>
|
||||
</div>
|
||||
${this.render_children()}
|
||||
</div>
|
||||
`;
|
||||
} else if (content.type === 'blog') {
|
||||
let self = this;
|
||||
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
||||
self.blog_data = data;
|
||||
});
|
||||
let payload =
|
||||
this.expanded[(this.message.id || '') + ':blog'] ?
|
||||
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
||||
undefined;
|
||||
let body = this.raw ?
|
||||
this.render_raw() :
|
||||
html`
|
||||
<div
|
||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||
@click=${x => self.toggle_expanded(':blog')}>
|
||||
<h2>${content.title}</h2>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<img src=/${content.thumbnail}/view></img>
|
||||
<span>${content.summary}</span>
|
||||
</div>
|
||||
</div>
|
||||
${payload}
|
||||
`;
|
||||
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 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">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
<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>
|
||||
|
||||
<div>${body}</div>
|
||||
${this.render_mentions()}
|
||||
${this.render_votes()}
|
||||
</div>
|
||||
`;
|
||||
} else if (content.type === 'pub') {
|
||||
return small_frame(html`
|
||||
<span>
|
||||
<div>
|
||||
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
|
||||
</div>
|
||||
<pre>${content.address.host}:${content.address.port}</pre>
|
||||
</span>`);
|
||||
} else if (content.type === 'channel') {
|
||||
return small_frame(html`
|
||||
<div>
|
||||
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
|
||||
</div>
|
||||
`);
|
||||
} else if (typeof(this.message.content) == 'string') {
|
||||
return small_frame(html`<span>🔒</span>`);
|
||||
} else {
|
||||
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
||||
}
|
||||
} else {
|
||||
return small_frame(this.render_raw());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-message', TfMessageElement);
|
@ -1,180 +0,0 @@
|
||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import * as tfutils from './tf-utils.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfProfileElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
editing: {type: Object},
|
||||
whoami: {type: String},
|
||||
id: {type: String},
|
||||
users: {type: Object},
|
||||
size: {type: Number},
|
||||
}
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.editing = null;
|
||||
this.whoami = null;
|
||||
this.id = null;
|
||||
this.users = {};
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
modify(change) {
|
||||
tfrpc.rpc.appendMessage(this.whoami,
|
||||
Object.assign({
|
||||
type: 'contact',
|
||||
contact: this.id,
|
||||
}, change)).catch(function(error) {
|
||||
alert(error?.message);
|
||||
})
|
||||
}
|
||||
|
||||
follow() {
|
||||
this.modify({following: true});
|
||||
}
|
||||
|
||||
unfollow() {
|
||||
this.modify({following: false});
|
||||
}
|
||||
|
||||
block() {
|
||||
this.modify({blocking: true});
|
||||
}
|
||||
|
||||
unblock() {
|
||||
this.modify({blocking: false});
|
||||
}
|
||||
|
||||
edit() {
|
||||
let original = this.users[this.id];
|
||||
this.editing = {
|
||||
name: original.name,
|
||||
description: original.description,
|
||||
image: original.image,
|
||||
};
|
||||
console.log(this.editing);
|
||||
}
|
||||
|
||||
save_edits() {
|
||||
let self = this;
|
||||
let message = {
|
||||
type: 'about',
|
||||
about: this.whoami,
|
||||
};
|
||||
for (let key of Object.keys(this.editing)) {
|
||||
if (this.editing[key] !== this.users[this.id][key]) {
|
||||
message[key] = this.editing[key];
|
||||
}
|
||||
}
|
||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||
self.editing = null;
|
||||
}).catch(function(error) {
|
||||
alert(error?.message);
|
||||
});
|
||||
}
|
||||
|
||||
discard_edits() {
|
||||
this.editing = null;
|
||||
}
|
||||
|
||||
attach_image() {
|
||||
let self = this;
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.onchange = function(event) {
|
||||
let file = event.target.files[0];
|
||||
file.arrayBuffer().then(function(buffer) {
|
||||
let bin = Array.from(new Uint8Array(buffer));
|
||||
return tfrpc.rpc.store_blob(bin);
|
||||
}).then(function(id) {
|
||||
self.editing = Object.assign({}, self.editing, {image: id});
|
||||
console.log(self.editing);
|
||||
}).catch(function(e) {
|
||||
alert(e.message);
|
||||
});
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
let profile = this.users[this.id] || {};
|
||||
tfrpc.rpc.query(
|
||||
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
||||
[this.id]).then(function(result) {
|
||||
self.size = result[0].size;
|
||||
});
|
||||
let edit;
|
||||
let follow;
|
||||
let block;
|
||||
if (this.id === this.whoami) {
|
||||
if (this.editing) {
|
||||
edit = html`
|
||||
<input type="button" value="Save Profile" @click=${this.save_edits}></input>
|
||||
<input type="button" value="Discard" @click=${this.discard_edits}></input>
|
||||
`;
|
||||
} else {
|
||||
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
|
||||
}
|
||||
}
|
||||
if (this.id !== this.whoami &&
|
||||
this.users[this.whoami]?.following) {
|
||||
follow =
|
||||
this.users[this.whoami].following[this.id] ?
|
||||
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
|
||||
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
|
||||
}
|
||||
if (this.id !== this.whoami &&
|
||||
this.users[this.whoami]?.blocking) {
|
||||
block =
|
||||
this.users[this.whoami].blocking[this.id] ?
|
||||
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
|
||||
html`<input type="button" value="Block" @click=${this.block}></input>`;
|
||||
}
|
||||
let edit_profile = this.editing ? html`
|
||||
<div style="flex: 1 0 50%">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
||||
</div>` : null;
|
||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
||||
image = this.editing?.image ?? image;
|
||||
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">
|
||||
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||
<div style="display: flex; flex-direction: row">
|
||||
${edit_profile}
|
||||
<div style="flex: 1 0 50%">
|
||||
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
||||
<div>${unsafeHTML(tfutils.markdown(description))}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Following ${Object.keys(profile.following || {}).length} identities.
|
||||
Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities.
|
||||
Blocking ${Object.keys(profile.blocking || {}).length} identities.
|
||||
Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities.
|
||||
</div>
|
||||
<div>
|
||||
${edit}
|
||||
${follow}
|
||||
${block}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-profile', TfProfileElement);
|
@ -1,32 +0,0 @@
|
||||
import {css} from './lit-all.min.js';
|
||||
|
||||
export let styles = css`
|
||||
a:link {
|
||||
color: #bbf;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ddf;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: min(640px, 100%);
|
||||
max-height: min(480px, auto);
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab:disabled {
|
||||
color: #088;
|
||||
background-color: #fff;
|
||||
}
|
||||
`;
|
@ -1,211 +0,0 @@
|
||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfTabNewsFeedElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
users: {type: Object},
|
||||
hash: {type: String},
|
||||
following: {type: Array},
|
||||
messages: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.whoami = null;
|
||||
this.users = {};
|
||||
this.hash = '#';
|
||||
this.following = [];
|
||||
this.drafts = {};
|
||||
this.expanded = {};
|
||||
}
|
||||
|
||||
async fetch_messages() {
|
||||
if (this.hash.startsWith('#@')) {
|
||||
let r = await tfrpc.rpc.query(
|
||||
`
|
||||
WITH mine AS (SELECT messages.*
|
||||
FROM messages
|
||||
WHERE messages.author = ?
|
||||
ORDER BY sequence DESC
|
||||
LIMIT 20)
|
||||
SELECT messages.*
|
||||
FROM mine
|
||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
UNION
|
||||
SELECT * FROM mine
|
||||
`,
|
||||
[
|
||||
this.hash.substring(1),
|
||||
]);
|
||||
return r;
|
||||
} else if (this.hash.startsWith('#%')) {
|
||||
return await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.*
|
||||
FROM messages
|
||||
WHERE id = ?1
|
||||
UNION
|
||||
SELECT messages.*
|
||||
FROM messages JOIN messages_refs
|
||||
ON messages.id = messages_refs.message
|
||||
WHERE messages_refs.ref = ?1
|
||||
`,
|
||||
[
|
||||
this.hash.substring(1),
|
||||
]);
|
||||
} else {
|
||||
return await tfrpc.rpc.query(
|
||||
`
|
||||
WITH news AS (SELECT messages.*
|
||||
FROM messages
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.timestamp > ?
|
||||
ORDER BY messages.timestamp DESC)
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
UNION
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.message
|
||||
JOIN messages ON messages_refs.ref = messages.id
|
||||
UNION
|
||||
SELECT news.* FROM news
|
||||
`,
|
||||
[
|
||||
JSON.stringify(this.following),
|
||||
new Date().valueOf() - 24 * 60 * 60 * 1000,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.messages ||
|
||||
this._messages_hash !== this.hash ||
|
||||
this._messages_following !== this.following) {
|
||||
console.log(`loading messages for ${this.whoami}`);
|
||||
let self = this;
|
||||
this.messages = [];
|
||||
this._messages_hash = this.hash;
|
||||
this._messages_following = this.following;
|
||||
this.fetch_messages().then(function(messages) {
|
||||
self.messages = messages;
|
||||
console.log(`loading mesages done for ${self.whoami}`);
|
||||
}).catch(function(error) {
|
||||
alert(JSON.stringify(error, null, 2));
|
||||
});
|
||||
}
|
||||
return html`<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>`;
|
||||
}
|
||||
}
|
||||
|
||||
class TfTabNewsElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
users: {type: Object},
|
||||
hash: {type: String},
|
||||
unread: {type: Array},
|
||||
following: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.whoami = null;
|
||||
this.users = {};
|
||||
this.hash = '#';
|
||||
this.unread = [];
|
||||
this.following = [];
|
||||
this.cache = {};
|
||||
this.drafts = {};
|
||||
this.expanded = {};
|
||||
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
|
||||
self.drafts = JSON.parse(d || '{}');
|
||||
});
|
||||
}
|
||||
|
||||
show_more() {
|
||||
let unread = this.unread;
|
||||
let news = this.renderRoot?.getElementById('news');
|
||||
if (news) {
|
||||
console.log('injecting messages', news.messages);
|
||||
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
|
||||
this.dispatchEvent(new CustomEvent('refresh'));
|
||||
}
|
||||
}
|
||||
|
||||
new_messages_text() {
|
||||
if (!this.unread?.length) {
|
||||
return 'No new messages.';
|
||||
}
|
||||
let counts = {};
|
||||
for (let message of this.unread) {
|
||||
let type = 'private';
|
||||
try {
|
||||
type = JSON.parse(message.content).type || type;
|
||||
} catch {
|
||||
}
|
||||
counts[type] = (counts[type] || 0) + 1;
|
||||
}
|
||||
return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
||||
}
|
||||
|
||||
draft(event) {
|
||||
let id = event.detail.id || '';
|
||||
let previous = this.drafts[id];
|
||||
if (event.detail.draft !== undefined) {
|
||||
this.drafts[id] = event.detail.draft;
|
||||
} else {
|
||||
delete this.drafts[id];
|
||||
}
|
||||
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
|
||||
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
|
||||
this.drafts = Object.assign({}, this.drafts);
|
||||
}
|
||||
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||
}
|
||||
|
||||
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() {
|
||||
let profile = this.hash.startsWith('#@') ?
|
||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
||||
return html`
|
||||
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
|
||||
<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
|
||||
<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>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
||||
customElements.define('tf-tab-news', TfTabNewsElement);
|
@ -1,39 +0,0 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfUserElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
id: {type: String},
|
||||
users: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = null;
|
||||
this.users = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.users[this.id]) {
|
||||
let image = this.users[this.id].image;
|
||||
image = typeof(image) == 'string' ? image : image?.link;
|
||||
return html`
|
||||
<div style="display: inline-block; font-weight: bold">
|
||||
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
||||
<a target="_top" href=${'#' + this.id}>${this.users[this.id].name ?? this.id}</a>
|
||||
</div>`;
|
||||
} else {
|
||||
return html`
|
||||
<div style="display: inline-block; font-weight: bold; word-wrap: anywhere">
|
||||
<a target="_top" href=${'#' + this.id}>${this.id}</a>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-user', TfUserElement);
|
@ -1,48 +0,0 @@
|
||||
import * as linkify from './commonmark-linkify.js';
|
||||
import * as hashtagify from './commonmark-hashtag.js';
|
||||
|
||||
export function markdown(md) {
|
||||
var reader = new commonmark.Parser({safe: true});
|
||||
var writer = new commonmark.HtmlRenderer();
|
||||
var parsed = reader.parse(md || '');
|
||||
parsed = linkify.transform(parsed);
|
||||
parsed = hashtagify.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}`;
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&QUR1tKa15B5Or8AfPX/8Zs87teSeX0Mh/HF7PEPBom0=.sha256","index.html":"&QXhwvxhHc9fa8iL6088hGDu9FgWdY7wkXgvU2BMNv0A=.sha256","lit-core.min.js":"&tP9KhbgwF1chFqPtkNZ12Yx9AfkpnSjFiPcX5Pw5J9g=.sha256","script.js":"&KgOaUVjBM4MzSy7PpUVQHETuvgXAx2JGPJABksBg+QY=.sha256"}}
|
@ -1,187 +0,0 @@
|
||||
import {LitElement, html} from './lit-core.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TodosElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
lists: {type: Array}
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.lists = [];
|
||||
let self = this;
|
||||
tfrpc.rpc.todo_get_all().then(function(lists) {
|
||||
self.lists = lists;
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
async new_list() {
|
||||
await tfrpc.rpc.todo_add('new list');
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.lists = await tfrpc.rpc.todo_get_all();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<div style="display: flex">
|
||||
${this.lists.map(x => html`
|
||||
<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list>
|
||||
`)}
|
||||
</div>
|
||||
<input type="button" @click=${this.new_list} value="+ List"></input>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
class TodoListElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
name: {type: String},
|
||||
items: {type: Array},
|
||||
editing: {type: Number},
|
||||
editing_name: {type: Boolean},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
save() {
|
||||
let self = this;
|
||||
console.log('saving', self.name, self.items);
|
||||
tfrpc.rpc.todo_set(self.name, self.items).then(function() {
|
||||
console.log('saved', self.name, self.items);
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
remove_item(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
this.items = [].concat(this.items.slice(0, index), this.items.slice(index + 1));
|
||||
this.save();
|
||||
}
|
||||
|
||||
handle_check(event, item) {
|
||||
item.x = event.srcElement.checked;
|
||||
this.save();
|
||||
}
|
||||
|
||||
input_blur(item) {
|
||||
this.save();
|
||||
this.editing = undefined;
|
||||
}
|
||||
|
||||
input_change(event, item) {
|
||||
item.text = event.srcElement.value;
|
||||
}
|
||||
|
||||
input_keydown(event, item) {
|
||||
if (event.key === 'Enter' || event.key === 'Escape') {
|
||||
item.text = event.srcElement.value;
|
||||
this.editing = undefined;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
updated() {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
if (edit) {
|
||||
edit.select();
|
||||
}
|
||||
}
|
||||
|
||||
render_item(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
let self = this;
|
||||
if (index === this.editing) {
|
||||
return html`
|
||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
||||
<input
|
||||
id="edit"
|
||||
type="text"
|
||||
value=${item.text}
|
||||
@change=${event => self.input_change(event, item)}
|
||||
@keydown=${event => self.input_keydown(event, item)}
|
||||
@blur=${x => self.input_blur(item)}></input>
|
||||
<span @click=${x => self.remove_item(item)}>x</span></div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
||||
<span @click=${x => self.editing = index}>${item.text}</span>
|
||||
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
add_item() {
|
||||
this.items = [].concat(this.items || [], [{text: 'new item'}]);
|
||||
this.editing = this.items.length - 1;
|
||||
this.save();
|
||||
}
|
||||
|
||||
async remove_list() {
|
||||
if (confirm(`Are you sure you want to remove "${this.name}"?`)) {
|
||||
await tfrpc.rpc.todo_remove(this.name);
|
||||
this.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
rename(new_name) {
|
||||
let self = this;
|
||||
return tfrpc.rpc.todo_rename(this.name, new_name).then(function() {
|
||||
self.dispatchEvent(new Event('change'));
|
||||
self.editing_name = false;
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
alert(error.message);
|
||||
self.editing_name = false;
|
||||
});
|
||||
}
|
||||
|
||||
name_blur(new_name) {
|
||||
this.rename(new_name);
|
||||
}
|
||||
|
||||
name_keydown(event, item) {
|
||||
let self = this;
|
||||
if (event.key == 'Enter' || event.key === 'Escape') {
|
||||
let new_name = event.srcElement.value;
|
||||
this.rename(new_name);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
let name = this.editing_name ?
|
||||
html`<input
|
||||
type="text"
|
||||
id="edit"
|
||||
@keydown=${event => self.name_keydown(event)}
|
||||
@blur=${event => self.name_blur(event.srcElement.value)}
|
||||
value=${this.name}></input>` :
|
||||
html`<h2 @click=${x => this.editing_name = true}>${this.name}</h2>`;
|
||||
return html`
|
||||
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
||||
${name}
|
||||
${(this.items || []).map(x => self.render_item(x))}
|
||||
<button @click=${self.add_item}>+ Item</button>
|
||||
<button @click=${self.remove_list}>- List</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-todo-list', TodoListElement);
|
||||
customElements.define('tf-todos', TodosElement);
|
4
apps/db.json
Normal file
4
apps/db.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💽"
|
||||
}
|
70
apps/db/app.js
Normal file
70
apps/db/app.js
Normal file
@ -0,0 +1,70 @@
|
||||
async function database_list() {
|
||||
var dbs = await databases();
|
||||
var doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<h1>Databases</h1>
|
||||
<ul id="dbs"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_dbs(id, dbs) {
|
||||
var list = document.getElementById(id);
|
||||
for (let db of dbs) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
var a = document.createElement('a');
|
||||
a.innerText = db;
|
||||
a.href = './#' + db;
|
||||
a.target = '_top';
|
||||
li.appendChild(a);
|
||||
}
|
||||
}
|
||||
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
||||
</script>
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
async function key_list(db) {
|
||||
let keys = await db.getAll();
|
||||
let object = {};
|
||||
for (let key of keys) {
|
||||
object[key] = await db.get(key);
|
||||
}
|
||||
let doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<a href="#" target="_top">back</a>
|
||||
<h1>Keys</h1>
|
||||
<ul id="keys"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_dbs(id, keys) {
|
||||
var list = document.getElementById(id);
|
||||
for (let [key, value] of Object.entries(keys)) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
li.innerText = key + ' = ' + value;
|
||||
}
|
||||
}
|
||||
populate_dbs('keys', ${JSON.stringify(object)});
|
||||
</script>
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
core.register('message', async function(message) {
|
||||
if (message.event == 'hashChange') {
|
||||
let hash = message.hash.substring(1);
|
||||
if (hash.startsWith(':shared:')) {
|
||||
let parts = hash.split(':');
|
||||
let packageName = parts[3];
|
||||
let key = parts.slice(4).join(':');
|
||||
key_list(await my_shared_database(packageName, key));
|
||||
} else if (hash.length) {
|
||||
key_list(await database(hash.split(':').slice(1).join(':')));
|
||||
} else {
|
||||
database_list();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
database_list();
|
4
apps/docs.json
Normal file
4
apps/docs.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📚"
|
||||
}
|
@ -16,9 +16,7 @@
|
||||
- / => Something good.
|
||||
- update docs
|
||||
- audit + document API exposed to apps
|
||||
- sqlStream => sqlExec or something
|
||||
- fix weird HTTP warnings
|
||||
- ssb from child process?
|
||||
- channels
|
||||
- placeholder/missing images
|
||||
- no denial of service
|
||||
@ -57,7 +55,9 @@
|
||||
- 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
|
||||
- logging to browser
|
4
apps/follow.json
Normal file
4
apps/follow.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "➡️"
|
||||
}
|
267
apps/follow/app.js
Normal file
267
apps/follow/app.js
Normal file
@ -0,0 +1,267 @@
|
||||
let g_about_cache = {};
|
||||
|
||||
async function query(sql, args) {
|
||||
let result = [];
|
||||
await ssb.sqlAsync(sql, args, function(row) {
|
||||
result.push(row);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function 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 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 function contact(id, last_row_id, following, max_row_id) {
|
||||
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) {
|
||||
if (g_about_cache[id]) {
|
||||
return g_about_cache[id];
|
||||
}
|
||||
let o = await db.get(id + ":about");
|
||||
const k_version = 4;
|
||||
let f = o ? JSON.parse(o) : o;
|
||||
if (!f || f.version != k_version) {
|
||||
f = {about: {}, sequence: 0, version: k_version};
|
||||
}
|
||||
await ssb.sqlAsync(
|
||||
"SELECT "+
|
||||
" sequence, "+
|
||||
" content "+
|
||||
"FROM messages "+
|
||||
"WHERE "+
|
||||
" author = ?1 AND "+
|
||||
" sequence > ?2 AND "+
|
||||
" json_extract(content, '$.type') = 'about' AND "+
|
||||
" json_extract(content, '$.about') = ?1 "+
|
||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
||||
"ORDER BY sequence",
|
||||
[id, f.sequence],
|
||||
function(row) {
|
||||
f.sequence = row.sequence;
|
||||
if (row.content) {
|
||||
let about = {};
|
||||
try {
|
||||
about = JSON.parse(row.content);
|
||||
} catch {
|
||||
}
|
||||
delete about.about;
|
||||
delete about.type;
|
||||
f.about = Object.assign(f.about, about);
|
||||
}
|
||||
});
|
||||
let j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":about", j);
|
||||
}
|
||||
g_about_cache[id] = f.about;
|
||||
return f.about;
|
||||
}
|
||||
|
||||
async function getSize(db, id) {
|
||||
let size = 0;
|
||||
await ssb.sqlAsync(
|
||||
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
||||
[id],
|
||||
function (row) {
|
||||
size += row.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) {
|
||||
let value = bytes;
|
||||
let unit = 'B';
|
||||
const k_units = ['kB', 'MB', 'GB', 'TB'];
|
||||
for (let u of k_units) {
|
||||
if (value >= 1024) {
|
||||
value /= 1024;
|
||||
unit = u;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Math.round(value * 10) / 10 + ' ' + unit;
|
||||
}
|
||||
|
||||
function escape(value) {
|
||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
||||
let db = await database('ssb');
|
||||
let whoami = await ssb.getIdentities();
|
||||
let tree = '';
|
||||
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
|
||||
let following = await following_deep(whoami, 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('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
|
||||
}
|
||||
|
||||
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/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();
|
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);
|
4
apps/sneaker.json
Normal file
4
apps/sneaker.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "👟"
|
||||
}
|
30
apps/sneaker/app.js
Normal file
30
apps/sneaker/app.js
Normal file
@ -0,0 +1,30 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
tfrpc.register(async function getAllIdentities() {
|
||||
return ssb.getAllIdentities();
|
||||
});
|
||||
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 (Array.isArray(blob)) {
|
||||
blob = Uint8Array.from(blob);
|
||||
}
|
||||
return await ssb.blobStore(blob);
|
||||
});
|
||||
tfrpc.register(async function get_blob(id) {
|
||||
return Array.from(new Uint8Array(await ssb.blobGet(id)));
|
||||
});
|
||||
tfrpc.register(async function store_message(message) {
|
||||
return await ssb.storeMessage(message);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||
}
|
||||
main();
|
3
apps/sneaker/filesaver.min.js
vendored
Normal file
3
apps/sneaker/filesaver.min.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||
|
||||
//# sourceMappingURL=FileSaver.min.js.map
|
14
apps/sneaker/index.html
Normal file
14
apps/sneaker/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-sneaker-app/>
|
||||
<script>window.litDisableBundleWarning = true;</script>
|
||||
<script src="filesaver.min.js"></script>
|
||||
<script src="jszip.min.js"></script>
|
||||
<script src="script.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
13
apps/sneaker/jszip.min.js
vendored
Normal file
13
apps/sneaker/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
120
apps/sneaker/lit-all.min.js
vendored
Normal file
120
apps/sneaker/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/sneaker/lit-all.min.js.map
Normal file
1
apps/sneaker/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user