Compare commits
280 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
298
Makefile
298
Makefile
@ -3,9 +3,13 @@
|
|||||||
MAKEFLAGS += --warn-undefined-variables
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
|
VERSION_CODE := 11
|
||||||
|
VERSION_NUMBER := 0.0.11
|
||||||
|
VERSION_NAME := Be nothing, and you will have everything to give to others.
|
||||||
|
|
||||||
PROJECT = tildefriends
|
PROJECT = tildefriends
|
||||||
BUILD_DIR ?= out
|
BUILD_DIR ?= out
|
||||||
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease
|
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
|
||||||
UNAME_M := $(shell uname -m)
|
UNAME_M := $(shell uname -m)
|
||||||
|
|
||||||
CFLAGS += \
|
CFLAGS += \
|
||||||
@ -16,17 +20,54 @@ CFLAGS += \
|
|||||||
-MMD \
|
-MMD \
|
||||||
-ffunction-sections \
|
-ffunction-sections \
|
||||||
-fdata-sections \
|
-fdata-sections \
|
||||||
-fno-omit-frame-pointer \
|
-fno-exceptions \
|
||||||
-g
|
-g
|
||||||
LDFLAGS += -Wl,--gc-sections
|
LDFLAGS += -Wl,--gc-sections
|
||||||
|
|
||||||
NDK_PATH := /usr/lib/android-sdk/ndk-bundle
|
ANDROID_SDK ?= ~/Android/Sdk
|
||||||
NDK_API_VERSION := 30
|
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
|
||||||
NDK_TARGET_TRIPLE := aarch64-linux-android
|
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
|
||||||
|
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
|
||||||
|
ANDROID_MIN_SDK_VERSION := 28
|
||||||
|
|
||||||
debug windebug androiddebug: CFLAGS += -Og
|
ANDROID_ARM64_TARGETS := \
|
||||||
debug release androidrelease: LDFLAGS += -rdynamic
|
out/androiddebug/tildefriends \
|
||||||
release winrelease: CFLAGS += -DNDEBUG -O3
|
out/androidrelease/tildefriends
|
||||||
|
ANDROID_X86_64_TARGETS := \
|
||||||
|
out/androiddebug-x86_64/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends
|
||||||
|
ANDROID_TARGETS := \
|
||||||
|
$(ANDROID_X86_64_TARGETS) \
|
||||||
|
$(ANDROID_ARM64_TARGETS)
|
||||||
|
|
||||||
|
DEBUG_TARGETS := \
|
||||||
|
out/debug/tildefriends \
|
||||||
|
out/windebug/tildefriends \
|
||||||
|
out/androiddebug/tildefriends \
|
||||||
|
out/androiddebug-x86_64/tildefriends
|
||||||
|
RELEASE_TARGETS := \
|
||||||
|
out/release/tildefriends \
|
||||||
|
out/winrelease/tildefriends \
|
||||||
|
out/androidrelease/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends
|
||||||
|
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
||||||
|
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||||
|
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(DEBUG_TARGETS) $(RELEASE_TARGETS))
|
||||||
|
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||||
|
$(NONANDROID_TARGETS): LDFLAGS += -rdynamic
|
||||||
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||||
|
-fPIC \
|
||||||
|
-fdebug-compilation-dir . \
|
||||||
|
-fomit-frame-pointer \
|
||||||
|
-fno-asynchronous-unwind-tables \
|
||||||
|
-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
|
||||||
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
|
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
|
||||||
windebug winrelease: AS = $(CC)
|
windebug winrelease: AS = $(CC)
|
||||||
windebug winrelease: CFLAGS += \
|
windebug winrelease: CFLAGS += \
|
||||||
@ -38,15 +79,17 @@ windebug winrelease: LDFLAGS += \
|
|||||||
-static \
|
-static \
|
||||||
-lm \
|
-lm \
|
||||||
-Ldeps/openssl/mingw64/lib
|
-Ldeps/openssl/mingw64/lib
|
||||||
androiddebug androidrelease: CC = $(NDK_PATH)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
|
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
|
||||||
androiddebug androidrelease: AS = $(CC)
|
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
|
||||||
androiddebug androidrelease: CFLAGS += \
|
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/$(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION)-clang
|
||||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
$(ANDROID_TARGETS): AS = $(CC)
|
||||||
-Ideps/openssl/android/arm64-v8a/usr/local/include \
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
|
||||||
-Wno-unknown-warning-option
|
-Wno-unknown-warning-option
|
||||||
androiddebug androidrelease: LDFLAGS += \
|
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
|
||||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
||||||
-Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||||
|
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||||
|
|
||||||
ifeq ($(UNAME_M),x86_64)
|
ifeq ($(UNAME_M),x86_64)
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
@ -57,8 +100,8 @@ get_objs = \
|
|||||||
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
|
$(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,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,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 androiddebug-x86_64 androidrelease-x86_64,$(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)))))
|
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
|
||||||
|
|
||||||
APP_SOURCES := $(wildcard src/*.c)
|
APP_SOURCES := $(wildcard src/*.c)
|
||||||
APP_OBJS := $(call get_objs,APP_SOURCES)
|
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||||
@ -69,12 +112,19 @@ $(APP_OBJS): CFLAGS += \
|
|||||||
-Ideps/libsodium \
|
-Ideps/libsodium \
|
||||||
-Ideps/libsodium/src/libsodium/include \
|
-Ideps/libsodium/src/libsodium/include \
|
||||||
-Ideps/libuv/include \
|
-Ideps/libuv/include \
|
||||||
|
-Ideps/zlib \
|
||||||
|
-Ideps/zlib/contrib/minizip \
|
||||||
-Ideps/picohttpparser \
|
-Ideps/picohttpparser \
|
||||||
-Ideps/quickjs \
|
-Ideps/quickjs \
|
||||||
-Ideps/sqlite \
|
-Ideps/sqlite \
|
||||||
-Ideps/valgrind \
|
-Ideps/valgrind \
|
||||||
-Ideps/xopt \
|
-Ideps/xopt \
|
||||||
|
-Wdouble-promotion \
|
||||||
-Werror
|
-Werror
|
||||||
|
ifeq ($(UNAME_M),x86_64)
|
||||||
|
$(filter-out $(BUILD_DIR)/android%,$(APP_OBJS)): CFLAGS += \
|
||||||
|
-fanalyzer
|
||||||
|
endif
|
||||||
|
|
||||||
BLOWFISH_SOURCES := \
|
BLOWFISH_SOURCES := \
|
||||||
deps/crypt_blowfish/crypt_blowfish.c \
|
deps/crypt_blowfish/crypt_blowfish.c \
|
||||||
@ -100,13 +150,10 @@ UV_SOURCES_unix := \
|
|||||||
deps/libuv/src/unix/async.c \
|
deps/libuv/src/unix/async.c \
|
||||||
deps/libuv/src/unix/core.c \
|
deps/libuv/src/unix/core.c \
|
||||||
deps/libuv/src/unix/dl.c \
|
deps/libuv/src/unix/dl.c \
|
||||||
deps/libuv/src/unix/epoll.c \
|
|
||||||
deps/libuv/src/unix/fs.c \
|
deps/libuv/src/unix/fs.c \
|
||||||
deps/libuv/src/unix/getaddrinfo.c \
|
deps/libuv/src/unix/getaddrinfo.c \
|
||||||
deps/libuv/src/unix/getnameinfo.c \
|
deps/libuv/src/unix/getnameinfo.c \
|
||||||
deps/libuv/src/unix/linux-core.c \
|
deps/libuv/src/unix/linux.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-watcher.c \
|
||||||
deps/libuv/src/unix/loop.c \
|
deps/libuv/src/unix/loop.c \
|
||||||
deps/libuv/src/unix/pipe.c \
|
deps/libuv/src/unix/pipe.c \
|
||||||
@ -124,7 +171,6 @@ UV_SOURCES_unix := \
|
|||||||
deps/libuv/src/unix/tty.c \
|
deps/libuv/src/unix/tty.c \
|
||||||
deps/libuv/src/unix/udp.c
|
deps/libuv/src/unix/udp.c
|
||||||
UV_SOURCES_android := \
|
UV_SOURCES_android := \
|
||||||
deps/libuv/src/unix/pthread-fixes.c \
|
|
||||||
deps/libuv/src/unix/random-getentropy.c
|
deps/libuv/src/unix/random-getentropy.c
|
||||||
UV_SOURCES_win := \
|
UV_SOURCES_win := \
|
||||||
deps/libuv/src/win/async.c \
|
deps/libuv/src/win/async.c \
|
||||||
@ -156,12 +202,13 @@ UV_OBJS := $(call get_objs,UV_SOURCES)
|
|||||||
$(UV_OBJS): CFLAGS += \
|
$(UV_OBJS): CFLAGS += \
|
||||||
-Ideps/libuv/include \
|
-Ideps/libuv/include \
|
||||||
-Ideps/libuv/src \
|
-Ideps/libuv/src \
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-incompatible-pointer-types \
|
|
||||||
-Wno-sign-compare \
|
|
||||||
-Wno-unused-variable \
|
|
||||||
-Wno-dangling-pointer \
|
-Wno-dangling-pointer \
|
||||||
|
-Wno-incompatible-pointer-types \
|
||||||
-Wno-maybe-uninitialized \
|
-Wno-maybe-uninitialized \
|
||||||
|
-Wno-sign-compare \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-result \
|
||||||
|
-Wno-unused-variable \
|
||||||
-D_GNU_SOURCE
|
-D_GNU_SOURCE
|
||||||
|
|
||||||
SODIUM_SOURCES := \
|
SODIUM_SOURCES := \
|
||||||
@ -204,7 +251,8 @@ SODIUM_SOURCES := \
|
|||||||
deps/libsodium/src/libsodium/sodium/core.c \
|
deps/libsodium/src/libsodium/sodium/core.c \
|
||||||
deps/libsodium/src/libsodium/sodium/codecs.c \
|
deps/libsodium/src/libsodium/sodium/codecs.c \
|
||||||
deps/libsodium/src/libsodium/sodium/runtime.c \
|
deps/libsodium/src/libsodium/sodium/runtime.c \
|
||||||
deps/libsodium/src/libsodium/sodium/utils.c
|
deps/libsodium/src/libsodium/sodium/utils.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/version.c
|
||||||
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
||||||
$(SODIUM_OBJS): CFLAGS += \
|
$(SODIUM_OBJS): CFLAGS += \
|
||||||
-DCONFIGURED=1 \
|
-DCONFIGURED=1 \
|
||||||
@ -213,30 +261,44 @@ $(SODIUM_OBJS): CFLAGS += \
|
|||||||
-Wno-unused-variable \
|
-Wno-unused-variable \
|
||||||
-Wno-type-limits \
|
-Wno-type-limits \
|
||||||
-Wno-unknown-pragmas \
|
-Wno-unknown-pragmas \
|
||||||
|
-Ideps/libsodium/builds/msvc \
|
||||||
-Ideps/libsodium/src/libsodium/include/sodium
|
-Ideps/libsodium/src/libsodium/include/sodium
|
||||||
|
|
||||||
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||||
$(SQLITE_OBJS): CFLAGS += \
|
$(SQLITE_OBJS): CFLAGS += \
|
||||||
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
||||||
|
-DSQLITE_DEFAULT_MEMSTATUS=0 \
|
||||||
|
-DSQLITE_DQS=0 \
|
||||||
|
-DSQLITE_ENABLE_MEMSYS5 \
|
||||||
-DSQLITE_ENABLE_FTS5 \
|
-DSQLITE_ENABLE_FTS5 \
|
||||||
-DSQLITE_ENABLE_JSON1 \
|
-DSQLITE_ENABLE_JSON1 \
|
||||||
-DSQLITE_THREADSAFE=1 \
|
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
|
||||||
-DSQLITE_MAX_LENGTH=5242880 \
|
-DSQLITE_MAX_ATTACHED=1 \
|
||||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
|
||||||
-DSQLITE_MAX_COLUMN=100 \
|
-DSQLITE_MAX_COLUMN=100 \
|
||||||
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
|
||||||
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||||
-DSQLITE_MAX_ATTACHED=0 \
|
-DSQLITE_MAX_LENGTH=5242880 \
|
||||||
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
||||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
-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_SECURE_DELETE \
|
||||||
|
-DSQLITE_THREADSAFE=0 \
|
||||||
|
-DSQLITE_UNTESTABLE \
|
||||||
|
-DSQLITE_USE_ALLOCA \
|
||||||
|
-DHAVE_ISNAN \
|
||||||
-Wno-implicit-fallthrough \
|
-Wno-implicit-fallthrough \
|
||||||
-Wno-unused-but-set-variable \
|
-Wno-unused-but-set-variable \
|
||||||
-Wno-unused-function
|
-Wno-unused-function \
|
||||||
|
-Wno-unused-variable
|
||||||
|
|
||||||
XOPT_SOURCES := deps/xopt/xopt.c
|
XOPT_SOURCES := deps/xopt/xopt.c
|
||||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
||||||
@ -246,25 +308,27 @@ $(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
|||||||
-DHAVE_VASNPRINTF \
|
-DHAVE_VASNPRINTF \
|
||||||
-DHAVE_VASPRINTF \
|
-DHAVE_VASPRINTF \
|
||||||
-Dvsnprintf=rpl_vsnprintf
|
-Dvsnprintf=rpl_vsnprintf
|
||||||
|
$(XOPT_OBJS): CFLAGS += \
|
||||||
|
-Wno-implicit-const-int-float-conversion
|
||||||
|
|
||||||
QUICKJS_SOURCES := \
|
QUICKJS_SOURCES := \
|
||||||
deps/quickjs/cutils.c \
|
deps/quickjs/cutils.c \
|
||||||
deps/quickjs/libbf.c \
|
deps/quickjs/libbf.c \
|
||||||
deps/quickjs/libregexp.c \
|
deps/quickjs/libregexp.c \
|
||||||
deps/quickjs/libunicode.c \
|
deps/quickjs/libunicode.c \
|
||||||
deps/quickjs/quickjs-libc.c \
|
|
||||||
deps/quickjs/quickjs.c
|
deps/quickjs/quickjs.c
|
||||||
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
||||||
$(QUICKJS_OBJS): CFLAGS += \
|
$(QUICKJS_OBJS): CFLAGS += \
|
||||||
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
||||||
-DCONFIG_BIGNUM \
|
-DCONFIG_BIGNUM \
|
||||||
-DDUMP_LEAKS \
|
|
||||||
-D_GNU_SOURCE \
|
-D_GNU_SOURCE \
|
||||||
-Wno-sign-compare \
|
-Wno-enum-conversion \
|
||||||
|
-Wno-implicit-const-int-float-conversion \
|
||||||
-Wno-implicit-fallthrough \
|
-Wno-implicit-fallthrough \
|
||||||
-Wno-unused-variable \
|
-Wno-sign-compare \
|
||||||
-Wno-unused-but-set-variable \
|
-Wno-unused-but-set-variable \
|
||||||
-Wno-enum-conversion
|
-Wno-unused-variable
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
||||||
|
|
||||||
LIBBACKTRACE_SOURCES := \
|
LIBBACKTRACE_SOURCES := \
|
||||||
deps/libbacktrace/atomic.c \
|
deps/libbacktrace/atomic.c \
|
||||||
@ -296,7 +360,20 @@ $(LIBBACKTRACE_OBJS): CFLAGS += \
|
|||||||
PICOHTTPPARSER_SOURCES := \
|
PICOHTTPPARSER_SOURCES := \
|
||||||
deps/picohttpparser/picohttpparser.c
|
deps/picohttpparser/picohttpparser.c
|
||||||
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
||||||
# $(PICOHTTPPARSER_OBJS): CFLAGS +=
|
|
||||||
|
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 += \
|
LDFLAGS += \
|
||||||
-pthread \
|
-pthread \
|
||||||
@ -306,29 +383,34 @@ debug release: LDFLAGS += \
|
|||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto
|
-lcrypto
|
||||||
windebug winrelease: LDFLAGS += \
|
windebug winrelease: LDFLAGS += \
|
||||||
-lwsock32 \
|
|
||||||
-lws2_32 \
|
|
||||||
-lkernel32 \
|
|
||||||
-liphlpapi \
|
|
||||||
-luserenv \
|
|
||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto \
|
-lcrypto \
|
||||||
|
-lcrypt32 \
|
||||||
|
-ldbghelp \
|
||||||
|
-liphlpapi \
|
||||||
|
-lkernel32 \
|
||||||
|
-lole32 \
|
||||||
|
-luserenv \
|
||||||
|
-luuid \
|
||||||
-lws2_32 \
|
-lws2_32 \
|
||||||
-lcrypt32
|
-lwsock32
|
||||||
androiddebug androidrelease: LDFLAGS += \
|
$(ANDROID_TARGETS): LDFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
|
||||||
-ldl \
|
-ldl \
|
||||||
|
-llog \
|
||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto
|
-lcrypto
|
||||||
|
|
||||||
unix: debug release
|
unix: debug release
|
||||||
win: windebug winrelease
|
win: windebug winrelease
|
||||||
all: $(BUILD_TYPES)
|
all: $(BUILD_TYPES) out/TildeFriends-debug.apk out/TildeFriends-release.apk
|
||||||
.PHONY: all win unix
|
.PHONY: all win unix
|
||||||
|
|
||||||
ALL_APP_OBJS := \
|
ALL_APP_OBJS := \
|
||||||
$(APP_OBJS) \
|
$(APP_OBJS) \
|
||||||
$(BLOWFISH_OBJS) \
|
$(BLOWFISH_OBJS) \
|
||||||
$(LIBBACKTRACE_OBJS) \
|
$(LIBBACKTRACE_OBJS) \
|
||||||
|
$(MINIUNZIP_OBJS) \
|
||||||
$(PICOHTTPPARSER_OBJS) \
|
$(PICOHTTPPARSER_OBJS) \
|
||||||
$(QUICKJS_OBJS) \
|
$(QUICKJS_OBJS) \
|
||||||
$(SODIUM_OBJS) \
|
$(SODIUM_OBJS) \
|
||||||
@ -345,7 +427,7 @@ $(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
|||||||
|
|
||||||
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
||||||
@echo [link] $$@
|
@echo [link] $$@
|
||||||
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
@$$(CC) -o $$@ -Wl,-Map,$$@.map $$^ $$(LDFLAGS)
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.c
|
$(BUILD_DIR)/$(1)/%.o: %.c
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@ -360,6 +442,120 @@ endef
|
|||||||
|
|
||||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||||
|
|
||||||
|
src/version.h : $(firstword $(MAKEFILE_LIST))
|
||||||
|
@echo [version] $@
|
||||||
|
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"\n#define VERSION_NAME \"$(VERSION_NAME)\"\n" > $@
|
||||||
|
|
||||||
|
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
||||||
|
@echo [android_version] $@
|
||||||
|
@sed -i \
|
||||||
|
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
||||||
|
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
||||||
|
-e 's/android:minSdkVersion=".*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \
|
||||||
|
$@
|
||||||
|
|
||||||
|
# Android support.
|
||||||
|
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo [aapt2] $@
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
||||||
|
|
||||||
|
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo [aapt2] $@
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
||||||
|
|
||||||
|
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
||||||
|
|
||||||
|
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||||
|
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||||
|
|
||||||
|
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||||
|
@echo [javac] $(CLASS_FILES)
|
||||||
|
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
||||||
|
|
||||||
|
out/apk/classes.dex: $(CLASS_FILES)
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo [d8] $@
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||||
|
|
||||||
|
PACKAGE_DIRS := \
|
||||||
|
apps/ \
|
||||||
|
core/ \
|
||||||
|
deps/codemirror/ \
|
||||||
|
deps/lit/
|
||||||
|
|
||||||
|
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
|
||||||
|
|
||||||
|
out/apk/TildeFriends-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
|
out/apk/TildeFriends-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
|
||||||
|
out/apk/TildeFriends-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
|
||||||
|
out/%.unsigned.apk:
|
||||||
|
@mkdir -p $(dir $@) out/apk$(BUILD_TYPE)/bin/aarch64/ out/apk$(BUILD_TYPE)/bin/x86_64/
|
||||||
|
@echo [aapt] $@
|
||||||
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk$(BUILD_TYPE)/bin/aarch64/
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk$(BUILD_TYPE)/bin/x86_64/
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/aarch64/tildefriends
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/x86_64/tildefriends
|
||||||
|
@cp out/apk/res.apk $@
|
||||||
|
@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
|
||||||
|
@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||||
|
@zip -u $@ -q -9 -x '*.map' -r $(PACKAGE_DIRS) $(RAW_FILES)
|
||||||
|
|
||||||
|
out/%.apk: out/apk/%.unsigned.apk
|
||||||
|
@echo [apksigner] $(notdir $@)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
||||||
|
|
||||||
|
apk: out/TildeFriends-release.apk
|
||||||
|
.PHONY: apk
|
||||||
|
|
||||||
|
apkgo: out/TildeFriends-release.apk
|
||||||
|
@adb install $<
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
||||||
|
.PHONY: apkgo
|
||||||
|
|
||||||
|
apklog:
|
||||||
|
@adb logcat *:S tildefriends
|
||||||
|
.PHONY: apklog
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
|
||||||
|
dist: apk
|
||||||
|
@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=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-$(VERSION_NUMBER).apk"
|
||||||
|
@cp out/TildeFriends-release.apk TildeFriends-$(VERSION_NUMBER).apk
|
||||||
|
.PHONY: dist
|
||||||
|
|
||||||
|
dist-test: dist
|
||||||
|
@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
|
@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release
|
||||||
|
@rm -rf tildefriends-$(VERSION_NUMBER)
|
||||||
|
.PHONY: dist-test
|
||||||
|
@ -28,7 +28,7 @@ privileges. Further administration can be done at
|
|||||||
<http://localhost:12345/~core/admin/`>.
|
<http://localhost:12345/~core/admin/`>.
|
||||||
|
|
||||||
## Documentation
|
## 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/>.
|
that can be read in-place or at <http://localhost:12345/~core/docs/>.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🎛"
|
||||||
}
|
}
|
@ -9,14 +9,18 @@ tfrpc.register(function global_settings_set(key, value) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let data = {
|
try {
|
||||||
users: {},
|
let data = {
|
||||||
granted: await core.allPermissionsGranted(),
|
users: {},
|
||||||
settings: await core.globalSettingsDescriptions(),
|
granted: await core.allPermissionsGranted(),
|
||||||
};
|
settings: await core.globalSettingsDescriptions(),
|
||||||
for (let user of await core.users()) {
|
};
|
||||||
data.users[user] = await core.permissionsForUser(user);
|
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>');
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
|
||||||
}
|
}
|
||||||
main();
|
main();
|
@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html style="width: 100%">
|
||||||
<head>
|
<head>
|
||||||
<script>const g_data = $data;</script>
|
<script>const g_data = $data;</script>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body style="color: #fff; width: 100%">
|
||||||
<h1>Tilde Friends Administration</h1>
|
<h1>Tilde Friends Administration</h1>
|
||||||
</body>
|
</body>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
|
@ -25,29 +25,37 @@ window.addEventListener('load', function() {
|
|||||||
function input_template(key, description) {
|
function input_template(key, description) {
|
||||||
if (description.type === 'boolean') {
|
if (description.type === 'boolean') {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em">
|
||||||
<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
|
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
||||||
<span>${description.description}</span>
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (description.type === 'textarea') {
|
} else if (description.type === 'textarea') {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em"">
|
||||||
<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div style="width: 100%; padding: 0; margin: 0">
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
<div style="width: 90%; padding: 0 margin: 0">
|
||||||
<span>${description.description}</span>
|
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
|
||||||
|
</div>
|
||||||
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em">
|
||||||
<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||||
<span>${description.description}</span>
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -67,12 +75,13 @@ window.addEventListener('load', function() {
|
|||||||
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
const page_template = (data) =>
|
const page_template = (data) =>
|
||||||
html`<div>
|
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||||
<h2>Global Settings</h2>
|
<h2>Global Settings</h2>
|
||||||
<div style="display: grid">
|
<div>
|
||||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
||||||
|
</div>
|
||||||
|
${users_template(data.users)}
|
||||||
</div>
|
</div>
|
||||||
${users_template(data.users)}
|
`;
|
||||||
</div>`;
|
|
||||||
render(page_template(g_data), document.body);
|
render(page_template(g_data), document.body);
|
||||||
});
|
});
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📜"
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
313
apps/api/docs.js
Normal file
313
apps/api/docs.js
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
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['ssb.sqlAsync()'] = `
|
||||||
|
Run an SQL query against the sqlite database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **query** The sqlite query.
|
||||||
|
* *Array* **args** The query arguments to bind.
|
||||||
|
* *Function* **callback** Callback called for each row result.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.appendMessageWithIdentity()'] = `
|
||||||
|
Signs and stores a message in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **id** The public key of an SSB identity owned by the authenticated user.
|
||||||
|
* *Object* **message** The unsigned message.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.storeMessage()'] = `
|
||||||
|
Verifies and stores a signed message in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *Object* **message** The valid, signed message to store.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.blobStore()'] = `
|
||||||
|
Store a blob in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *String*/*Uint8Array* **blob** The blob contents to store
|
||||||
|
### Returns
|
||||||
|
*String* The stored blob ID.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.blobGet()'] = `
|
||||||
|
Fetches a blob from the database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **blob_id** The blob identifier to fetch (\`&....sha256\`).
|
||||||
|
### Returns
|
||||||
|
*ArrayBuffer* The blob data.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['print()'] = `
|
||||||
|
Log debug information both to the server's console and to the visiting user's browser console when possible.
|
||||||
|
### Parameters
|
||||||
|
* **...** Whatever you want to log. Will be joined with spaces.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database()'] = `
|
||||||
|
Returns a database instance that is specific to the authenticated user and the given key.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['my_shared_database()'] = `
|
||||||
|
Returns a database instance that is specific to the authenticated user and the given key.
|
||||||
|
### Parameters
|
||||||
|
* *String* **package_name** The database package name.
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['shared_database()'] = `
|
||||||
|
Returns a database instance that is shared between all users of the app, determined by its owner and app name.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['base64Decode()'] = `
|
||||||
|
Decode a base64 string to bytes.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* value The base64-encoded string.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The decoded bytes.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['base64Encode()'] = `
|
||||||
|
Encode bytes to a base64 string.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* The bytes to encode.
|
||||||
|
### Returns
|
||||||
|
*String* The base64-encoded string.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['utf8Decode()'] = `
|
||||||
|
Decode UTF-8 bytes to a string.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **value** The value to decode.
|
||||||
|
### Returns
|
||||||
|
*String* The value as a string.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['utf8Encode()'] = `
|
||||||
|
Encodes a string to UTF-8 bytes.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* **value** The value to encode.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The encoded \`value\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['setTimeout()'] = `
|
||||||
|
Call a function after some delay.
|
||||||
|
### Parameters
|
||||||
|
* *Function* **callback** The function to call.
|
||||||
|
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['parseHttpRequest()'] = `
|
||||||
|
Parses an HTTP request.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
|
||||||
|
indicate when and where it is complete.
|
||||||
|
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
|
||||||
|
### Returns
|
||||||
|
* *Integer* **-2** if the request is incomplete.
|
||||||
|
* *Integer* **-1** if the request could not be parsed.
|
||||||
|
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['parseHttpResponse()'] = `
|
||||||
|
Parses an HTTP response.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
|
||||||
|
indicate when and where it is complete.
|
||||||
|
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
|
||||||
|
### Returns
|
||||||
|
* *Integer* **-2** if the response is incomplete.
|
||||||
|
* *Integer* **-1** if the response could not be parsed.
|
||||||
|
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['sha1Digest()'] =`
|
||||||
|
Calculates a SHA1 digest.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* **value** The value for which to calculate the digest.
|
||||||
|
### Returns
|
||||||
|
*String* The SHA1 digest of UTF-8 encoded \`value\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['maskBytes()'] = `
|
||||||
|
Masks bytes for WebSocket communication.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **bytes** The byte array of data to mask.
|
||||||
|
* *Uint32* **mask** The mask to apply.
|
||||||
|
### Returns
|
||||||
|
*Uint32Array* The masked bytes.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['exit()'] = `
|
||||||
|
Exits the app. But why would you want to do that?
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Integer* **exit_code** System exit code.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['version()'] = `
|
||||||
|
Gets version information for the running server.
|
||||||
|
### Returns
|
||||||
|
*Object* Keys are things like \`name\` and \`number\` for the server itself and \`libuv\` and \`openssl\` for
|
||||||
|
dependencies. Values are *String* version numbers.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['platform()'] = `
|
||||||
|
Gets the host operating system platform of the running server.
|
||||||
|
### Returns
|
||||||
|
*String* The platform, one of \`windows\`, \`android\`, \`linux\`, or \`other\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['getFile()'] = `
|
||||||
|
Gets a file from the running app.
|
||||||
|
### Parameters
|
||||||
|
* *String* **name** Name of the file to retrieve.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The contents of a file from the app with the given name, or *undefined*.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs.database = `
|
||||||
|
# <span style="color: #aaf">Database</span>
|
||||||
|
Local-only storage is provided by a \`Database\` type representing a key-value store.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.get()'] = `
|
||||||
|
Gets a value from the database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
### Returns
|
||||||
|
*String* The value from the database or undefined if not found.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.getAll()'] = `
|
||||||
|
Gets all keys from the database.
|
||||||
|
### Returns
|
||||||
|
*Array* An array of *String* key names for all keys in the given database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.getLike()'] = `
|
||||||
|
Gets all keys and values from the database matching a pattern.
|
||||||
|
### Parameters
|
||||||
|
* *String* **pattern** An sqlite \`LIKE\` pattern to match keys against.
|
||||||
|
### Returns
|
||||||
|
*Object* An object whose keys are the database keys and values are the database values that match the given pattern.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.set()'] = `
|
||||||
|
Sets a value in the database, creating a new entry or replacing an existing entry.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
* *String* **value** The value.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.exchange()'] = `
|
||||||
|
Performs an atomic compare and exchange operation, setting a value in the database only if its current value matches what is expected.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
* *String* **expected** The expected value.
|
||||||
|
* *String* **value** The new value.
|
||||||
|
### Returns
|
||||||
|
*Boolean* true if the value is now the given value.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.remove()'] = `
|
||||||
|
Removes an entry from the database if it exists.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs.tfrpc = `
|
||||||
|
# <span style="color: #aaf" id="tfrpc">tfrpc</span>
|
||||||
|
\`tfrpc.js\` is a small helper script that is available to be used to facilitate communication between parts of an application.
|
||||||
|
|
||||||
|
\`tfrpc.js\` can be used to asynchronously make calls between the app code running in a sandboxed iframe in the browser
|
||||||
|
and the app process on the server.
|
||||||
|
|
||||||
|
From \`app.js\`:
|
||||||
|
\`\`\`
|
||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
|
||||||
|
From script running in the browser:
|
||||||
|
\`\`\`
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Either side can register or call functions, though they must be registered before they can be called. Arguments and return
|
||||||
|
values are ultimately serialized by means that attempt to preserve most JSON-serializable values as well as functions themselves.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['tfrpc.register()'] = `
|
||||||
|
Register a function, allowing it to be called remotely.
|
||||||
|
### Parameters
|
||||||
|
* *Function* **function** The function to register. Its name will be how it will be called.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['tfrpc.rpc.*()'] = `
|
||||||
|
Call a remote function.
|
||||||
|
### Parameters
|
||||||
|
* **...** Parameters to pass to the function.
|
||||||
|
### Returns
|
||||||
|
The return value of the called function.
|
||||||
|
`;
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "💻"
|
||||||
}
|
}
|
@ -1,31 +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() {
|
async function main() {
|
||||||
var apps = await core.apps();
|
var apps = await fetch_info(await core.apps());
|
||||||
var core_apps = await core.apps('core');
|
var core_apps = await fetch_info(await core.apps('core'));
|
||||||
var doc = `<!DOCTYPE html>
|
var doc = `<!DOCTYPE html>
|
||||||
<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">
|
<body style="background: #888">
|
||||||
<h1>Apps</h1>
|
<h1 id="apps_title">Apps</h1>
|
||||||
<ul id="apps"></ul>
|
<div id="apps" class="container"></div>
|
||||||
<h1>Core Apps</h1>
|
<h1>Core Apps</h1>
|
||||||
<ul id="core_apps"></ul>
|
<div id="core_apps" class="container"></div>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
function populate_apps(id, name, apps) {
|
function populate_apps(id, name, apps) {
|
||||||
var list = document.getElementById(id);
|
var list = document.getElementById(id);
|
||||||
for (let app of Object.keys(apps).sort()) {
|
for (let app of Object.keys(apps).sort()) {
|
||||||
var li = list.appendChild(document.createElement('li'));
|
let div = list.appendChild(document.createElement('div'));
|
||||||
var a = document.createElement('a');
|
div.classList.add('app');
|
||||||
a.innerText = 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.href = '/~' + name + '/' + app + '/';
|
||||||
a.target = '_top';
|
a.target = '_top';
|
||||||
li.appendChild(a);
|
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('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||||
</script>
|
</script>
|
||||||
</html>`
|
</html>`;
|
||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
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,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "💽"
|
||||||
}
|
}
|
@ -20,7 +20,7 @@ async function database_list() {
|
|||||||
}
|
}
|
||||||
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
||||||
</script>
|
</script>
|
||||||
</html>`
|
</html>`;
|
||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ async function key_list(db) {
|
|||||||
}
|
}
|
||||||
populate_dbs('keys', ${JSON.stringify(object)});
|
populate_dbs('keys', ${JSON.stringify(object)});
|
||||||
</script>
|
</script>
|
||||||
</html>`
|
</html>`;
|
||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📚"
|
||||||
}
|
}
|
@ -16,9 +16,7 @@
|
|||||||
- / => Something good.
|
- / => Something good.
|
||||||
- update docs
|
- update docs
|
||||||
- audit + document API exposed to apps
|
- audit + document API exposed to apps
|
||||||
- sqlStream => sqlExec or something
|
|
||||||
- fix weird HTTP warnings
|
- fix weird HTTP warnings
|
||||||
- ssb from child process?
|
|
||||||
- channels
|
- channels
|
||||||
- placeholder/missing images
|
- placeholder/missing images
|
||||||
- no denial of service
|
- no denial of service
|
||||||
@ -57,7 +55,9 @@
|
|||||||
- keep working on good error feedback
|
- keep working on good error feedback
|
||||||
- build for windows
|
- build for windows
|
||||||
- installable apps (bring back an app message?)
|
- installable apps (bring back an app message?)
|
||||||
|
- sqlStream => sqlExec or something
|
||||||
|
- !ssb from child process?
|
||||||
|
|
||||||
## Done
|
## Done
|
||||||
- update LICENSE
|
- update LICENSE
|
||||||
- logging to browser
|
- logging to browser
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "➡️"
|
||||||
}
|
}
|
@ -1,75 +1,163 @@
|
|||||||
"use strict";
|
let g_about_cache = {};
|
||||||
|
|
||||||
var g_following_cache = {};
|
async function query(sql, args) {
|
||||||
var g_following_deep_cache = {};
|
let result = [];
|
||||||
var g_about_cache = {};
|
await ssb.sqlAsync(sql, args, function(row) {
|
||||||
|
result.push(row);
|
||||||
async function following(db, id) {
|
});
|
||||||
if (g_following_cache[id]) {
|
return result;
|
||||||
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.sqlAsync(
|
|
||||||
"SELECT "+
|
|
||||||
" sequence, "+
|
|
||||||
" json_extract(content, '$.contact') AS contact, "+
|
|
||||||
" json_extract(content, '$.following') AS following "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" author = ?1 AND "+
|
|
||||||
" sequence > ?2 AND "+
|
|
||||||
" json_extract(content, '$.type') = 'contact' "+
|
|
||||||
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
|
|
||||||
"ORDER BY sequence",
|
|
||||||
[id, f.sequence],
|
|
||||||
function(row) {
|
|
||||||
if (row.following) {
|
|
||||||
f.users.add(row.contact);
|
|
||||||
} else {
|
|
||||||
f.users.delete(row.contact);
|
|
||||||
}
|
|
||||||
f.sequence = row.sequence;
|
|
||||||
});
|
|
||||||
var as_set = f.users;
|
|
||||||
f.users = Array.from(f.users).sort();
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":following", j);
|
|
||||||
}
|
|
||||||
f.users = as_set;
|
|
||||||
g_following_cache[id] = f.users;
|
|
||||||
return f.users;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function followingDeep(db, seed_ids, depth) {
|
async function contacts_internal(id, last_row_id, following, max_row_id) {
|
||||||
if (depth <= 0) {
|
let result = Object.assign({}, following[id] || {});
|
||||||
return seed_ids;
|
result.following = result.following || {};
|
||||||
|
result.blocking = result.blocking || {};
|
||||||
|
let contacts = await query(
|
||||||
|
`
|
||||||
|
SELECT content FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
rowid > ? AND
|
||||||
|
rowid <= ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact'
|
||||||
|
ORDER BY sequence
|
||||||
|
`,
|
||||||
|
[id, last_row_id, max_row_id]);
|
||||||
|
for (let row of contacts) {
|
||||||
|
let contact = JSON.parse(row.content);
|
||||||
|
if (contact.following === true) {
|
||||||
|
result.following[contact.contact] = true;
|
||||||
|
} else if (contact.following === false) {
|
||||||
|
delete result.following[contact.contact];
|
||||||
|
} else if (contact.blocking === true) {
|
||||||
|
result.blocking[contact.contact] = true;
|
||||||
|
} else if (contact.blocking === false) {
|
||||||
|
delete result.blocking[contact.contact];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var key = JSON.stringify([seed_ids, depth]);
|
following[id] = result;
|
||||||
if (g_following_deep_cache[key]) {
|
return result;
|
||||||
return g_following_deep_cache[key];
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
}
|
}
|
||||||
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
|
return [...new Set(Object.values(result).flat())];
|
||||||
var ids = [].concat(...f);
|
}
|
||||||
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
|
|
||||||
x = [...new Set([].concat(...x, ...seed_ids))].sort();
|
async function following_deep(ids, depth, blocking) {
|
||||||
g_following_deep_cache[key] = x;
|
let db = await database('cache');
|
||||||
return x;
|
const k_cache_version = 5;
|
||||||
|
let cache = await db.get('following');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
following: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = (await query(`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`, []))[0].max_row_id;
|
||||||
|
let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
let store = JSON.stringify(cache);
|
||||||
|
await db.set('following', store);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch_about(db, ids, users) {
|
||||||
|
const k_cache_version = 1;
|
||||||
|
let cache = await db.get('about');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
about: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = 0;
|
||||||
|
await ssb.sqlAsync(`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
function(row) {
|
||||||
|
max_row_id = row.max_row_id;
|
||||||
|
});
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
if (ids.indexOf(id) == -1) {
|
||||||
|
delete cache.about[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let abouts = [];
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?1) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid > ?3 AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?2) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
ORDER BY messages.author, messages.sequence
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(ids.filter(id => cache.about[id])),
|
||||||
|
JSON.stringify(ids.filter(id => !cache.about[id])),
|
||||||
|
cache.last_row_id,
|
||||||
|
max_row_id,
|
||||||
|
]);
|
||||||
|
for (let about of abouts) {
|
||||||
|
let content = JSON.parse(about.content);
|
||||||
|
if (content.about === about.author) {
|
||||||
|
delete content.type;
|
||||||
|
delete content.about;
|
||||||
|
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
await db.set('about', JSON.stringify(cache));
|
||||||
|
users = users || {};
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
users[id] = Object.assign(users[id] || {}, cache.about[id]);
|
||||||
|
}
|
||||||
|
return Object.assign({}, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAbout(db, id) {
|
async function getAbout(db, id) {
|
||||||
if (g_about_cache[id]) {
|
if (g_about_cache[id]) {
|
||||||
return g_about_cache[id];
|
return g_about_cache[id];
|
||||||
}
|
}
|
||||||
var o = await db.get(id + ":about");
|
let o = await db.get(id + ":about");
|
||||||
const k_version = 4;
|
const k_version = 4;
|
||||||
var f = o ? JSON.parse(o) : o;
|
let f = o ? JSON.parse(o) : o;
|
||||||
if (!f || f.version != k_version) {
|
if (!f || f.version != k_version) {
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
f = {about: {}, sequence: 0, version: k_version};
|
||||||
}
|
}
|
||||||
@ -89,7 +177,7 @@ async function getAbout(db, id) {
|
|||||||
function(row) {
|
function(row) {
|
||||||
f.sequence = row.sequence;
|
f.sequence = row.sequence;
|
||||||
if (row.content) {
|
if (row.content) {
|
||||||
var about = {};
|
let about = {};
|
||||||
try {
|
try {
|
||||||
about = JSON.parse(row.content);
|
about = JSON.parse(row.content);
|
||||||
} catch {
|
} catch {
|
||||||
@ -99,7 +187,7 @@ async function getAbout(db, id) {
|
|||||||
f.about = Object.assign(f.about, about);
|
f.about = Object.assign(f.about, about);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var j = JSON.stringify(f);
|
let j = JSON.stringify(f);
|
||||||
if (o != j) {
|
if (o != j) {
|
||||||
await db.set(id + ":about", j);
|
await db.set(id + ":about", j);
|
||||||
}
|
}
|
||||||
@ -110,7 +198,7 @@ async function getAbout(db, id) {
|
|||||||
async function getSize(db, id) {
|
async function getSize(db, id) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
||||||
[id],
|
[id],
|
||||||
function (row) {
|
function (row) {
|
||||||
size += row.size;
|
size += row.size;
|
||||||
@ -118,6 +206,25 @@ async function getSize(db, id) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getSizes(ids) {
|
||||||
|
let sizes = {};
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
author,
|
||||||
|
(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS ids ON author = ids.value
|
||||||
|
GROUP BY author
|
||||||
|
`,
|
||||||
|
[JSON.stringify(ids)],
|
||||||
|
function (row) {
|
||||||
|
sizes[row.author] = row.size;
|
||||||
|
});
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
function niceSize(bytes) {
|
function niceSize(bytes) {
|
||||||
let value = bytes;
|
let value = bytes;
|
||||||
let unit = 'B';
|
let unit = 'B';
|
||||||
@ -133,27 +240,28 @@ function niceSize(bytes) {
|
|||||||
return Math.round(value * 10) / 10 + ' ' + unit;
|
return Math.round(value * 10) / 10 + ' ' + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildTree(db, root, indent, depth) {
|
function escape(value) {
|
||||||
var f = await following(db, root);
|
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||||
var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
|
|
||||||
if (depth > 0) {
|
|
||||||
for (let next of f) {
|
|
||||||
result += await buildTree(db, next, indent + ' ', depth - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
||||||
var db = await database('ssb');
|
let db = await database('ssb');
|
||||||
var whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
var tree = '';
|
let tree = '';
|
||||||
for (let id of whoami) {
|
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
|
||||||
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
|
let following = await following_deep(whoami, 2, {});
|
||||||
tree += await buildTree(db, id, '', 2);
|
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`);
|
||||||
|
let [about, sizes] = await Promise.all([
|
||||||
|
fetch_about(db, following, {}),
|
||||||
|
getSizes(following),
|
||||||
|
]);
|
||||||
|
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
||||||
|
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0)));
|
||||||
|
for (let id of following) {
|
||||||
|
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
||||||
}
|
}
|
||||||
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
|
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
4
apps/gg.json
Normal file
4
apps/gg.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🗺"
|
||||||
|
}
|
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();
|
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
126
apps/gg/lit-all.min.js
vendored
Normal file
126
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 };
|
789
apps/gg/script.js
Normal file
789
apps/gg/script.js
Normal file
@ -0,0 +1,789 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
const k_marker_snap = {x: 5, y: 1};
|
||||||
|
|
||||||
|
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},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
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) {
|
||||||
|
let position = this.leaflet.options.crs.latLngToPoint(latlng, 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, 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_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)});
|
||||||
|
}
|
||||||
|
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 = 'top';
|
||||||
|
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.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let placed of self.placed_emojis) {
|
||||||
|
let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position), 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.fillText(placed.emoji, position.x, position.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
return html`
|
||||||
|
<h2>Store</h2>
|
||||||
|
<div><b>Your balance:</b> ${this.currency}</div>
|
||||||
|
${Object.entries(k_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`,
|
||||||
|
});
|
||||||
|
}
|
4
apps/issues.json
Normal file
4
apps/issues.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🦟"
|
||||||
|
}
|
105
apps/issues/app.js
Normal file
105
apps/issues/app.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
let g_database;
|
||||||
|
let g_hash;
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageGet(key) {
|
||||||
|
return app.localStorageGet(key);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function localStorageSet(key, value) {
|
||||||
|
return app.localStorageSet(key, value);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseGet(key) {
|
||||||
|
return g_database ? g_database.get(key) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseSet(key, value) {
|
||||||
|
return g_database ? g_database.set(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createIdentity() {
|
||||||
|
return ssb.createIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getIdentities() {
|
||||||
|
return ssb.getIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getAllIdentities() {
|
||||||
|
return ssb.getAllIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getBroadcasts() {
|
||||||
|
return ssb.getBroadcasts();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getConnections() {
|
||||||
|
return ssb.connections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getStoredConnections() {
|
||||||
|
return ssb.storedConnections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function forgetStoredConnection(connection) {
|
||||||
|
return ssb.forgetStoredConnection(connection);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createTunnel(portal, target) {
|
||||||
|
return ssb.createTunnel(portal, target);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function connect(token) {
|
||||||
|
await ssb.connect(token);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function closeConnection(id) {
|
||||||
|
await ssb.closeConnection(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
|
return ssb.appendMessageWithIdentity(id, message);
|
||||||
|
});
|
||||||
|
core.register('message', async function message_handler(message) {
|
||||||
|
if (message.event == 'hashChange') {
|
||||||
|
g_hash = message.hash;
|
||||||
|
await tfrpc.rpc.hashChanged(message.hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tfrpc.register(function getHash(id, message) {
|
||||||
|
return g_hash;
|
||||||
|
});
|
||||||
|
tfrpc.register(function setHash(hash) {
|
||||||
|
return app.setHash(hash);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('message', async function(id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_message(message) {
|
||||||
|
return await ssb.storeMessage(message);
|
||||||
|
});
|
||||||
|
tfrpc.register(function apps() {
|
||||||
|
return core.apps();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('broadcasts', async function() {
|
||||||
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
|
});
|
||||||
|
|
||||||
|
core.register('onConnectionsChanged', async function() {
|
||||||
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (typeof(database) !== 'undefined') {
|
||||||
|
g_database = await database('ssb');
|
||||||
|
}
|
||||||
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
}
|
||||||
|
main();
|
91
apps/issues/commonmark-linkify.js
Normal file
91
apps/issues/commonmark-linkify.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
function textNode(text) {
|
||||||
|
const node = new commonmark.Node("text", undefined);
|
||||||
|
node.literal = text;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkNode(text, url) {
|
||||||
|
const urlNode = new commonmark.Node("link", undefined);
|
||||||
|
urlNode.destination = url;
|
||||||
|
urlNode.appendChild(textNode(text));
|
||||||
|
|
||||||
|
return urlNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMatches(text, regexp) {
|
||||||
|
// Regexp must be sticky.
|
||||||
|
regexp = new RegExp(regexp, "gm");
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let match = regexp.exec(text);
|
||||||
|
while (match) {
|
||||||
|
const matchText = match[0];
|
||||||
|
|
||||||
|
if (match.index > i) {
|
||||||
|
result.push([text.substring(i, match.index), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push([matchText, true]);
|
||||||
|
i = match.index + matchText.length;
|
||||||
|
|
||||||
|
match = regexp.exec(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < text.length) {
|
||||||
|
result.push([text.substring(i, text.length), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
|
||||||
|
|
||||||
|
function splitURLs(textNodes) {
|
||||||
|
const text = textNodes.map(n => n.literal).join("");
|
||||||
|
const parts = splitMatches(text, urlRegexp);
|
||||||
|
|
||||||
|
return parts.map(part => {
|
||||||
|
if (part[1]) {
|
||||||
|
return linkNode(part[0], part[0]);
|
||||||
|
} else {
|
||||||
|
return textNode(part[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transform(parsed) {
|
||||||
|
const walker = parsed.walker();
|
||||||
|
let event;
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
const node = event.node;
|
||||||
|
if (event.entering && node.type === "text") {
|
||||||
|
nodes.push(node);
|
||||||
|
} else {
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
nodes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
1
apps/issues/commonmark.min.js
vendored
Normal file
1
apps/issues/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
apps/issues/index.html
Normal file
14
apps/issues/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html style="color: #fff">
|
||||||
|
<head>
|
||||||
|
<title>Tilde Friends</title>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<tf-issues-app/>
|
||||||
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
|
<script src="commonmark.min.js"></script>
|
||||||
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
|
<script src="script.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
120
apps/issues/lit-all.min.js
vendored
Normal file
120
apps/issues/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/issues/lit-all.min.js.map
Normal file
1
apps/issues/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
260
apps/issues/script.js
Normal file
260
apps/issues/script.js
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import * as tfutils from './tf-utils.js';
|
||||||
|
|
||||||
|
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
||||||
|
|
||||||
|
class TfIdPickerElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
ids: {type: Array},
|
||||||
|
selected: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.selected = await tfrpc.rpc.localStorageGet('whoami');
|
||||||
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
changed(event) {
|
||||||
|
this.selected = event.srcElement.value;
|
||||||
|
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.ids) {
|
||||||
|
return html`
|
||||||
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
|
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return html`<div>Loading...</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-id-picker', TfIdPickerElement);
|
||||||
|
|
||||||
|
class TfComposeElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
value: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
input() {
|
||||||
|
let input = this.renderRoot.getElementById('input');
|
||||||
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
|
if (input && preview) {
|
||||||
|
preview.innerHTML = tfutils.markdown(input.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.dispatchEvent(new CustomEvent('tf-submit', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
value: this.renderRoot.getElementById('input').value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
this.renderRoot.getElementById('input').value = '';
|
||||||
|
this.input();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea>
|
||||||
|
<div id="preview" style="flex: 1 1"></div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Submit" @click=${this.submit}></input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-compose', TfComposeElement);
|
||||||
|
|
||||||
|
class TfIssuesAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
issues: {type: Array},
|
||||||
|
selected: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.issues = [];
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
let issues = {};
|
||||||
|
let messages = await tfrpc.rpc.query(`
|
||||||
|
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
|
||||||
|
messages.id = messages_refs.message
|
||||||
|
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
||||||
|
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
|
||||||
|
issues.id = messages_refs.ref JOIN messages ON
|
||||||
|
messages.id = messages_refs.message
|
||||||
|
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
||||||
|
SELECT * FROM issues
|
||||||
|
UNION
|
||||||
|
SELECT * FROM edits ORDER BY timestamp
|
||||||
|
`, [k_project]);
|
||||||
|
for (let message of messages) {
|
||||||
|
let content = JSON.parse(message.content);
|
||||||
|
switch (content.type) {
|
||||||
|
case 'issue':
|
||||||
|
issues[message.id] = {
|
||||||
|
id: message.id,
|
||||||
|
author: message.author,
|
||||||
|
text: content.text,
|
||||||
|
updates: [],
|
||||||
|
created: message.timestamp,
|
||||||
|
open: true,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'issue-edit':
|
||||||
|
case 'post':
|
||||||
|
for (let issue of (content.issues || [])) {
|
||||||
|
if (issues[issue.link]) {
|
||||||
|
if (issue.open !== undefined) {
|
||||||
|
issues[issue.link].open = issue.open;
|
||||||
|
message.open = issue.open;
|
||||||
|
}
|
||||||
|
issues[issue.link].updates.push(message);
|
||||||
|
issues[issue.link].updated = message.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.issues = Object.values(issues).sort((x, y) => y.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}`;
|
||||||
|
}
|
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
232
apps/sneaker/script.js
Normal file
232
apps/sneaker/script.js
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfSneakerAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
feeds: {type: Object},
|
||||||
|
progress: {type: Object},
|
||||||
|
result: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.feeds = [];
|
||||||
|
this.progress = undefined;
|
||||||
|
this.result = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search() {
|
||||||
|
let q = this.renderRoot.getElementById('search').value;
|
||||||
|
let result = await tfrpc.rpc.query(`
|
||||||
|
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
||||||
|
FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE
|
||||||
|
json_extract(messages.content, '$.type') = 'about' AND
|
||||||
|
json_extract(messages.content, '$.about') = messages.author AND
|
||||||
|
json_extract(messages.content, '$.name') IS NOT NULL
|
||||||
|
GROUP BY messages.author
|
||||||
|
HAVING MAX(messages.sequence)
|
||||||
|
ORDER BY COUNT(*) DESC
|
||||||
|
`,
|
||||||
|
[`"${q.replaceAll('"', '""')}"`]);
|
||||||
|
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
|
||||||
|
}
|
||||||
|
|
||||||
|
format_message(message) {
|
||||||
|
let out = {
|
||||||
|
previous: message.previous ?? null,
|
||||||
|
};
|
||||||
|
if (message.sequence_before_author) {
|
||||||
|
out.sequence = message.sequence;
|
||||||
|
out.author = message.author;
|
||||||
|
} else {
|
||||||
|
out.author = message.author;
|
||||||
|
out.sequence = message.sequence;
|
||||||
|
}
|
||||||
|
out.timestamp = message.timestamp;
|
||||||
|
out.hash = message.hash;
|
||||||
|
out.content = JSON.parse(message.content);
|
||||||
|
out.signature = message.signature;
|
||||||
|
return {key: message.id, value: out};
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitize(value) {
|
||||||
|
return value.replaceAll('/', '_').replaceAll('+', '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
guess_ext(data) {
|
||||||
|
function startsWith(prefix) {
|
||||||
|
if (data.length < prefix.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < prefix.length; i++) {
|
||||||
|
if (prefix[i] !== null && data[i] !== prefix[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
||||||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
||||||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
||||||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
||||||
|
return '.jpg';
|
||||||
|
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
||||||
|
return '.png';
|
||||||
|
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||||
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
||||||
|
return '.gif';
|
||||||
|
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
||||||
|
return '.webp';
|
||||||
|
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
||||||
|
return '.svg';
|
||||||
|
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||||
|
return '.mp3';
|
||||||
|
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
||||||
|
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||||
|
return '.mp4';
|
||||||
|
} else {
|
||||||
|
return '.bin';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async export(id) {
|
||||||
|
let all_messages = '';
|
||||||
|
let sequence = -1;
|
||||||
|
let messages_done = 0;
|
||||||
|
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
|
||||||
|
while (true) {
|
||||||
|
let messages = await tfrpc.rpc.query(
|
||||||
|
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
||||||
|
[id, sequence]
|
||||||
|
);
|
||||||
|
if (messages?.length) {
|
||||||
|
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
|
||||||
|
sequence = messages[messages.length - 1].sequence;
|
||||||
|
messages_done += messages.length;
|
||||||
|
this.progress = {name: 'messages', value: messages_done, max: messages_max};
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let zip = new JSZip();
|
||||||
|
zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);
|
||||||
|
|
||||||
|
let blobs = await tfrpc.rpc.query(
|
||||||
|
`SELECT messages_refs.ref AS id
|
||||||
|
FROM messages
|
||||||
|
JOIN messages_refs ON messages.id = messages_refs.message
|
||||||
|
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
||||||
|
[id]);
|
||||||
|
let blobs_done = 0;
|
||||||
|
for (let row of blobs) {
|
||||||
|
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
||||||
|
let blob;
|
||||||
|
try {
|
||||||
|
blob = await tfrpc.rpc.get_blob(row.id);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed to get ${row.id}: ${e.message}`);
|
||||||
|
}
|
||||||
|
if (blob) {
|
||||||
|
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
||||||
|
}
|
||||||
|
blobs_done++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progress = {name: 'saving'};
|
||||||
|
let blob = await zip.generateAsync({type: 'blob'});
|
||||||
|
saveAs(blob, `${this.sanitize(id)}.zip`);
|
||||||
|
this.progress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
keypress(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async import(event) {
|
||||||
|
let file = event.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progress = {name: 'loading'};
|
||||||
|
let zip = new JSZip();
|
||||||
|
file = await zip.loadAsync(file);
|
||||||
|
let messages = [];
|
||||||
|
let blobs = [];
|
||||||
|
file.forEach(function(path, entry) {
|
||||||
|
if (!entry.dir) {
|
||||||
|
if (path.startsWith('message/classic/')) {
|
||||||
|
messages.push(entry);
|
||||||
|
} else {
|
||||||
|
blobs.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let success = {messages: 0, blobs: 0};
|
||||||
|
let progress = 0;
|
||||||
|
let total_messages = 0;
|
||||||
|
for (let entry of messages) {
|
||||||
|
let lines = (await entry.async('string')).split('\n');
|
||||||
|
total_messages += lines.length;
|
||||||
|
for (let line of lines) {
|
||||||
|
if (!line.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let message = JSON.parse(line);
|
||||||
|
this.progress = {name: 'messages', value: progress++, max: total_messages};
|
||||||
|
if (await tfrpc.rpc.store_message(message.value)) {
|
||||||
|
success.messages++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress = 0;
|
||||||
|
for (let blob of blobs) {
|
||||||
|
this.progress = {name: 'blobs', value: progress++, max: blobs.length};
|
||||||
|
if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) {
|
||||||
|
success.blobs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.progress = undefined;
|
||||||
|
this.result = `imported ${success.messages} messages and ${success.blobs} blobs`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let progress;
|
||||||
|
if (this.progress) {
|
||||||
|
if (this.progress.max) {
|
||||||
|
progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
|
||||||
|
} else {
|
||||||
|
progress = html`<div><span>${this.progress.name}</span></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html`<h1>SSB 👟net</h1>
|
||||||
|
<code>${this.result}</code>
|
||||||
|
${progress}
|
||||||
|
|
||||||
|
<h2>Import</h2>
|
||||||
|
<input type="file" id="import" @change=${this.import}></input>
|
||||||
|
|
||||||
|
<h2>Export</h2>
|
||||||
|
<input type="text" id="search" @keypress=${this.keypress}></input>
|
||||||
|
<input type="button" value="Search Users" @click=${this.search}></input>
|
||||||
|
<ul>
|
||||||
|
${Object.entries(this.feeds).map(([id, name]) => html`
|
||||||
|
<li>
|
||||||
|
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||||
|
${name}
|
||||||
|
<code style="color: #ccc">${id}</code>
|
||||||
|
</li>
|
||||||
|
`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-sneaker-app', TfSneakerAppElement);
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🐌"
|
||||||
}
|
}
|
@ -79,9 +79,18 @@ tfrpc.register(async function store_blob(blob) {
|
|||||||
tfrpc.register(async function get_blob(id) {
|
tfrpc.register(async function get_blob(id) {
|
||||||
return utf8Decode(await ssb.blobGet(id));
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function store_message(message) {
|
||||||
|
return await ssb.storeMessage(message);
|
||||||
|
});
|
||||||
tfrpc.register(function apps() {
|
tfrpc.register(function apps() {
|
||||||
return core.apps();
|
return core.apps();
|
||||||
});
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
ssb.addEventListener('broadcasts', async function() {
|
ssb.addEventListener('broadcasts', async function() {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
@ -54,21 +54,27 @@ export function picker(callback, anchor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chosen(event) {
|
||||||
|
console.log(event.srcElement.innerText);
|
||||||
|
callback(event.srcElement.innerText);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
while (list.firstChild) {
|
while (list.firstChild) {
|
||||||
list.removeChild(list.firstChild);
|
list.removeChild(list.firstChild);
|
||||||
}
|
}
|
||||||
let search = input.value;
|
let search = input.value.toLowerCase();
|
||||||
let any_at_all = false;
|
let any_at_all = false;
|
||||||
Object.entries(json).forEach(function(row) {
|
for (let row of Object.entries(json)) {
|
||||||
let header = document.createElement('div');
|
let header = document.createElement('div');
|
||||||
header.appendChild(document.createTextNode(row[0]));
|
header.appendChild(document.createTextNode(row[0]));
|
||||||
list.appendChild(header);
|
list.appendChild(header);
|
||||||
let any = false;
|
let any = false;
|
||||||
for (let entry of row[1]) {
|
for (let entry of Object.entries(row[1])) {
|
||||||
if (search &&
|
if (search &&
|
||||||
search.length &&
|
search.length &&
|
||||||
entry.name.indexOf(search) == -1) {
|
entry[0].toLowerCase().indexOf(search) == -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let emoji = document.createElement('span');
|
let emoji = document.createElement('span');
|
||||||
@ -76,12 +82,9 @@ export function picker(callback, anchor) {
|
|||||||
emoji.style.display = 'inline-block';
|
emoji.style.display = 'inline-block';
|
||||||
emoji.style.overflow = 'hidden';
|
emoji.style.overflow = 'hidden';
|
||||||
emoji.style.cursor = 'pointer';
|
emoji.style.cursor = 'pointer';
|
||||||
emoji.onclick = function() {
|
emoji.onclick = chosen;
|
||||||
callback(entry);
|
emoji.title = entry[0];
|
||||||
cleanup();
|
emoji.appendChild(document.createTextNode(entry[1]));
|
||||||
}
|
|
||||||
emoji.title = entry.name;
|
|
||||||
emoji.appendChild(document.createTextNode(entry.emoji));
|
|
||||||
list.appendChild(emoji);
|
list.appendChild(emoji);
|
||||||
any = true;
|
any = true;
|
||||||
any_at_all = true;
|
any_at_all = true;
|
||||||
@ -89,7 +92,7 @@ export function picker(callback, anchor) {
|
|||||||
if (!any) {
|
if (!any) {
|
||||||
list.removeChild(header);
|
list.removeChild(header);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (!any_at_all) {
|
if (!any_at_all) {
|
||||||
list.appendChild(document.createTextNode('No matches found.'));
|
list.appendChild(document.createTextNode('No matches found.'));
|
||||||
}
|
}
|
||||||
|
15116
apps/ssb/emojis.json
15116
apps/ssb/emojis.json
File diff suppressed because one or more lines are too long
3
apps/ssb/filesaver.min.js
vendored
Normal file
3
apps/ssb/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
|
@ -13,6 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<tf-app/>
|
<tf-app/>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
<script src="commonmark-hashtag.js" type="module"></script>
|
<script src="commonmark-hashtag.js" type="module"></script>
|
||||||
|
54
apps/ssb/lit-all.min.js
vendored
54
apps/ssb/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -8,6 +8,10 @@ import * as tf_user from './tf-user.js';
|
|||||||
import * as tf_compose from './tf-compose.js';
|
import * as tf_compose from './tf-compose.js';
|
||||||
import * as tf_news from './tf-news.js';
|
import * as tf_news from './tf-news.js';
|
||||||
import * as tf_profile from './tf-profile.js';
|
import * as tf_profile from './tf-profile.js';
|
||||||
|
import * as tf_tab_mentions from './tf-tab-mentions.js';
|
||||||
import * as tf_tab_news from './tf-tab-news.js';
|
import * as tf_tab_news from './tf-tab-news.js';
|
||||||
|
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
||||||
import * as tf_tab_search from './tf-tab-search.js';
|
import * as tf_tab_search from './tf-tab-search.js';
|
||||||
import * as tf_tab_connections from './tf-tab-connections.js';
|
import * as tf_tab_connections from './tf-tab-connections.js';
|
||||||
|
import * as tf_tab_query from './tf-tab-query.js';
|
||||||
|
import * as tf_tag from './tf-tag.js';
|
@ -16,6 +16,7 @@ class TfElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
ids: {type: Array},
|
ids: {type: Array},
|
||||||
|
tags: {type: Array},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +33,9 @@ class TfElement extends LitElement {
|
|||||||
this.following = [];
|
this.following = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
|
this.tags = [];
|
||||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
|
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.rpc.getHash().then(hash => self.set_hash(hash));
|
||||||
tfrpc.register(function hashChanged(hash) {
|
tfrpc.register(function hashChanged(hash) {
|
||||||
self.set_hash(hash);
|
self.set_hash(hash);
|
||||||
@ -64,6 +66,10 @@ class TfElement extends LitElement {
|
|||||||
this.tab = 'search';
|
this.tab = 'search';
|
||||||
} else if (this.hash === '#connections') {
|
} else if (this.hash === '#connections') {
|
||||||
this.tab = 'connections';
|
this.tab = 'connections';
|
||||||
|
} else if (this.hash === '#mentions') {
|
||||||
|
this.tab = 'mentions';
|
||||||
|
} else if (this.hash.startsWith('#sql=')) {
|
||||||
|
this.tab = 'query';
|
||||||
} else {
|
} else {
|
||||||
this.tab = 'news';
|
this.tab = 'news';
|
||||||
}
|
}
|
||||||
@ -79,7 +85,7 @@ class TfElement extends LitElement {
|
|||||||
WHERE author = ? AND
|
WHERE author = ? AND
|
||||||
rowid > ? AND
|
rowid > ? AND
|
||||||
rowid <= ? AND
|
rowid <= ? AND
|
||||||
json_extract(content, "$.type") = "contact"
|
json_extract(content, '$.type') = 'contact'
|
||||||
ORDER BY sequence
|
ORDER BY sequence
|
||||||
`,
|
`,
|
||||||
[id, last_row_id, max_row_id]);
|
[id, last_row_id, max_row_id]);
|
||||||
@ -135,9 +141,9 @@ class TfElement extends LitElement {
|
|||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
let store = JSON.stringify(cache);
|
let store = JSON.stringify(cache);
|
||||||
/* 2023-02-20: Exceeding message size. */
|
/* 2023-02-20: Exceeding message size. */
|
||||||
if (store.length < 512 * 1024) {
|
//if (store.length < 512 * 1024) {
|
||||||
await tfrpc.rpc.databaseSet('following', store);
|
await tfrpc.rpc.databaseSet('following', store);
|
||||||
}
|
//}
|
||||||
return [result, cache.following];
|
return [result, cache.following];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,22 +247,47 @@ class TfElement extends LitElement {
|
|||||||
if (confirm("Are you sure you want to create a new identity?")) {
|
if (confirm("Are you sure you want to create a new identity?")) {
|
||||||
await tfrpc.rpc.createIdentity();
|
await tfrpc.rpc.createIdentity();
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
if (this.ids && !this.whoami) {
|
||||||
|
this.whoami = this.ids[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_id_picker() {
|
render_id_picker() {
|
||||||
return html`
|
return html`
|
||||||
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
|
<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>
|
<button @click=${this.create_identity} id="create_identity">Create Identity</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async load_recent_tags() {
|
||||||
|
let start = new Date();
|
||||||
|
this.tags = await tfrpc.rpc.query(`
|
||||||
|
WITH
|
||||||
|
recent AS (SELECT id, content FROM messages
|
||||||
|
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
||||||
|
ORDER BY timestamp DESC LIMIT 1024),
|
||||||
|
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
||||||
|
FROM recent
|
||||||
|
WHERE json_extract(content, '$.channel') IS NOT NULL),
|
||||||
|
recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
|
||||||
|
FROM recent, json_each(recent.content, '$.mentions') AS mention
|
||||||
|
WHERE json_valid(mention.value) AND tag LIKE '#%'),
|
||||||
|
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
||||||
|
by_message AS (SELECT DISTINCT id, tag FROM combined)
|
||||||
|
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
||||||
|
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
|
||||||
|
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
let whoami = this.whoami;
|
let whoami = this.whoami;
|
||||||
|
let tags = this.load_recent_tags();
|
||||||
let [following, users] = await this.following_deep([whoami], 2, {});
|
let [following, users] = await this.following_deep([whoami], 2, {});
|
||||||
users = await this.fetch_about(following.sort(), users);
|
users = await this.fetch_about(following.sort(), users);
|
||||||
this.following = following;
|
this.following = following;
|
||||||
this.users = users;
|
this.users = users;
|
||||||
|
await tags;
|
||||||
console.log(`load finished ${whoami} => ${this.whoami}`);
|
console.log(`load finished ${whoami} => ${this.whoami}`);
|
||||||
this.whoami = whoami;
|
this.whoami = whoami;
|
||||||
this.loaded = whoami;
|
this.loaded = whoami;
|
||||||
@ -267,35 +298,37 @@ class TfElement extends LitElement {
|
|||||||
let users = this.users;
|
let users = this.users;
|
||||||
if (this.tab === 'news') {
|
if (this.tab === 'news') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
||||||
`;
|
`;
|
||||||
|
} else if (this.tab === 'mentions') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
|
||||||
|
`;
|
||||||
} else if (this.tab === 'search') {
|
} else if (this.tab === 'search') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
||||||
`;
|
`;
|
||||||
|
} else if (this.tab === 'query') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add_fake_news() {
|
|
||||||
this.unread = [{
|
|
||||||
author: this.whoami,
|
|
||||||
placeholder: true,
|
|
||||||
id: '%fake_id',
|
|
||||||
text: 'text',
|
|
||||||
content: 'hello',
|
|
||||||
}, ...this.unread];
|
|
||||||
}
|
|
||||||
|
|
||||||
async set_tab(tab) {
|
async set_tab(tab) {
|
||||||
this.tab = tab;
|
this.tab = tab;
|
||||||
if (tab === 'news') {
|
if (tab === 'news') {
|
||||||
await tfrpc.rpc.setHash('#');
|
await tfrpc.rpc.setHash('#');
|
||||||
} else if (tab === 'connections') {
|
} else if (tab === 'connections') {
|
||||||
await tfrpc.rpc.setHash('#connections');
|
await tfrpc.rpc.setHash('#connections');
|
||||||
|
} else if (tab === 'mentions') {
|
||||||
|
await tfrpc.rpc.setHash('#mentions');
|
||||||
|
} else if (tab === 'query') {
|
||||||
|
await tfrpc.rpc.setHash('#sql=');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,7 +336,6 @@ class TfElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||||
console.log(`starting loading ${this.whoami} ${this.loaded}`);
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.load().finally(function() {
|
this.load().finally(function() {
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
@ -314,7 +346,9 @@ class TfElement extends LitElement {
|
|||||||
<div>
|
<div>
|
||||||
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
|
<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="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
|
||||||
|
<input type="button" class="tab" value="Mentions" ?disabled=${self.tab == 'mentions'} @click=${() => self.set_tab('mentions')}></input>
|
||||||
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
|
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
|
||||||
|
<input type="button" class="tab" value="Query" ?disabled=${self.tab == 'query'} @click=${() => self.set_tab('query')}></input>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
let contents =
|
let contents =
|
||||||
@ -326,10 +360,10 @@ class TfElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
${this.render_id_picker()}
|
${this.render_id_picker()}
|
||||||
${tabs}
|
${tabs}
|
||||||
<!-- <input type="button" value="Fake News" @click=${this.add_fake_news}></input> -->
|
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
|
||||||
${contents}
|
${contents}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-app', TfElement);
|
customElements.define('tf-app', TfElement);
|
||||||
|
@ -13,7 +13,7 @@ class TfComposeElement extends LitElement {
|
|||||||
branch: {type: String},
|
branch: {type: String},
|
||||||
apps: {type: Object},
|
apps: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -56,7 +56,7 @@ class TfComposeElement extends LitElement {
|
|||||||
if (!draft.mentions[link]) {
|
if (!draft.mentions[link]) {
|
||||||
draft.mentions[link] = {
|
draft.mentions[link] = {
|
||||||
link: link,
|
link: link,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
||||||
updated = true;
|
updated = true;
|
||||||
@ -111,7 +111,7 @@ class TfComposeElement extends LitElement {
|
|||||||
let data_url = canvas.toDataURL(mime_type);
|
let data_url = canvas.toDataURL(mime_type);
|
||||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
||||||
resolve(result);
|
resolve(result);
|
||||||
}
|
};
|
||||||
img.onerror = function(event) {
|
img.onerror = function(event) {
|
||||||
reject(new Error('Failed to load image.'));
|
reject(new Error('Failed to load image.'));
|
||||||
};
|
};
|
||||||
@ -176,7 +176,7 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
async submit() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
@ -195,14 +195,25 @@ class TfComposeElement extends LitElement {
|
|||||||
message.contentWarning = draft.content_warning;
|
message.contentWarning = draft.content_warning;
|
||||||
}
|
}
|
||||||
console.log('Would post:', message);
|
console.log('Would post:', message);
|
||||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
if (draft.encrypt_to) {
|
||||||
edit.value = '';
|
let to = new Set(draft.encrypt_to);
|
||||||
self.change();
|
to.add(this.whoami);
|
||||||
self.notify(undefined);
|
to = [...to];
|
||||||
self.requestUpdate();
|
message.recps = to;
|
||||||
}).catch(function(error) {
|
console.log('message is now', message);
|
||||||
|
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
|
||||||
|
console.log('encrypted as', message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||||
|
edit.value = '';
|
||||||
|
self.change();
|
||||||
|
self.notify(undefined);
|
||||||
|
self.requestUpdate();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
@ -226,12 +237,47 @@ class TfComposeElement extends LitElement {
|
|||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async autocomplete(text, callback) {
|
||||||
|
this.last_autocomplete = text;
|
||||||
|
let results = [];
|
||||||
|
try {
|
||||||
|
let rows = await tfrpc.rpc.query(`
|
||||||
|
SELECT messages.content FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE messages.content LIKE ?
|
||||||
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
|
`, ['"' + text.replace('"', '""') + '"', `%%`]);
|
||||||
|
for (let row of rows) {
|
||||||
|
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
||||||
|
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
||||||
|
results.push({key: match[1], value: match[2]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (this.last_autocomplete === text) {
|
||||||
|
callback(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
collection: [
|
||||||
selectTemplate: function(item) {
|
{
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||||
},
|
selectTemplate: function(item) {
|
||||||
|
return `[@${item.original.key}](${item.original.value})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: '&',
|
||||||
|
values: this.autocomplete,
|
||||||
|
selectTemplate: function(item) {
|
||||||
|
return ``;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
tribute.attach(this.renderRoot.getElementById('edit'));
|
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||||
}
|
}
|
||||||
@ -244,6 +290,16 @@ class TfComposeElement extends LitElement {
|
|||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.value);
|
||||||
this.last_updated_text = edit.value;
|
this.last_updated_text = edit.value;
|
||||||
}
|
}
|
||||||
|
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||||
|
if (encrypt) {
|
||||||
|
let tribute = new Tribute({
|
||||||
|
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||||
|
selectTemplate: function(item) {
|
||||||
|
return item.original.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tribute.attach(encrypt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_mention(id) {
|
remove_mention(id) {
|
||||||
@ -256,9 +312,18 @@ class TfComposeElement extends LitElement {
|
|||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div style="display: flex; flex-direction: row">
|
||||||
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
|
<div style="align-self: center; margin: 0.5em">
|
||||||
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
|
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: column">
|
||||||
|
<h3>${mention.name}</h3>
|
||||||
|
<div style="padding-left: 1em">
|
||||||
|
${Object.entries(mention)
|
||||||
|
.filter(x => x[0] != 'name')
|
||||||
|
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,11 +348,11 @@ class TfComposeElement extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let draft = this.get_draft();
|
let draft = self.get_draft();
|
||||||
draft.mentions = Object.assign(draft.mentions || {}, mentions);
|
draft.mentions = Object.assign(draft.mentions || {}, mentions);
|
||||||
this.requestUpdate();
|
self.requestUpdate();
|
||||||
this.notify(draft);
|
self.notify(draft);
|
||||||
this.apps = null;
|
self.apps = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.apps) {
|
if (this.apps) {
|
||||||
@ -304,13 +369,14 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_attach_app_button() {
|
render_attach_app_button() {
|
||||||
|
let self = this;
|
||||||
async function attach_app() {
|
async function attach_app() {
|
||||||
this.apps = await tfrpc.rpc.apps();
|
self.apps = await tfrpc.rpc.apps();
|
||||||
}
|
}
|
||||||
if (!this.apps) {
|
if (!this.apps) {
|
||||||
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
|
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
|
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,6 +410,45 @@ class TfComposeElement extends LitElement {
|
|||||||
return this.drafts[this.branch || ''] || {};
|
return this.drafts[this.branch || ''] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_encrypt(event) {
|
||||||
|
let input = event.srcElement;
|
||||||
|
let matches = input.value.match(/@.*?\.ed25519/g);
|
||||||
|
if (matches) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
let to = [...new Set(matches.concat(draft.encrypt_to))];
|
||||||
|
this.set_encrypt(to);
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_encrypt() {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
if (draft.encrypt_to === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
|
<label for="encrypt_to">🔐 To:</label>
|
||||||
|
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||||
|
<input type="button" value="🚮" @click=${() => this.set_encrypt(undefined)}></input>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
${draft.encrypt_to.map(x => html`
|
||||||
|
<li>
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
<input type="button" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
|
||||||
|
</li>`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_encrypt(encrypt) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
draft.encrypt_to = encrypt;
|
||||||
|
this.notify(draft);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let draft = self.get_draft();
|
let draft = self.get_draft();
|
||||||
@ -351,7 +456,11 @@ class TfComposeElement extends LitElement {
|
|||||||
draft.content_warning !== undefined ?
|
draft.content_warning !== undefined ?
|
||||||
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
|
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
|
||||||
undefined;
|
undefined;
|
||||||
|
let encrypt = draft.encrypt_to !== undefined ?
|
||||||
|
undefined :
|
||||||
|
html`<input type="button" value="🔐" @click=${() => this.set_encrypt([])}></input>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
|
${this.render_encrypt()}
|
||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<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%">${draft.text}</textarea>
|
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
@ -362,13 +471,14 @@ class TfComposeElement extends LitElement {
|
|||||||
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
||||||
${this.render_content_warning()}
|
${this.render_content_warning()}
|
||||||
${this.render_attach_app()}
|
${this.render_attach_app()}
|
||||||
<input type="button" value="Submit" @click=${this.submit}></input>
|
<input type="button" id="submit" value="Submit" @click=${this.submit}></input>
|
||||||
<input type="button" value="Attach" @click=${this.attach}></input>
|
<input type="button" value="Attach" @click=${this.attach}></input>
|
||||||
${this.render_attach_app_button()}
|
${this.render_attach_app_button()}
|
||||||
|
${encrypt}
|
||||||
<input type="button" value="Discard" @click=${this.discard}></input>
|
<input type="button" value="Discard" @click=${this.discard}></input>
|
||||||
`;
|
`;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-compose', TfComposeElement);
|
customElements.define('tf-compose', TfComposeElement);
|
||||||
|
@ -9,12 +9,11 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
return {
|
return {
|
||||||
ids: {type: Array},
|
ids: {type: Array},
|
||||||
selected: {type: String},
|
selected: {type: String},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
|
||||||
this.ids = [];
|
this.ids = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,4 +33,4 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
||||||
|
@ -11,10 +11,10 @@ class TfMessageElement extends LitElement {
|
|||||||
message: {type: Object},
|
message: {type: Object},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
raw: {type: Boolean},
|
format: {type: String},
|
||||||
blog_data: {type: String},
|
blog_data: {type: String},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -26,12 +26,14 @@ class TfMessageElement extends LitElement {
|
|||||||
this.message = {};
|
this.message = {};
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.raw = false;
|
this.format = 'message';
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
show_reply() {
|
show_reply() {
|
||||||
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
|
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
|
||||||
|
encrypt_to: this.message?.decrypted?.recps,
|
||||||
|
}}});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,12 +71,12 @@ class TfMessageElement extends LitElement {
|
|||||||
hash: this.message?.hash,
|
hash: this.message?.hash,
|
||||||
content: this.message?.content,
|
content: this.message?.content,
|
||||||
signature: this.message?.signature,
|
signature: this.message?.signature,
|
||||||
}
|
};
|
||||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
|
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
vote(emoji) {
|
vote(emoji) {
|
||||||
let reaction = emoji.emoji;
|
let reaction = emoji;
|
||||||
let message = this.message.id;
|
let message = this.message.id;
|
||||||
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
||||||
tfrpc.rpc.appendMessage(
|
tfrpc.rpc.appendMessage(
|
||||||
@ -127,6 +129,13 @@ class TfMessageElement extends LitElement {
|
|||||||
body_click(event) {
|
body_click(event) {
|
||||||
if (event.srcElement.tagName == 'IMG') {
|
if (event.srcElement.tagName == 'IMG') {
|
||||||
this.show_image(event.srcElement.src);
|
this.show_image(event.srcElement.src);
|
||||||
|
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
|
||||||
|
let next = event.srcElement.nextSibling;
|
||||||
|
if (next.style.display == 'block') {
|
||||||
|
next.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
next.style.display = 'block';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +157,7 @@ class TfMessageElement extends LitElement {
|
|||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (mention.link?.startsWith('&') &&
|
||||||
mention.name?.startsWith('video:')) {
|
mention.name?.startsWith('video:')) {
|
||||||
return html`
|
return html`
|
||||||
<video controls style="max-height: 240px">
|
<video controls style="max-height: 240px; max-width: 128px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
@ -168,10 +177,7 @@ class TfMessageElement extends LitElement {
|
|||||||
|
|
||||||
render_mentions() {
|
render_mentions() {
|
||||||
let mentions = this.message?.content?.mentions || [];
|
let mentions = this.message?.content?.mentions || [];
|
||||||
mentions = mentions.filter(x =>
|
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
|
||||||
x.name?.startsWith('audio:') ||
|
|
||||||
x.name?.startsWith('video:') ||
|
|
||||||
this.message?.content?.text?.indexOf(x.link) === -1);
|
|
||||||
if (mentions.length) {
|
if (mentions.length) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
@ -214,24 +220,68 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_channels() {
|
||||||
|
let content = this.message?.content;
|
||||||
|
if (this?.messsage?.decrypted?.type == 'post') {
|
||||||
|
content = this.message.decrypted;
|
||||||
|
}
|
||||||
|
let channels = [];
|
||||||
|
if (typeof content.channel === 'string') {
|
||||||
|
channels.push(`#${content.channel}`);
|
||||||
|
}
|
||||||
|
if (Array.isArray(content.mentions)) {
|
||||||
|
for (let mention of content.mentions) {
|
||||||
|
if (typeof mention?.link === 'string' &&
|
||||||
|
mention.link.startsWith('#')) {
|
||||||
|
channels.push(mention.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let content = this.message?.content;
|
let content = this.message?.content;
|
||||||
|
if (this.message?.decrypted?.type == 'post') {
|
||||||
|
content = this.message.decrypted;
|
||||||
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
let raw_button = this.raw ?
|
let raw_button;
|
||||||
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
|
switch (this.format) {
|
||||||
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
|
case 'raw':
|
||||||
|
if (content?.type == 'post' || content?.type == 'blog') {
|
||||||
|
raw_button = html`<input type="button" value="Markdown" @click=${() => self.format = 'md'}></input>`;
|
||||||
|
} else {
|
||||||
|
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
raw_button = html`<input type="button" value="Raw" @click=${() => self.format = 'raw'}></input>`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
function small_frame(inner) {
|
function small_frame(inner) {
|
||||||
|
let body;
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block">
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
|
||||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||||
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
||||||
${raw_button}
|
${raw_button}
|
||||||
${self.raw ? self.render_raw() : inner}
|
${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
${self.render_votes()}
|
${self.render_votes()}
|
||||||
</div>
|
</div>
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
if (this.message.placeholder) {
|
if (this.message?.type === 'contact_group') {
|
||||||
|
return html`
|
||||||
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||||
|
${this.message.messages.map(x =>
|
||||||
|
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
} else if (this.message.placeholder) {
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
||||||
@ -258,7 +308,7 @@ class TfMessageElement extends LitElement {
|
|||||||
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
||||||
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
let update = content.about == this.message.author ?
|
let update = content.about == this.message.author ?
|
||||||
html`<div style="font-weight: bold">Updated profile.</div>` :
|
html`<div style="font-weight: bold">Updated profile.</div>` :
|
||||||
@ -270,8 +320,9 @@ class TfMessageElement extends LitElement {
|
|||||||
${description}
|
${description}
|
||||||
`);
|
`);
|
||||||
} else if (content.type == 'contact') {
|
} else if (content.type == 'contact') {
|
||||||
return small_frame(html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
is
|
is
|
||||||
${
|
${
|
||||||
content.blocking === true ? 'blocking' :
|
content.blocking === true ? 'blocking' :
|
||||||
@ -282,7 +333,7 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`;
|
||||||
} else if (content.type == 'post') {
|
} else if (content.type == 'post') {
|
||||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
||||||
<tf-compose
|
<tf-compose
|
||||||
@ -296,14 +347,24 @@ class TfMessageElement extends LitElement {
|
|||||||
<input type="button" value="Reply" @click=${this.show_reply}></input>
|
<input type="button" value="Reply" @click=${this.show_reply}></input>
|
||||||
`;
|
`;
|
||||||
let self = this;
|
let self = this;
|
||||||
let body = this.raw ?
|
let body;
|
||||||
this.render_raw() :
|
switch (this.format) {
|
||||||
unsafeHTML(tfutils.markdown(content.text));
|
case 'raw':
|
||||||
|
body = this.render_raw();
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</code>`;
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
body = unsafeHTML(tfutils.markdown(content.text));
|
||||||
|
break;
|
||||||
|
}
|
||||||
let content_warning = html`
|
let content_warning = html`
|
||||||
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
||||||
`;
|
`;
|
||||||
let content_html =
|
let content_html =
|
||||||
html`
|
html`
|
||||||
|
${this.render_channels()}
|
||||||
<div @click=${this.body_click}>${body}</div>
|
<div @click=${this.body_click}>${body}</div>
|
||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
`;
|
`;
|
||||||
@ -316,6 +377,8 @@ class TfMessageElement extends LitElement {
|
|||||||
` :
|
` :
|
||||||
content_warning :
|
content_warning :
|
||||||
content_html;
|
content_html;
|
||||||
|
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
||||||
|
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -331,9 +394,10 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<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 style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
@ -347,6 +411,40 @@ class TfMessageElement extends LitElement {
|
|||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
} else if (content.type === 'issue') {
|
||||||
|
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
||||||
|
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
${is_encrypted}
|
||||||
|
<span style="flex: 1"></span>
|
||||||
|
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
|
<span>${raw_button}</span>
|
||||||
|
</div>
|
||||||
|
${content.text}
|
||||||
|
${this.render_votes()}
|
||||||
|
<div>
|
||||||
|
<input type="button" value="React" @click=${this.react}></input>
|
||||||
|
</div>
|
||||||
|
${this.render_children()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else if (content.type === 'blog') {
|
} else if (content.type === 'blog') {
|
||||||
let self = this;
|
let self = this;
|
||||||
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
||||||
@ -356,9 +454,16 @@ class TfMessageElement extends LitElement {
|
|||||||
this.expanded[(this.message.id || '') + ':blog'] ?
|
this.expanded[(this.message.id || '') + ':blog'] ?
|
||||||
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
||||||
undefined;
|
undefined;
|
||||||
let body = this.raw ?
|
let body;
|
||||||
this.render_raw() :
|
switch (this.format) {
|
||||||
html`
|
case 'raw':
|
||||||
|
body = this.render_raw();
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
body = content.summary;
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
body = html`
|
||||||
<div
|
<div
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||||
@click=${x => self.toggle_expanded(':blog')}>
|
@click=${x => self.toggle_expanded(':blog')}>
|
||||||
@ -370,6 +475,8 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload}
|
||||||
`;
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -418,7 +525,11 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
} else if (typeof(this.message.content) == 'string') {
|
} else if (typeof(this.message.content) == 'string') {
|
||||||
return small_frame(html`<span>🔒</span>`);
|
if (this.message?.decrypted) {
|
||||||
|
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.decrypted, null, 2)}</pre>`);
|
||||||
|
} else {
|
||||||
|
return small_frame(html`<span>🔒</span>`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ class TfNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -145,9 +145,29 @@ class TfNewsElement extends LitElement {
|
|||||||
return recursive_sort(roots, true);
|
return recursive_sort(roots, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group_following(messages) {
|
||||||
|
let result = [];
|
||||||
|
let group = [];
|
||||||
|
for (let message of messages) {
|
||||||
|
if (message?.content?.type === 'contact') {
|
||||||
|
group.push(message);
|
||||||
|
} else {
|
||||||
|
if (group.length > 0) {
|
||||||
|
result.push({
|
||||||
|
type: 'contact_group',
|
||||||
|
messages: group,
|
||||||
|
});
|
||||||
|
group = [];
|
||||||
|
}
|
||||||
|
result.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
load_and_render(messages) {
|
load_and_render(messages) {
|
||||||
let messages_by_id = this.process_messages(messages);
|
let messages_by_id = this.process_messages(messages);
|
||||||
let final_messages = this.finalize_messages(messages_by_id);
|
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
|
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
|
||||||
|
@ -11,7 +11,7 @@ class TfProfileElement extends LitElement {
|
|||||||
id: {type: String},
|
id: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
size: {type: Number},
|
size: {type: Number},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -33,7 +33,7 @@ class TfProfileElement extends LitElement {
|
|||||||
contact: this.id,
|
contact: this.id,
|
||||||
}, change)).catch(function(error) {
|
}, change)).catch(function(error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
follow() {
|
follow() {
|
||||||
@ -148,6 +148,10 @@ class TfProfileElement extends LitElement {
|
|||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
|
<input type="checkbox" id="public_web_hosting" value=${this.editing.public_web_hosting} @input=${event => this.editing = Object.assign({}, this.editing, {publicWebHosting: event.srcElement.checked})}></input>
|
||||||
|
</div>
|
||||||
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
||||||
</div>` : null;
|
</div>` : null;
|
||||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
||||||
|
@ -36,4 +36,19 @@ img {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.img_caption {
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.img_caption::after {
|
||||||
|
content: ' ±';
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #fff;
|
||||||
|
margin-left: 0px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
`;
|
`;
|
@ -9,7 +9,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
connections: {type: Array},
|
connections: {type: Array},
|
||||||
stored_connections: {type: Array},
|
stored_connections: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -42,11 +42,8 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
||||||
if (peers.length) {
|
if (peers.length) {
|
||||||
return html`
|
let connections = this.connections.map(x => x.id);
|
||||||
<ul>
|
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`;
|
||||||
${peers.map(x => html`${self.render_room_peer(x)}`)}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +56,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
|
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -71,7 +68,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
${this.render_connection_summary(connection)}
|
${this.render_connection_summary(connection)}
|
||||||
</li>
|
</li>
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async forget_stored_connection(connection) {
|
async forget_stored_connection(connection) {
|
||||||
@ -79,6 +76,18 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
|
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_connection(connection) {
|
||||||
|
return html`
|
||||||
|
<input type="button" @click=${() => tfrpc.rpc.closeConnection(connection.id)} value="Close"></input>
|
||||||
|
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
||||||
|
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
|
||||||
|
<ul>
|
||||||
|
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
|
${this.render_room_peers(connection.id)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
@ -93,12 +102,8 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
</ul>
|
</ul>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections.map(x => html`
|
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
|
||||||
<li>
|
<li>${this.render_connection(x)}</li>
|
||||||
<input type="button" @click=${() => tfrpc.rpc.closeConnection(x)} value="Close"></input>
|
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
|
||||||
${self.render_room_peers(x)}
|
|
||||||
</li>
|
|
||||||
`)}
|
`)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Stored Connections (WIP)</h2>
|
<h2>Stored Connections (WIP)</h2>
|
||||||
|
65
apps/ssb/tf-tab-mentions.js
Normal file
65
apps/ssb/tf-tab-mentions.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabMentionsElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
following: {type: Array},
|
||||||
|
expanded: {type: Object},
|
||||||
|
messages: {type: Array},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.following = [];
|
||||||
|
this.expanded = {};
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
console.log('Loading...', this.whoami);
|
||||||
|
let results = await tfrpc.rpc.query(`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.author != ?
|
||||||
|
ORDER BY timestamp DESC limit 20
|
||||||
|
`,
|
||||||
|
['"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), this.whoami]);
|
||||||
|
console.log('Done.');
|
||||||
|
this.messages = results;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 self = this;
|
||||||
|
if (!this.loading) {
|
||||||
|
this.loading = true;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-tab-mentions', TfTabMentionsElement);
|
190
apps/ssb/tf-tab-news-feed.js
Normal file
190
apps/ssb/tf-tab-news-feed.js
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
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 = {};
|
||||||
|
this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 > ? AND 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),
|
||||||
|
this.start_time,
|
||||||
|
/*
|
||||||
|
** Don't show messages more than a day into the future to prevent
|
||||||
|
** messages with far-future timestamps from staying at the top forever.
|
||||||
|
*/
|
||||||
|
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_more() {
|
||||||
|
let last_start_time = this.start_time;
|
||||||
|
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
||||||
|
let more = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH news AS (SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.timestamp > ?
|
||||||
|
AND 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),
|
||||||
|
this.start_time,
|
||||||
|
last_start_time,
|
||||||
|
]);
|
||||||
|
this.messages = await this.decrypt([...more, ...this.messages]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(messages) {
|
||||||
|
let result = [];
|
||||||
|
for (let message of messages) {
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = JSON.parse(message?.content);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
if (typeof(content) === 'string') {
|
||||||
|
let decrypted;
|
||||||
|
try {
|
||||||
|
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
if (decrypted) {
|
||||||
|
try {
|
||||||
|
message.decrypted = JSON.parse(decrypted);
|
||||||
|
} catch {
|
||||||
|
message.decrypted = decrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(message);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add_messages(messages) {
|
||||||
|
this.messages = await this.decrypt([...messages, ...this.messages]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
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(this.decrypt.bind(this)).then(function(messages) {
|
||||||
|
self.messages = messages;
|
||||||
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(JSON.stringify(error, null, 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let more;
|
||||||
|
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||||
|
more = html`
|
||||||
|
<input type="button" value="Load More" @click=${this.load_more}></input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
${more}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
@ -2,114 +2,6 @@ import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
|||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
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 {
|
class TfTabNewsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
@ -120,7 +12,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -141,12 +33,22 @@ class TfTabNewsElement extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
document.body.addEventListener('keypress', this.on_keypress.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
show_more() {
|
show_more() {
|
||||||
let unread = this.unread;
|
let unread = this.unread;
|
||||||
let news = this.renderRoot?.getElementById('news');
|
let news = this.shadowRoot?.getElementById('news');
|
||||||
if (news) {
|
if (news) {
|
||||||
console.log('injecting messages', news.messages);
|
console.log('injecting messages', news.messages);
|
||||||
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
|
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x]))));
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,6 +95,13 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on_keypress(event) {
|
||||||
|
if (event.target === document.body &&
|
||||||
|
event.key == '.') {
|
||||||
|
this.show_more();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let profile = this.hash.startsWith('#@') ?
|
let profile = this.hash.startsWith('#@') ?
|
||||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
||||||
@ -200,12 +109,11 @@ class TfTabNewsElement extends LitElement {
|
|||||||
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
|
<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>
|
<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>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
||||||
<div><tf-compose whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
||||||
${profile}
|
${profile}
|
||||||
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
|
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
customElements.define('tf-tab-news', TfTabNewsElement);
|
||||||
customElements.define('tf-tab-news', TfTabNewsElement);
|
|
||||||
|
114
apps/ssb/tf-tab-query.js
Normal file
114
apps/ssb/tf-tab-query.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabQueryElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
following: {type: Array},
|
||||||
|
query: {type: String},
|
||||||
|
expanded: {type: Object},
|
||||||
|
results: {type: Array},
|
||||||
|
error: {type: Object},
|
||||||
|
duration: {type: Number},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.following = [];
|
||||||
|
this.expanded = {};
|
||||||
|
this.duration = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
console.log('Searching...', this.whoami, query);
|
||||||
|
this.results = [];
|
||||||
|
this.error = undefined;
|
||||||
|
this.duration = undefined;
|
||||||
|
let search = this.renderRoot.getElementById('search');
|
||||||
|
if (search) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
||||||
|
let start_time = new Date();
|
||||||
|
try {
|
||||||
|
this.results = await tfrpc.rpc.query(query, [])
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
let end_time = new Date();
|
||||||
|
this.duration = (end_time - start_time).valueOf();
|
||||||
|
console.log('Done.');
|
||||||
|
search = this.renderRoot.getElementById('search');
|
||||||
|
if (search) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search_keydown(event) {
|
||||||
|
if (event.keyCode == 13 && event.ctrlKey) {
|
||||||
|
this.query = this.renderRoot.getElementById('search').value;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_expand(event) {
|
||||||
|
if (event.detail.expanded) {
|
||||||
|
let expand = {};
|
||||||
|
expand[event.detail.id] = true;
|
||||||
|
this.expanded = Object.assign({}, this.expanded, expand);
|
||||||
|
} else {
|
||||||
|
delete this.expanded[event.detail.id];
|
||||||
|
this.expanded = Object.assign({}, this.expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_results() {
|
||||||
|
if (!this.results?.length) {
|
||||||
|
return html`<div>No results.</div>`;
|
||||||
|
} else {
|
||||||
|
let keys = Object.keys(this.results[0]).sort();
|
||||||
|
return html`<table style="width: 100%; max-width: 100%">
|
||||||
|
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr>
|
||||||
|
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</tr>`)}
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_error() {
|
||||||
|
if (this.error) {
|
||||||
|
return html`<h2 style="color: red">${this.error.message}</h2>
|
||||||
|
<pre style="color: red">${this.error.stack}</pre>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.query !== this.last_query) {
|
||||||
|
this.last_query = this.query;
|
||||||
|
this.search(this.query);
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<textarea id="search" rows=8 style="flex: 1" @keydown=${this.search_keydown}>${this.query}</textarea>
|
||||||
|
<input type="button" value="Execute" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
|
||||||
|
</div>
|
||||||
|
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
|
||||||
|
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
||||||
|
${this.render_error()}
|
||||||
|
${this.render_results()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-query', TfTabQueryElement);
|
@ -9,7 +9,8 @@ class TfTabSearchElement extends LitElement {
|
|||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
query: {type: String},
|
query: {type: String},
|
||||||
}
|
expanded: {type: Object},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -20,6 +21,7 @@ class TfTabSearchElement extends LitElement {
|
|||||||
this.whoami = null;
|
this.whoami = null;
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.following = [];
|
this.following = [];
|
||||||
|
this.expanded = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
@ -55,8 +57,20 @@ class TfTabSearchElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
render() {
|
||||||
if (this.query !== this.last_query) {
|
if (this.query !== this.last_query) {
|
||||||
|
this.last_query = this.query;
|
||||||
this.search(this.query);
|
this.search(this.query);
|
||||||
}
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
@ -65,7 +79,7 @@ class TfTabSearchElement extends LitElement {
|
|||||||
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||||
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
|
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
|
||||||
</div>
|
</div>
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users}></tf-news>
|
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
apps/ssb/tf-tag.js
Normal file
24
apps/ssb/tf-tag.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTagElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
tag: {type: String},
|
||||||
|
count: {type: Number},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let number = this.count ? html` (${this.count})` : undefined;
|
||||||
|
return html`<a href="#q=${this.tag}" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px">${this.tag}${number}</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tag', TfTagElement);
|
@ -7,7 +7,7 @@ class TfUserElement extends LitElement {
|
|||||||
return {
|
return {
|
||||||
id: {type: String},
|
id: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -19,18 +19,23 @@ class TfUserElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let name = this.users?.[this.id]?.name;
|
||||||
|
name = name !== undefined ?
|
||||||
|
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
|
||||||
|
html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
||||||
|
|
||||||
if (this.users[this.id]) {
|
if (this.users[this.id]) {
|
||||||
let image = this.users[this.id].image;
|
let image = this.users[this.id].image;
|
||||||
image = typeof(image) == 'string' ? image : image?.link;
|
image = typeof(image) == 'string' ? image : image?.link;
|
||||||
return html`
|
return html`
|
||||||
<div style="display: inline-block; font-weight: bold">
|
<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}">
|
<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>
|
${name}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<div style="display: inline-block; font-weight: bold; word-wrap: anywhere">
|
<div style="display: inline-block; font-weight: bold">
|
||||||
<a target="_top" href=${'#' + this.id}>${this.id}</a>
|
${name}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,54 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
import * as linkify from './commonmark-linkify.js';
|
||||||
import * as hashtagify from './commonmark-hashtag.js';
|
import * as hashtagify from './commonmark-hashtag.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) {
|
export function markdown(md) {
|
||||||
var reader = new commonmark.Parser({safe: true});
|
var reader = new commonmark.Parser({safe: true});
|
||||||
var writer = new commonmark.HtmlRenderer();
|
var writer = new commonmark.HtmlRenderer();
|
||||||
|
writer.image = image;
|
||||||
var parsed = reader.parse(md || '');
|
var parsed = reader.parse(md || '');
|
||||||
parsed = linkify.transform(parsed);
|
parsed = linkify.transform(parsed);
|
||||||
parsed = hashtagify.transform(parsed);
|
parsed = hashtagify.transform(parsed);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "☑️"
|
||||||
}
|
}
|
@ -114,13 +114,12 @@ class TodoListElement extends LitElement {
|
|||||||
@change=${event => self.input_change(event, item)}
|
@change=${event => self.input_change(event, item)}
|
||||||
@keydown=${event => self.input_keydown(event, item)}
|
@keydown=${event => self.input_keydown(event, item)}
|
||||||
@blur=${x => self.input_blur(item)}></input>
|
@blur=${x => self.input_blur(item)}></input>
|
||||||
<span @click=${x => self.remove_item(item)}>x</span></div>
|
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
<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.editing = index}>${item.text || '(empty)'}</span>
|
||||||
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -175,7 +174,8 @@ class TodoListElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
||||||
${name}
|
${name}
|
||||||
${(this.items || []).map(x => self.render_item(x))}
|
${(this.items || []).filter(item => !item.x).map(x => self.render_item(x))}
|
||||||
|
${(this.items || []).filter(item => item.x).map(x => self.render_item(x))}
|
||||||
<button @click=${self.add_item}>+ Item</button>
|
<button @click=${self.add_item}>+ Item</button>
|
||||||
<button @click=${self.remove_list}>- List</button>
|
<button @click=${self.remove_list}>- List</button>
|
||||||
</div>
|
</div>
|
||||||
|
14
core/app.js
14
core/app.js
@ -61,8 +61,7 @@ function socket(request, response, client) {
|
|||||||
let process;
|
let process;
|
||||||
let options = {};
|
let options = {};
|
||||||
let credentials = auth.query(request.headers);
|
let credentials = auth.query(request.headers);
|
||||||
let refresh_token = credentials?.refresh?.token;
|
let refresh = auth.make_refresh(credentials);
|
||||||
let refresh_interval = credentials?.refresh?.interval;
|
|
||||||
|
|
||||||
response.onClose = async function() {
|
response.onClose = async function() {
|
||||||
if (process && process.task) {
|
if (process && process.task) {
|
||||||
@ -125,9 +124,14 @@ function socket(request, response, client) {
|
|||||||
options.credentials = credentials;
|
options.credentials = credentials;
|
||||||
options.packageOwner = packageOwner;
|
options.packageOwner = packageOwner;
|
||||||
options.packageName = packageName;
|
options.packageName = packageName;
|
||||||
|
options.url = message.url;
|
||||||
let sessionId = makeSessionId();
|
let sessionId = makeSessionId();
|
||||||
if (blobId) {
|
if (blobId) {
|
||||||
process = await core.getSessionProcessBlob(blobId, sessionId, options);
|
if (message.edit_only) {
|
||||||
|
response.send(JSON.stringify({action: 'ready', edit_only: true}), 0x1);
|
||||||
|
} else {
|
||||||
|
process = await core.getSessionProcessBlob(blobId, sessionId, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (process) {
|
if (process) {
|
||||||
process.app.readOutput(function(message) {
|
process.app.readOutput(function(message) {
|
||||||
@ -198,9 +202,9 @@ function socket(request, response, client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refresh_token) {
|
if (refresh) {
|
||||||
return {
|
return {
|
||||||
'Set-Cookie': `session=${refresh_token}; path=/; Max-Age=${refresh_interval}; Secure; SameSite=Strict`,
|
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
133
core/auth.html
133
core/auth.html
@ -2,18 +2,137 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends Sign-in</title>
|
<title>Tilde Friends Sign-in</title>
|
||||||
<script>
|
|
||||||
function showHideConfirm() {
|
|
||||||
document.getElementById("confirmPassword").style.display = document.getElementById("register").checked ? "block" : "none";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
||||||
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
|
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<!--HEAD-->
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 style="text-align: center">Tilde Friends Sign-in</h1>
|
<h1 style="text-align: center">Tilde Friends Sign-in</h1>
|
||||||
<div id="content"><!--SESSION--></div>
|
<tf-auth id="auth"></tf-auth>
|
||||||
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
|
<script type="module">
|
||||||
|
import {LitElement, html} from '/static/lit/lit-all.min.js';
|
||||||
|
let g_data = $AUTH_DATA;
|
||||||
|
let app = document.getElementById('auth');
|
||||||
|
Object.assign(app, g_data);
|
||||||
|
|
||||||
|
class TfAuthElement extends LitElement {
|
||||||
|
static get_properties() {
|
||||||
|
return {
|
||||||
|
name: {type: String},
|
||||||
|
tab: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.tab = 'login';
|
||||||
|
}
|
||||||
|
|
||||||
|
tab_changed(name) {
|
||||||
|
this.tab = name;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
[name="tab"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
[name="tab"]+label {
|
||||||
|
background-color: #586e75;
|
||||||
|
padding: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
flex: 1 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
[name="tab"]+label:hover {
|
||||||
|
background-color: #dc322f;
|
||||||
|
}
|
||||||
|
[name="tab"]:checked+label {
|
||||||
|
background-color: #93a1a1;
|
||||||
|
border: 2px solid #eee8d5;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #f00;
|
||||||
|
border: 1px solid #f00;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
form label {
|
||||||
|
padding-top: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
form input {
|
||||||
|
font-size: x-large;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
input[type="submit"] {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div style="display: flex; flex-direction: column; max-width: 1280px; margin: auto">
|
||||||
|
<h1 ?hidden=${this.name}>Welcome.</h1>
|
||||||
|
<h1 ?hidden=${this.name === undefined}>Welcome, ${this.name}.</h1>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
|
<input type="radio" name="tab" id="login" value="Login" ?checked=${this.tab == 'login'} @change=${() => self.tab_changed('login')}></input>
|
||||||
|
<label for="login" id="login_label">Login</label>
|
||||||
|
|
||||||
|
<input type="radio" name="tab" id="register" value="Register" ?checked=${this.tab == 'register'} @change=${() => self.tab_changed('register')}></input>
|
||||||
|
<label for="register" id="register_label">Register</label>
|
||||||
|
|
||||||
|
<input type="radio" name="tab" id="guest" value="Guest" ?checked=${this.tab == 'guest'} @change=${() => self.tab_changed('guest')}></input>
|
||||||
|
<label for="guest" id="guest_label">Guest</label>
|
||||||
|
|
||||||
|
<input type="radio" name="tab" id="change" value="Change Password" ?checked=${this.tab == 'change'} @change=${() => self.tab_changed('change')}></input>
|
||||||
|
<label for="change" id="change_label">Change Password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ?hidden=${this.tab != 'login' && this.tab != 'register' && this.tab != 'change'}>
|
||||||
|
<div id="error" ?hidden=${this.error === undefined} class="error">
|
||||||
|
${this.error}
|
||||||
|
</div>
|
||||||
|
<form method="POST" style="display: flex; flex-direction: column">
|
||||||
|
<label for="name">Name:</label>
|
||||||
|
<input type="text" id="name" name="name"></input>
|
||||||
|
|
||||||
|
<label for="password">${this.tab == 'change' ? 'Old ' : ''}Password:</label>
|
||||||
|
<input type="password" id="password" name="password"></input>
|
||||||
|
|
||||||
|
<label ?hidden=${this.tab != 'change'} for="new_password">New Password:</label>
|
||||||
|
<input ?hidden=${this.tab != 'change'} type="password" id="new_password" name="new_password"></input>
|
||||||
|
|
||||||
|
<label ?hidden=${this.tab != 'register' && this.tab != 'change'} for="confirm">Confirm ${this.tab == 'change' ? 'New ' : ''}Password:</label>
|
||||||
|
<input ?hidden=${this.tab != 'register' && this.tab != 'change'} type="password" id="confirm" name="confirm"></input>
|
||||||
|
|
||||||
|
<input id="loginButton" type="submit" name="submit" value="Login"></input>
|
||||||
|
<input type="hidden" name="register" value="${this.tab == 'register' ? 1 : 0}"></input>
|
||||||
|
<input type="hidden" name="change" value="${this.tab == 'change' ? 1 : 0}"></input>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div ?hidden=${this.tab != 'guest'}>
|
||||||
|
<form method="POST" style="display: flex; flex-direction: column">
|
||||||
|
<input type="submit" id="guestButton" name="guestButton" value="Proceed as Guest"></input>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ?hidden=${this.have_administrator} class="notice">
|
||||||
|
There is currently no administrator. You will be made administrator.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Code of Conduct</h2>
|
||||||
|
<textarea readonly rows="20" cols="80" style="resize: none">${this.code_of_conduct}</textarea>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-auth', TfAuthElement);
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
107
core/auth.js
107
core/auth.js
@ -113,22 +113,37 @@ function getCookies(headers) {
|
|||||||
return cookies;
|
return cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNameValid(name) {
|
||||||
|
let c = name.charAt(0);
|
||||||
|
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && name.split().map(x => x >= ('a' && x <= 'z') || x >= ('A' && x <= 'Z') || x >= ('0' && x <= '9'));
|
||||||
|
}
|
||||||
|
|
||||||
function handler(request, response) {
|
function handler(request, response) {
|
||||||
let session = getCookies(request.headers).session;
|
let session = getCookies(request.headers).session;
|
||||||
if (request.uri == "/login") {
|
if (request.uri == "/login") {
|
||||||
|
let formData = form.decodeForm(request.query);
|
||||||
|
if (query(request.headers)?.permissions?.authenticated) {
|
||||||
|
if (formData.return) {
|
||||||
|
response.writeHead(303, {"Location": formData.return});
|
||||||
|
} else {
|
||||||
|
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + '/', "Content-Length": "0"});
|
||||||
|
}
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let sessionIsNew = false;
|
let sessionIsNew = false;
|
||||||
let loginError;
|
let loginError;
|
||||||
|
|
||||||
let formData = form.decodeForm(request.query);
|
|
||||||
|
|
||||||
if (request.method == "POST" || formData.submit) {
|
if (request.method == "POST" || formData.submit) {
|
||||||
sessionIsNew = true;
|
sessionIsNew = true;
|
||||||
formData = form.decodeForm(utf8Decode(request.body), formData);
|
formData = form.decodeForm(utf8Decode(request.body), formData);
|
||||||
if (formData.submit == "Login") {
|
if (formData.submit == "Login") {
|
||||||
let account = gDatabase.get("user:" + formData.name);
|
let account = gDatabase.get("user:" + formData.name);
|
||||||
account = account ? JSON.parse(account) : account;
|
account = account ? JSON.parse(account) : account;
|
||||||
if (formData.register == "1") {
|
if (formData.register == '1') {
|
||||||
if (!account &&
|
if (!account &&
|
||||||
|
isNameValid(formData.name) &&
|
||||||
formData.password == formData.confirm) {
|
formData.password == formData.confirm) {
|
||||||
let users = new Set();
|
let users = new Set();
|
||||||
let users_original = gDatabase.get('users');
|
let users_original = gDatabase.get('users');
|
||||||
@ -145,12 +160,23 @@ function handler(request, response) {
|
|||||||
}
|
}
|
||||||
session = makeJwt({name: formData.name});
|
session = makeJwt({name: formData.name});
|
||||||
account = {password: hashPassword(formData.password)};
|
account = {password: hashPassword(formData.password)};
|
||||||
gDatabase.set("user:" + formData.name, JSON.stringify(account));
|
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
||||||
if (noAdministrator()) {
|
if (noAdministrator()) {
|
||||||
makeAdministrator(formData.name);
|
makeAdministrator(formData.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loginError = "Error registering account.";
|
loginError = 'Error registering account.';
|
||||||
|
}
|
||||||
|
} else if (formData.change == '1') {
|
||||||
|
if (account &&
|
||||||
|
isNameValid(formData.name) &&
|
||||||
|
formData.new_password == formData.confirm &&
|
||||||
|
verifyPassword(formData.password, account.password)) {
|
||||||
|
session = makeJwt({name: formData.name});
|
||||||
|
account = {password: hashPassword(formData.new_password)};
|
||||||
|
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
||||||
|
} else {
|
||||||
|
loginError = 'Error changing password.';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (account &&
|
if (account &&
|
||||||
@ -161,7 +187,7 @@ function handler(request, response) {
|
|||||||
makeAdministrator(formData.name);
|
makeAdministrator(formData.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loginError = "Invalid username or password.";
|
loginError = 'Invalid username or password.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -170,7 +196,7 @@ function handler(request, response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; Secure; SameSite=Strict`;
|
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict`;
|
||||||
let entry = readSession(session);
|
let entry = readSession(session);
|
||||||
if (entry && formData.return) {
|
if (entry && formData.return) {
|
||||||
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
||||||
@ -178,53 +204,23 @@ function handler(request, response) {
|
|||||||
} else {
|
} else {
|
||||||
File.readFile("core/auth.html").then(function(data) {
|
File.readFile("core/auth.html").then(function(data) {
|
||||||
let html = utf8Decode(data);
|
let html = utf8Decode(data);
|
||||||
let contents = "";
|
let auth_data = {
|
||||||
|
session_is_new: sessionIsNew,
|
||||||
if (entry) {
|
name: entry?.name,
|
||||||
if (sessionIsNew) {
|
error: loginError,
|
||||||
contents += '<div>Welcome back, ' + entry.name + '.</div>\n';
|
code_of_conduct: core.globalSettings.code_of_conduct,
|
||||||
} else {
|
have_administrator: !noAdministrator(),
|
||||||
contents += '<div>You are already logged in, ' + entry.name + '.</div>\n';
|
};
|
||||||
}
|
html = utf8Encode(html.replace('$AUTH_DATA', JSON.stringify(auth_data)));
|
||||||
contents += '<div><a href="/login/logout">Logout</a></div>\n';
|
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": html.length});
|
||||||
} else {
|
response.end(html);
|
||||||
contents += '<form method="POST">\n';
|
|
||||||
if (loginError) {
|
|
||||||
contents += "<p>" + loginError + "</p>\n";
|
|
||||||
}
|
|
||||||
contents += '<div id="auth_greeting"><b>Halt. Who goes there?</b></div>\n'
|
|
||||||
contents += '<div id="auth">\n';
|
|
||||||
contents += '<div id="auth_login">\n'
|
|
||||||
if (noAdministrator()) {
|
|
||||||
contents += '<div class="notice">There is currently no administrator. You will be made administrator.</div>\n';
|
|
||||||
}
|
|
||||||
contents += '<div><label for="name">Name:</label> <input type="text" id="name" name="name" value=""></div>\n';
|
|
||||||
contents += '<div><label for="password">Password:</label> <input type="password" id="password" name="password" value=""></div>\n';
|
|
||||||
contents += '<div id="confirmPassword" style="display: none"><label for="confirm">Confirm:</label> <input type="password" id="confirm" name="confirm" value=""></div>\n';
|
|
||||||
contents += '<div><input type="checkbox" id="register" name="register" value="1" onchange="showHideConfirm()"> <label for="register">Register a new account</label></div>\n';
|
|
||||||
contents += '<div><input id="loginButton" type="submit" name="submit" value="Login"></div>\n';
|
|
||||||
contents += '</div>';
|
|
||||||
contents += '<div class="auth_or"> - or - </div>';
|
|
||||||
contents += '<div id="auth_guest">\n';
|
|
||||||
contents += '<input id="guestButton" type="submit" name="submit" value="Proceeed as Guest">\n';
|
|
||||||
contents += '</div>\n';
|
|
||||||
contents += '</div>\n';
|
|
||||||
contents += '<div style="text-align: center">\n';
|
|
||||||
contents += '<h2>Code of Conduct</h2>\n';
|
|
||||||
contents += `<div><textarea readonly rows=20 cols=80>${core.globalSettings.code_of_conduct}</textarea></div>\n`;
|
|
||||||
contents += '</div>\n';
|
|
||||||
contents += '</form>';
|
|
||||||
}
|
|
||||||
let text = html.replace("<!--SESSION-->", contents);
|
|
||||||
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": text.length});
|
|
||||||
response.end(text);
|
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
||||||
response.end("404 File not found");
|
response.end("404 File not found");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (request.uri == "/login/logout") {
|
} else if (request.uri == "/login/logout") {
|
||||||
response.writeHead(303, {"Set-Cookie": "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
|
response.writeHead(303, {"Set-Cookie": `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT`, "Location": "/login" + (request.query ? "?" + request.query : "")});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
||||||
@ -260,12 +256,17 @@ function query(headers) {
|
|||||||
return {
|
return {
|
||||||
session: entry,
|
session: entry,
|
||||||
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
|
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
|
||||||
refresh: {
|
|
||||||
token: makeJwt({name: entry.name}),
|
|
||||||
interval: kRefreshInterval,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { handler, query };
|
function make_refresh(credentials) {
|
||||||
|
if (credentials?.session?.name) {
|
||||||
|
return {
|
||||||
|
token: makeJwt({name: credentials.session.name}),
|
||||||
|
interval: kRefreshInterval,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler, query, make_refresh };
|
||||||
|
704
core/client.js
704
core/client.js
@ -1,14 +1,11 @@
|
|||||||
|
import {LitElement, html, css, svg} from '/static/lit/lit-all.min.js';
|
||||||
|
|
||||||
let gSocket;
|
let gSocket;
|
||||||
let gCredentials;
|
|
||||||
let gPermissions;
|
|
||||||
|
|
||||||
let gCurrentFile;
|
let gCurrentFile;
|
||||||
let gFiles = {};
|
let gFiles = {};
|
||||||
let gApp = {files: {}};
|
let gApp = {files: {}, emoji: '📦'};
|
||||||
let gEditor;
|
let gEditor;
|
||||||
let gSplit;
|
|
||||||
let gGraphs = {};
|
|
||||||
let gTimeSeries = {};
|
|
||||||
let gOriginalInput;
|
let gOriginalInput;
|
||||||
|
|
||||||
let kErrorColor = "#dc322f";
|
let kErrorColor = "#dc322f";
|
||||||
@ -26,6 +23,310 @@ const k_api = {
|
|||||||
setHash: {args: ['hash'], func: api_setHash},
|
setHash: {args: ['hash'], func: api_setHash},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const k_global_style = css`
|
||||||
|
a:link {
|
||||||
|
color: #268bd2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #6c71c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #859900;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: #2aa198;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class TfNavigationElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
credentials: {type: Object},
|
||||||
|
permissions: {type: Object},
|
||||||
|
show_permissions: {type: Boolean},
|
||||||
|
status: {type: Object},
|
||||||
|
spark_lines: {type: Object},
|
||||||
|
version: {type: Object},
|
||||||
|
show_version: {type: Boolean},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.permissions = {};
|
||||||
|
this.show_permissions = false;
|
||||||
|
this.status = {};
|
||||||
|
this.spark_lines = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_edit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (editing()) {
|
||||||
|
closeEditor();
|
||||||
|
} else {
|
||||||
|
edit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_permission(key) {
|
||||||
|
send({action: "resetPermission", permission: key});
|
||||||
|
}
|
||||||
|
|
||||||
|
get_spark_line(key, options) {
|
||||||
|
if (!this.spark_lines[key]) {
|
||||||
|
let spark_line = document.createElement('tf-sparkline');
|
||||||
|
spark_line.style.display = 'flex';
|
||||||
|
spark_line.style.flexDirection = 'row';
|
||||||
|
spark_line.style.flex = '0 50 5em';
|
||||||
|
spark_line.title = key;
|
||||||
|
if (options) {
|
||||||
|
if (options.max) {
|
||||||
|
spark_line.max = options.max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.spark_lines[key] = spark_line;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
return this.spark_lines[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
render_login() {
|
||||||
|
if (this?.credentials?.session?.name) {
|
||||||
|
return html`<a id="login" href="/login/logout?return=${url() + hash()}">logout ${this.credentials.session.name}</a>`;
|
||||||
|
} else {
|
||||||
|
return html`<a id="login" href="/login?return=${url() + hash()}">login</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_permissions() {
|
||||||
|
if (this.show_permissions) {
|
||||||
|
return html`
|
||||||
|
<div style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%">
|
||||||
|
<div style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff">
|
||||||
|
<div>This app has the following permissions:</div>
|
||||||
|
${Object.keys(this.permissions).map(key => html`
|
||||||
|
<div>
|
||||||
|
<span>${key}</span>: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
|
||||||
|
<button @click=${() => this.reset_permission(key)}>Reset</button>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
<button @click=${() => this.show_permissions = false}>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${k_global_style}
|
||||||
|
</style>
|
||||||
|
<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center">
|
||||||
|
<span style="cursor: pointer" @click=${() => this.show_version = !this.show_version}>😎</span>
|
||||||
|
<span ?hidden=${!this.show_version} style="flex: 0 0; white-space: nowrap" title=${this.version?.name + ' ' + Object.entries(this.version || {}).filter(x => ['name', 'number'].indexOf(x[0]) == -1).map(x => `\n* ${x[0]}: ${x[1]}`)}>${this.version?.number}</span>
|
||||||
|
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
|
||||||
|
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
|
||||||
|
<a accesskey="e" data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
|
||||||
|
<a accesskey="p" data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
|
||||||
|
<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
|
||||||
|
<span id="requests"></span>
|
||||||
|
${this.render_permissions()}
|
||||||
|
<span style="flex: 1 1; display: flex; flex-direction: row; white-space: nowrap; margin: 0; padding: 0">${Object.keys(this.spark_lines).sort().map(x => this.spark_lines[x]).map(x => [html`<span style="font-size: xx-small">${x.dataset.emoji}</span>`, x])}</span>
|
||||||
|
<span style="flex: 0 0; white-space: nowrap">${this.render_login()}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-navigation', TfNavigationElement);
|
||||||
|
|
||||||
|
class TfFilesElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
current: {type: String},
|
||||||
|
files: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.files = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
file_click(file) {
|
||||||
|
this.dispatchEvent(new CustomEvent('file_click', {
|
||||||
|
detail: {
|
||||||
|
file: file,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
render_file(file) {
|
||||||
|
let classes = ['file'];
|
||||||
|
if (file == this.current) {
|
||||||
|
classes.push('current');
|
||||||
|
}
|
||||||
|
if (!this.files[file].clean) {
|
||||||
|
classes.push('dirty');
|
||||||
|
}
|
||||||
|
return html`<div class="${classes.join(' ')}" @click=${x => this.file_click(file)}>${file}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
div.file {
|
||||||
|
padding: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
div.file:hover {
|
||||||
|
background-color: #1a9188;
|
||||||
|
}
|
||||||
|
div.file::before {
|
||||||
|
content: '📄 ';
|
||||||
|
}
|
||||||
|
|
||||||
|
div.file.current {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #2aa198;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.file.dirty::after {
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
${Object.keys(this.files).sort().map(x => self.render_file(x))}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-files', TfFilesElement);
|
||||||
|
|
||||||
|
class TfFilesPaneElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
expanded: {type: Boolean},
|
||||||
|
current: {type: String},
|
||||||
|
files: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.expanded = window.localStorage.getItem('files') != '0';
|
||||||
|
this.files = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
set_expanded(expanded) {
|
||||||
|
this.expanded = expanded;
|
||||||
|
window.localStorage.setItem('files', expanded ? '1' : '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
let expander = this.expanded ?
|
||||||
|
html`<span @click=${() => self.set_expanded(false)} class="expander">«</span>` :
|
||||||
|
html`<span @click=${() => self.set_expanded(true)} class="expander">»</span>`;
|
||||||
|
let content = html`
|
||||||
|
<div id="files_content">
|
||||||
|
<tf-files .files=${self.files} current=${self.current} @file_click=${event => openFile(event.detail.file)}></tf-files>
|
||||||
|
<br>
|
||||||
|
<div><button @click=${() => newFile()}>New File</button></div>
|
||||||
|
<div><button @click=${() => removeFile()}>Remove File</button></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.expander {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
right: 0;
|
||||||
|
flex: 0;
|
||||||
|
padding: 0.25em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
${this.expanded ? html`<span style="font-weight: bold; text-align: center; flex: 1">Files</span>` : undefined}
|
||||||
|
${expander}
|
||||||
|
</div>
|
||||||
|
${this.expanded ? content : undefined}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-files-pane', TfFilesPaneElement);
|
||||||
|
|
||||||
|
class TfSparkLineElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
lines: {type: Array},
|
||||||
|
min: {type: Number},
|
||||||
|
max: {type: Number},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.min = 0;
|
||||||
|
this.max = 1.0;
|
||||||
|
this.lines = [];
|
||||||
|
this.k_values_max = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(key, value) {
|
||||||
|
let line = null;
|
||||||
|
for (let it of this.lines) {
|
||||||
|
if (it.name == key) {
|
||||||
|
line = it;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!line) {
|
||||||
|
const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
|
||||||
|
line = {
|
||||||
|
name: key,
|
||||||
|
style: k_colors[this.lines.length % k_colors.length],
|
||||||
|
values: Array(this.k_values_max).fill(0),
|
||||||
|
};
|
||||||
|
this.lines.push(line);
|
||||||
|
}
|
||||||
|
if (line.values.length >= this.k_values_max) {
|
||||||
|
line.values.shift();
|
||||||
|
}
|
||||||
|
line.values.push(value);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_line(line) {
|
||||||
|
if (line?.values?.length >= 2) {
|
||||||
|
let max = Math.max(this.max, ...line.values);
|
||||||
|
let points = [].concat(...line.values.map((x, i) => [50.0 * i / (line.values.length - 1), 10.0 - 10.0 * (x - this.min) / (max - this.min)]));
|
||||||
|
return svg`<polyline points=${points.join(' ')} stroke=${line.style} fill="none"/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let max = Math.round(10.0 * Math.max(...this.lines.map(line => line.values[line.values.length - 1]))) / 10.0;
|
||||||
|
return html`
|
||||||
|
<svg style="max-width: 7.5em; max-height: 1.5em; margin: 0; padding: 0; background: #000" viewBox="0 0 50 10" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
${this.lines.map(x => this.render_line(x))}
|
||||||
|
<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">${max}</text>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-sparkline', TfSparkLineElement);
|
||||||
|
|
||||||
window.addEventListener("keydown", function(event) {
|
window.addEventListener("keydown", function(event) {
|
||||||
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
|
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
|
||||||
if (editing()) {
|
if (editing()) {
|
||||||
@ -81,12 +382,8 @@ function editing() {
|
|||||||
return document.getElementById("editPane").style.display != 'none';
|
return document.getElementById("editPane").style.display != 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEdit() {
|
function is_edit_only() {
|
||||||
if (editing()) {
|
return window.location.search == '?editonly=1' || window.innerWidth < 1024;
|
||||||
closeEditor();
|
|
||||||
} else {
|
|
||||||
edit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
@ -95,11 +392,8 @@ function edit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.localStorage.setItem('editing', '1');
|
window.localStorage.setItem('editing', '1');
|
||||||
if (gSplit) {
|
document.getElementById("editPane").style.display = 'flex';
|
||||||
gSplit.destroy();
|
document.getElementById('viewPane').style.display = is_edit_only() ? 'none' : 'flex';
|
||||||
gSplit = undefined;
|
|
||||||
}
|
|
||||||
gSplit = Split(['#editPane', '#viewPane'], {minSize: 0});
|
|
||||||
|
|
||||||
ensureLoaded([
|
ensureLoaded([
|
||||||
{tagName: "script", attributes: {src: "/codemirror/codemirror.min.js"}},
|
{tagName: "script", attributes: {src: "/codemirror/codemirror.min.js"}},
|
||||||
@ -107,6 +401,7 @@ function edit() {
|
|||||||
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/matchesonscrollbar.min.css"}},
|
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/matchesonscrollbar.min.css"}},
|
||||||
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/dialog.min.css"}},
|
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/dialog.min.css"}},
|
||||||
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/codemirror.min.css"}},
|
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/codemirror.min.css"}},
|
||||||
|
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/lint.css"}},
|
||||||
{tagName: "script", attributes: {src: "/codemirror/trailingspace.min.js"}},
|
{tagName: "script", attributes: {src: "/codemirror/trailingspace.min.js"}},
|
||||||
{tagName: "script", attributes: {src: "/codemirror/dialog.min.js"}},
|
{tagName: "script", attributes: {src: "/codemirror/dialog.min.js"}},
|
||||||
{tagName: "script", attributes: {src: "/codemirror/search.min.js"}},
|
{tagName: "script", attributes: {src: "/codemirror/search.min.js"}},
|
||||||
@ -118,6 +413,9 @@ function edit() {
|
|||||||
{tagName: "script", attributes: {src: "/codemirror/css.min.js"}},
|
{tagName: "script", attributes: {src: "/codemirror/css.min.js"}},
|
||||||
{tagName: "script", attributes: {src: "/codemirror/xml.min.js"}},
|
{tagName: "script", attributes: {src: "/codemirror/xml.min.js"}},
|
||||||
{tagName: "script", attributes: {src: "/codemirror/htmlmixed.min.js"}},
|
{tagName: "script", attributes: {src: "/codemirror/htmlmixed.min.js"}},
|
||||||
|
{tagName: "script", attributes: {src: "/codemirror/lint.js"}},
|
||||||
|
{tagName: "script", attributes: {src: "/codemirror/jshint.js"}},
|
||||||
|
{tagName: "script", attributes: {src: "/codemirror/javascript-lint.min.js"}},
|
||||||
], function() {
|
], function() {
|
||||||
load().catch(function(error) {
|
load().catch(function(error) {
|
||||||
alert(error);
|
alert(error);
|
||||||
@ -126,40 +424,10 @@ function edit() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideFiles() {
|
|
||||||
window.localStorage.setItem('files', '0');
|
|
||||||
document.getElementById('filesPane').classList.add('collapsed');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFiles() {
|
|
||||||
window.localStorage.setItem('files', '1');
|
|
||||||
document.getElementById('filesPane').classList.remove('collapsed');
|
|
||||||
}
|
|
||||||
|
|
||||||
function trace() {
|
function trace() {
|
||||||
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
|
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stats() {
|
|
||||||
window.localStorage.setItem('stats', '1');
|
|
||||||
document.getElementById("statsPane").style.display = 'flex';
|
|
||||||
send({action: 'enableStats', enabled: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeStats() {
|
|
||||||
window.localStorage.setItem('stats', '0');
|
|
||||||
document.getElementById("statsPane").style.display = 'none';
|
|
||||||
send({action: 'enableStats', enabled: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStats() {
|
|
||||||
if (document.getElementById("statsPane").style.display == 'none') {
|
|
||||||
stats();
|
|
||||||
} else {
|
|
||||||
closeStats();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function guessMode(name) {
|
function guessMode(name) {
|
||||||
return name.endsWith(".js") ? "javascript" :
|
return name.endsWith(".js") ? "javascript" :
|
||||||
name.endsWith(".html") ? "htmlmixed" :
|
name.endsWith(".html") ? "htmlmixed" :
|
||||||
@ -175,7 +443,6 @@ function loadFile(name, id) {
|
|||||||
}).then(function(text) {
|
}).then(function(text) {
|
||||||
gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name));
|
gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name));
|
||||||
if (!Object.values(gFiles).some(x => !x.doc)) {
|
if (!Object.values(gFiles).some(x => !x.doc)) {
|
||||||
document.getElementById("editPane").style.display = 'flex';
|
|
||||||
openFile(Object.keys(gFiles).sort()[0]);
|
openFile(Object.keys(gFiles).sort()[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -200,6 +467,13 @@ function load(path) {
|
|||||||
'indentUnit': 4,
|
'indentUnit': 4,
|
||||||
'indentWithTabs': true,
|
'indentWithTabs': true,
|
||||||
'showTrailingSpace': true,
|
'showTrailingSpace': true,
|
||||||
|
'gutters': ['CodeMirror-lint-markers'],
|
||||||
|
'mode': {'js': 'javascript'}[(path || url()).split('.').pop()],
|
||||||
|
'lint': {
|
||||||
|
'options': {
|
||||||
|
'esversion': 2021,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
gEditor.on('changes', function() {
|
gEditor.on('changes', function() {
|
||||||
updateFiles();
|
updateFiles();
|
||||||
@ -219,6 +493,8 @@ function load(path) {
|
|||||||
document.getElementById("editPane").style.display = 'flex';
|
document.getElementById("editPane").style.display = 'flex';
|
||||||
}
|
}
|
||||||
gApp = json;
|
gApp = json;
|
||||||
|
gApp.emoji = gApp.emoji || '📦';
|
||||||
|
document.getElementById('icon').value = gApp.emoji;
|
||||||
}
|
}
|
||||||
if (!isApp) {
|
if (!isApp) {
|
||||||
document.getElementById("editPane").style.display = 'flex';
|
document.getElementById("editPane").style.display = 'flex';
|
||||||
@ -236,10 +512,7 @@ function load(path) {
|
|||||||
function closeEditor() {
|
function closeEditor() {
|
||||||
window.localStorage.setItem('editing', '0');
|
window.localStorage.setItem('editing', '0');
|
||||||
document.getElementById("editPane").style.display = 'none';
|
document.getElementById("editPane").style.display = 'none';
|
||||||
if (gSplit) {
|
document.getElementById('viewPane').style.display = 'flex';
|
||||||
gSplit.destroy();
|
|
||||||
gSplit = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function explodePath() {
|
function explodePath() {
|
||||||
@ -293,6 +566,7 @@ function save(save_to) {
|
|||||||
let app = {
|
let app = {
|
||||||
type: "tildefriends-app",
|
type: "tildefriends-app",
|
||||||
files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])),
|
files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])),
|
||||||
|
emoji: gApp.emoji || '📦',
|
||||||
};
|
};
|
||||||
Object.values(gFiles).forEach(function(file) { delete file.id; });
|
Object.values(gFiles).forEach(function(file) { delete file.id; });
|
||||||
gApp = JSON.parse(JSON.stringify(app));
|
gApp = JSON.parse(JSON.stringify(app));
|
||||||
@ -325,6 +599,14 @@ function save(save_to) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeIcon() {
|
||||||
|
let value = prompt('Enter a new app icon emoji:');
|
||||||
|
if (value !== undefined) {
|
||||||
|
gApp.emoji = value || '📦';
|
||||||
|
document.getElementById('icon').value = gApp.emoji;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deleteApp() {
|
function deleteApp() {
|
||||||
let name = document.getElementById("name");
|
let name = document.getElementById("name");
|
||||||
let path = name && name.value ? name.value : url();
|
let path = name && name.value ? name.value : url();
|
||||||
@ -390,7 +672,8 @@ function api_localStorageGet(key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function api_requestPermission(permission, id) {
|
function api_requestPermission(permission, id) {
|
||||||
let permissions = document.getElementById('permissions');
|
let outer = document.createElement('div');
|
||||||
|
outer.classList.add('permissions');
|
||||||
|
|
||||||
let container = document.createElement('div');
|
let container = document.createElement('div');
|
||||||
container.classList.add('permissions_contents');
|
container.classList.add('permissions_contents');
|
||||||
@ -417,11 +700,13 @@ function api_requestPermission(permission, id) {
|
|||||||
|
|
||||||
const k_options = [
|
const k_options = [
|
||||||
{
|
{
|
||||||
|
id: 'allow',
|
||||||
text: '✅ Allow',
|
text: '✅ Allow',
|
||||||
grant: ['allow once', 'allow'],
|
grant: ['allow once', 'allow'],
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'deny',
|
||||||
text: '❌ Deny',
|
text: '❌ Deny',
|
||||||
grant: ['deny once', 'deny'],
|
grant: ['deny once', 'deny'],
|
||||||
},
|
},
|
||||||
@ -432,19 +717,17 @@ function api_requestPermission(permission, id) {
|
|||||||
for (let option of k_options) {
|
for (let option of k_options) {
|
||||||
let button = document.createElement('button');
|
let button = document.createElement('button');
|
||||||
button.innerText = option.text;
|
button.innerText = option.text;
|
||||||
|
button.id = option.id;
|
||||||
button.onclick = function() {
|
button.onclick = function() {
|
||||||
resolve(option.grant[check.checked ? 1 : 0]);
|
resolve(option.grant[check.checked ? 1 : 0]);
|
||||||
while (permissions.firstChild) {
|
document.body.removeChild(outer);
|
||||||
permissions.removeChild(permissions.firstChild);
|
|
||||||
}
|
|
||||||
permissions.style.visibility = 'hidden';
|
|
||||||
}
|
}
|
||||||
div.appendChild(button);
|
div.appendChild(button);
|
||||||
}
|
}
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
|
outer.appendChild(container);
|
||||||
|
|
||||||
permissions.appendChild(container);
|
document.body.appendChild(outer);
|
||||||
permissions.style.visibility = 'visible';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,85 +739,20 @@ function api_setHash(hash) {
|
|||||||
window.location.hash = hash;
|
window.location.hash = hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hidePermissions() {
|
|
||||||
let permissions = document.getElementById('permissions_settings');
|
|
||||||
while (permissions.firstChild) {
|
|
||||||
permissions.removeChild(permissions.firstChild);
|
|
||||||
}
|
|
||||||
permissions.style.visibility = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPermissions() {
|
|
||||||
let permissions = document.getElementById('permissions_settings');
|
|
||||||
|
|
||||||
let container = document.createElement('div');
|
|
||||||
container.classList.add('permissions_contents');
|
|
||||||
|
|
||||||
let div = document.createElement('div');
|
|
||||||
div.appendChild(document.createTextNode('This app has the following permission:'));
|
|
||||||
for (let key of Object.keys(gPermissions || {})) {
|
|
||||||
let row = document.createElement('div');
|
|
||||||
|
|
||||||
let span = document.createElement('span');
|
|
||||||
span.appendChild(document.createTextNode(key));
|
|
||||||
row.appendChild(span);
|
|
||||||
|
|
||||||
span = document.createElement('span');
|
|
||||||
span.appendChild(document.createTextNode(': '));
|
|
||||||
row.appendChild(span);
|
|
||||||
|
|
||||||
span = document.createElement('span');
|
|
||||||
span.appendChild(document.createTextNode(gPermissions[key] ? '✅ Allowed' : '❌ Denied'));
|
|
||||||
row.appendChild(span);
|
|
||||||
|
|
||||||
span = document.createElement('span');
|
|
||||||
span.appendChild(document.createTextNode(' '));
|
|
||||||
row.appendChild(span);
|
|
||||||
|
|
||||||
let button = document.createElement('button');
|
|
||||||
button.innerText = 'Reset';
|
|
||||||
button.onclick = function() {
|
|
||||||
send({action: "resetPermission", permission: key});
|
|
||||||
};
|
|
||||||
row.appendChild(button);
|
|
||||||
div.appendChild(row);
|
|
||||||
}
|
|
||||||
container.appendChild(div);
|
|
||||||
|
|
||||||
div = document.createElement('div');
|
|
||||||
let button = document.createElement('button');
|
|
||||||
button.innerText = 'Close';
|
|
||||||
button.onclick = function() {
|
|
||||||
hidePermissions();
|
|
||||||
}
|
|
||||||
div.appendChild(button);
|
|
||||||
container.appendChild(div);
|
|
||||||
|
|
||||||
permissions.appendChild(container);
|
|
||||||
permissions.style.visibility = 'visible';
|
|
||||||
}
|
|
||||||
|
|
||||||
function _receive_websocket_message(message) {
|
function _receive_websocket_message(message) {
|
||||||
if (message && message.action == "session") {
|
if (message && message.action == "session") {
|
||||||
setStatusMessage("🟢 Executing...", kStatusColor);
|
setStatusMessage("🟢 Executing...", kStatusColor);
|
||||||
gCredentials = message.credentials;
|
document.getElementsByTagName('tf-navigation')[0].credentials = message.credentials;
|
||||||
updateLogin();
|
|
||||||
} else if (message && message.action == 'permissions') {
|
} else if (message && message.action == 'permissions') {
|
||||||
gPermissions = message.permissions;
|
document.getElementsByTagName('tf-navigation')[0].permissions = message.permissions ?? {};
|
||||||
let permissions = document.getElementById('permissions_settings');
|
|
||||||
if (permissions.firstChild) {
|
|
||||||
hidePermissions();
|
|
||||||
showPermissions();
|
|
||||||
}
|
|
||||||
} else if (message && message.action == "ready") {
|
} else if (message && message.action == "ready") {
|
||||||
setStatusMessage(null);
|
setStatusMessage(null);
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
send({event: "hashChange", hash: window.location.hash});
|
send({event: "hashChange", hash: window.location.hash});
|
||||||
}
|
}
|
||||||
if (window.localStorage.getItem('stats') == '1') {
|
document.getElementsByTagName('tf-navigation')[0].version = message.version;
|
||||||
/* Stats were opened before we connected. */
|
document.getElementById('viewPane').style.display = message.edit_only ? 'none' : 'flex';
|
||||||
send({action: 'enableStats', enabled: true});
|
send({action: 'enableStats', enabled: true});
|
||||||
}
|
|
||||||
} else if (message && message.action == "ping") {
|
} else if (message && message.action == "ping") {
|
||||||
send({action: "pong"});
|
send({action: "pong"});
|
||||||
} else if (message && message.action == "stats") {
|
} else if (message && message.action == "stats") {
|
||||||
@ -555,8 +773,8 @@ function _receive_websocket_message(message) {
|
|||||||
tls_malloc_percent: {group: 'memory', name: 'tls'},
|
tls_malloc_percent: {group: 'memory', name: 'tls'},
|
||||||
uv_malloc_percent: {group: 'memory', name: 'uv'},
|
uv_malloc_percent: {group: 'memory', name: 'uv'},
|
||||||
|
|
||||||
messages_stored: {group: 'stored', name: 'messages'},
|
messages_stored: {group: 'store', name: 'messages'},
|
||||||
blobs_stored: {group: 'stored', name: 'blobs'},
|
blobs_stored: {group: 'store', name: 'blobs'},
|
||||||
|
|
||||||
socket_count: {group: 'socket', name: 'total'},
|
socket_count: {group: 'socket', name: 'total'},
|
||||||
socket_open_count: {group: 'socket', name: 'open'},
|
socket_open_count: {group: 'socket', name: 'open'},
|
||||||
@ -566,56 +784,16 @@ function _receive_websocket_message(message) {
|
|||||||
};
|
};
|
||||||
const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
|
const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
|
||||||
let graph_key = k_groups[key]?.group || key;
|
let graph_key = k_groups[key]?.group || key;
|
||||||
let graph = gGraphs[graph_key];
|
if (['cpu', 'rpc', 'store', 'memory'].indexOf(graph_key) != -1) {
|
||||||
if (!graph) {
|
let line = document.getElementsByTagName('tf-navigation')[0].get_spark_line(graph_key, { max: 100 });
|
||||||
graph = {
|
line.dataset.emoji = {
|
||||||
chart: new SmoothieChart({
|
'cpu': '💻',
|
||||||
millisPerPixel: 100,
|
'rpc': '🔁',
|
||||||
minValue: 0,
|
'store': '💾',
|
||||||
grid: {
|
'memory': '🐏',
|
||||||
millisPerLine: 1000,
|
}[graph_key];
|
||||||
verticalSections: 10,
|
line.append(key, message.stats[key]);
|
||||||
},
|
|
||||||
tooltip: true,
|
|
||||||
}),
|
|
||||||
canvas: document.createElement('canvas'),
|
|
||||||
title: document.createElement('div'),
|
|
||||||
series: [],
|
|
||||||
};
|
|
||||||
gGraphs[graph_key] = graph;
|
|
||||||
graph.canvas.width = 240;
|
|
||||||
graph.canvas.height = 64;
|
|
||||||
graph.title.innerText = graph_key;
|
|
||||||
graph.title.style.flex = '0';
|
|
||||||
document.getElementById('graphs').appendChild(graph.title);
|
|
||||||
document.getElementById('graphs').appendChild(graph.canvas);
|
|
||||||
graph.chart.streamTo(graph.canvas, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeseries = gTimeSeries[key];
|
|
||||||
if (!timeseries) {
|
|
||||||
let is_multi = key != graph_key || graph.series.length > 1;
|
|
||||||
timeseries = new TimeSeries();
|
|
||||||
gTimeSeries[key] = timeseries;
|
|
||||||
graph.chart.addTimeSeries(timeseries, {lineWidth: 2, strokeStyle: is_multi ? k_colors[graph.series.length] : '#fff'});
|
|
||||||
graph.series.push(k_groups[key]?.name || key);
|
|
||||||
if (is_multi) {
|
|
||||||
while (graph.title.firstChild) {
|
|
||||||
graph.title.removeChild(graph.title.firstChild);
|
|
||||||
}
|
|
||||||
function makeColoredText(text, color) {
|
|
||||||
let element = document.createElement('span');
|
|
||||||
element.style.color = color;
|
|
||||||
element.innerText = text;
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
graph.title.appendChild(makeColoredText(graph_key + ':', '#fff'));
|
|
||||||
for (let series of graph.series) {
|
|
||||||
graph.title.appendChild(makeColoredText(' ' + series, k_colors[graph.series.indexOf(series)]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
timeseries.append(now, message.stats[key]);
|
|
||||||
}
|
}
|
||||||
} else if (message &&
|
} else if (message &&
|
||||||
message.message === 'tfrpc' &&
|
message.message === 'tfrpc' &&
|
||||||
@ -641,27 +819,8 @@ function _receive_websocket_message(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyEvent(event) {
|
|
||||||
send({
|
|
||||||
event: "key",
|
|
||||||
type: event.type,
|
|
||||||
which: event.which,
|
|
||||||
keyCode: event.keyCode,
|
|
||||||
charCode: event.charCode,
|
|
||||||
character: String.fromCharCode(event.keyCode || event.which),
|
|
||||||
altKey: event.altKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatusMessage(message, color) {
|
function setStatusMessage(message, color) {
|
||||||
let node = document.getElementById("status");
|
document.getElementsByTagName('tf-navigation')[0].status = {message: message, color: color};
|
||||||
while (node.firstChild) {
|
|
||||||
node.removeChild(node.firstChild);
|
|
||||||
}
|
|
||||||
if (message) {
|
|
||||||
node.appendChild(document.createTextNode(message));
|
|
||||||
node.setAttribute("style", "display: inline-block; vertical-align: top; white-space: pre; color: " + (color || kErrorColor));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(value) {
|
function send(value) {
|
||||||
@ -674,39 +833,6 @@ function send(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLogin() {
|
|
||||||
let login = document.getElementById("login");
|
|
||||||
while (login.firstChild) {
|
|
||||||
login.removeChild(login.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = document.createElement("a");
|
|
||||||
if (gCredentials && gCredentials.session) {
|
|
||||||
a.appendChild(document.createTextNode("logout " + gCredentials.session.name));
|
|
||||||
a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url() + hash()));
|
|
||||||
} else {
|
|
||||||
a.appendChild(document.createTextNode("login"));
|
|
||||||
a.setAttribute("href", "/login?return=" + encodeURIComponent(url() + hash()));
|
|
||||||
}
|
|
||||||
login.appendChild(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragHover(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
let input = document.getElementById("input");
|
|
||||||
if (event.type == "dragover") {
|
|
||||||
if (!input.classList.contains("drop")) {
|
|
||||||
input.classList.add("drop");
|
|
||||||
gOriginalInput = input.value;
|
|
||||||
input.value = "drop file to upload";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
input.classList.remove("drop");
|
|
||||||
input.value = gOriginalInput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixImage(sourceData, maxWidth, maxHeight, callback) {
|
function fixImage(sourceData, maxWidth, maxHeight, callback) {
|
||||||
let result = sourceData;
|
let result = sourceData;
|
||||||
let image = new Image();
|
let image = new Image();
|
||||||
@ -736,52 +862,6 @@ function sendImage(image) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileDropRead(event) {
|
|
||||||
sendImage(event.target.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileDrop(event) {
|
|
||||||
dragHover(event);
|
|
||||||
|
|
||||||
let done = false;
|
|
||||||
if (!done) {
|
|
||||||
let files = event.target.files || event.dataTransfer.files;
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let file = files[i];
|
|
||||||
if (file.type.substring(0, "image/".length) == "image/") {
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.onloadend = fileDropRead;
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
let html = event.dataTransfer.getData("text/html");
|
|
||||||
let match = /<img.*src="([^"]+)"/.exec(html);
|
|
||||||
if (match) {
|
|
||||||
sendImage(match[1]);
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
let text = event.dataTransfer.getData("text/plain");
|
|
||||||
if (text) {
|
|
||||||
send(text);
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableDragDrop() {
|
|
||||||
let body = document.body;
|
|
||||||
body.addEventListener("dragover", dragHover);
|
|
||||||
body.addEventListener("dragleave", dragHover);
|
|
||||||
body.addEventListener("drop", fileDrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashChange() {
|
function hashChange() {
|
||||||
send({event: 'hashChange', hash: window.location.hash});
|
send({event: 'hashChange', hash: window.location.hash});
|
||||||
}
|
}
|
||||||
@ -831,10 +911,12 @@ function message(event) {
|
|||||||
function reconnect(path) {
|
function reconnect(path) {
|
||||||
let oldSocket = gSocket;
|
let oldSocket = gSocket;
|
||||||
gSocket = null
|
gSocket = null
|
||||||
oldSocket.onopen = null;
|
if (oldSocket) {
|
||||||
oldSocket.onclose = null;
|
oldSocket.onopen = null;
|
||||||
oldSocket.onmessage = null;
|
oldSocket.onclose = null;
|
||||||
oldSocket.close();
|
oldSocket.onmessage = null;
|
||||||
|
oldSocket.close();
|
||||||
|
}
|
||||||
connectSocket(path);
|
connectSocket(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -858,6 +940,8 @@ function connectSocket(path) {
|
|||||||
gSocket.send(JSON.stringify({
|
gSocket.send(JSON.stringify({
|
||||||
action: "hello",
|
action: "hello",
|
||||||
path: connect_path,
|
path: connect_path,
|
||||||
|
url: window.location.href,
|
||||||
|
edit_only: editing() && is_edit_only(),
|
||||||
api: Object.entries(k_api).map(([key, value]) => [].concat([key], value.args)),
|
api: Object.entries(k_api).map(([key, value]) => [].concat([key], value.args)),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -898,27 +982,13 @@ function openFile(name) {
|
|||||||
gEditor.focus();
|
gEditor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFileClicked(event) {
|
|
||||||
openFile(event.target.textContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFiles() {
|
function updateFiles() {
|
||||||
let node = document.getElementById("files");
|
let files = document.getElementsByTagName("tf-files-pane")[0];
|
||||||
while (node.firstChild) {
|
if (files) {
|
||||||
node.removeChild(node.firstChild);
|
files.files = Object.fromEntries(Object.keys(gFiles).map(file => [file, {
|
||||||
}
|
clean: gFiles[file].doc.isClean(gFiles[file].generation),
|
||||||
|
}]));
|
||||||
for (let file of Object.keys(gFiles).sort()) {
|
files.current = gCurrentFile;
|
||||||
let li = document.createElement("li");
|
|
||||||
li.onclick = onFileClicked;
|
|
||||||
li.appendChild(document.createTextNode(file));
|
|
||||||
if (file == gCurrentFile) {
|
|
||||||
li.classList.add("current");
|
|
||||||
}
|
|
||||||
if (!gFiles[file].doc.isClean(gFiles[file].generation)) {
|
|
||||||
li.classList.add("dirty");
|
|
||||||
}
|
|
||||||
node.appendChild(li);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gEditor.focus();
|
gEditor.focus();
|
||||||
@ -953,27 +1023,14 @@ window.addEventListener("load", function() {
|
|||||||
window.addEventListener("message", message, false);
|
window.addEventListener("message", message, false);
|
||||||
window.addEventListener("online", connectSocket);
|
window.addEventListener("online", connectSocket);
|
||||||
document.getElementById("name").value = window.location.pathname;
|
document.getElementById("name").value = window.location.pathname;
|
||||||
document.getElementById('edit_link').addEventListener('click', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
toggleEdit();
|
|
||||||
});
|
|
||||||
document.getElementById('show_permissions_link').addEventListener('click', () => showPermissions());
|
|
||||||
document.getElementById('files_hide').addEventListener('click', () => hideFiles());
|
|
||||||
document.getElementById('files_show').addEventListener('click', () => showFiles());
|
|
||||||
document.getElementById('closeStats').addEventListener('click', () => closeStats());
|
|
||||||
document.getElementById('closeEditor').addEventListener('click', () => closeEditor());
|
document.getElementById('closeEditor').addEventListener('click', () => closeEditor());
|
||||||
document.getElementById('save').addEventListener('click', () => save());
|
document.getElementById('save').addEventListener('click', () => save());
|
||||||
|
document.getElementById('icon').addEventListener('click', () => changeIcon());
|
||||||
document.getElementById('delete').addEventListener('click', () => deleteApp());
|
document.getElementById('delete').addEventListener('click', () => deleteApp());
|
||||||
document.getElementById('trace_button').addEventListener('click', function(event) {
|
document.getElementById('trace_button').addEventListener('click', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
trace();
|
trace();
|
||||||
});
|
});
|
||||||
document.getElementById('stats_button').addEventListener('click', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
toggleStats();
|
|
||||||
});
|
|
||||||
document.getElementById('new_file_button').addEventListener('click', () => newFile());
|
|
||||||
document.getElementById('remove_file_button').addEventListener('click', () => removeFile());
|
|
||||||
for (let tag of document.getElementsByTagName('a')) {
|
for (let tag of document.getElementsByTagName('a')) {
|
||||||
if (tag.accessKey) {
|
if (tag.accessKey) {
|
||||||
tag.classList.add('tooltip_parent');
|
tag.classList.add('tooltip_parent');
|
||||||
@ -998,7 +1055,6 @@ window.addEventListener("load", function() {
|
|||||||
tag.appendChild(tooltip);
|
tag.appendChild(tooltip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enableDragDrop();
|
|
||||||
connectSocket(window.location.pathname);
|
connectSocket(window.location.pathname);
|
||||||
|
|
||||||
if (window.localStorage.getItem('editing') == '1') {
|
if (window.localStorage.getItem('editing') == '1') {
|
||||||
@ -1006,14 +1062,4 @@ window.addEventListener("load", function() {
|
|||||||
} else {
|
} else {
|
||||||
closeEditor();
|
closeEditor();
|
||||||
}
|
}
|
||||||
if (window.localStorage.getItem('files') == '1') {
|
|
||||||
showFiles();
|
|
||||||
} else {
|
|
||||||
hideFiles();
|
|
||||||
}
|
|
||||||
if (window.localStorage.getItem('stats') == '1') {
|
|
||||||
stats();
|
|
||||||
} else {
|
|
||||||
closeStats();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
390
core/core.js
390
core/core.js
@ -1,11 +1,46 @@
|
|||||||
import * as auth from './auth.js';
|
|
||||||
import * as app from './app.js';
|
import * as app from './app.js';
|
||||||
|
import * as auth from './auth.js';
|
||||||
|
import * as form from './form.js';
|
||||||
|
import * as http from './http.js';
|
||||||
import * as httpd from './httpd.js';
|
import * as httpd from './httpd.js';
|
||||||
|
|
||||||
let gProcessIndex = 0;
|
let gProcessIndex = 0;
|
||||||
let gProcesses = {};
|
let gProcesses = {};
|
||||||
let gStatsTimer = false;
|
let gStatsTimer = false;
|
||||||
|
|
||||||
|
const k_mime_types = {
|
||||||
|
'css': 'text/css',
|
||||||
|
'html': 'text/html',
|
||||||
|
'js': 'text/javascript',
|
||||||
|
'json': 'text/json',
|
||||||
|
'map': 'application/json',
|
||||||
|
'svg': 'image/svg+xml',
|
||||||
|
};
|
||||||
|
|
||||||
|
const k_magic_bytes = [
|
||||||
|
{bytes: [0xff, 0xd8, 0xff, 0xdb], type: 'image/jpeg'},
|
||||||
|
{bytes: [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01], type: 'image/jpeg'},
|
||||||
|
{bytes: [0xff, 0xd8, 0xff, 0xee], type: 'image/jpeg'},
|
||||||
|
{bytes: [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00], type: 'image/jpeg'},
|
||||||
|
{bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], type: 'image/png'},
|
||||||
|
{bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], type: 'image/gif'},
|
||||||
|
{bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], type: 'image/gif'},
|
||||||
|
{bytes: [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50], type: 'image/webp'},
|
||||||
|
{bytes: [0x3c, 0x73, 0x76, 0x67], type: 'image/svg+xml'},
|
||||||
|
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'audio/mpeg'},
|
||||||
|
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d], type: 'video/mp4'},
|
||||||
|
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'video/mp4'},
|
||||||
|
];
|
||||||
|
|
||||||
|
let k_static_files = [
|
||||||
|
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
||||||
|
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
|
||||||
|
{uri: '/favicon.png', type: 'image/png'},
|
||||||
|
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
|
||||||
|
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
|
||||||
|
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
|
||||||
|
];
|
||||||
|
|
||||||
const k_global_settings = {
|
const k_global_settings = {
|
||||||
index: {
|
index: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -32,6 +67,21 @@ const k_global_settings = {
|
|||||||
default_value: undefined,
|
default_value: undefined,
|
||||||
description: 'If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "https://example.com")',
|
description: 'If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "https://example.com")',
|
||||||
},
|
},
|
||||||
|
fetch_hosts: {
|
||||||
|
type: 'string',
|
||||||
|
default_value: undefined,
|
||||||
|
description: 'Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.',
|
||||||
|
},
|
||||||
|
blob_fetch_age_seconds: {
|
||||||
|
type: 'integer',
|
||||||
|
default_value: (platform() == 'android' ? 0.5 * 365 * 24 * 60 * 60 : undefined),
|
||||||
|
description: 'Only blobs mentioned more recently than this age will be automatically fetched.',
|
||||||
|
},
|
||||||
|
blob_expire_age_seconds: {
|
||||||
|
type: 'integer',
|
||||||
|
default_value: (platform() == 'android' ? 1.0 * 365 * 24 * 60 * 60 : undefined),
|
||||||
|
description: 'Blobs older than this will be automatically deleted.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let gGlobalSettings = {
|
let gGlobalSettings = {
|
||||||
@ -45,8 +95,8 @@ function printError(out, error) {
|
|||||||
out.print(error.fileName + ":" + error.lineNumber + ": " + error.message);
|
out.print(error.fileName + ":" + error.lineNumber + ": " + error.message);
|
||||||
out.print(error.stackTrace);
|
out.print(error.stackTrace);
|
||||||
} else {
|
} else {
|
||||||
for (let i in error) {
|
for (let [k, v] of Object.entries(error)) {
|
||||||
out.print(i);
|
out.print(k, v);
|
||||||
}
|
}
|
||||||
out.print(error.toString());
|
out.print(error.toString());
|
||||||
}
|
}
|
||||||
@ -158,7 +208,9 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
process.credentials = options.credentials || {};
|
process.credentials = options.credentials || {};
|
||||||
process.task = new Task();
|
process.task = new Task();
|
||||||
process.eventHandlers = {};
|
process.eventHandlers = {};
|
||||||
process.app = new app.App();
|
if (!options?.script || options?.script === 'app.js') {
|
||||||
|
process.app = new app.App();
|
||||||
|
}
|
||||||
process.lastActive = Date.now();
|
process.lastActive = Date.now();
|
||||||
process.lastPing = null;
|
process.lastPing = null;
|
||||||
process.timeout = options.timeout;
|
process.timeout = options.timeout;
|
||||||
@ -242,7 +294,7 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
throw Error(`Permission denied: ${permission}.`);
|
throw Error(`Permission denied: ${permission}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else if (process.app) {
|
||||||
return process.app.makeFunction(['requestPermission'])(permission).then(function(value) {
|
return process.app.makeFunction(['requestPermission'])(permission).then(function(value) {
|
||||||
if (value == 'allow') {
|
if (value == 'allow') {
|
||||||
storePermission(user, options.packageOwner, options.packageName, permission, true);
|
storePermission(user, options.packageOwner, options.packageName, permission, true);
|
||||||
@ -259,8 +311,11 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
}
|
}
|
||||||
throw Error(`Permission denied: ${permission}.`);
|
throw Error(`Permission denied: ${permission}.`);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
throw Error(`Permission denied: ${permission}.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
url: options?.url,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (process.credentials?.permissions?.administration) {
|
if (process.credentials?.permissions?.administration) {
|
||||||
@ -283,7 +338,7 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
print('Done.');
|
print('Done.');
|
||||||
};
|
};
|
||||||
imports.core.deleteUser = function(user) {
|
imports.core.deleteUser = function(user) {
|
||||||
return imports.core.permissionTest('delete_user').then(function() {
|
return Promise.resolve(imports.core.permissionTest('delete_user')).then(function() {
|
||||||
let db = new Database('auth');
|
let db = new Database('auth');
|
||||||
|
|
||||||
db.remove('user:' + user);
|
db.remove('user:' + user);
|
||||||
@ -309,14 +364,23 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
imports.app[api[0]] = process.app.makeFunction(api);
|
imports.app[api[0]] = process.app.makeFunction(api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let [name, f] of Object.entries(options?.imports || {})) {
|
||||||
|
imports[name] = f;
|
||||||
|
}
|
||||||
process.task.onPrint = function(args) {
|
process.task.onPrint = function(args) {
|
||||||
imports.app.print(...args);
|
if (imports.app) {
|
||||||
|
imports.app.print(...args);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
process.task.onError = function(error) {
|
process.task.onError = function(error) {
|
||||||
try {
|
try {
|
||||||
process.app.makeFunction(['error'])(error);
|
if (process.app) {
|
||||||
} catch(e) {
|
process.app.makeFunction(['error'])(error);
|
||||||
print(e);
|
} else {
|
||||||
|
printError({print: print}, error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
printError({print: print}, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)]));
|
imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)]));
|
||||||
@ -338,7 +402,7 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
if (process.credentials &&
|
if (process.credentials &&
|
||||||
process.credentials.session &&
|
process.credentials.session &&
|
||||||
process.credentials.session.name) {
|
process.credentials.session.name) {
|
||||||
return imports.core.permissionTest('ssb_append').then(function() {
|
return Promise.resolve(imports.core.permissionTest('ssb_append')).then(function() {
|
||||||
return ssb.appendMessageWithIdentity(process.credentials.session.name, id, message);
|
return ssb.appendMessageWithIdentity(process.credentials.session.name, id, message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -357,6 +421,9 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
return ssb.privateMessageDecrypt(process.credentials.session.name, id, message);
|
return ssb.privateMessageDecrypt(process.credentials.session.name, id, message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
imports.fetch = function(url, options) {
|
||||||
|
return http.fetch(url, options, gGlobalSettings.fetch_hosts);
|
||||||
|
}
|
||||||
|
|
||||||
if (process.credentials &&
|
if (process.credentials &&
|
||||||
process.credentials.session &&
|
process.credentials.session &&
|
||||||
@ -395,7 +462,7 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
try {
|
try {
|
||||||
let appObject = JSON.parse(appSource);
|
let appObject = JSON.parse(appSource);
|
||||||
if (appObject.type == "tildefriends-app") {
|
if (appObject.type == "tildefriends-app") {
|
||||||
appSourceName = 'app.js';
|
appSourceName = options?.script ?? 'app.js';
|
||||||
let id = appObject.files[appSourceName];
|
let id = appObject.files[appSourceName];
|
||||||
let blob = await getBlobOrContent(id);
|
let blob = await getBlobOrContent(id);
|
||||||
appSource = utf8Decode(blob);
|
appSource = utf8Decode(blob);
|
||||||
@ -408,19 +475,23 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
printError({print: print}, e);
|
printError({print: print}, e);
|
||||||
}
|
}
|
||||||
broadcastEvent('onSessionBegin', [getUser(process, process)]);
|
broadcastEvent('onSessionBegin', [getUser(process, process)]);
|
||||||
resolveReady(process);
|
|
||||||
if (process.app) {
|
if (process.app) {
|
||||||
process.app.send({action: "ready"});
|
process.app.send({action: "ready", version: version()});
|
||||||
process.sendPermissions();
|
process.sendPermissions();
|
||||||
}
|
}
|
||||||
await process.task.execute({name: appSourceName, source: appSource});
|
await process.task.execute({name: appSourceName, source: appSource});
|
||||||
|
resolveReady(process);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (process?.task?.onError) {
|
if (process.app) {
|
||||||
process.task.onError(error);
|
if (process?.task?.onError) {
|
||||||
|
process.task.onError(error);
|
||||||
|
} else {
|
||||||
|
printError({print: print}, error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
printError({print: print}, error);
|
printError({print: print}, error);
|
||||||
}
|
}
|
||||||
rejectReady();
|
rejectReady(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return process;
|
return process;
|
||||||
@ -435,20 +506,11 @@ function setGlobalSettings(settings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let kStaticFiles = [
|
|
||||||
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
|
||||||
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
|
|
||||||
{uri: '/favicon.png', type: 'image/png'},
|
|
||||||
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
|
|
||||||
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
|
|
||||||
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
|
|
||||||
];
|
|
||||||
|
|
||||||
function startsWithBytes(data, bytes) {
|
function startsWithBytes(data, bytes) {
|
||||||
if (data.byteLength >= bytes.length) {
|
if (data.byteLength >= bytes.length) {
|
||||||
let dataBytes = new Uint8Array(data.slice(0, bytes.length));
|
let dataBytes = new Uint8Array(data.slice(0, bytes.length));
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
if (dataBytes[i] != bytes[i] && bytes[i] !== null) {
|
if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -457,16 +519,16 @@ function startsWithBytes(data, bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function staticFileHandler(request, response, blobId, uri) {
|
async function staticFileHandler(request, response, blobId, uri) {
|
||||||
for (let i in kStaticFiles) {
|
for (let i in k_static_files) {
|
||||||
if (uri === kStaticFiles[i].uri) {
|
if (uri === k_static_files[i].uri) {
|
||||||
let path = kStaticFiles[i].path || uri.substring(1);
|
let path = k_static_files[i].path || uri.substring(1);
|
||||||
let type = kStaticFiles[i].type || guessType(path);
|
let type = k_static_files[i].type || guessTypeFromName(path);
|
||||||
|
|
||||||
let stat = await File.stat('core/' + path);
|
let stat = await File.stat('core/' + path);
|
||||||
let id = `${stat.mtime}_${stat.size}`;
|
let id = `${stat.mtime}_${stat.size}`;
|
||||||
|
|
||||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||||
response.writeHead(304, {});
|
response.writeHead(304, {'Content-Length': '0'});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
let data = await File.readFile('core/' + path);
|
let data = await File.readFile('core/' + path);
|
||||||
@ -476,7 +538,7 @@ async function staticFileHandler(request, response, blobId, uri) {
|
|||||||
'Content-Length': data.byteLength,
|
'Content-Length': data.byteLength,
|
||||||
'etag': '"' + id + '"',
|
'etag': '"' + id + '"',
|
||||||
},
|
},
|
||||||
kStaticFiles[i].headers || {}));
|
k_static_files[i].headers || {}));
|
||||||
response.end(data);
|
response.end(data);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -487,14 +549,6 @@ async function staticFileHandler(request, response, blobId, uri) {
|
|||||||
response.end("File not found");
|
response.end("File not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const k_mime_types = {
|
|
||||||
'json': 'text/json',
|
|
||||||
'js': 'text/javascript',
|
|
||||||
'html': 'text/html',
|
|
||||||
'css': 'text/css',
|
|
||||||
'map': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
async function staticDirectoryHandler(request, response, directory, uri) {
|
async function staticDirectoryHandler(request, response, directory, uri) {
|
||||||
let filename = uri || 'index.html';
|
let filename = uri || 'index.html';
|
||||||
if (filename.indexOf('..') != -1) {
|
if (filename.indexOf('..') != -1) {
|
||||||
@ -508,7 +562,7 @@ async function staticDirectoryHandler(request, response, directory, uri) {
|
|||||||
let id = `${stat.mtime}_${stat.size}`;
|
let id = `${stat.mtime}_${stat.size}`;
|
||||||
|
|
||||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||||
response.writeHead(304, {});
|
response.writeHead(304, {'Content-Length': '0'});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
let data = await File.readFile(directory + filename);
|
let data = await File.readFile(directory + filename);
|
||||||
@ -536,40 +590,25 @@ async function wellKnownHandler(request, response, path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendData(response, data, type, headers) {
|
function guessTypeFromName(path) {
|
||||||
if (data) {
|
let extension = path.split('.').pop();
|
||||||
if (startsWithBytes(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
return k_mime_types[extension];
|
||||||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
}
|
||||||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
|
||||||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
function guessTypeFromMagicBytes(data) {
|
||||||
response.writeHead(200, Object.assign({"Content-Type": "image/jpeg", "Content-Length": data.byteLength}, headers || {}));
|
for (let magic of k_magic_bytes) {
|
||||||
response.end(data);
|
if (startsWithBytes(data, magic.bytes)) {
|
||||||
} else if (startsWithBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
return magic.type;
|
||||||
response.writeHead(200, Object.assign({"Content-Type": "image/png", "Content-Length": data.byteLength}, headers || {}));
|
|
||||||
response.end(data);
|
|
||||||
} else if (startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
|
||||||
startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
|
||||||
response.writeHead(200, Object.assign({"Content-Type": "image/gif", "Content-Length": data.byteLength}, headers || {}));
|
|
||||||
response.end(data);
|
|
||||||
} else if (startsWithBytes(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
|
||||||
response.writeHead(200, Object.assign({"Content-Type": "image/webp", "Content-Length": data.byteLength}, headers || {}));
|
|
||||||
response.end(data);
|
|
||||||
} else if (startsWithBytes(data, [0x3c, 0x73, 0x76, 0x67])) {
|
|
||||||
response.writeHead(200, Object.assign({"Content-Type": "image/svg+xml", "Content-Length": data.byteLength}, headers || {}));
|
|
||||||
response.end(data);
|
|
||||||
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
|
||||||
response.writeHead(200, Object.assign({"Content-Type": "audio/mpeg", "Content-Length": data.byteLength}, headers || {}));
|
|
||||||
response.end(data);
|
|
||||||
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
|
||||||
startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
|
||||||
response.writeHead(200, Object.assign({"Content-Type": "video/mp4", "Content-Length": data.byteLength}, headers || {}));
|
|
||||||
response.end(data);
|
|
||||||
} else {
|
|
||||||
response.writeHead(200, Object.assign({"Content-Type": type || "application/binary", "Content-Length": data.byteLength}, headers || {}));
|
|
||||||
response.end(data);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendData(response, data, type, headers, status_code) {
|
||||||
|
if (data) {
|
||||||
|
response.writeHead(status_code ?? 200, Object.assign({"Content-Type": type || guessTypeFromMagicBytes(data) || "application/binary", "Content-Length": data.byteLength}, headers || {}));
|
||||||
|
response.end(data);
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {}));
|
response.writeHead(status_code ?? 404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {}));
|
||||||
response.end("File not found");
|
response.end("File not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -584,35 +623,58 @@ async function getBlobOrContent(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function guessType(path) {
|
let g_handler_index = 0;
|
||||||
const k_extension_to_type = {
|
async function useAppHandler(response, handler_blob_id, path, query, headers, packageOwner, packageName) {
|
||||||
'css': 'text/css',
|
print('useAppHandler', packageOwner, packageName);
|
||||||
'html': 'text/html',
|
let do_resolve;
|
||||||
'js': 'text/javascript',
|
let promise = new Promise(async function(resolve, reject) {
|
||||||
'svg': 'image/svg+xml',
|
do_resolve = resolve;
|
||||||
};
|
});
|
||||||
let extension = path.split('.').pop();
|
let process;
|
||||||
return k_extension_to_type[extension];
|
let result;
|
||||||
|
try {
|
||||||
|
process = await getProcessBlob(handler_blob_id, 'handler_' + g_handler_index++, {
|
||||||
|
script: 'handler.js',
|
||||||
|
imports: {
|
||||||
|
request: {
|
||||||
|
path: path,
|
||||||
|
query: query,
|
||||||
|
},
|
||||||
|
respond: do_resolve,
|
||||||
|
},
|
||||||
|
credentials: auth.query(headers),
|
||||||
|
packageOwner: packageOwner,
|
||||||
|
packageName: packageName,
|
||||||
|
});
|
||||||
|
await process.ready;
|
||||||
|
|
||||||
|
result = await promise;
|
||||||
|
} finally {
|
||||||
|
if (process?.task) {
|
||||||
|
await process.task.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function blobHandler(request, response, blobId, uri) {
|
async function blobHandler(request, response, blobId, uri) {
|
||||||
for (let i in kStaticFiles) {
|
for (let i in k_static_files) {
|
||||||
if (uri === kStaticFiles[i].uri && kStaticFiles[i].path) {
|
if (uri === k_static_files[i].uri && k_static_files[i].path) {
|
||||||
let stat = await File.stat('core/' + kStaticFiles[i].path);
|
let stat = await File.stat('core/' + k_static_files[i].path);
|
||||||
let id = `${stat.mtime}_${stat.size}`;
|
let id = `${stat.mtime}_${stat.size}`;
|
||||||
|
|
||||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||||
response.writeHead(304, {});
|
response.writeHead(304, {'Content-Length': '0'});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
let data = await File.readFile('core/' + kStaticFiles[i].path);
|
let data = await File.readFile('core/' + k_static_files[i].path);
|
||||||
response.writeHead(200, Object.assign(
|
response.writeHead(200, Object.assign(
|
||||||
{
|
{
|
||||||
'Content-Type': kStaticFiles[i].type,
|
'Content-Type': k_static_files[i].type,
|
||||||
'Content-Length': data.byteLength,
|
'Content-Length': data.byteLength,
|
||||||
'etag': '"' + id + '"',
|
'etag': '"' + id + '"',
|
||||||
},
|
},
|
||||||
kStaticFiles[i].headers || {}));
|
k_static_files[i].headers || {}));
|
||||||
response.end(data);
|
response.end(data);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -629,11 +691,19 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
if (uri == "/view") {
|
if (uri == "/view") {
|
||||||
let data;
|
let data;
|
||||||
let match;
|
let match;
|
||||||
|
let query = form.decodeForm(request.query);
|
||||||
|
let headers = {
|
||||||
|
'Content-Security-Policy': 'sandbox',
|
||||||
|
};
|
||||||
|
if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) {
|
||||||
|
headers['Content-Disposition'] = `attachment; filename=${query.filename}`;
|
||||||
|
}
|
||||||
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
|
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
|
||||||
let id = await new Database(match[1]).get('path:' + match[2]);
|
let id = await new Database(match[1]).get('path:' + match[2]);
|
||||||
if (id) {
|
if (id) {
|
||||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||||
response.writeHead(304, {});
|
headers['Content-Length'] = '0';
|
||||||
|
response.writeHead(304, headers);
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
data = await getBlobOrContent(id);
|
data = await getBlobOrContent(id);
|
||||||
@ -641,23 +711,25 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
let appObject = JSON.parse(data);
|
let appObject = JSON.parse(data);
|
||||||
data = appObject.files[match[3]];
|
data = appObject.files[match[3]];
|
||||||
}
|
}
|
||||||
sendData(response, data, undefined, {etag: '"' + id + '"'});
|
sendData(response, data, undefined, Object.assign({etag: '"' + id + '"'}, headers));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (request.headers['if-none-match'] === '"' + blobId + '"') {
|
if (request.headers['if-none-match'] === '"' + blobId + '"') {
|
||||||
response.writeHead(304, {});
|
headers['Content-Length'] = '0';
|
||||||
|
response.writeHead(304, headers);
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
sendData(response, data, undefined, {etag: '"' + blobId + '"'});
|
sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (request.headers['if-none-match'] === '"' + blobId + '"') {
|
if (request.headers['if-none-match'] === '"' + blobId + '"') {
|
||||||
response.writeHead(304, {});
|
headers['Content-Length'] = '0';
|
||||||
|
response.writeHead(304, headers);
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
data = await getBlobOrContent(blobId);
|
data = await getBlobOrContent(blobId);
|
||||||
sendData(response, data, undefined, {etag: '"' + blobId + '"'});
|
sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (uri == "/save") {
|
} else if (uri == "/save") {
|
||||||
@ -732,43 +804,58 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
response.end('OK');
|
response.end('OK');
|
||||||
} else {
|
} else {
|
||||||
let data;
|
let data;
|
||||||
let type;
|
|
||||||
let headers;
|
|
||||||
let match;
|
let match;
|
||||||
|
let id;
|
||||||
|
let app_id = blobId;
|
||||||
|
let packageOwner;
|
||||||
|
let packageName;
|
||||||
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
|
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
|
||||||
|
packageOwner = match[1];
|
||||||
|
packageName = match[2];
|
||||||
let db = new Database(match[1]);
|
let db = new Database(match[1]);
|
||||||
let id = await db.get('path:' + match[2]);
|
app_id = await db.get('path:' + match[2]);
|
||||||
if (id) {
|
}
|
||||||
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
|
|
||||||
headers = {
|
let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id)));
|
||||||
'Access-Control-Allow-Origin': '*',
|
id = app_object.files[uri.substring(1)];
|
||||||
};
|
if (!id && app_object.files['handler.js']) {
|
||||||
response.writeHead(304, headers);
|
let answer;
|
||||||
response.end();
|
try {
|
||||||
} else {
|
answer = await useAppHandler(response, app_id, uri.substring(1), request.query ? form.decodeForm(request.query) : undefined, request.headers, packageOwner, packageName);
|
||||||
data = utf8Decode(await getBlobOrContent(id));
|
} catch (error) {
|
||||||
let appObject = JSON.parse(data);
|
data = utf8Encode(`Internal Server Error\n\n${error?.message}\n${error?.stack}`);
|
||||||
data = appObject.files[uri.substring(1)];
|
response.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': data.length});
|
||||||
data = await getBlobOrContent(data);
|
response.end(data);
|
||||||
type = guessType(uri);
|
return;
|
||||||
headers = {
|
}
|
||||||
'ETag': '"' + id + '"',
|
if (answer && typeof answer.data == 'string') {
|
||||||
'Access-Control-Allow-Origin': '*',
|
answer.data = utf8Encode(answer.data);
|
||||||
};
|
}
|
||||||
sendData(response, data, type, headers);
|
sendData(response, answer?.data, answer?.content_type, Object.assign(answer?.headers ?? {}, {
|
||||||
}
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Security-Policy': 'sandbox',
|
||||||
|
}), answer.status_code);
|
||||||
|
} else if (id) {
|
||||||
|
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
|
||||||
|
let headers = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Security-Policy': 'sandbox',
|
||||||
|
'Content-Length': '0',
|
||||||
|
};
|
||||||
|
response.writeHead(304, headers);
|
||||||
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
|
let headers = {
|
||||||
|
'ETag': '"' + id + '"',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Security-Policy': 'sandbox',
|
||||||
|
};
|
||||||
|
data = await getBlobOrContent(id);
|
||||||
|
let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
|
||||||
sendData(response, data, type, headers);
|
sendData(response, data, type, headers);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data = utf8Decode(await getBlobOrContent(blobId));
|
sendData(response, data, undefined, {});
|
||||||
let appObject = JSON.parse(data);
|
|
||||||
data = appObject.files[uri.substring(1)];
|
|
||||||
data = await getBlobOrContent(data);
|
|
||||||
headers = {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
};
|
|
||||||
sendData(response, data, type, headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -791,6 +878,11 @@ async function loadSettings() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
print("Settings not found in database:", error);
|
print("Settings not found in database:", error);
|
||||||
}
|
}
|
||||||
|
for (let [key, value] of Object.entries(k_global_settings)) {
|
||||||
|
if (data[key] === undefined) {
|
||||||
|
data[key] = value.default_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
gGlobalSettings = data;
|
gGlobalSettings = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,6 +907,16 @@ function enableStats(process, enabled) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringResponse(response, data) {
|
||||||
|
let bytes = utf8Encode(data);
|
||||||
|
response.writeHead(200, {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Content-Length": bytes.byteLength.toString(),
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
});
|
||||||
|
return response.end(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
loadSettings().then(function() {
|
loadSettings().then(function() {
|
||||||
httpd.all("/login", auth.handler);
|
httpd.all("/login", auth.handler);
|
||||||
httpd.all("", function(request, response) {
|
httpd.all("", function(request, response) {
|
||||||
@ -826,40 +928,28 @@ loadSettings().then(function() {
|
|||||||
return blobHandler(request, response, match[1], match[2]);
|
return blobHandler(request, response, match[1], match[2]);
|
||||||
} else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) {
|
} else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) {
|
||||||
return blobHandler(request, response, match[1], match[2]);
|
return blobHandler(request, response, match[1], match[2]);
|
||||||
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
|
} else if (match = /^\/static\/lit\/([\.\w-/]*)$/.exec(request.uri)) {
|
||||||
return staticFileHandler(request, response, null, match[1]);
|
return staticDirectoryHandler(request, response, 'deps/lit/', match[1]);
|
||||||
} else if (match = /^\/codemirror\/([\.\w-/]*)$/.exec(request.uri)) {
|
} else if (match = /^\/codemirror\/([\.\w-/]*)$/.exec(request.uri)) {
|
||||||
return staticDirectoryHandler(request, response, 'deps/codemirror/', match[1]);
|
return staticDirectoryHandler(request, response, 'deps/codemirror/', match[1]);
|
||||||
} else if (match = /^\/speedscope\/([\.\w-/]*)$/.exec(request.uri)) {
|
} else if (match = /^\/speedscope\/([\.\w-/]*)$/.exec(request.uri)) {
|
||||||
return staticDirectoryHandler(request, response, 'deps/speedscope/', match[1]);
|
return staticDirectoryHandler(request, response, 'deps/speedscope/', match[1]);
|
||||||
} else if (match = /^\/split\/([\.\w-/]*)$/.exec(request.uri)) {
|
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
|
||||||
return staticDirectoryHandler(request, response, 'deps/split/', match[1]);
|
return staticFileHandler(request, response, null, match[1]);
|
||||||
} else if (match = /^\/smoothie\/([\.\w-/]*)$/.exec(request.uri)) {
|
} else if (request.uri == "/robots.txt") {
|
||||||
return staticDirectoryHandler(request, response, 'deps/smoothie/', match[1]);
|
return staticFileHandler(request, response, null, request.uri);
|
||||||
} else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) {
|
} else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) {
|
||||||
return blobHandler(request, response, match[1], match[2]);
|
return blobHandler(request, response, match[1], match[2]);
|
||||||
} else if (match = /^\/trace$/.exec(request.uri)) {
|
} else if (match = /^\/trace$/.exec(request.uri)) {
|
||||||
let data = trace();
|
return stringResponse(response, trace());
|
||||||
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
|
|
||||||
return response.end(data);
|
|
||||||
} else if (match = /^\/disconnections$/.exec(request.uri)) {
|
} else if (match = /^\/disconnections$/.exec(request.uri)) {
|
||||||
let data = utf8Encode(JSON.stringify(disconnectionsDebug(), null, 2));
|
return stringResponse(response, JSON.stringify(disconnectionsDebug(), null, 2));
|
||||||
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.byteLength.toString()});
|
|
||||||
return response.end(data);
|
|
||||||
} else if (match = /^\/debug$/.exec(request.uri)) {
|
} else if (match = /^\/debug$/.exec(request.uri)) {
|
||||||
let data = JSON.stringify(getDebug(), null, 2);
|
return stringResponse(response, JSON.stringify(getDebug(), null, 2));
|
||||||
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
|
} else if (match = /^\/hitches$/.exec(request.uri)) {
|
||||||
return response.end(data);
|
return stringResponse(response, JSON.stringify(getHitches(), null, 2));
|
||||||
} else if (match = /^\/mem$/.exec(request.uri)) {
|
} else if (match = /^\/mem$/.exec(request.uri)) {
|
||||||
let data = JSON.stringify(getAllocations(), null, 2);
|
return stringResponse(response, JSON.stringify(getAllocations(), null, 2));
|
||||||
response.writeHead(200, {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
"Content-Length": data.length.toString(),
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
});
|
|
||||||
return response.end(data);
|
|
||||||
} else if (request.uri == "/robots.txt") {
|
|
||||||
return blobHandler(request, response, null, request.uri);
|
|
||||||
} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
|
} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
|
||||||
return wellKnownHandler(request, response, match[1]);
|
return wellKnownHandler(request, response, match[1]);
|
||||||
} else {
|
} else {
|
||||||
|
86
core/http.js
Normal file
86
core/http.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
function parseUrl(url) {
|
||||||
|
// XXX: Hack.
|
||||||
|
let match = url.match(new RegExp("(\\w+)://([^/:]+)(?::(\\d+))?(.*)"));
|
||||||
|
return {
|
||||||
|
protocol: match[1],
|
||||||
|
host: match[2],
|
||||||
|
path: match[4],
|
||||||
|
port: match[3] ? parseInt(match[3]) : match[1] == "http" ? 80 : 443,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResponse(data) {
|
||||||
|
let firstLine;
|
||||||
|
let headers = {};
|
||||||
|
while (true) {
|
||||||
|
let endLine = data.indexOf('\r\n');
|
||||||
|
let line = data.substring(0, endLine);
|
||||||
|
data = data.substring(endLine + 2);
|
||||||
|
if (!line.length) {
|
||||||
|
break;
|
||||||
|
} else if (!firstLine) {
|
||||||
|
firstLine = line;
|
||||||
|
} else {
|
||||||
|
let colon = line.indexOf(":");
|
||||||
|
headers[line.substring(colon)] = line.substring(colon + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {body: data};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetch(url, options, allowed_hosts) {
|
||||||
|
let parsed = parseUrl(url);
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
|
||||||
|
throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
|
||||||
|
}
|
||||||
|
let socket = new Socket();
|
||||||
|
let buffer = new Uint8Array(0);
|
||||||
|
|
||||||
|
return socket.connect(parsed.host, parsed.port).then(function() {
|
||||||
|
socket.read(function(data) {
|
||||||
|
if (data && data.length) {
|
||||||
|
let newBuffer = new Uint8Array(buffer.length + data.length);
|
||||||
|
newBuffer.set(buffer, 0);
|
||||||
|
newBuffer.set(data, buffer.length);
|
||||||
|
buffer = newBuffer;
|
||||||
|
} else {
|
||||||
|
let result = parseHttpResponse(buffer);
|
||||||
|
if (!result) {
|
||||||
|
reject(new Exception('Parse failed.'));
|
||||||
|
}
|
||||||
|
if (typeof result == 'number') {
|
||||||
|
if (result == -2) {
|
||||||
|
reject('Incomplete request.');
|
||||||
|
} else {
|
||||||
|
reject('Bad request.');
|
||||||
|
}
|
||||||
|
} else if (typeof result == 'object') {
|
||||||
|
resolve({
|
||||||
|
body: buffer.slice(result.bytes_parsed),
|
||||||
|
status: result.status,
|
||||||
|
message: result.message,
|
||||||
|
headers: result.headers,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Exception('Unexpected parse result.'));
|
||||||
|
}
|
||||||
|
resolve(parseResponse(utf8Decode(buffer)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsed.port == 443) {
|
||||||
|
return socket.startTls();
|
||||||
|
}
|
||||||
|
}).then(function() {
|
||||||
|
let body = typeof options?.body == 'string' ? utf8Encode(options.body) : (options.body || new Uint8Array(0));
|
||||||
|
let headers = utf8Encode(`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`);
|
||||||
|
let fullRequest = new Uint8Array(headers.length + body.length);
|
||||||
|
fullRequest.set(headers, 0);
|
||||||
|
fullRequest.set(body, headers.length);
|
||||||
|
socket.write(fullRequest);
|
||||||
|
}).catch(function(error) {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -4,7 +4,7 @@ let gHandlers = [];
|
|||||||
let gSocketHandlers = [];
|
let gSocketHandlers = [];
|
||||||
let gBadRequests = {};
|
let gBadRequests = {};
|
||||||
|
|
||||||
const kRequestTimeout = 15000;
|
const kRequestTimeout = 5000;
|
||||||
const kStallTimeout = 60000;
|
const kStallTimeout = 60000;
|
||||||
|
|
||||||
function logError(error) {
|
function logError(error) {
|
||||||
@ -395,41 +395,10 @@ function handleConnection(client) {
|
|||||||
let parsing_header = true;
|
let parsing_header = true;
|
||||||
let bodyToRead = -1;
|
let bodyToRead = -1;
|
||||||
let body;
|
let body;
|
||||||
let requestCount = -1;
|
|
||||||
let readCount = 0;
|
let readCount = 0;
|
||||||
let isWebsocket = false;
|
let isWebsocket = false;
|
||||||
|
|
||||||
function resetTimeout(requestIndex) {
|
client.setActivityTimeout(kRequestTimeout);
|
||||||
if (isWebsocket) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (bodyToRead == -1) {
|
|
||||||
setTimeout(function() {
|
|
||||||
if (requestCount == requestIndex) {
|
|
||||||
client.info = 'timed out';
|
|
||||||
if (requestCount == 0) {
|
|
||||||
badRequest(client, 'Timed out waiting for request.');
|
|
||||||
} else {
|
|
||||||
client.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, kRequestTimeout);
|
|
||||||
} else {
|
|
||||||
let lastReadCount = readCount;
|
|
||||||
setTimeout(function() {
|
|
||||||
if (readCount == lastReadCount) {
|
|
||||||
client.info = 'stalled';
|
|
||||||
if (requestCount == 0) {
|
|
||||||
badRequest(client, 'Request stalled.');
|
|
||||||
} else {
|
|
||||||
client.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, kStallTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTimeout(++requestCount);
|
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
request = undefined;
|
request = undefined;
|
||||||
@ -438,7 +407,7 @@ function handleConnection(client) {
|
|||||||
bodyToRead = -1;
|
bodyToRead = -1;
|
||||||
body = undefined;
|
body = undefined;
|
||||||
client.info = 'reset';
|
client.info = 'reset';
|
||||||
resetTimeout(++requestCount);
|
client.setActivityTimeout(kRequestTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
@ -463,9 +432,6 @@ function handleConnection(client) {
|
|||||||
client.read(function(data) {
|
client.read(function(data) {
|
||||||
readCount++;
|
readCount++;
|
||||||
if (data) {
|
if (data) {
|
||||||
if (bodyToRead != -1 && !isWebsocket) {
|
|
||||||
resetTimeout(requestCount);
|
|
||||||
}
|
|
||||||
let newBuffer = new Uint8Array(inputBuffer.length + data.length);
|
let newBuffer = new Uint8Array(inputBuffer.length + data.length);
|
||||||
newBuffer.set(inputBuffer, 0);
|
newBuffer.set(inputBuffer, 0);
|
||||||
newBuffer.set(data, inputBuffer.length);
|
newBuffer.set(data, inputBuffer.length);
|
||||||
@ -473,7 +439,7 @@ function handleConnection(client) {
|
|||||||
|
|
||||||
if (parsing_header)
|
if (parsing_header)
|
||||||
{
|
{
|
||||||
let result = parseHttp(inputBuffer, inputBuffer.length - data.length);
|
let result = parseHttpRequest(inputBuffer, inputBuffer.length - data.length);
|
||||||
if (result) {
|
if (result) {
|
||||||
if (typeof result === 'number') {
|
if (typeof result === 'number') {
|
||||||
if (result == -2) {
|
if (result == -2) {
|
||||||
@ -483,6 +449,7 @@ function handleConnection(client) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (typeof result === 'object') {
|
} else if (typeof result === 'object') {
|
||||||
|
client.setActivityTimeout(kStallTimeout);
|
||||||
request = [
|
request = [
|
||||||
result.method,
|
result.method,
|
||||||
result.path,
|
result.path,
|
||||||
@ -493,7 +460,7 @@ function handleConnection(client) {
|
|||||||
parsing_header = false;
|
parsing_header = false;
|
||||||
inputBuffer = inputBuffer.slice(result.bytes_parsed);
|
inputBuffer = inputBuffer.slice(result.bytes_parsed);
|
||||||
|
|
||||||
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect) {
|
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect && !result.path.startsWith('/.well-known/')) {
|
||||||
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
||||||
let response = new Response(requestObject, client);
|
let response = new Response(requestObject, client);
|
||||||
response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"});
|
response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"});
|
||||||
@ -509,7 +476,6 @@ function handleConnection(client) {
|
|||||||
}
|
}
|
||||||
body = new Uint8Array(bodyToRead);
|
body = new Uint8Array(bodyToRead);
|
||||||
client.info = 'waiting for body';
|
client.info = 'waiting for body';
|
||||||
resetTimeout(requestCount);
|
|
||||||
} else if (headers["connection"]
|
} else if (headers["connection"]
|
||||||
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
|
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
|
||||||
&& headers["upgrade"]
|
&& headers["upgrade"]
|
||||||
@ -520,7 +486,7 @@ function handleConnection(client) {
|
|||||||
let response = new Response(requestObject, client);
|
let response = new Response(requestObject, client);
|
||||||
handleWebSocketRequest(requestObject, response, client);
|
handleWebSocketRequest(requestObject, response, client);
|
||||||
/* Prevent the timeout from disconnecting us. */
|
/* Prevent the timeout from disconnecting us. */
|
||||||
requestCount++;
|
client.setActivityTimeout();
|
||||||
} else {
|
} else {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@ -552,10 +518,20 @@ function handleConnection(client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let kBacklog = 8;
|
let kBacklog = 8;
|
||||||
let kHost = "0.0.0.0"
|
let kHost = '::';
|
||||||
|
|
||||||
let socket = new Socket();
|
let socket = new Socket();
|
||||||
socket.bind(kHost, tildefriends.http_port).then(function() {
|
socket.bind(kHost, tildefriends.http_port).then(function(port) {
|
||||||
|
print("bound to", port);
|
||||||
|
print("checking", tildefriends.args.out_http_port_file);
|
||||||
|
if (tildefriends.args.out_http_port_file) {
|
||||||
|
print("going to write the file");
|
||||||
|
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
|
||||||
|
print("wrote port file", tildefriends.args.out_http_port_file, r);
|
||||||
|
}).catch(function() {
|
||||||
|
print("failed to write port file");
|
||||||
|
});
|
||||||
|
}
|
||||||
let listenResult = socket.listen(kBacklog, async function() {
|
let listenResult = socket.listen(kBacklog, async function() {
|
||||||
try {
|
try {
|
||||||
let client = await socket.accept();
|
let client = await socket.accept();
|
||||||
|
@ -7,48 +7,19 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
<body style="display: flex; flex-flow: column">
|
<body style="display: flex; flex-flow: column">
|
||||||
<div class="navigation">
|
<tf-navigation></tf-navigation>
|
||||||
<span>😎</span>
|
|
||||||
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff">Tilde Friends</a>
|
|
||||||
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
|
|
||||||
<a accesskey="e" data-tip="Toggle the app editor." href="#" id="edit_link">edit</a>
|
|
||||||
<a accesskey="p" data-tip="View and change permissions." href="#" id="show_permissions_link">🎛️</a>
|
|
||||||
<span id="status"></span>
|
|
||||||
<span id="requests"></span>
|
|
||||||
<span id="permissions_settings"></span>
|
|
||||||
<span id="permissions"></span>
|
|
||||||
<span id="login"></span>
|
|
||||||
</div>
|
|
||||||
<div id="content" class="hbox" style="flex: 1 1; width: 100%">
|
<div id="content" class="hbox" style="flex: 1 1; width: 100%">
|
||||||
<div id="statsPane" class="vbox" style="display: none; flex 1 1">
|
<div id="editPane" class="vbox" style="flex: 1 1; display: none">
|
||||||
<div class="hbox">
|
|
||||||
<input type="button" id="closeStats" name="closeStats" value="Close">
|
|
||||||
</div>
|
|
||||||
<div id="graphs" class="vbox" style="height: 100%"></div>
|
|
||||||
</div>
|
|
||||||
<div id="editPane" class="vbox" style="display: none">
|
|
||||||
<div class="navigation hbox">
|
<div class="navigation hbox">
|
||||||
<input type="button" id="closeEditor" name="closeEditor" value="Close">
|
<input type="button" id="closeEditor" name="closeEditor" value="Close">
|
||||||
<input type="button" id="save" name="save" value="Save">
|
<input type="button" id="save" name="save" value="Save">
|
||||||
|
<input type="button" id="icon" name="icon" value="📦">
|
||||||
<input type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
|
<input type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
|
||||||
<input type="button" id="delete" name="delete" value="Delete">
|
<input type="button" id="delete" name="delete" value="Delete">
|
||||||
<input type="button" id="trace_button" value="Trace">
|
<input type="button" id="trace_button" value="Trace">
|
||||||
<input type="button" id="stats_button" value="Stats">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hbox" style="height: 100%">
|
<div class="hbox" style="height: 100%">
|
||||||
<div id="filesPane">
|
<tf-files-pane></tf-files-pane>
|
||||||
<div class="hbox">
|
|
||||||
<span id="files_header">Files</span>
|
|
||||||
<span id="files_hide">«</span>
|
|
||||||
<span id="files_show">»</span>
|
|
||||||
</div>
|
|
||||||
<div id="files_content">
|
|
||||||
<ul id="files"></ul>
|
|
||||||
<br>
|
|
||||||
<div><button id="new_file_button">New File</button></div>
|
|
||||||
<div><button id="remove_file_button">Remove File</button></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="docPane" style="display: flex; flex: 1 1 50%; flex-flow: column">
|
<div id="docPane" style="display: flex; flex: 1 1 50%; flex-flow: column">
|
||||||
<div style="flex: 1 1 50%; position: relative">
|
<div style="flex: 1 1 50%; position: relative">
|
||||||
<textarea id="editor" class="main"></textarea>
|
<textarea id="editor" class="main"></textarea>
|
||||||
@ -56,12 +27,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="viewPane" class="vbox" style="flex: 1 0; overflow: auto">
|
<div id="viewPane" class="vbox" style="flex: 1 1; overflow: auto">
|
||||||
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
|
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/split/split.min.js"></script>
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
<script src="/smoothie/smoothie.js"></script>
|
|
||||||
<script src="/static/client.js" type="module"></script>
|
<script src="/static/client.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
104
core/style.css
104
core/style.css
@ -15,11 +15,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation {
|
|
||||||
height: auto;
|
|
||||||
margin: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link {
|
a:link {
|
||||||
color: #268bd2;
|
color: #268bd2;
|
||||||
}
|
}
|
||||||
@ -69,11 +64,6 @@ a:active {
|
|||||||
color: #eee8d5;
|
color: #eee8d5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#input.drop {
|
|
||||||
border: 2px solid;
|
|
||||||
color: #cb4b16;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -110,33 +100,6 @@ a:active {
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#auth_greeting {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#auth {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
align-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#auth_login {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth_or {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#auth_guest {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice {
|
.notice {
|
||||||
color: #cb4b16;
|
color: #cb4b16;
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
@ -162,70 +125,6 @@ a:active {
|
|||||||
.cyan { color: #2aa198; }
|
.cyan { color: #2aa198; }
|
||||||
.green { color: #859900; }
|
.green { color: #859900; }
|
||||||
|
|
||||||
#files_header {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files_hide {
|
|
||||||
font-weight: bold;
|
|
||||||
width: 100%;
|
|
||||||
right: 0;
|
|
||||||
flex: 0;
|
|
||||||
padding: 0.25em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files_show {
|
|
||||||
display: none;
|
|
||||||
padding: 0.25em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#filesPane {
|
|
||||||
flex: 1 1 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#filesPane.collapsed {
|
|
||||||
flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsed #files_header {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsed #files_content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsed #files_hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsed #files_show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files > li {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files > li.current {
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: #2aa198;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files > li.dirty::after {
|
|
||||||
content: '*';
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -254,8 +153,7 @@ kbd {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#permissions, #permissions_settings {
|
.permissions {
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -39,9 +39,9 @@ function send(response) {
|
|||||||
|
|
||||||
function call_rpc(message) {
|
function call_rpc(message) {
|
||||||
if (message && message.message === 'tfrpc') {
|
if (message && message.message === 'tfrpc') {
|
||||||
let method = g_api[message.method];
|
|
||||||
let id = message.id;
|
let id = message.id;
|
||||||
if (message.method) {
|
if (message.method) {
|
||||||
|
let method = g_api[message.method];
|
||||||
if (method) {
|
if (method) {
|
||||||
try {
|
try {
|
||||||
Promise.resolve(method(...message.params)).then(function(result) {
|
Promise.resolve(method(...message.params)).then(function(result) {
|
||||||
@ -53,7 +53,7 @@ function call_rpc(message) {
|
|||||||
send({message: 'tfrpc', id: id, error: error});
|
send({message: 'tfrpc', id: id, error: error});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(message.method + ' not found.');
|
send({message: 'tfrpc', id: id, error: `Method '${message.method}' not found.`});
|
||||||
}
|
}
|
||||||
} else if (message.error !== undefined) {
|
} else if (message.error !== undefined) {
|
||||||
if (g_calls[id]) {
|
if (g_calls[id]) {
|
||||||
|
2
deps/codemirror/annotatescrollbar.min.js
vendored
2
deps/codemirror/annotatescrollbar.min.js
vendored
@ -1 +1 @@
|
|||||||
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(t){"use strict";function e(t,e){function i(t){clearTimeout(n.doRedraw),n.doRedraw=setTimeout(function(){n.redraw()},t)}this.cm=t,this.options=e,this.buttonHeight=e.scrollButtonHeight||t.getOption("scrollButtonHeight"),this.annotations=[],this.doRedraw=this.doUpdate=null,this.div=t.getWrapperElement().appendChild(document.createElement("div")),this.div.style.cssText="position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none",this.computeScale();var n=this;t.on("refresh",this.resizeHandler=function(){clearTimeout(n.doUpdate),n.doUpdate=setTimeout(function(){n.computeScale()&&i(20)},100)}),t.on("markerAdded",this.resizeHandler),t.on("markerCleared",this.resizeHandler),!1!==e.listenForChanges&&t.on("changes",this.changeHandler=function(){i(250)})}t.defineExtension("annotateScrollbar",function(t){return new e(this,t="string"==typeof t?{className:t}:t)}),t.defineOption("scrollButtonHeight",0),e.prototype.computeScale=function(){var t=this.cm,t=(t.getWrapperElement().clientHeight-t.display.barHeight-2*this.buttonHeight)/t.getScrollerElement().scrollHeight;if(t!=this.hScale)return this.hScale=t,!0},e.prototype.update=function(t){this.annotations=t,this.redraw()},e.prototype.redraw=function(t){!1!==t&&this.computeScale();var n=this.cm,e=this.hScale,i=document.createDocumentFragment(),o=this.annotations,r=n.getOption("lineWrapping"),a=r&&1.5*n.defaultTextHeight(),s=null,h=null;function l(t,e){var i;return s!=t.line&&(s=t.line,h=n.getLineHandle(t.line),(i=n.getLineHandleVisualStart(h))!=h&&(s=n.getLineNumber(i),h=i)),h.widgets&&h.widgets.length||r&&h.height>a?n.charCoords(t,"local")[e?"top":"bottom"]:n.heightAtLine(h,"local")+(e?0:h.height)}var d=n.lastLine();if(n.display.barWidth)for(var c,p=0;p<o.length;p++){var u=o[p];if(!(u.to.line>d)){for(var m,f,g=c||l(u.from,!0)*e,H=l(u.to,!1)*e;p<o.length-1&&!(o[p+1].to.line>d)&&!(H+.9<(c=l(o[p+1].from,!0)*e));)H=l((u=o[++p]).to,!1)*e;H!=g&&(m=Math.max(H-g,3),(f=i.appendChild(document.createElement("div"))).style.cssText="position: absolute; right: 0px; width: "+Math.max(n.display.barWidth-1,2)+"px; top: "+(g+this.buttonHeight)+"px; height: "+m+"px",f.className=this.options.className,u.id&&f.setAttribute("annotation-id",u.id))}}this.div.textContent="",this.div.appendChild(i)},e.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler),this.cm.off("markerAdded",this.resizeHandler),this.cm.off("markerCleared",this.resizeHandler),this.changeHandler&&this.cm.off("changes",this.changeHandler),this.div.parentNode.removeChild(this.div)}});
|
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(t){"use strict";function e(t,e){function i(t){clearTimeout(n.doRedraw),n.doRedraw=setTimeout(function(){n.redraw()},t)}this.cm=t,this.options=e,this.buttonHeight=e.scrollButtonHeight||t.getOption("scrollButtonHeight"),this.annotations=[],this.doRedraw=this.doUpdate=null,this.div=t.getWrapperElement().appendChild(document.createElement("div")),this.div.style.cssText="position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none",this.computeScale();var n=this;t.on("refresh",this.resizeHandler=function(){clearTimeout(n.doUpdate),n.doUpdate=setTimeout(function(){n.computeScale()&&i(20)},100)}),t.on("markerAdded",this.resizeHandler),t.on("markerCleared",this.resizeHandler),!1!==e.listenForChanges&&t.on("changes",this.changeHandler=function(){i(250)})}t.defineExtension("annotateScrollbar",function(t){return new e(this,t="string"==typeof t?{className:t}:t)}),t.defineOption("scrollButtonHeight",0),e.prototype.computeScale=function(){var t=this.cm,t=(t.getWrapperElement().clientHeight-t.display.barHeight-2*this.buttonHeight)/t.getScrollerElement().scrollHeight;if(t!=this.hScale)return this.hScale=t,!0},e.prototype.update=function(t){this.annotations=t,this.redraw()},e.prototype.redraw=function(t){!1!==t&&this.computeScale();var n=this.cm,e=this.hScale,i=document.createDocumentFragment(),o=this.annotations,r=n.getOption("lineWrapping"),a=r&&1.5*n.defaultTextHeight(),s=null,h=null;function l(t,e){var i;return s!=t.line&&(s=t.line,h=n.getLineHandle(t.line),(i=n.getLineHandleVisualStart(h))!=h)&&(s=n.getLineNumber(i),h=i),h.widgets&&h.widgets.length||r&&h.height>a?n.charCoords(t,"local")[e?"top":"bottom"]:n.heightAtLine(h,"local")+(e?0:h.height)}var d=n.lastLine();if(n.display.barWidth)for(var c,p=0;p<o.length;p++){var u=o[p];if(!(u.to.line>d)){for(var m,f,g=c||l(u.from,!0)*e,H=l(u.to,!1)*e;p<o.length-1&&!(o[p+1].to.line>d)&&!(H+.9<(c=l(o[p+1].from,!0)*e));)H=l((u=o[++p]).to,!1)*e;H!=g&&(m=Math.max(H-g,3),(f=i.appendChild(document.createElement("div"))).style.cssText="position: absolute; right: 0px; width: "+Math.max(n.display.barWidth-1,2)+"px; top: "+(g+this.buttonHeight)+"px; height: "+m+"px",f.className=this.options.className,u.id)&&f.setAttribute("annotation-id",u.id)}}this.div.textContent="",this.div.appendChild(i)},e.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler),this.cm.off("markerAdded",this.resizeHandler),this.cm.off("markerCleared",this.resizeHandler),this.changeHandler&&this.cm.off("changes",this.changeHandler),this.div.parentNode.removeChild(this.div)}});
|
2
deps/codemirror/codemirror.min.js
vendored
2
deps/codemirror/codemirror.min.js
vendored
File diff suppressed because one or more lines are too long
2
deps/codemirror/css.min.js
vendored
2
deps/codemirror/css.min.js
vendored
File diff suppressed because one or more lines are too long
2
deps/codemirror/dialog.min.js
vendored
2
deps/codemirror/dialog.min.js
vendored
@ -1 +1 @@
|
|||||||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(s){function f(e,o,n){var e=e.getWrapperElement(),t=e.appendChild(document.createElement("div"));return t.className=n?"CodeMirror-dialog CodeMirror-dialog-bottom":"CodeMirror-dialog CodeMirror-dialog-top","string"==typeof o?t.innerHTML=o:t.appendChild(o),s.addClass(e,"dialog-opened"),t}function p(e,o){e.state.currentNotificationClose&&e.state.currentNotificationClose(),e.state.currentNotificationClose=o}s.defineExtension("openDialog",function(e,o,n){n=n||{},p(this,null);var t=f(this,e,n.bottom),i=!1,r=this;function u(e){"string"==typeof e?l.value=e:i||(i=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),r.focus(),n.onClose&&n.onClose(t))}var l=t.getElementsByTagName("input")[0];return l?(l.focus(),n.value&&(l.value=n.value,!1!==n.selectValueOnOpen&&l.select()),n.onInput&&s.on(l,"input",function(e){n.onInput(e,l.value,u)}),n.onKeyUp&&s.on(l,"keyup",function(e){n.onKeyUp(e,l.value,u)}),s.on(l,"keydown",function(e){n&&n.onKeyDown&&n.onKeyDown(e,l.value,u)||((27==e.keyCode||!1!==n.closeOnEnter&&13==e.keyCode)&&(l.blur(),s.e_stop(e),u()),13==e.keyCode&&o(l.value,e))}),!1!==n.closeOnBlur&&s.on(t,"focusout",function(e){null!==e.relatedTarget&&u()})):(e=t.getElementsByTagName("button")[0])&&(s.on(e,"click",function(){u(),r.focus()}),!1!==n.closeOnBlur&&s.on(e,"blur",u),e.focus()),u}),s.defineExtension("openConfirm",function(e,o,n){p(this,null);var t=f(this,e,n&&n.bottom),i=t.getElementsByTagName("button"),r=!1,u=this,l=1;function a(){r||(r=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),u.focus())}i[0].focus();for(var c=0;c<i.length;++c){var d=i[c];!function(o){s.on(d,"click",function(e){s.e_preventDefault(e),a(),o&&o(u)})}(o[c]),s.on(d,"blur",function(){--l,setTimeout(function(){l<=0&&a()},200)}),s.on(d,"focus",function(){++l})}}),s.defineExtension("openNotification",function(e,o){p(this,r);var n,t=f(this,e,o&&o.bottom),i=!1,e=o&&void 0!==o.duration?o.duration:5e3;function r(){i||(i=!0,clearTimeout(n),s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t))}return s.on(t,"click",function(e){s.e_preventDefault(e),r()}),e&&(n=setTimeout(r,e)),r})});
|
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(s){function f(e,o,n){var e=e.getWrapperElement(),t=e.appendChild(document.createElement("div"));return t.className=n?"CodeMirror-dialog CodeMirror-dialog-bottom":"CodeMirror-dialog CodeMirror-dialog-top","string"==typeof o?t.innerHTML=o:t.appendChild(o),s.addClass(e,"dialog-opened"),t}function p(e,o){e.state.currentNotificationClose&&e.state.currentNotificationClose(),e.state.currentNotificationClose=o}s.defineExtension("openDialog",function(e,o,n){n=n||{},p(this,null);var t=f(this,e,n.bottom),i=!1,r=this;function u(e){"string"==typeof e?l.value=e:i||(i=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),r.focus(),n.onClose&&n.onClose(t))}var l=t.getElementsByTagName("input")[0];return l?(l.focus(),n.value&&(l.value=n.value,!1!==n.selectValueOnOpen)&&l.select(),n.onInput&&s.on(l,"input",function(e){n.onInput(e,l.value,u)}),n.onKeyUp&&s.on(l,"keyup",function(e){n.onKeyUp(e,l.value,u)}),s.on(l,"keydown",function(e){n&&n.onKeyDown&&n.onKeyDown(e,l.value,u)||((27==e.keyCode||!1!==n.closeOnEnter&&13==e.keyCode)&&(l.blur(),s.e_stop(e),u()),13==e.keyCode&&o(l.value,e))}),!1!==n.closeOnBlur&&s.on(t,"focusout",function(e){null!==e.relatedTarget&&u()})):(e=t.getElementsByTagName("button")[0])&&(s.on(e,"click",function(){u(),r.focus()}),!1!==n.closeOnBlur&&s.on(e,"blur",u),e.focus()),u}),s.defineExtension("openConfirm",function(e,o,n){p(this,null);var t=f(this,e,n&&n.bottom),i=t.getElementsByTagName("button"),r=!1,u=this,l=1;function a(){r||(r=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),u.focus())}i[0].focus();for(var c=0;c<i.length;++c){var d=i[c];!function(o){s.on(d,"click",function(e){s.e_preventDefault(e),a(),o&&o(u)})}(o[c]),s.on(d,"blur",function(){--l,setTimeout(function(){l<=0&&a()},200)}),s.on(d,"focus",function(){++l})}}),s.defineExtension("openNotification",function(e,o){p(this,r);var n,t=f(this,e,o&&o.bottom),i=!1,e=o&&void 0!==o.duration?o.duration:5e3;function r(){i||(i=!0,clearTimeout(n),s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t))}return s.on(t,"click",function(e){s.e_preventDefault(e),r()}),e&&(n=setTimeout(r,e)),r})});
|
2
deps/codemirror/htmlmixed.min.js
vendored
2
deps/codemirror/htmlmixed.min.js
vendored
@ -1 +1 @@
|
|||||||
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol()&&(e.inTag+=" ")),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});
|
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol())&&(e.inTag+=" "),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});
|
1
deps/codemirror/javascript-lint.min.js
vendored
Normal file
1
deps/codemirror/javascript-lint.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(a){"use strict";a.registerHelper("lint","javascript",function(e,r){if(!window.JSHINT)return window.console&&window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run."),[];if(r.indent||(r.indent=1),JSHINT(e,r,r.globals),e=JSHINT.data().errors,r=[],e)for(var n=e,o=r,i=0;i<n.length;i++){var t,d,s,c=n[i];c&&(c.line<=0?window.console&&window.console.warn("Cannot display JSHint error (invalid line "+c.line+")",c):(t=c.character-1,d=1+t,c.evidence&&-1<(s=c.evidence.substring(t).search(/.\b/))&&(d+=s),s={message:c.reason,severity:c.code&&c.code.startsWith("W")?"warning":"error",from:a.Pos(c.line-1,t),to:a.Pos(c.line-1,d)},o.push(s)))}return r})});
|
2
deps/codemirror/javascript.min.js
vendored
2
deps/codemirror/javascript.min.js
vendored
File diff suppressed because one or more lines are too long
32076
deps/codemirror/jshint.js
vendored
Normal file
32076
deps/codemirror/jshint.js
vendored
Normal file
File diff suppressed because one or more lines are too long
80
deps/codemirror/lint.css
vendored
Normal file
80
deps/codemirror/lint.css
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/* The lint marker gutter */
|
||||||
|
.CodeMirror-lint-markers {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-tooltip {
|
||||||
|
background-color: #ffd;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
color: black;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10pt;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2px 5px;
|
||||||
|
position: fixed;
|
||||||
|
white-space: pre;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
z-index: 100;
|
||||||
|
max-width: 600px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .4s;
|
||||||
|
-moz-transition: opacity .4s;
|
||||||
|
-webkit-transition: opacity .4s;
|
||||||
|
-o-transition: opacity .4s;
|
||||||
|
-ms-transition: opacity .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark {
|
||||||
|
background-position: left bottom;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-warning {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-error {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker {
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-message {
|
||||||
|
padding-left: 18px;
|
||||||
|
background-position: top left;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-multiple {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right bottom;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-line-error {
|
||||||
|
background-color: rgba(183, 76, 81, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-line-warning {
|
||||||
|
background-color: rgba(255, 211, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
292
deps/codemirror/lint.js
vendored
Normal file
292
deps/codemirror/lint.js
vendored
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
var GUTTER_ID = "CodeMirror-lint-markers";
|
||||||
|
var LINT_LINE_ID = "CodeMirror-lint-line-";
|
||||||
|
|
||||||
|
function showTooltip(cm, e, content) {
|
||||||
|
var tt = document.createElement("div");
|
||||||
|
tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme;
|
||||||
|
tt.appendChild(content.cloneNode(true));
|
||||||
|
if (cm.state.lint.options.selfContain)
|
||||||
|
cm.getWrapperElement().appendChild(tt);
|
||||||
|
else
|
||||||
|
document.body.appendChild(tt);
|
||||||
|
|
||||||
|
function position(e) {
|
||||||
|
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
|
||||||
|
tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
|
||||||
|
tt.style.left = (e.clientX + 5) + "px";
|
||||||
|
}
|
||||||
|
CodeMirror.on(document, "mousemove", position);
|
||||||
|
position(e);
|
||||||
|
if (tt.style.opacity != null) tt.style.opacity = 1;
|
||||||
|
return tt;
|
||||||
|
}
|
||||||
|
function rm(elt) {
|
||||||
|
if (elt.parentNode) elt.parentNode.removeChild(elt);
|
||||||
|
}
|
||||||
|
function hideTooltip(tt) {
|
||||||
|
if (!tt.parentNode) return;
|
||||||
|
if (tt.style.opacity == null) rm(tt);
|
||||||
|
tt.style.opacity = 0;
|
||||||
|
setTimeout(function() { rm(tt); }, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTooltipFor(cm, e, content, node) {
|
||||||
|
var tooltip = showTooltip(cm, e, content);
|
||||||
|
function hide() {
|
||||||
|
CodeMirror.off(node, "mouseout", hide);
|
||||||
|
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
|
||||||
|
}
|
||||||
|
var poll = setInterval(function() {
|
||||||
|
if (tooltip) for (var n = node;; n = n.parentNode) {
|
||||||
|
if (n && n.nodeType == 11) n = n.host;
|
||||||
|
if (n == document.body) return;
|
||||||
|
if (!n) { hide(); break; }
|
||||||
|
}
|
||||||
|
if (!tooltip) return clearInterval(poll);
|
||||||
|
}, 400);
|
||||||
|
CodeMirror.on(node, "mouseout", hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LintState(cm, conf, hasGutter) {
|
||||||
|
this.marked = [];
|
||||||
|
if (conf instanceof Function) conf = {getAnnotations: conf};
|
||||||
|
if (!conf || conf === true) conf = {};
|
||||||
|
this.options = {};
|
||||||
|
this.linterOptions = conf.options || {};
|
||||||
|
for (var prop in defaults) this.options[prop] = defaults[prop];
|
||||||
|
for (var prop in conf) {
|
||||||
|
if (defaults.hasOwnProperty(prop)) {
|
||||||
|
if (conf[prop] != null) this.options[prop] = conf[prop];
|
||||||
|
} else if (!conf.options) {
|
||||||
|
this.linterOptions[prop] = conf[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.timeout = null;
|
||||||
|
this.hasGutter = hasGutter;
|
||||||
|
this.onMouseOver = function(e) { onMouseOver(cm, e); };
|
||||||
|
this.waitingFor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaults = {
|
||||||
|
highlightLines: false,
|
||||||
|
tooltips: true,
|
||||||
|
delay: 500,
|
||||||
|
lintOnChange: true,
|
||||||
|
getAnnotations: null,
|
||||||
|
async: false,
|
||||||
|
selfContain: null,
|
||||||
|
formatAnnotation: null,
|
||||||
|
onUpdateLinting: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMarks(cm) {
|
||||||
|
var state = cm.state.lint;
|
||||||
|
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
|
||||||
|
if (state.options.highlightLines) clearErrorLines(cm);
|
||||||
|
for (var i = 0; i < state.marked.length; ++i)
|
||||||
|
state.marked[i].clear();
|
||||||
|
state.marked.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearErrorLines(cm) {
|
||||||
|
cm.eachLine(function(line) {
|
||||||
|
var has = line.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(line.wrapClass);
|
||||||
|
if (has) cm.removeLineClass(line, "wrap", has[0]);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMarker(cm, labels, severity, multiple, tooltips) {
|
||||||
|
var marker = document.createElement("div"), inner = marker;
|
||||||
|
marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity;
|
||||||
|
if (multiple) {
|
||||||
|
inner = marker.appendChild(document.createElement("div"));
|
||||||
|
inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
|
||||||
|
showTooltipFor(cm, e, labels, inner);
|
||||||
|
});
|
||||||
|
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxSeverity(a, b) {
|
||||||
|
if (a == "error") return a;
|
||||||
|
else return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByLine(annotations) {
|
||||||
|
var lines = [];
|
||||||
|
for (var i = 0; i < annotations.length; ++i) {
|
||||||
|
var ann = annotations[i], line = ann.from.line;
|
||||||
|
(lines[line] || (lines[line] = [])).push(ann);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotationTooltip(ann) {
|
||||||
|
var severity = ann.severity;
|
||||||
|
if (!severity) severity = "error";
|
||||||
|
var tip = document.createElement("div");
|
||||||
|
tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity;
|
||||||
|
if (typeof ann.messageHTML != 'undefined') {
|
||||||
|
tip.innerHTML = ann.messageHTML;
|
||||||
|
} else {
|
||||||
|
tip.appendChild(document.createTextNode(ann.message));
|
||||||
|
}
|
||||||
|
return tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lintAsync(cm, getAnnotations) {
|
||||||
|
var state = cm.state.lint
|
||||||
|
var id = ++state.waitingFor
|
||||||
|
function abort() {
|
||||||
|
id = -1
|
||||||
|
cm.off("change", abort)
|
||||||
|
}
|
||||||
|
cm.on("change", abort)
|
||||||
|
getAnnotations(cm.getValue(), function(annotations, arg2) {
|
||||||
|
cm.off("change", abort)
|
||||||
|
if (state.waitingFor != id) return
|
||||||
|
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
|
||||||
|
cm.operation(function() {updateLinting(cm, annotations)})
|
||||||
|
}, state.linterOptions, cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLinting(cm) {
|
||||||
|
var state = cm.state.lint;
|
||||||
|
if (!state) return;
|
||||||
|
var options = state.options;
|
||||||
|
/*
|
||||||
|
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
|
||||||
|
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
|
||||||
|
*/
|
||||||
|
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
|
||||||
|
if (!getAnnotations) return;
|
||||||
|
if (options.async || getAnnotations.async) {
|
||||||
|
lintAsync(cm, getAnnotations)
|
||||||
|
} else {
|
||||||
|
var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);
|
||||||
|
if (!annotations) return;
|
||||||
|
if (annotations.then) annotations.then(function(issues) {
|
||||||
|
cm.operation(function() {updateLinting(cm, issues)})
|
||||||
|
});
|
||||||
|
else cm.operation(function() {updateLinting(cm, annotations)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLinting(cm, annotationsNotSorted) {
|
||||||
|
var state = cm.state.lint;
|
||||||
|
if (!state) return;
|
||||||
|
var options = state.options;
|
||||||
|
clearMarks(cm);
|
||||||
|
|
||||||
|
var annotations = groupByLine(annotationsNotSorted);
|
||||||
|
|
||||||
|
for (var line = 0; line < annotations.length; ++line) {
|
||||||
|
var anns = annotations[line];
|
||||||
|
if (!anns) continue;
|
||||||
|
|
||||||
|
// filter out duplicate messages
|
||||||
|
var message = [];
|
||||||
|
anns = anns.filter(function(item) { return message.indexOf(item.message) > -1 ? false : message.push(item.message) });
|
||||||
|
|
||||||
|
var maxSeverity = null;
|
||||||
|
var tipLabel = state.hasGutter && document.createDocumentFragment();
|
||||||
|
|
||||||
|
for (var i = 0; i < anns.length; ++i) {
|
||||||
|
var ann = anns[i];
|
||||||
|
var severity = ann.severity;
|
||||||
|
if (!severity) severity = "error";
|
||||||
|
maxSeverity = getMaxSeverity(maxSeverity, severity);
|
||||||
|
|
||||||
|
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
|
||||||
|
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
|
||||||
|
|
||||||
|
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
|
||||||
|
className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity,
|
||||||
|
__annotation: ann
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// use original annotations[line] to show multiple messages
|
||||||
|
if (state.hasGutter)
|
||||||
|
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, annotations[line].length > 1,
|
||||||
|
options.tooltips));
|
||||||
|
|
||||||
|
if (options.highlightLines)
|
||||||
|
cm.addLineClass(line, "wrap", LINT_LINE_ID + maxSeverity);
|
||||||
|
}
|
||||||
|
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(cm) {
|
||||||
|
var state = cm.state.lint;
|
||||||
|
if (!state) return;
|
||||||
|
clearTimeout(state.timeout);
|
||||||
|
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function popupTooltips(cm, annotations, e) {
|
||||||
|
var target = e.target || e.srcElement;
|
||||||
|
var tooltip = document.createDocumentFragment();
|
||||||
|
for (var i = 0; i < annotations.length; i++) {
|
||||||
|
var ann = annotations[i];
|
||||||
|
tooltip.appendChild(annotationTooltip(ann));
|
||||||
|
}
|
||||||
|
showTooltipFor(cm, e, tooltip, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseOver(cm, e) {
|
||||||
|
var target = e.target || e.srcElement;
|
||||||
|
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
|
||||||
|
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
|
||||||
|
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
|
||||||
|
|
||||||
|
var annotations = [];
|
||||||
|
for (var i = 0; i < spans.length; ++i) {
|
||||||
|
var ann = spans[i].__annotation;
|
||||||
|
if (ann) annotations.push(ann);
|
||||||
|
}
|
||||||
|
if (annotations.length) popupTooltips(cm, annotations, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.defineOption("lint", false, function(cm, val, old) {
|
||||||
|
if (old && old != CodeMirror.Init) {
|
||||||
|
clearMarks(cm);
|
||||||
|
if (cm.state.lint.options.lintOnChange !== false)
|
||||||
|
cm.off("change", onChange);
|
||||||
|
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
|
||||||
|
clearTimeout(cm.state.lint.timeout);
|
||||||
|
delete cm.state.lint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val) {
|
||||||
|
var gutters = cm.getOption("gutters"), hasLintGutter = false;
|
||||||
|
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
|
||||||
|
var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
|
||||||
|
if (state.options.lintOnChange)
|
||||||
|
cm.on("change", onChange);
|
||||||
|
if (state.options.tooltips != false && state.options.tooltips != "gutter")
|
||||||
|
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
|
||||||
|
|
||||||
|
startLinting(cm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineExtension("performLint", function() {
|
||||||
|
startLinting(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
2
deps/codemirror/search.min.js
vendored
2
deps/codemirror/search.min.js
vendored
File diff suppressed because one or more lines are too long
2
deps/codemirror/searchcursor.min.js
vendored
2
deps/codemirror/searchcursor.min.js
vendored
File diff suppressed because one or more lines are too long
22
deps/codemirror/update.sh
vendored
Executable file
22
deps/codemirror/update.sh
vendored
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
LINKS="
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/dialog/dialog.min.css
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/dialog/dialog.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/edit/trailingspace.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/lint/javascript-lint.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/scroll/annotatescrollbar.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/matchesonscrollbar.min.css
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/matchesonscrollbar.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/search.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/searchcursor.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.css
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/css/css.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/htmlmixed/htmlmixed.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/javascript/javascript.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/xml/xml.min.js
|
||||||
|
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/theme/base16-dark.min.css
|
||||||
|
"
|
||||||
|
|
||||||
|
for link in $LINKS; do
|
||||||
|
wget $link -O `basename $link`
|
||||||
|
done
|
2
deps/codemirror/xml.min.js
vendored
2
deps/codemirror/xml.min.js
vendored
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