forked from cory/tildefriends
Compare commits
245 Commits
390668ec34
...
tasiaiso-d
Author | SHA1 | Date | |
---|---|---|---|
9c8772c898
|
|||
f31ec0338b
|
|||
1b3b9e570e
|
|||
580688381e | |||
e63d69a440 | |||
be64fe04fb | |||
801ab20723 | |||
d974a5e044 | |||
1be94ae0be | |||
b883e6a485 | |||
a0210379ae | |||
912747bdac
|
|||
80c1463a5c
|
|||
f2a3c790dd
|
|||
43f6a3a482 | |||
e56dc207d1 | |||
523c9c9ad2 | |||
74bb2151c1 | |||
f79d7b35a4 | |||
3b36496dac
|
|||
4ebd6c24a9
|
|||
05451d98b3
|
|||
22a4bce3c8
|
|||
76d499f00b | |||
f0772f9b99
|
|||
46e711f0a5 | |||
abffac3f82 | |||
27b275548e | |||
93ce253d1e | |||
a5af312b39 | |||
4b5e8e8a43 | |||
443dd4d168 | |||
907479df84 | |||
9887a78e98 | |||
f669371349 | |||
d7eda01c16
|
|||
12599b5723
|
|||
5b7d0f1aa1
|
|||
ae3430bf56
|
|||
7d77e398d4
|
|||
9f3a3808f9
|
|||
24c720c79a | |||
4485234980
|
|||
b6871c0b1f
|
|||
fae2771645
|
|||
2bb6d68122
|
|||
5c8c6e8760
|
|||
85ac8080f4
|
|||
0751699bc8
|
|||
5551fd2dea
|
|||
69b2e2a955
|
|||
34c7fa8312
|
|||
47838d5e48 | |||
69fccd56d3 | |||
ca00c4fb5d | |||
427ca3f265 | |||
c1a80e50e7 | |||
52962f3a5e | |||
b3f095b61f | |||
a5004c8ba9 | |||
7d9b1b508b | |||
5e265dfc83 | |||
3a43d6f8ac | |||
11a6649847 | |||
396f37ee3b
|
|||
7caf4a0173 | |||
385524352c | |||
5ca5323782 | |||
ba6da856bb | |||
c0e72246cc | |||
c7ab5447ea | |||
5fdd461159 | |||
421955f2a0 | |||
a28f6985ed | |||
8244dddab7 | |||
a5ca436eaa | |||
d7fc1c2c88 | |||
382627ef8d | |||
17667b4cf8 | |||
5231ec22e7 | |||
929ae1b709 | |||
f01f7a5ab9 | |||
a2dce833f8 | |||
de6c7a4fd4 | |||
4edee0f7f6 | |||
988a807fa4 | |||
5258e4253d | |||
09ba86dec5 | |||
78d8a1aa23 | |||
22def15209 | |||
4cbda7a849 | |||
be85a620ef | |||
0b07b678b4 | |||
4733ce9287 | |||
48d6bf4c15 | |||
8c759bcbac | |||
b5ed7014f6 | |||
6cd9dea186 | |||
202b416acf | |||
93d46f5610 | |||
c5ddf3ac99 | |||
a9cb913a47 | |||
b7b5d4f1a5 | |||
a947396bad | |||
d528bc808e | |||
c6fd05c2cf | |||
d6bb9d311a | |||
53b4cbbf8c | |||
628716ec28 | |||
bd14168627 | |||
96037d4da6 | |||
5448e773d8 | |||
848ef21c7c | |||
2ecae7da93 | |||
d9ce569eb9 | |||
eacaf392b1 | |||
ce16592b6a | |||
295d76d354 | |||
23b3c998bd | |||
b5e966c9a1 | |||
96cb6f4b12 | |||
e2c0f82ec0 | |||
dbf28c03e6 | |||
26165e30de | |||
c52331a23a | |||
8007e71e1d | |||
28d08e013f | |||
64bbd383de | |||
8a9f53102b | |||
0412b97170 | |||
c8b8a8fc03 | |||
95d3090b9b | |||
49129ee6dd | |||
6a7ecb0d4a | |||
1ceeed1007 | |||
a7922ff44e | |||
a421604ed5 | |||
7d182db32f | |||
c5cb9979d3 | |||
b9a73106ed | |||
c674cca482 | |||
81d1228b92 | |||
6ae61d5b81 | |||
9cb872eec2 | |||
68e8c010b7 | |||
9671413906 | |||
4c8d24c319 | |||
e50144bd34 | |||
9f3171e3f1 | |||
cc92748747 | |||
0a0b0c1adb | |||
92a74026a6 | |||
3fa1c6c420 | |||
b04eccdbda | |||
9ce30dee70 | |||
3c0b680b8e | |||
895356897b | |||
9164be2f37 | |||
5385264f94 | |||
610e756c07 | |||
15c9f8f458 | |||
fb704a5b83 | |||
fdda628be8 | |||
2b45d8aa05 | |||
0e2fc65301 | |||
e8ef7e74de | |||
c32e1b9583 | |||
9d0f6ec155 | |||
855d603795 | |||
af25782185 | |||
e5ba51b80a | |||
5e240de677 | |||
418cfac0e3 | |||
9d09607013 | |||
eddf25b622 | |||
537a8654fa | |||
9de33d06d2 | |||
0e5f320664 | |||
88d8e60511 | |||
439f07162e | |||
efe2b6cbd9 | |||
0aa1ed9464 | |||
cb94ed6a2a | |||
cf187ee46b | |||
3e71fc20fd | |||
f3601321f7 | |||
540059368c | |||
7ce89123f7 | |||
e3c7c86212 | |||
794804e27f | |||
6d89c1da6e | |||
d059554464 | |||
3a392d4a9f | |||
e3071b372a | |||
18bd279b0c | |||
5b93db7463 | |||
5b7e5eb91b | |||
78ca383e3c | |||
c1eed9ada3 | |||
8d6feb5394 | |||
42994f8977 | |||
f0a871e1f8 | |||
a710c30572 | |||
c991763b00 | |||
72dae14f87 | |||
5800340762 | |||
c5f5adcac6 | |||
591642efb3 | |||
6182ffa1d4 | |||
402a898d96 | |||
13d43d8319 | |||
7bcdbd3813 | |||
60ada22674 | |||
637119d46d | |||
40f3da6a65 | |||
f4697fe7f7 | |||
3bc18b9021 | |||
c21581aefa | |||
165f25db69 | |||
9aa0617aa1 | |||
ddce88dce6 | |||
6aa2bce2be | |||
a43c1d3d1e | |||
1ed0e817e8 | |||
709ca55e65 | |||
8c13f5dbba | |||
4cb82d81b7 | |||
0c42921387 | |||
70a3e7fc7d | |||
d5267be38c | |||
8e7e0ed490 | |||
8cf2837725 | |||
63ae186c76 | |||
dbf5c7b832 | |||
bfbfc01e99 | |||
8fa9d0e843 | |||
2d3e108fd9 | |||
7822b30dcb | |||
2701b7d04e | |||
e361c3f975 | |||
260706c172 | |||
1d5cdf9607 | |||
a4bf3542e0 | |||
df82cfe66b | |||
53f9547cc5 |
@ -1,4 +1,5 @@
|
|||||||
.svn
|
.svn
|
||||||
db.sqlite
|
db.*
|
||||||
out/**/*.o
|
out/**/*.o
|
||||||
out/**/*.d
|
out/**/*.d
|
||||||
|
NOTES.md
|
||||||
|
3
.gitea/ISSUE_TEMPLATE/bug-report.md
Normal file
3
.gitea/ISSUE_TEMPLATE/bug-report.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
name: 'Bug Report'
|
||||||
|
---
|
5
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
5
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Documentation
|
||||||
|
url: https://dev.tildefriends.net/cory/tildefriends/src/branch/main/docs/index.md
|
||||||
|
about: Read the documentation
|
3
.gitea/ISSUE_TEMPLATE/feature-rquest.md
Normal file
3
.gitea/ISSUE_TEMPLATE/feature-rquest.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
name: 'Feature Request'
|
||||||
|
---
|
9
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
9
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
To Do List
|
||||||
|
|
||||||
|
- [ ] My changes are documented in the [documentation](https://dev.tildefriends.net/cory/tildefriends/src/branch/main/docs/index.md)
|
||||||
|
- [ ] I have tested my changes
|
||||||
|
- [ ] I agree to the contribution guidelines
|
||||||
|
- [ ] [C](https://dev.tildefriends.net/cory/tildefriends/src/branch/main/docs/guidelines/c-guidelines.md)
|
||||||
|
- [ ] [JavaScript](https://dev.tildefriends.net/cory/tildefriends/src/branch/main/docs/guidelines/javascript-guidelines.md)
|
||||||
|
- [ ] [documentation](https://dev.tildefriends.net/cory/tildefriends/src/branch/main/docs/guidelines/documentation-guidelines.md)
|
||||||
|
<!-- - [ ] I agree to the [Code of Conduct]() -->
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,8 +1,12 @@
|
|||||||
**/node_modules
|
|
||||||
.keys
|
|
||||||
.zsign_cache/
|
|
||||||
db.*
|
db.*
|
||||||
deps/ios_toolchain/
|
deps/ios_toolchain/
|
||||||
deps/openssl/
|
deps/openssl/
|
||||||
dist/
|
dist/
|
||||||
|
.keys
|
||||||
|
**/node_modules
|
||||||
out
|
out
|
||||||
|
*.swo
|
||||||
|
*.swp
|
||||||
|
.zsign_cache/
|
||||||
|
result
|
||||||
|
NOTES.md
|
||||||
|
21
.gitmodules
vendored
Normal file
21
.gitmodules
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[submodule "deps/zlib"]
|
||||||
|
path = deps/zlib
|
||||||
|
url = https://github.com/madler/zlib.git
|
||||||
|
[submodule "deps/libsodium"]
|
||||||
|
path = deps/libsodium
|
||||||
|
url = https://github.com/jedisct1/libsodium.git
|
||||||
|
[submodule "deps/quickjs"]
|
||||||
|
path = deps/quickjs
|
||||||
|
url = https://github.com/bellard/quickjs.git
|
||||||
|
[submodule "deps/crypt_blowfish"]
|
||||||
|
path = deps/crypt_blowfish
|
||||||
|
url = https://github.com/openwall/crypt_blowfish.git
|
||||||
|
[submodule "deps/libbacktrace"]
|
||||||
|
path = deps/libbacktrace
|
||||||
|
url = https://github.com/ianlancetaylor/libbacktrace.git
|
||||||
|
[submodule "deps/libuv"]
|
||||||
|
path = deps/libuv
|
||||||
|
url = https://github.com/libuv/libuv.git
|
||||||
|
[submodule "deps/picohttpparser"]
|
||||||
|
path = deps/picohttpparser
|
||||||
|
url = https://github.com/h2o/picohttpparser.git
|
5
.markdownlint.yaml
Normal file
5
.markdownlint.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
default: true
|
||||||
|
MD010: false # Ignore tabs in code blocks
|
||||||
|
MD013: false # Don't wrap lines by default
|
||||||
|
MD046:
|
||||||
|
style: 'fenced' # Force fenced code blocks
|
@ -2,6 +2,7 @@ node_modules
|
|||||||
src
|
src
|
||||||
deps
|
deps
|
||||||
.clang-format
|
.clang-format
|
||||||
|
flake.lock
|
||||||
|
|
||||||
# Minified files
|
# Minified files
|
||||||
**/*.min.css
|
**/*.min.css
|
||||||
@ -12,3 +13,8 @@ deps
|
|||||||
apps/ssb/tribute.esm.js
|
apps/ssb/tribute.esm.js
|
||||||
apps/api/app.js
|
apps/api/app.js
|
||||||
**/emojis.json
|
**/emojis.json
|
||||||
|
|
||||||
|
# only markdownlint should deal with the documentation
|
||||||
|
docs/**/*.md
|
||||||
|
|
||||||
|
NOTES.md
|
||||||
|
@ -3,8 +3,3 @@ useTabs: true
|
|||||||
semi: true
|
semi: true
|
||||||
singleQuote: true
|
singleQuote: true
|
||||||
bracketSpacing: false
|
bracketSpacing: false
|
||||||
# overrides:
|
|
||||||
# - files: '**/*.json'
|
|
||||||
# options:
|
|
||||||
# useTabs: false
|
|
||||||
# tabWidth: 2
|
|
||||||
|
37
CONTRIBUTING.md
Normal file
37
CONTRIBUTING.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Contributing to Tilde Friends
|
||||||
|
|
||||||
|
Thank you for your interest in Tilde Friends.
|
||||||
|
|
||||||
|
Above all, Tilde Friends aims to be a fun, safe place to play. When that is at
|
||||||
|
odds with the course of development, we will work through it with respectful
|
||||||
|
communication.
|
||||||
|
|
||||||
|
## How can I contribute?
|
||||||
|
|
||||||
|
The nature of Tilde Friends makes for a wide range of ways to contribute
|
||||||
|
|
||||||
|
- Just use it. Really, just kicking the tires will probably shake out issues
|
||||||
|
in useful ways at this point.
|
||||||
|
- Report and comment on bugs: https://dev.tildefriends.net/issues.
|
||||||
|
- Make apps. You don't need my permission to make and share apps with Tilde
|
||||||
|
Friends. I hope that an ecosystem of good apps grows outside of this
|
||||||
|
repository. If you want to recreate better versions of the stock apps, just
|
||||||
|
do it. If you make a better ssb app or whatever and drop me a line however
|
||||||
|
is most convenient for you, I will probably take a look and consider
|
||||||
|
replacing the stock one with it.
|
||||||
|
- Write about it. Docs in the git repository, blog posts, private messages to
|
||||||
|
me with ideas...really there is no wrong answer. Just make some noise, and
|
||||||
|
I'll do my best to incorporate or otherwise link your feedback and make the
|
||||||
|
most of it.
|
||||||
|
- Write C code in the git repository. I'm really striving for it to be the
|
||||||
|
case that other people don't really need to meddle in there, but if you can
|
||||||
|
help out, I will gladly review your pull requests via
|
||||||
|
https://dev.tildefriends.net/pulls.
|
||||||
|
|
||||||
|
## Best practices
|
||||||
|
|
||||||
|
- The C code is formatted with clang-format. Run `make format`.
|
||||||
|
- The rest is formatted with prettier. Run `npm run prettier`.
|
||||||
|
- We strive to have code compile on all platforms with no warnings and run with
|
||||||
|
no sanitizer issues.
|
||||||
|
- There are tests. Run `out/debug/tildefriends test`.
|
125
GNUmakefile
125
GNUmakefile
@ -3,9 +3,12 @@
|
|||||||
MAKEFLAGS += --warn-undefined-variables
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
VERSION_CODE := 16
|
VERSION_CODE := 19
|
||||||
VERSION_NUMBER := 0.0.16-wip
|
VERSION_NUMBER := 0.0.19-wip
|
||||||
VERSION_NAME := Medium English breakfast tea.
|
VERSION_NAME := Don't let your loyalty become a burden.
|
||||||
|
|
||||||
|
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
|
||||||
|
LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz
|
||||||
|
|
||||||
PROJECT = tildefriends
|
PROJECT = tildefriends
|
||||||
BUILD_DIR ?= out
|
BUILD_DIR ?= out
|
||||||
@ -14,6 +17,8 @@ UNAME_M := $(shell uname -m)
|
|||||||
|
|
||||||
ANDROID_SDK ?= ~/Android/Sdk
|
ANDROID_SDK ?= ~/Android/Sdk
|
||||||
|
|
||||||
|
HAVE_WIN := 0
|
||||||
|
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
||||||
else ifeq ($(UNAME_S),Linux)
|
else ifeq ($(UNAME_S),Linux)
|
||||||
@ -36,7 +41,6 @@ LDFLAGS += \
|
|||||||
-lc++abi
|
-lc++abi
|
||||||
HAVE_ANDROID := 0
|
HAVE_ANDROID := 0
|
||||||
HAVE_LINUX_IOS := 0
|
HAVE_LINUX_IOS := 0
|
||||||
HAVE_WIN := 0
|
|
||||||
else
|
else
|
||||||
$(error Unexpected host platform $(UNAME_S).)
|
$(error Unexpected host platform $(UNAME_S).)
|
||||||
endif
|
endif
|
||||||
@ -53,11 +57,11 @@ CFLAGS += \
|
|||||||
-fno-exceptions \
|
-fno-exceptions \
|
||||||
-g
|
-g
|
||||||
|
|
||||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
|
||||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-34
|
|
||||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.1.10909125
|
|
||||||
ANDROID_MIN_SDK_VERSION := 24
|
ANDROID_MIN_SDK_VERSION := 24
|
||||||
ANDROID_TARGET_SDK_VERSION := 34
|
ANDROID_TARGET_SDK_VERSION := 34
|
||||||
|
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
||||||
|
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
|
||||||
|
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.2.11394342
|
||||||
|
|
||||||
ANDROID_ARMV7A_TARGETS := \
|
ANDROID_ARMV7A_TARGETS := \
|
||||||
out/androiddebug-armv7a/tildefriends \
|
out/androiddebug-armv7a/tildefriends \
|
||||||
@ -209,14 +213,14 @@ $(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/li
|
|||||||
|
|
||||||
ifeq ($(UNAME_M),x86_64)
|
ifeq ($(UNAME_M),x86_64)
|
||||||
ifneq ($(UNAME_S),Haiku)
|
ifneq ($(UNAME_S),Haiku)
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(UNAME_M),aarch64)
|
ifeq ($(UNAME_M),aarch64)
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
endif
|
endif
|
||||||
|
|
||||||
get_objs = \
|
get_objs = \
|
||||||
@ -245,7 +249,6 @@ $(APP_OBJS): CFLAGS += \
|
|||||||
-Ideps/quickjs \
|
-Ideps/quickjs \
|
||||||
-Ideps/sqlite \
|
-Ideps/sqlite \
|
||||||
-Ideps/valgrind \
|
-Ideps/valgrind \
|
||||||
-Ideps/xopt \
|
|
||||||
-Wdouble-promotion \
|
-Wdouble-promotion \
|
||||||
-Werror
|
-Werror
|
||||||
ifeq ($(UNAME_M),x86_64)
|
ifeq ($(UNAME_M),x86_64)
|
||||||
@ -497,18 +500,6 @@ $(SQLITE_OBJS): CFLAGS += \
|
|||||||
-Wno-unused-function \
|
-Wno-unused-function \
|
||||||
-Wno-unused-variable
|
-Wno-unused-variable
|
||||||
|
|
||||||
XOPT_SOURCES := deps/xopt/xopt.c
|
|
||||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
|
||||||
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
|
||||||
-DHAVE_SNPRINTF \
|
|
||||||
-DHAVE_VSNPRINTF \
|
|
||||||
-DHAVE_VASNPRINTF \
|
|
||||||
-DHAVE_VASPRINTF \
|
|
||||||
-Dvsnprintf=rpl_vsnprintf
|
|
||||||
$(XOPT_OBJS): CFLAGS += \
|
|
||||||
-Wno-implicit-const-int-float-conversion \
|
|
||||||
-Wno-pointer-to-int-cast
|
|
||||||
|
|
||||||
QUICKJS_SOURCES := \
|
QUICKJS_SOURCES := \
|
||||||
deps/quickjs/cutils.c \
|
deps/quickjs/cutils.c \
|
||||||
deps/quickjs/libbf.c \
|
deps/quickjs/libbf.c \
|
||||||
@ -588,7 +579,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
|
|||||||
LDFLAGS += \
|
LDFLAGS += \
|
||||||
-pthread \
|
-pthread \
|
||||||
-lm
|
-lm
|
||||||
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto
|
-lcrypto
|
||||||
ifneq ($(UNAME_S),Haiku)
|
ifneq ($(UNAME_S),Haiku)
|
||||||
@ -625,7 +616,7 @@ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
|||||||
|
|
||||||
unix: debug release
|
unix: debug release
|
||||||
win: windebug winrelease
|
win: windebug winrelease
|
||||||
all: $(BUILD_TYPES)
|
all: $(BUILD_TYPES) default.nix
|
||||||
.PHONY: all win unix
|
.PHONY: all win unix
|
||||||
|
|
||||||
ALL_APP_OBJS := \
|
ALL_APP_OBJS := \
|
||||||
@ -637,8 +628,7 @@ ALL_APP_OBJS := \
|
|||||||
$(QUICKJS_OBJS) \
|
$(QUICKJS_OBJS) \
|
||||||
$(SODIUM_OBJS) \
|
$(SODIUM_OBJS) \
|
||||||
$(SQLITE_OBJS) \
|
$(SQLITE_OBJS) \
|
||||||
$(UV_OBJS) \
|
$(UV_OBJS)
|
||||||
$(XOPT_OBJS)
|
|
||||||
|
|
||||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||||
-include $(DEPS)
|
-include $(DEPS)
|
||||||
@ -683,6 +673,10 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
|||||||
-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
|
-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
|
||||||
$@
|
$@
|
||||||
|
|
||||||
|
default.nix : $(firstword $(MAKEFILE_LIST))
|
||||||
|
@echo "[version] $@"
|
||||||
|
@sed -i -e 's/version = ".*";/version = "$(VERSION_NUMBER)";/' $@
|
||||||
|
|
||||||
# Android support.
|
# Android support.
|
||||||
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@ -703,7 +697,7 @@ CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefrien
|
|||||||
|
|
||||||
$(CLASS_FILES) &: $(JAVA_FILES)
|
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||||
@echo "[javac] $(CLASS_FILES)"
|
@echo "[javac] $(CLASS_FILES)"
|
||||||
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
@javac --release 8 -encoding UTF-8 -Xlint:deprecation -XDuseUnsharedTable=true -classpath $(ANDROID_PLATFORM)/android.jar:$(ANDROID_BUILD_TOOLS)/core-lambda-stubs.jar -d out/classes $(JAVA_FILES)
|
||||||
|
|
||||||
out/apk/classes.dex: $(CLASS_FILES)
|
out/apk/classes.dex: $(CLASS_FILES)
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@ -717,7 +711,7 @@ PACKAGE_DIRS := \
|
|||||||
deps/prettier/ \
|
deps/prettier/ \
|
||||||
deps/lit/
|
deps/lit/
|
||||||
|
|
||||||
RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
RAW_FILES := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
||||||
|
|
||||||
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
||||||
@ -736,10 +730,11 @@ out/apk/TildeFriends-arm-%.unsigned.apk:
|
|||||||
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||||
@cp out/apk/res.apk $@
|
@cp out/apk/res.apk $@.zip
|
||||||
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
||||||
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
out/apk/TildeFriends-x86-%.unsigned.apk:
|
out/apk/TildeFriends-x86-%.unsigned.apk:
|
||||||
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||||
@ -748,21 +743,27 @@ out/apk/TildeFriends-x86-%.unsigned.apk:
|
|||||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||||
@cp out/apk/res.apk $@
|
@cp out/apk/res.apk $@.zip
|
||||||
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
||||||
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
out/%.apk: out/apk/%.unsigned.apk
|
out/%.apk: out/apk/%.unsigned.apk
|
||||||
@echo "[apksigner] $(notdir $@)"
|
@echo "[apksigner] $(notdir $@)"
|
||||||
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
|
||||||
|
|
||||||
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk
|
out/%.zopfli.apk: out/%.apk
|
||||||
|
@echo "[zopfli] $(notdir $@)"
|
||||||
|
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
|
||||||
|
|
||||||
|
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk
|
||||||
.PHONY: release-apk
|
.PHONY: release-apk
|
||||||
|
|
||||||
releaseapkgo: out/TildeFriends-arm-release.apk
|
releaseapkgo: out/TildeFriends-arm-release.apk
|
||||||
@adb install -r $<
|
@adb install -r $<
|
||||||
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||||
.PHONY: releaseapkgo
|
.PHONY: releaseapkgo
|
||||||
|
|
||||||
# iOS Support
|
# iOS Support
|
||||||
@ -773,10 +774,10 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
|
|||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@cp -v $< $@
|
@cp -v $< $@
|
||||||
|
|
||||||
out/%/data.zip: $(RAW_FILES)
|
out/data.zip: $(RAW_FILES)
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@ -q -9 $(RAW_FILES)
|
||||||
|
|
||||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip
|
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@cp -v $< $@
|
@cp -v $< $@
|
||||||
ifeq ($(HAVE_LINUX_IOS),1)
|
ifeq ($(HAVE_LINUX_IOS),1)
|
||||||
@ -791,6 +792,16 @@ out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
|||||||
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
||||||
@rm -rf $@.tmp/
|
@rm -rf $@.tmp/
|
||||||
|
|
||||||
|
|
||||||
|
out/%/tildefriends.standalone: out/%/tildefriends out/data.zip
|
||||||
|
@echo "[standalone] $@"
|
||||||
|
@cat $< out/data.zip > $@
|
||||||
|
@chmod +x $@
|
||||||
|
out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip
|
||||||
|
@echo "[standalone] $@"
|
||||||
|
@cat $< out/data.zip > $@
|
||||||
|
@chmod +x $@
|
||||||
|
|
||||||
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
||||||
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
||||||
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
||||||
@ -814,11 +825,13 @@ apklog:
|
|||||||
|
|
||||||
fetchdeps:
|
fetchdeps:
|
||||||
@echo "[fetch] libuv"
|
@echo "[fetch] libuv"
|
||||||
@test -f out/deps/libuv.tar.gz || (mkdir -p out/deps/ && curl -q https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz -o out/deps/libuv.tar.gz)
|
@test -f out/deps/libuv.tar.gz && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (mkdir -p out/deps/ && curl -q $(LIBUV_URL) -o out/deps/libuv.tar.gz)
|
||||||
@test -d deps/libuv/ || (mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
|
@test -d deps/libuv/ && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (rm -rf deps/libuv/ && mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
|
||||||
|
@echo -n $(LIBUV_URL) > out/deps/libuv.txt
|
||||||
@echo "[fetch] sqlite"
|
@echo "[fetch] sqlite"
|
||||||
@test -f out/deps/sqlite.zip || (mkdir -p out/deps/ && curl -q https://www.sqlite.org/2024/sqlite-amalgamation-3450100.zip -o out/deps/sqlite.zip)
|
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
|
||||||
@test -d deps/sqlite/ || (mkdir -p deps/sqlite/ && unzip -qDj -d deps/sqlite/ out/deps/sqlite.zip)
|
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
|
||||||
|
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
|
||||||
@echo "[fetch] prettier"
|
@echo "[fetch] prettier"
|
||||||
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
|
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
|
||||||
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
|
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
|
||||||
@ -839,7 +852,7 @@ $(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
IOS_DEPS := deps/openssl/ios/usr/local/lib/libssl.a
|
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
|
||||||
$(IOS_DEPS):
|
$(IOS_DEPS):
|
||||||
+@tools/ssl-ios
|
+@tools/ssl-ios
|
||||||
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
|
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
|
||||||
@ -849,13 +862,12 @@ clean:
|
|||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
|
||||||
dist: release-apk iosrelease-ipa
|
dist: release-apk iosrelease-ipa $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) default.nix
|
||||||
@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
|
@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
@rm -rf out/tildefriends-$(VERSION_NUMBER)
|
@rm -rf out/tildefriends-$(VERSION_NUMBER)
|
||||||
@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
|
@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
|
||||||
@git archive main | tar -x -C out/tildefriends-$(VERSION_NUMBER)
|
@git ls-files --recurse-submodules | tar -c -T- | tar -x -C out/tildefriends-$(VERSION_NUMBER)
|
||||||
@tar \
|
@tar \
|
||||||
--exclude=apps/gg* \
|
|
||||||
--exclude=apps/welcome* \
|
--exclude=apps/welcome* \
|
||||||
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
||||||
--exclude=deps/libsodium/builds/msvc/vs* \
|
--exclude=deps/libsodium/builds/msvc/vs* \
|
||||||
@ -870,14 +882,17 @@ dist: release-apk iosrelease-ipa
|
|||||||
--exclude=deps/sqlite/shell.c \
|
--exclude=deps/sqlite/shell.c \
|
||||||
--exclude=deps/zlib/contrib/vstudio \
|
--exclude=deps/zlib/contrib/vstudio \
|
||||||
--exclude=deps/zlib/doc \
|
--exclude=deps/zlib/doc \
|
||||||
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz out/tildefriends-$(VERSION_NUMBER)
|
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \
|
||||||
#@rm -rf out/tildefriends-$(VERSION_NUMBER)
|
-C out/ \
|
||||||
|
tildefriends-$(VERSION_NUMBER)
|
||||||
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
||||||
@cp out/TildeFriends-x86-release.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
|
@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
|
||||||
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
||||||
@cp out/TildeFriends-arm-release.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
|
@cp out/TildeFriends-arm-release.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
|
||||||
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
||||||
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
|
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
|
||||||
|
@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
|
||||||
|
@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
|
||||||
.PHONY: dist
|
.PHONY: dist
|
||||||
|
|
||||||
dist-test: dist
|
dist-test: dist
|
||||||
@ -891,6 +906,10 @@ format:
|
|||||||
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
|
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
@npm run prettier
|
||||||
|
.PHONY: prettier
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
@doxygen
|
@doxygen
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
|||||||
Copyright 2014 Cory McWilliams
|
Copyright 2014-2024 Cory McWilliams
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
21
README.md
21
README.md
@ -12,26 +12,11 @@ It is both a peer-to-peer social network client, participating in Secure Scuttle
|
|||||||
2. Provide security that is easy to understand and protects your data.
|
2. Provide security that is easy to understand and protects your data.
|
||||||
3. Make creating and sharing web applications accessible to anyone with a browser.
|
3. Make creating and sharing web applications accessible to anyone with a browser.
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for all of those host platforms plus mingw64, iOS, and android.
|
|
||||||
|
|
||||||
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies are kept up to date in the tree.
|
|
||||||
2. To build, run `make debug` or `make release`. An executable will be generated in a subdirectory of `out/`.
|
|
||||||
3. It's possible to build for Android, iOS, and Windows on Linux, if you have the right dependencies in the right places. `make windebug winrelease iosdebug-ipa iosrelease-ipa release-apk`.
|
|
||||||
4. To build in docker, `docker build .`.
|
|
||||||
5. `make format` will normalize formatting to the coding standard.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
By default, running the built `tildefriends` executable will start a web server at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
|
||||||
|
|
||||||
The first user to create an account and log in will be granted administrative privileges. Further administration can be done at <http://localhost:12345/~core/admin/>.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Docs are a work in progress: <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
Docs are a work in progress: [documentation](https://dev.tildefriends.net/cory/tildefriends/src/branch/main/docs/index.md), or alternatively in Tilde Friends: <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All code unless otherwise noted in is provided under the [MIT](https://opensource.org/licenses/MIT) license.
|
All code, documentation and assets unless otherwise noted in is provided under the
|
||||||
|
[MIT](https://opensource.org/licenses/MIT/) license.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🎛"
|
"emoji": "🎛",
|
||||||
|
"previous": "&vrpS/vE7n588iYv1p8HafDxHB+YDHTrtUbJiu9nGA9I=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,38 @@
|
|||||||
<script>
|
<script>
|
||||||
const g_data = $data;
|
const g_data = $data;
|
||||||
</script>
|
</script>
|
||||||
|
<link rel="stylesheet" href="w3.css" />
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<style>
|
||||||
|
/* 2018 Valiant Poppy */
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#96302e !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#702423 !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#bd3d3a !important}
|
||||||
|
.w3-text-theme {color:#bd3d3a !important}
|
||||||
|
.w3-border-theme {border-color:#bd3d3a !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#bd3d3a !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#bd3d3a !important}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff; width: 100%">
|
<body class="w3-theme-l4">
|
||||||
<h1>Tilde Friends Administration</h1>
|
<header class="w3-row w3-padding w3-header w3-theme-l1">
|
||||||
|
<h1>Tilde Friends Administration</h1>
|
||||||
|
</header>
|
||||||
</body>
|
</body>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
@ -32,61 +32,77 @@ 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`
|
||||||
<div style="margin-top: 1em">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
|
||||||
<div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
<input class="w3-quarter w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
||||||
<div>${description.description}</div>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
} else if (description.type === 'textarea') {
|
} else if (description.type === 'textarea') {
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 1em"">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
|
||||||
<div style="width: 100%; padding: 0; margin: 0">
|
>${key}</label
|
||||||
<div style="width: 90%; padding: 0 margin: 0">
|
>
|
||||||
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
|
<div class="w3-rest w3-padding">${description.description}</div>
|
||||||
</div>
|
<textarea
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
|
class="w3-input"
|
||||||
<div>${description.description}</div>
|
style="vertical-align: top; resize: vertical"
|
||||||
</div>
|
id=${'gs_' + key}
|
||||||
</div>
|
>
|
||||||
|
${description.value}</textarea
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-right w3-quarter w3-theme-action"
|
||||||
|
@click=${(e) =>
|
||||||
|
global_settings_set(
|
||||||
|
key,
|
||||||
|
e.srcElement.previousElementSibling.value
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Set
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 1em">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
|
||||||
<div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||||
<div>${description.description}</div>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const user_template = (user, permissions) => html`
|
const user_template = (user, permissions) => html`
|
||||||
<li>
|
<li class="w3-card w3-margin">
|
||||||
<button @click=${(e) => delete_user(user)}>Delete</button>
|
<button
|
||||||
|
class="w3-button w3-theme-action"
|
||||||
|
@click=${(e) => delete_user(user)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
${user}: ${permissions.map((x) => permission_template(x))}
|
${user}: ${permissions.map((x) => permission_template(x))}
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
const users_template = (users) =>
|
const users_template = (users) =>
|
||||||
html`<h2>Users</h2>
|
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
|
||||||
<ul>
|
<ul class="w3-ul">
|
||||||
${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 style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||||
<h2>Global Settings</h2>
|
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
|
||||||
<div>
|
<div class="w3-container">
|
||||||
${Object.keys(data.settings)
|
<ul class="w3-ul">
|
||||||
.sort()
|
${Object.keys(data.settings)
|
||||||
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
.sort()
|
||||||
|
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
${users_template(data.users)}
|
${users_template(data.users)}
|
||||||
</div>`;
|
</div> `;
|
||||||
render(page_template(g_data), document.body);
|
render(page_template(g_data), document.body);
|
||||||
});
|
});
|
||||||
|
235
apps/admin/w3.css
Normal file
235
apps/admin/w3.css
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💻",
|
"emoji": "💻",
|
||||||
"previous": "&RdVEsVscZm3aWzcMrEZS8mskO5tUmvaEUihex2MMfZQ=.sha256"
|
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -28,10 +28,10 @@ async function fetch_shared_apps() {
|
|||||||
|
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages_fts('"application/tildefriends"')
|
FROM messages_fts('"application/tildefriends"')
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
ORDER BY timestamp
|
ORDER BY messages.timestamp
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
function (row) {
|
function (row) {
|
||||||
|
4
apps/blog/lit-all.min.js
vendored
4
apps/blog/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💽"
|
"emoji": "💽"
|
||||||
}
|
}
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app",
|
|
||||||
"emoji": "🗺",
|
|
||||||
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
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();
|
|
File diff suppressed because one or more lines are too long
@ -1,84 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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();
|
|
@ -1,26 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,661 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
120
apps/gg/lit-all.min.js
vendored
120
apps/gg/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,162 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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};
|
|
@ -1,909 +0,0 @@
|
|||||||
import {
|
|
||||||
LitElement,
|
|
||||||
html,
|
|
||||||
unsafeHTML,
|
|
||||||
css,
|
|
||||||
guard,
|
|
||||||
until,
|
|
||||||
} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
import * as polyline from './polyline.js';
|
|
||||||
import {gpx_parse} from './gpx.js';
|
|
||||||
|
|
||||||
const k_client_id = '28276';
|
|
||||||
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
|
|
||||||
|
|
||||||
const k_color_snow = [128, 128, 255, 255];
|
|
||||||
const k_color_ice = [160, 160, 255, 255];
|
|
||||||
const k_color_water = [0, 0, 255, 255];
|
|
||||||
const k_color_dirt = [128, 129, 130, 255];
|
|
||||||
const k_color_pavement = [32, 32, 32, 255];
|
|
||||||
const k_color_grass = [0, 255, 0, 255];
|
|
||||||
const k_color_default = [128, 128, 128, 255];
|
|
||||||
|
|
||||||
const k_store = {
|
|
||||||
'🦞': 15,
|
|
||||||
'🛶': 10,
|
|
||||||
'🏠': 10,
|
|
||||||
'⛰': 10,
|
|
||||||
'🐠': 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const k_marker_snap = {x: 5, y: 4};
|
|
||||||
|
|
||||||
class GgAppElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
user: {type: Object},
|
|
||||||
strava: {type: Object},
|
|
||||||
activities: {type: Array},
|
|
||||||
activity: {type: Object},
|
|
||||||
world: {type: Object},
|
|
||||||
whoami: {type: String},
|
|
||||||
status: {type: Object},
|
|
||||||
tab: {type: String},
|
|
||||||
url: {type: String},
|
|
||||||
currency: {type: Number},
|
|
||||||
to_build: {type: String},
|
|
||||||
emoji_of_the_day: {type: String},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.activities = [];
|
|
||||||
this.activity = {};
|
|
||||||
this.loaded_activities = [];
|
|
||||||
this.placed_emojis = [];
|
|
||||||
this.strava = {};
|
|
||||||
this.min_lat = Number.MAX_VALUE;
|
|
||||||
this.min_lon = Number.MAX_VALUE;
|
|
||||||
this.max_lat = -Number.MAX_VALUE;
|
|
||||||
this.max_lon = -Number.MAX_VALUE;
|
|
||||||
this.focus = undefined;
|
|
||||||
this.status = undefined;
|
|
||||||
this.tab = 'map';
|
|
||||||
this.load().catch(function (e) {
|
|
||||||
console.log('load error', e);
|
|
||||||
});
|
|
||||||
this.to_build = '🏠';
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
console.log('load');
|
|
||||||
let emojis = await (await fetch('emojis.json')).json();
|
|
||||||
emojis = Object.values(emojis)
|
|
||||||
.map((x) => Object.values(x))
|
|
||||||
.flat();
|
|
||||||
let today = new Date();
|
|
||||||
let date_index =
|
|
||||||
today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
|
|
||||||
this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length];
|
|
||||||
this.user = await tfrpc.rpc.getUser();
|
|
||||||
this.url = (await tfrpc.rpc.url()).split('?')[0];
|
|
||||||
try {
|
|
||||||
await this.update_credentials();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('update_credentials failed', e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.update_activities();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('update_activities failed', e);
|
|
||||||
}
|
|
||||||
await this.acquire_ssb_identity();
|
|
||||||
if (this.whoami && this.activities?.length) {
|
|
||||||
await this.sync_activities();
|
|
||||||
}
|
|
||||||
await this.get_activities_from_ssb();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b?permalink_comment_id=3591045#gistcomment-3591045 */
|
|
||||||
async promise_all(promises, max_concurrent) {
|
|
||||||
let index = 0;
|
|
||||||
let results = [];
|
|
||||||
async function exec_thread() {
|
|
||||||
while (index < promises.length) {
|
|
||||||
const current = index++;
|
|
||||||
results[current] = await promises[current];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const threads = [];
|
|
||||||
for (let thread = 0; thread < max_concurrent; thread++) {
|
|
||||||
threads.push(exec_thread());
|
|
||||||
}
|
|
||||||
await Promise.all(threads);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_activities_from_ssb() {
|
|
||||||
this.status = {text: 'loading activities'};
|
|
||||||
this.loaded_activities = [];
|
|
||||||
let rows = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
|
|
||||||
FROM messages_fts('"gg-activity"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid,
|
|
||||||
json_each(messages.content, '$.mentions') as mention
|
|
||||||
WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
|
|
||||||
json_extract(mention.value, '$.name') = 'activity_data'
|
|
||||||
ORDER BY messages.timestamp DESC
|
|
||||||
`,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
this.status = {text: 'loading activity data'};
|
|
||||||
let authors = rows.map((x) => x.author);
|
|
||||||
let blobs = await this.promise_all(
|
|
||||||
rows.map((x) => tfrpc.rpc.get_blob(x.blob_id)),
|
|
||||||
8
|
|
||||||
);
|
|
||||||
this.status = {text: 'processing activity data'};
|
|
||||||
for (let [index, blob] of blobs.entries()) {
|
|
||||||
let activity;
|
|
||||||
try {
|
|
||||||
activity = JSON.parse(blob);
|
|
||||||
} catch {
|
|
||||||
activity = gpx_parse(blob);
|
|
||||||
}
|
|
||||||
if (activity) {
|
|
||||||
activity.author = authors[index];
|
|
||||||
this.loaded_activities.push(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.status = {text: 'calculating balance'};
|
|
||||||
rows = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
|
|
||||||
`,
|
|
||||||
[this.whoami]
|
|
||||||
);
|
|
||||||
let currency = rows[0].currency;
|
|
||||||
rows = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
|
|
||||||
`,
|
|
||||||
[this.whoami]
|
|
||||||
);
|
|
||||||
let spent = rows[0].cost;
|
|
||||||
this.currency = currency - spent;
|
|
||||||
this.status = {text: 'getting placed emojis'};
|
|
||||||
rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT messages.content
|
|
||||||
FROM messages_fts('"gg-place"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
||||||
WHERE json_extract(messages.content, '$.type') = 'gg-place'
|
|
||||||
ORDER BY messages.timestamp
|
|
||||||
`);
|
|
||||||
for (let row of rows) {
|
|
||||||
console.log(row.content);
|
|
||||||
let content = JSON.parse(row.content);
|
|
||||||
this.placed_emojis.push({
|
|
||||||
position: content.position,
|
|
||||||
emoji: content.emoji,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log(this.placed_emojis);
|
|
||||||
this.status = undefined;
|
|
||||||
this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
async sync_activities() {
|
|
||||||
let ids = this.activities.map(
|
|
||||||
(x) => `https://www.strava.com/activities/${x.id}`
|
|
||||||
);
|
|
||||||
let missing = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
WITH my_activities AS (
|
|
||||||
SELECT json_extract(mention.value, '$.link') AS url
|
|
||||||
FROM messages, json_each(messages.content, '$.mentions') AS mention
|
|
||||||
WHERE
|
|
||||||
author = ? AND
|
|
||||||
json_extract(messages.content, '$.type') = 'gg-activity' AND
|
|
||||||
json_extract(mention.value, '$.name') = 'activity_url')
|
|
||||||
SELECT from_strava.value FROM json_each(?) AS from_strava
|
|
||||||
LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
|
|
||||||
WHERE my_activities.url IS NULL
|
|
||||||
`,
|
|
||||||
[this.whoami, JSON.stringify(ids)]
|
|
||||||
);
|
|
||||||
console.log('missing = ', missing);
|
|
||||||
for (let [index, row] of missing.entries()) {
|
|
||||||
this.status = {
|
|
||||||
text: 'syncing from strava',
|
|
||||||
value: index,
|
|
||||||
max: missing.length,
|
|
||||||
};
|
|
||||||
let url = row.value;
|
|
||||||
let id = url.match(/.*\/(\d+)/)[1];
|
|
||||||
let response = await fetch(
|
|
||||||
`https://www.strava.com/api/v3/activities/${id}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${this.strava.access_token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let activity = await response.json();
|
|
||||||
let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
|
|
||||||
let message = {
|
|
||||||
type: 'gg-activity',
|
|
||||||
mentions: [
|
|
||||||
{
|
|
||||||
link: url,
|
|
||||||
name: 'activity_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: blob_id,
|
|
||||||
name: 'activity_data',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
}
|
|
||||||
this.status = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquire_ssb_identity() {
|
|
||||||
let user = await tfrpc.rpc.getUser();
|
|
||||||
if (!user?.credentials?.session?.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ids = await tfrpc.rpc.getIdentities();
|
|
||||||
let players = ids.length
|
|
||||||
? (
|
|
||||||
await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
|
|
||||||
WHERE
|
|
||||||
json_extract(messages.content, '$.type') = 'gg-player' AND
|
|
||||||
json_extract(messages.content, '$.active')
|
|
||||||
ORDER BY timestamp DESC limit 1
|
|
||||||
`,
|
|
||||||
[JSON.stringify(ids)]
|
|
||||||
)
|
|
||||||
).map((row) => row.author)
|
|
||||||
: [];
|
|
||||||
if (!players.length) {
|
|
||||||
this.whoami = await tfrpc.rpc.createIdentity();
|
|
||||||
if (this.whoami) {
|
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, {
|
|
||||||
type: 'gg-player',
|
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
players.sort();
|
|
||||||
this.whoami = players[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_credentials() {
|
|
||||||
let name = this.user?.credentials?.session?.name;
|
|
||||||
if (!name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let shared = await tfrpc.rpc.sharedDatabaseGet(name);
|
|
||||||
if (shared) {
|
|
||||||
await tfrpc.rpc.databaseSet('strava', shared);
|
|
||||||
await tfrpc.rpc.sharedDatabaseRemove(name);
|
|
||||||
}
|
|
||||||
this.strava = JSON.parse((await tfrpc.rpc.databaseGet('strava')) || '{}');
|
|
||||||
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
|
|
||||||
console.log(
|
|
||||||
'this looks expired',
|
|
||||||
new Date().valueOf() / 1000,
|
|
||||||
'>',
|
|
||||||
this.strava.expires_at
|
|
||||||
);
|
|
||||||
let x = await tfrpc.rpc.refresh_token(this.strava);
|
|
||||||
if (x) {
|
|
||||||
this.strava = x;
|
|
||||||
await tfrpc.rpc.databaseSet('strava', JSON.stringify(x));
|
|
||||||
} else {
|
|
||||||
this.strava = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_activities() {
|
|
||||||
if (this?.strava?.access_token) {
|
|
||||||
let response = await fetch(
|
|
||||||
'https://www.strava.com/api/v3/athlete/activities',
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${this.strava.access_token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this.activities = await response.json();
|
|
||||||
this.activities.sort((a, b) => a.id - b.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color_to_emoji(color) {
|
|
||||||
const k_map = [
|
|
||||||
[k_color_snow, '⬜'],
|
|
||||||
[k_color_ice, '🟦'],
|
|
||||||
[k_color_water, '🟦'],
|
|
||||||
[k_color_dirt, '🟫'],
|
|
||||||
[k_color_pavement, '⬛'],
|
|
||||||
[k_color_grass, '🟩'],
|
|
||||||
[k_color_default, '🟧'],
|
|
||||||
];
|
|
||||||
for (let m of k_map) {
|
|
||||||
if (
|
|
||||||
m[0][0] == color[0] &&
|
|
||||||
m[0][1] == color[1] &&
|
|
||||||
m[0][2] == color[2] &&
|
|
||||||
m[0][3] == color[3]
|
|
||||||
) {
|
|
||||||
return m[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity_bounds(activity) {
|
|
||||||
let min_lat = Number.MAX_VALUE;
|
|
||||||
let min_lon = Number.MAX_VALUE;
|
|
||||||
let max_lat = -Number.MAX_VALUE;
|
|
||||||
let max_lon = -Number.MAX_VALUE;
|
|
||||||
if (activity?.map?.polyline) {
|
|
||||||
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
||||||
min_lat = Math.min(min_lat, pt[0]);
|
|
||||||
min_lon = Math.min(min_lon, pt[1]);
|
|
||||||
max_lat = Math.max(max_lat, pt[0]);
|
|
||||||
max_lon = Math.max(max_lon, pt[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activity?.segments) {
|
|
||||||
for (let segment of activity.segments) {
|
|
||||||
for (let pt of segment) {
|
|
||||||
min_lat = Math.min(min_lat, pt.lat);
|
|
||||||
min_lon = Math.min(min_lon, pt.lon);
|
|
||||||
max_lat = Math.max(max_lat, pt.lat);
|
|
||||||
max_lon = Math.max(max_lon, pt.lon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
min: {
|
|
||||||
lat: min_lat,
|
|
||||||
lng: min_lon,
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
lat: max_lat,
|
|
||||||
lng: max_lon,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
on_click(event) {
|
|
||||||
let popup = L.popup()
|
|
||||||
.setLatLng(event.latlng)
|
|
||||||
.setContent(
|
|
||||||
`
|
|
||||||
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.openOn(this.leaflet);
|
|
||||||
}
|
|
||||||
|
|
||||||
async build() {
|
|
||||||
if (this.popup) {
|
|
||||||
this.popup.remove();
|
|
||||||
}
|
|
||||||
if (!this.marker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let latlng = this.marker.getLatLng();
|
|
||||||
|
|
||||||
let cost = k_store[this.to_build];
|
|
||||||
if (cost > this.currency) {
|
|
||||||
alert('Insufficient funds.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let message = {
|
|
||||||
type: 'gg-place',
|
|
||||||
position: {lat: latlng.lat, lng: latlng.lng},
|
|
||||||
emoji: this.to_build,
|
|
||||||
cost: cost,
|
|
||||||
};
|
|
||||||
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
this.marker.remove();
|
|
||||||
this.placed_emojis.push({
|
|
||||||
position: {lat: latlng.lat, lng: latlng.lng},
|
|
||||||
emoji: this.to_build,
|
|
||||||
});
|
|
||||||
this.currency -= cost;
|
|
||||||
return this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
on_marker_click(event) {
|
|
||||||
this.popup = L.popup()
|
|
||||||
.setLatLng(event.latlng)
|
|
||||||
.setContent(
|
|
||||||
`
|
|
||||||
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.openOn(this.leaflet);
|
|
||||||
}
|
|
||||||
|
|
||||||
snap_to_grid(latlng, fudge, zoom) {
|
|
||||||
let position = this.leaflet.options.crs.latLngToPoint(
|
|
||||||
latlng,
|
|
||||||
zoom ?? this.leaflet.getZoom()
|
|
||||||
);
|
|
||||||
position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
|
|
||||||
position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
|
|
||||||
position = this.leaflet.options.crs.pointToLatLng(
|
|
||||||
position,
|
|
||||||
zoom ?? this.leaflet.getZoom()
|
|
||||||
);
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
on_marker_move(event) {
|
|
||||||
if (!this.no_snap && this.marker) {
|
|
||||||
this.no_snap = true;
|
|
||||||
this.marker.setLatLng(
|
|
||||||
this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)
|
|
||||||
);
|
|
||||||
this.no_snap = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_zoom(event) {
|
|
||||||
if (this.marker) {
|
|
||||||
this.marker.setLatLng(
|
|
||||||
this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_mouse_down(event) {
|
|
||||||
if (this.marker) {
|
|
||||||
this.marker.remove();
|
|
||||||
this.marker = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.to_build) {
|
|
||||||
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {
|
|
||||||
icon: L.divIcon({className: 'build-icon'}),
|
|
||||||
draggable: true,
|
|
||||||
}).addTo(this.leaflet);
|
|
||||||
this.marker.on({click: this.on_marker_click.bind(this)});
|
|
||||||
this.marker.on({drag: this.on_marker_move.bind(this)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_map() {
|
|
||||||
let map = this.shadowRoot.getElementById('map');
|
|
||||||
if (!map || !this.loaded_activities.length) {
|
|
||||||
this.leaflet = undefined;
|
|
||||||
this.grid_layer = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.leaflet) {
|
|
||||||
this.leaflet = L.map(map, {
|
|
||||||
attributionControl: false,
|
|
||||||
maxZoom: 16,
|
|
||||||
bounceAtZoomLimits: false,
|
|
||||||
});
|
|
||||||
this.leaflet.on({contextmenu: this.on_click.bind(this)});
|
|
||||||
this.leaflet.on({click: this.on_mouse_down.bind(this)});
|
|
||||||
this.leaflet.on({zoom: this.on_zoom.bind(this)});
|
|
||||||
}
|
|
||||||
let self = this;
|
|
||||||
let grid_layer = L.GridLayer.extend({
|
|
||||||
createTile: function (coords) {
|
|
||||||
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
|
||||||
var size = this.getTileSize();
|
|
||||||
tile.width = size.x;
|
|
||||||
tile.height = size.y;
|
|
||||||
var context = tile.getContext('2d');
|
|
||||||
context.font = '10pt sans';
|
|
||||||
let bounds = this._tileCoordsToBounds(coords);
|
|
||||||
let degrees = 360.0 / 2 ** coords.z;
|
|
||||||
let ul = bounds.getNorthWest();
|
|
||||||
let lr = bounds.getSouthEast();
|
|
||||||
|
|
||||||
let mini = document.createElement('canvas');
|
|
||||||
mini.width = Math.floor(size.x / 16.0);
|
|
||||||
mini.height = Math.floor(size.y / 16.0);
|
|
||||||
let mini_context = mini.getContext('2d');
|
|
||||||
let image_data = context.getImageData(0, 0, mini.width, mini.height);
|
|
||||||
for (let activity of self.loaded_activities) {
|
|
||||||
self.draw_activity_to_tile(
|
|
||||||
image_data,
|
|
||||||
mini.width,
|
|
||||||
mini.height,
|
|
||||||
ul,
|
|
||||||
lr,
|
|
||||||
activity
|
|
||||||
);
|
|
||||||
}
|
|
||||||
context.textAlign = 'left';
|
|
||||||
context.textBaseline = 'bottom';
|
|
||||||
for (let x = 0; x < mini.width; x++) {
|
|
||||||
for (let y = 0; y < mini.height; y++) {
|
|
||||||
let start = (y * mini.width + x) * 4;
|
|
||||||
let pixel = self.color_to_emoji(
|
|
||||||
image_data.data.slice(start, start + 4)
|
|
||||||
);
|
|
||||||
if (pixel) {
|
|
||||||
//context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height);
|
|
||||||
context.fillText(
|
|
||||||
pixel,
|
|
||||||
(x * size.x) / mini.width,
|
|
||||||
(y * size.y) / mini.height + mini.height
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let placed of self.placed_emojis) {
|
|
||||||
let position = self.leaflet.options.crs.latLngToPoint(
|
|
||||||
self.snap_to_grid(placed.position, undefined, coords.z),
|
|
||||||
coords.z
|
|
||||||
);
|
|
||||||
let tile_x = Math.floor(position.x / size.x);
|
|
||||||
let tile_y = Math.floor(position.y / size.y);
|
|
||||||
position.x = position.x - tile_x * size.x;
|
|
||||||
position.y = position.y - tile_y * size.y;
|
|
||||||
if (tile_x == coords.x && tile_y == coords.y) {
|
|
||||||
//context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height);
|
|
||||||
context.fillText(
|
|
||||||
placed.emoji,
|
|
||||||
position.x,
|
|
||||||
position.y + mini.height
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tile;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (this.grid_layer) {
|
|
||||||
this.grid_layer.redraw();
|
|
||||||
} else {
|
|
||||||
this.grid_layer = new grid_layer();
|
|
||||||
this.grid_layer.addTo(this.leaflet);
|
|
||||||
}
|
|
||||||
for (let activity of this.loaded_activities) {
|
|
||||||
let bounds = this.activity_bounds(activity);
|
|
||||||
this.min_lat = Math.min(this.min_lat, bounds.min.lat);
|
|
||||||
this.min_lon = Math.min(this.min_lon, bounds.min.lng);
|
|
||||||
this.max_lat = Math.max(this.max_lat, bounds.max.lat);
|
|
||||||
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
|
|
||||||
}
|
|
||||||
if (this.focus) {
|
|
||||||
this.leaflet.fitBounds([this.focus.min, this.focus.max]);
|
|
||||||
this.focus = undefined;
|
|
||||||
} else {
|
|
||||||
this.leaflet.fitBounds([
|
|
||||||
[this.min_lat, this.min_lon],
|
|
||||||
[this.max_lat, this.max_lon],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity_to_color(activity) {
|
|
||||||
let color = [0, 0, 0, 255];
|
|
||||||
switch (activity.sport_type) {
|
|
||||||
/* Implies snow. */
|
|
||||||
case 'AlpineSki':
|
|
||||||
case 'BackcountrySki':
|
|
||||||
case 'NordicSki':
|
|
||||||
case 'Snowshoe':
|
|
||||||
case 'Snowboard':
|
|
||||||
color = k_color_snow;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies ice. */
|
|
||||||
case 'IceSkate':
|
|
||||||
case 'InlineSkate':
|
|
||||||
color = k_color_ice;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies water. */
|
|
||||||
case 'Canoeing':
|
|
||||||
case 'Kayaking':
|
|
||||||
case 'Kitesurf':
|
|
||||||
case 'Rowing':
|
|
||||||
case 'Sail':
|
|
||||||
case 'StandUpPaddling':
|
|
||||||
case 'Surfing':
|
|
||||||
case 'Swim':
|
|
||||||
case 'Windsurf':
|
|
||||||
color = k_color_water;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies dirt. */
|
|
||||||
case 'EMountainBikeRide':
|
|
||||||
case 'Hike':
|
|
||||||
case 'MountainBikeRide':
|
|
||||||
case 'RockClimbing':
|
|
||||||
case 'TrailRun':
|
|
||||||
color = k_color_dirt;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies pavement. */
|
|
||||||
case 'EBikeRide':
|
|
||||||
case 'GravelRide':
|
|
||||||
case 'Handcycle':
|
|
||||||
case 'Ride':
|
|
||||||
case 'RollerSki':
|
|
||||||
case 'Run':
|
|
||||||
case 'Skateboard':
|
|
||||||
case 'Badminton':
|
|
||||||
case 'Tennis':
|
|
||||||
case 'Velomobile':
|
|
||||||
case 'Walk':
|
|
||||||
case 'Wheelchair':
|
|
||||||
color = k_color_pavement;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Grass, maybe? */
|
|
||||||
case 'Golf':
|
|
||||||
case 'Soccer':
|
|
||||||
case 'Squash':
|
|
||||||
color = k_color_grass;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Crossfit,
|
|
||||||
// Elliptical
|
|
||||||
// HighIntensityIntervalTraining
|
|
||||||
// Pickleball
|
|
||||||
// Pilates
|
|
||||||
// Racquetball
|
|
||||||
// StairStepper
|
|
||||||
// TableTennis,
|
|
||||||
// VirtualRide
|
|
||||||
// VirtualRow
|
|
||||||
// VirtualRun
|
|
||||||
// WeightTraining
|
|
||||||
// Workout
|
|
||||||
// Yoga
|
|
||||||
default:
|
|
||||||
color = k_color_default;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
line(image_data, x0, y0, x1, y1, value) {
|
|
||||||
/* <3 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm */
|
|
||||||
let dx = Math.abs(x1 - x0);
|
|
||||||
let sx = x0 < x1 ? 1 : -1;
|
|
||||||
let dy = -Math.abs(y1 - y0);
|
|
||||||
let sy = y0 < y1 ? 1 : -1;
|
|
||||||
let error = dx + dy;
|
|
||||||
while (true) {
|
|
||||||
if (
|
|
||||||
x0 >= 0 &&
|
|
||||||
y0 >= 0 &&
|
|
||||||
x0 < image_data.width &&
|
|
||||||
y0 < image_data.height
|
|
||||||
) {
|
|
||||||
let base = (y0 * image_data.width + x0) * 4;
|
|
||||||
image_data.data[base + 0] = value[0];
|
|
||||||
image_data.data[base + 1] = value[1];
|
|
||||||
image_data.data[base + 2] = value[2];
|
|
||||||
image_data.data[base + 3] = value[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x0 == x1 && y0 == y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let e2 = 2 * error;
|
|
||||||
if (e2 >= dy) {
|
|
||||||
if (x0 == x1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
error += dy;
|
|
||||||
x0 = Math.round(x0 + sx);
|
|
||||||
}
|
|
||||||
if (e2 <= dx) {
|
|
||||||
if (y0 == y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
error += dx;
|
|
||||||
y0 = Math.round(y0 + sy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_activity_to_tile(image_data, width, height, ul, lr, activity) {
|
|
||||||
let color = this.activity_to_color(activity);
|
|
||||||
if (activity?.map?.polyline) {
|
|
||||||
let last;
|
|
||||||
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
||||||
let px = [
|
|
||||||
Math.floor((width * (pt[1] - ul.lng)) / (lr.lng - ul.lng)),
|
|
||||||
Math.floor((height * (pt[0] - ul.lat)) / (lr.lat - ul.lat)),
|
|
||||||
];
|
|
||||||
if (last) {
|
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
||||||
}
|
|
||||||
last = px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activity?.segments) {
|
|
||||||
for (let segment of activity.segments) {
|
|
||||||
let last;
|
|
||||||
for (let pt of segment) {
|
|
||||||
let px = [
|
|
||||||
Math.floor((width * (pt.lon - ul.lng)) / (lr.lng - ul.lng)),
|
|
||||||
Math.floor((height * (pt.lat - ul.lat)) / (lr.lat - ul.lat)),
|
|
||||||
];
|
|
||||||
if (last) {
|
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
||||||
}
|
|
||||||
last = px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async on_upload(event) {
|
|
||||||
try {
|
|
||||||
let file = event.srcElement.files[0];
|
|
||||||
let xml = await file.text();
|
|
||||||
let gpx = gpx_parse(xml);
|
|
||||||
let blob_id = await tfrpc.rpc.store_blob(xml);
|
|
||||||
console.log('blob_id = ', blob_id);
|
|
||||||
console.log(gpx);
|
|
||||||
let message = {
|
|
||||||
type: 'gg-activity',
|
|
||||||
mentions: [
|
|
||||||
{
|
|
||||||
link: `https://${gpx.link}/activity/${gpx.time}`,
|
|
||||||
name: 'activity_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: blob_id,
|
|
||||||
name: 'activity_data',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
console.log('id =', this.whoami, 'message = ', message);
|
|
||||||
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
console.log('appended message', id);
|
|
||||||
alert('Activity uploaded.');
|
|
||||||
await this.get_activities_from_ssb();
|
|
||||||
} catch (e) {
|
|
||||||
alert(`Error: ${JSON.stringify(e, null, 2)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upload() {
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.onchange = (event) => this.on_upload(event);
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
focus_map(activity) {
|
|
||||||
let bounds = this.activity_bounds(activity);
|
|
||||||
if (bounds.min.lat < bounds.max.lat && bounds.min.lng < bounds.max.lng) {
|
|
||||||
this.tab = 'map';
|
|
||||||
this.focus = bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_news() {
|
|
||||||
return html`
|
|
||||||
<ul>
|
|
||||||
${this.loaded_activities.map(
|
|
||||||
(x) => html`
|
|
||||||
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>
|
|
||||||
${x.author} ${x.name ?? x.time}
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_store_item(item) {
|
|
||||||
let [emoji, cost] = item;
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
<input type="button" value="${emoji}" @click=${() => (this.to_build = emoji)}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_store() {
|
|
||||||
let store = Object.assign({}, k_store);
|
|
||||||
store[this.emoji_of_the_day] = 5;
|
|
||||||
return html`
|
|
||||||
<h2>Store</h2>
|
|
||||||
<div><b>Your balance:</b> ${this.currency}</div>
|
|
||||||
${Object.entries(store).map(this.render_store_item.bind(this))}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let header;
|
|
||||||
if (!this.user?.credentials?.session?.name) {
|
|
||||||
header = html`<div style="flex: 1 0">
|
|
||||||
Please <a target="_top" href="/login?return=${this.url}">login</a> to
|
|
||||||
Tilde Friends, first.
|
|
||||||
</div>`;
|
|
||||||
} else if (!this.strava?.access_token) {
|
|
||||||
let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
|
|
||||||
header = html`
|
|
||||||
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
|
||||||
<div style="flex: 1 1">Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>
|
|
||||||
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
|
||||||
<input type="button" value="📁" @click=${this.upload}></input>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
header = html`
|
|
||||||
<div>
|
|
||||||
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
|
||||||
<h1>Welcome, ${this.user.credentials.session.name}</h1>
|
|
||||||
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
|
||||||
<input type="button" value="📁" @click=${this.upload}></input>
|
|
||||||
</div>
|
|
||||||
<h3 ?hidden=${!this.status?.text}>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigation = html`
|
|
||||||
<style>
|
|
||||||
#navigation input[type="button"] {
|
|
||||||
min-width: 3em;
|
|
||||||
min-height: 3em;
|
|
||||||
flex: 1 0;
|
|
||||||
font-size: large;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id="navigation" style="display: flex; flex-direction: row">
|
|
||||||
<input type="button" id="button_map" @click=${() => (this.tab = 'map')} value="🗺️Map"></input>
|
|
||||||
<input type="button" id="button_news" @click=${() => (this.tab = 'news')} value="🏃News"></input>
|
|
||||||
<input type="button" id="button_friends" @click=${() => (this.tab = 'friends')} value="👫Friends"></input>
|
|
||||||
<input type="button" id="button_store" @click=${() => (this.tab = 'store')} value="🏗️Store"></input>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
let content;
|
|
||||||
switch (this.tab) {
|
|
||||||
case 'map':
|
|
||||||
content = html`<div id="map" style="width: 100%; height: 100%"></div>`;
|
|
||||||
break;
|
|
||||||
case 'news':
|
|
||||||
content = this.render_news();
|
|
||||||
break;
|
|
||||||
case 'friends':
|
|
||||||
content = html`<div>Friends</div>`;
|
|
||||||
break;
|
|
||||||
case 'store':
|
|
||||||
content = this.render_store();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
.build-icon::before {
|
|
||||||
content: '📍';
|
|
||||||
border: 2px solid red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link rel="stylesheet" href="leaflet.css" />
|
|
||||||
<div
|
|
||||||
style="width: 100%; height: 100%; display: flex; flex-direction: column"
|
|
||||||
>
|
|
||||||
${header}
|
|
||||||
<div style="flex: 1 0; overflow: scroll">${content}</div>
|
|
||||||
${navigation}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('gg-app', GgAppElement);
|
|
@ -1,20 +0,0 @@
|
|||||||
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`,
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🪪",
|
"emoji": "🪪",
|
||||||
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
"previous": "&de7q4A59auHP/34bXgeNH05JZoxsGr5TjwXPvehWH30=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,36 @@ tfrpc.register(async function reload() {
|
|||||||
async function main() {
|
async function main() {
|
||||||
let ids = await ssb.getIdentities();
|
let ids = await ssb.getIdentities();
|
||||||
await app.setDocument(
|
await app.setDocument(
|
||||||
`<body style="color: #fff">
|
`
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="w3.css"></link>
|
||||||
|
<style>
|
||||||
|
/* "2018 Sargasso Sea" */
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#f3f4f7 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#d7dbe3 !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#b0b6c8 !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#8892ac !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#636f8e !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#40485c !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#394052 !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#323848 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#2b303d !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#242833 !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#f3f4f7 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#242833 !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#242833 !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#485167 !important}
|
||||||
|
.w3-text-theme {color:#485167 !important}
|
||||||
|
.w3-border-theme {border-color:#485167 !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#485167 !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#485167 !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#485167 !important}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="w3-theme-l3">
|
||||||
<script>const handler = {};</script>
|
<script>const handler = {};</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
@ -27,7 +56,8 @@ async function main() {
|
|||||||
let id = event.srcElement.dataset.id;
|
let id = event.srcElement.dataset.id;
|
||||||
let element = document.createElement('textarea');
|
let element = document.createElement('textarea');
|
||||||
element.value = await tfrpc.rpc.get_private_key(id);
|
element.value = await tfrpc.rpc.get_private_key(id);
|
||||||
element.style = 'width: 100%; read-only: true';
|
element.style = 'width: 100%; height: auto; read-only: true; resize: none';
|
||||||
|
element.classList.add('w3-input');
|
||||||
element.readOnly = true;
|
element.readOnly = true;
|
||||||
event.srcElement.parentElement.appendChild(element);
|
event.srcElement.parentElement.appendChild(element);
|
||||||
event.srcElement.onclick = event => handler.hide_id(event, element);
|
event.srcElement.onclick = event => handler.hide_id(event, element);
|
||||||
@ -48,7 +78,7 @@ async function main() {
|
|||||||
alert('Successfully created: ' + id);
|
alert('Successfully created: ' + id);
|
||||||
await tfrpc.rpc.reload();
|
await tfrpc.rpc.reload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error creating identity: ' + e);
|
alert('Error creating identity: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler.hide_id = function hide_id(event, element) {
|
handler.hide_id = function hide_id(event, element) {
|
||||||
@ -69,23 +99,36 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<h1>SSB Identity Management</h1>
|
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
|
||||||
<h2>Create a new identity</h2>
|
<div class="w3-card-4 w3-margin">
|
||||||
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
|
<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header>
|
||||||
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
<footer class="w3-padding">
|
||||||
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button>
|
||||||
<h2>Identities</h2>
|
</footer>
|
||||||
<ul>` +
|
</div>
|
||||||
|
<div class="w3-card-4 w3-margin">
|
||||||
|
<header class="w3-container w3-theme-l2"><h2>Import an SSB Identity from 12 BIP39 English Words</h2></header>
|
||||||
|
<textarea id="add_id" style="width: 100%" rows="4" class="w3-input"></textarea>
|
||||||
|
<footer class="w3-padding">
|
||||||
|
<button id="add" onclick="handler.add_id(event)" class="w3-button w3-theme">Import Identity</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<div class="w3-card-4 w3-margin">
|
||||||
|
<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
|
||||||
|
<ul class="w3-ul">` +
|
||||||
ids
|
ids
|
||||||
.map(
|
.map(
|
||||||
(id) => `<li>
|
(
|
||||||
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
id
|
||||||
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
|
||||||
${id}
|
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
|
||||||
</li>`
|
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
|
||||||
|
${id}
|
||||||
|
</li>`
|
||||||
)
|
)
|
||||||
.join('\n') +
|
.join('\n') +
|
||||||
` </ul>
|
` </ul>
|
||||||
|
</div>
|
||||||
</body>`
|
</body>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
235
apps/identity/w3.css
Normal file
235
apps/identity/w3.css
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🦟",
|
"emoji": "🦟",
|
||||||
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -67,9 +67,6 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function (id) {
|
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
|
||||||
});
|
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
if (Array.isArray(blob)) {
|
if (Array.isArray(blob)) {
|
||||||
blob = Uint8Array.from(blob);
|
blob = Uint8Array.from(blob);
|
||||||
@ -85,13 +82,18 @@ tfrpc.register(async function store_message(message) {
|
|||||||
tfrpc.register(function apps() {
|
tfrpc.register(function apps() {
|
||||||
return core.apps();
|
return core.apps();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(function getActiveIdentity() {
|
||||||
|
return ssb.getActiveIdentity();
|
||||||
|
});
|
||||||
tfrpc.register(async function try_decrypt(id, content) {
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
return await ssb.privateMessageDecrypt(id, content);
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function () {
|
core.register('onMessage', async function (id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
core.register('onBroadcastsChanged', async function () {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function () {
|
core.register('onConnectionsChanged', async function () {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
4
apps/issues/lit-all.min.js
vendored
4
apps/issues/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
@ -4,48 +4,6 @@ import * as tfutils from './tf-utils.js';
|
|||||||
|
|
||||||
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
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 {
|
class TfComposeElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
@ -105,10 +63,10 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
let issues = {};
|
let issues = {};
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
|
WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON
|
||||||
messages.id = messages_refs.message
|
messages.id = messages_refs.message
|
||||||
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
||||||
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
|
edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON
|
||||||
issues.id = messages_refs.ref JOIN messages ON
|
issues.id = messages_refs.ref JOIN messages ON
|
||||||
messages.id = messages_refs.message
|
messages.id = messages_refs.message
|
||||||
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
||||||
@ -206,7 +164,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
if (
|
if (
|
||||||
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
|
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
|
||||||
) {
|
) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'issue-edit',
|
type: 'issue-edit',
|
||||||
issues: [
|
issues: [
|
||||||
@ -221,7 +179,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create_issue(event) {
|
async create_issue(event) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
project: k_project,
|
project: k_project,
|
||||||
@ -231,7 +189,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reply_to_issue(event) {
|
async reply_to_issue(event) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: event.detail.value,
|
text: event.detail.value,
|
||||||
@ -249,10 +207,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let header = html`
|
let header = html` <h1>Tilde Friends Issues</h1> `;
|
||||||
<h1>Tilde Friends Issues</h1>
|
|
||||||
<tf-id-picker id="picker"></tf-id-picker>
|
|
||||||
`;
|
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
return html`
|
return html`
|
||||||
${header}
|
${header}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ function new_message() {
|
|||||||
return g_new_message_promise;
|
return g_new_message_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssb.addEventListener('message', function (id) {
|
core.register('onMessage', function (id) {
|
||||||
let resolve = g_new_message_resolve;
|
let resolve = g_new_message_resolve;
|
||||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
|
4
apps/journal/lit-all.min.js
vendored
4
apps/journal/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
5
apps/room.json
Normal file
5
apps/room.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🚪",
|
||||||
|
"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256"
|
||||||
|
}
|
13
apps/room/app.js
Normal file
13
apps/room/app.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
async function main() {
|
||||||
|
let host = core.url.match(/.*\/\/(.*?)\//)[1];
|
||||||
|
let id = (await ssb.getServerIdentity()).substring(1);
|
||||||
|
let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
|
||||||
|
await app.setDocument(`
|
||||||
|
<body style="color: #fff">
|
||||||
|
<h1>Server</h1>
|
||||||
|
<div>The local server address is:</div>
|
||||||
|
<div><input type="text" readonly value="${room}" style="width: 100%"></input></div>
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
main();
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "👟"
|
"emoji": "👟",
|
||||||
|
"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256"
|
||||||
}
|
}
|
||||||
|
4
apps/sneaker/lit-all.min.js
vendored
4
apps/sneaker/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -38,10 +38,11 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
format_message(message) {
|
format_message(message) {
|
||||||
|
const k_flag_sequence_before_author = 1;
|
||||||
let out = {
|
let out = {
|
||||||
previous: message.previous ?? null,
|
previous: message.previous ?? null,
|
||||||
};
|
};
|
||||||
if (message.sequence_before_author) {
|
if (message.flags & k_flag_sequence_before_author) {
|
||||||
out.sequence = message.sequence;
|
out.sequence = message.sequence;
|
||||||
out.author = message.author;
|
out.author = message.author;
|
||||||
} else {
|
} else {
|
||||||
@ -72,25 +73,104 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
if (
|
||||||
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
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, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]
|
||||||
|
) ||
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
startsWith(data, [
|
||||||
|
0xff,
|
||||||
|
0xd8,
|
||||||
|
0xff,
|
||||||
|
0xe1,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x45,
|
||||||
|
0x78,
|
||||||
|
0x69,
|
||||||
|
0x66,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.jpg';
|
return '.jpg';
|
||||||
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
} else if (
|
||||||
|
startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||||
|
) {
|
||||||
return '.png';
|
return '.png';
|
||||||
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
} else if (
|
||||||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||||
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
|
||||||
|
) {
|
||||||
return '.gif';
|
return '.gif';
|
||||||
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
0x52,
|
||||||
|
0x49,
|
||||||
|
0x46,
|
||||||
|
0x46,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x57,
|
||||||
|
0x45,
|
||||||
|
0x42,
|
||||||
|
0x50,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.webp';
|
return '.webp';
|
||||||
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
||||||
return '.svg';
|
return '.svg';
|
||||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x6d,
|
||||||
|
0x70,
|
||||||
|
0x34,
|
||||||
|
0x32,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.mp3';
|
return '.mp3';
|
||||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
} else if (
|
||||||
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
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';
|
return '.mp4';
|
||||||
} else {
|
} else {
|
||||||
return '.bin';
|
return '.bin';
|
||||||
@ -109,7 +189,12 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
)[0].total;
|
)[0].total;
|
||||||
while (true) {
|
while (true) {
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
`
|
||||||
|
SELECT author, id, sequence, timestamp, hash, json(content) AS content, signature, flags
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ? AND SEQUENCE > ?
|
||||||
|
ORDER BY sequence LIMIT 100
|
||||||
|
`,
|
||||||
[id, sequence]
|
[id, sequence]
|
||||||
);
|
);
|
||||||
if (messages?.length) {
|
if (messages?.length) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🐌",
|
"emoji": "🐌",
|
||||||
"previous": "&DUxMMCJcuhm6S9jg/eKgEyWodkITu6Tg9g5I5wgLWFU=.sha256"
|
"previous": "&h0sTvkhc3zEJw/sH612fy5i554Gr1AKzCBbLkm0KH28=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function (id) {
|
core.register('onMessage', async function (id) {
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
});
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
@ -100,13 +100,19 @@ tfrpc.register(async function try_decrypt(id, content) {
|
|||||||
tfrpc.register(async function encrypt(id, recipients, content) {
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
return await ssb.privateMessageEncrypt(id, recipients, content);
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function () {
|
tfrpc.register(async function getActiveIdentity() {
|
||||||
|
return await ssb.getActiveIdentity();
|
||||||
|
});
|
||||||
|
core.register('onBroadcastsChanged', async function () {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function () {
|
core.register('onConnectionsChanged', async function () {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
core.register('setActiveIdentity', async function (id) {
|
||||||
|
await tfrpc.rpc.set('identity', id);
|
||||||
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (typeof database !== 'undefined') {
|
if (typeof database !== 'undefined') {
|
||||||
|
@ -1,90 +1,94 @@
|
|||||||
function textNode(text) {
|
function textNode(text) {
|
||||||
const node = new commonmark.Node("text", undefined);
|
const node = new commonmark.Node('text', undefined);
|
||||||
node.literal = text;
|
node.literal = text;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkNode(text, link) {
|
function linkNode(text, link) {
|
||||||
const linkNode = new commonmark.Node("link", undefined);
|
const linkNode = new commonmark.Node('link', undefined);
|
||||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
if (link.startsWith('#')) {
|
||||||
linkNode.appendChild(textNode(text));
|
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||||
return linkNode;
|
} else {
|
||||||
|
linkNode.destination = link;
|
||||||
|
}
|
||||||
|
linkNode.appendChild(textNode(text));
|
||||||
|
return linkNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitMatches(text, regexp) {
|
function splitMatches(text, regexp) {
|
||||||
// Regexp must be sticky.
|
// Regexp must be sticky.
|
||||||
regexp = new RegExp(regexp, "gm");
|
regexp = new RegExp(regexp, 'gm');
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
let match = regexp.exec(text);
|
let match = regexp.exec(text);
|
||||||
while (match) {
|
while (match) {
|
||||||
const matchText = match[0];
|
const matchText = match[0];
|
||||||
|
|
||||||
if (match.index > i) {
|
if (match.index > i) {
|
||||||
result.push([text.substring(i, match.index), false]);
|
result.push([text.substring(i, match.index), false]);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push([matchText, true]);
|
result.push([matchText, true]);
|
||||||
i = match.index + matchText.length;
|
i = match.index + matchText.length;
|
||||||
|
|
||||||
match = regexp.exec(text);
|
match = regexp.exec(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < text.length) {
|
if (i < text.length) {
|
||||||
result.push([text.substring(i, text.length), false]);
|
result.push([text.substring(i, text.length), false]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = new RegExp("(?<!\\w)#[\\w-]+");
|
const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)');
|
||||||
|
|
||||||
function split(textNodes) {
|
function split(textNodes) {
|
||||||
const text = textNodes.map(n => n.literal).join("");
|
const text = textNodes.map((n) => n.literal).join('');
|
||||||
const parts = splitMatches(text, regex);
|
const parts = splitMatches(text, regex);
|
||||||
|
|
||||||
return parts.map(part => {
|
return parts.map((part) => {
|
||||||
if (part[1]) {
|
if (part[1]) {
|
||||||
return linkNode(part[0], part[0]);
|
return linkNode(part[0], part[0]);
|
||||||
} else {
|
} else {
|
||||||
return textNode(part[0]);
|
return textNode(part[0]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transform(parsed) {
|
export function transform(parsed) {
|
||||||
const walker = parsed.walker();
|
const walker = parsed.walker();
|
||||||
let event;
|
let event;
|
||||||
|
|
||||||
let nodes = [];
|
let nodes = [];
|
||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
const node = event.node;
|
const node = event.node;
|
||||||
if (event.entering && node.type === "text") {
|
if (event.entering && node.type === 'text') {
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
split(nodes)
|
split(nodes)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(newNode => {
|
.forEach((newNode) => {
|
||||||
nodes[0].insertAfter(newNode);
|
nodes[0].insertAfter(newNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
nodes.forEach(n => n.unlink());
|
nodes.forEach((n) => n.unlink());
|
||||||
nodes = [];
|
nodes = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
split(nodes)
|
split(nodes)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(newNode => {
|
.forEach((newNode) => {
|
||||||
nodes[0].insertAfter(newNode);
|
nodes[0].insertAfter(newNode);
|
||||||
});
|
});
|
||||||
nodes.forEach(n => n.unlink());
|
nodes.forEach((n) => n.unlink());
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
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,3 +1,5 @@
|
|||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
let g_emojis;
|
let g_emojis;
|
||||||
|
|
||||||
function get_emojis() {
|
function get_emojis() {
|
||||||
@ -10,105 +12,154 @@ function get_emojis() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function picker(callback, anchor) {
|
async function get_recent(author) {
|
||||||
get_emojis().then(function (json) {
|
let recent = await tfrpc.rpc.query(
|
||||||
let div = document.createElement('div');
|
`
|
||||||
div.id = 'emoji_picker';
|
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
||||||
div.style.color = '#000';
|
FROM messages
|
||||||
div.style.background = '#fff';
|
WHERE author = ? AND
|
||||||
div.style.border = '1px solid #000';
|
content ->> '$.type' = 'vote'
|
||||||
div.style.display = 'block';
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
div.style.position = 'absolute';
|
`,
|
||||||
div.style.minWidth = 'min(16em, 90vw)';
|
[author]
|
||||||
div.style.width = 'min(16em, 90vw)';
|
);
|
||||||
div.style.maxWidth = 'min(16em, 90vw)';
|
return recent.map((x) => x.value);
|
||||||
div.style.maxHeight = '16em';
|
}
|
||||||
div.style.overflow = 'scroll';
|
|
||||||
div.style.fontWeight = 'bold';
|
|
||||||
div.style.fontSize = 'xx-large';
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.type = 'text';
|
|
||||||
input.style.display = 'block';
|
|
||||||
input.style.boxSizing = 'border-box';
|
|
||||||
input.style.width = '100%';
|
|
||||||
input.style.margin = '0';
|
|
||||||
input.style.position = 'relative';
|
|
||||||
div.appendChild(input);
|
|
||||||
let list = document.createElement('div');
|
|
||||||
div.appendChild(list);
|
|
||||||
div.addEventListener('mousedown', function (event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
function cleanup() {
|
export async function picker(callback, anchor, author) {
|
||||||
console.log('emoji cleanup');
|
let json = await get_emojis();
|
||||||
div.parentElement.removeChild(div);
|
let recent = await get_recent(author);
|
||||||
window.removeEventListener('keydown', key_down);
|
|
||||||
console.log('removing click');
|
|
||||||
document.body.removeEventListener('mousedown', cleanup);
|
|
||||||
}
|
|
||||||
|
|
||||||
function key_down(event) {
|
let div = document.createElement('div');
|
||||||
if (event.key == 'Escape') {
|
div.id = 'emoji_picker';
|
||||||
cleanup();
|
div.style.color = '#000';
|
||||||
}
|
div.style.background = '#fff';
|
||||||
}
|
div.style.border = '1px solid #000';
|
||||||
|
div.style.display = 'block';
|
||||||
|
div.style.position = 'absolute';
|
||||||
|
div.style.minWidth = 'min(16em, 90vw)';
|
||||||
|
div.style.width = 'min(16em, 90vw)';
|
||||||
|
div.style.maxWidth = 'min(16em, 90vw)';
|
||||||
|
div.style.maxHeight = '16em';
|
||||||
|
div.style.overflow = 'scroll';
|
||||||
|
div.style.fontWeight = 'bold';
|
||||||
|
div.style.fontSize = 'xx-large';
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.style.display = 'block';
|
||||||
|
input.style.boxSizing = 'border-box';
|
||||||
|
input.style.width = '100%';
|
||||||
|
input.style.margin = '0';
|
||||||
|
input.style.position = 'relative';
|
||||||
|
div.appendChild(input);
|
||||||
|
let list = document.createElement('div');
|
||||||
|
div.appendChild(list);
|
||||||
|
div.addEventListener('mousedown', function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
function chosen(event) {
|
function cleanup() {
|
||||||
console.log(event.srcElement.innerText);
|
console.log('emoji cleanup');
|
||||||
callback(event.srcElement.innerText);
|
div.parentElement.removeChild(div);
|
||||||
|
window.removeEventListener('keydown', key_down);
|
||||||
|
console.log('removing click');
|
||||||
|
document.body.removeEventListener('mousedown', cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
function key_down(event) {
|
||||||
|
if (event.key == 'Escape') {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function refresh() {
|
function chosen(event) {
|
||||||
while (list.firstChild) {
|
console.log(event.srcElement.innerText);
|
||||||
list.removeChild(list.firstChild);
|
callback(event.srcElement.innerText);
|
||||||
}
|
cleanup();
|
||||||
let search = input.value.toLowerCase();
|
}
|
||||||
let any_at_all = false;
|
|
||||||
for (let row of Object.entries(json)) {
|
function refresh() {
|
||||||
let header = document.createElement('div');
|
while (list.firstChild) {
|
||||||
header.appendChild(document.createTextNode(row[0]));
|
list.removeChild(list.firstChild);
|
||||||
list.appendChild(header);
|
}
|
||||||
let any = false;
|
let search = input.value.toLowerCase();
|
||||||
for (let entry of Object.entries(row[1])) {
|
let any_at_all = false;
|
||||||
if (
|
if (recent) {
|
||||||
search &&
|
let emoji_to_name = {};
|
||||||
search.length &&
|
for (let row of Object.values(json)) {
|
||||||
entry[0].toLowerCase().indexOf(search) == -1
|
for (let entry of Object.entries(row)) {
|
||||||
) {
|
emoji_to_name[entry[1]] = entry[0];
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let emoji = document.createElement('span');
|
|
||||||
const k_size = '1.25em';
|
|
||||||
emoji.style.display = 'inline-block';
|
|
||||||
emoji.style.overflow = 'hidden';
|
|
||||||
emoji.style.cursor = 'pointer';
|
|
||||||
emoji.onclick = chosen;
|
|
||||||
emoji.title = entry[0];
|
|
||||||
emoji.appendChild(document.createTextNode(entry[1]));
|
|
||||||
list.appendChild(emoji);
|
|
||||||
any = true;
|
|
||||||
any_at_all = true;
|
|
||||||
}
|
|
||||||
if (!any) {
|
|
||||||
list.removeChild(header);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!any_at_all) {
|
let header = document.createElement('div');
|
||||||
list.appendChild(document.createTextNode('No matches found.'));
|
header.appendChild(document.createTextNode('Recent'));
|
||||||
|
list.appendChild(header);
|
||||||
|
let any = false;
|
||||||
|
for (let entry of recent) {
|
||||||
|
if (
|
||||||
|
search &&
|
||||||
|
search.length &&
|
||||||
|
(emoji_to_name[entry] || '').toLowerCase().indexOf(search) == -1
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let emoji = document.createElement('span');
|
||||||
|
const k_size = '1.25em';
|
||||||
|
emoji.style.display = 'inline-block';
|
||||||
|
emoji.style.overflow = 'hidden';
|
||||||
|
emoji.style.cursor = 'pointer';
|
||||||
|
emoji.onclick = chosen;
|
||||||
|
emoji.title = emoji_to_name[entry] || entry;
|
||||||
|
emoji.appendChild(document.createTextNode(entry));
|
||||||
|
list.appendChild(emoji);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
list.removeChild(header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refresh();
|
for (let row of Object.entries(json)) {
|
||||||
input.oninput = refresh;
|
let header = document.createElement('div');
|
||||||
document.body.appendChild(div);
|
header.appendChild(document.createTextNode(row[0]));
|
||||||
div.style.position = 'fixed';
|
list.appendChild(header);
|
||||||
div.style.top = '50%';
|
let any = false;
|
||||||
div.style.left = '50%';
|
for (let entry of Object.entries(row[1])) {
|
||||||
div.style.transform = 'translate(-50%, -50%)';
|
if (
|
||||||
input.focus();
|
search &&
|
||||||
console.log('adding click');
|
search.length &&
|
||||||
document.body.addEventListener('mousedown', cleanup);
|
entry[0].toLowerCase().indexOf(search) == -1
|
||||||
window.addEventListener('keydown', key_down);
|
) {
|
||||||
});
|
continue;
|
||||||
|
}
|
||||||
|
let emoji = document.createElement('span');
|
||||||
|
const k_size = '1.25em';
|
||||||
|
emoji.style.display = 'inline-block';
|
||||||
|
emoji.style.overflow = 'hidden';
|
||||||
|
emoji.style.cursor = 'pointer';
|
||||||
|
emoji.onclick = chosen;
|
||||||
|
emoji.title = entry[0];
|
||||||
|
emoji.appendChild(document.createTextNode(entry[1]));
|
||||||
|
list.appendChild(emoji);
|
||||||
|
any = true;
|
||||||
|
any_at_all = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
list.removeChild(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any_at_all) {
|
||||||
|
list.appendChild(document.createTextNode('No matches found.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
input.oninput = refresh;
|
||||||
|
document.body.appendChild(div);
|
||||||
|
div.style.position = 'fixed';
|
||||||
|
div.style.top = '50%';
|
||||||
|
div.style.left = '50%';
|
||||||
|
div.style.transform = 'translate(-50%, -50%)';
|
||||||
|
input.focus();
|
||||||
|
console.log('adding click');
|
||||||
|
document.body.addEventListener('mousedown', cleanup);
|
||||||
|
window.addEventListener('keydown', key_down);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top" />
|
<base target="_top" />
|
||||||
@ -10,14 +10,14 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #223a5e">
|
<body style="margin: 0; padding: 0">
|
||||||
<tf-app class="w3-deep-purple" />
|
<tf-app></tf-app>
|
||||||
|
<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
|
||||||
<script>
|
<script>
|
||||||
window.litDisableBundleWarning = true;
|
window.litDisableBundleWarning = true;
|
||||||
</script>
|
</script>
|
||||||
<script src="filesaver.min.js"></script>
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
|
||||||
<script src="commonmark-hashtag.js" type="module"></script>
|
<script src="commonmark-hashtag.js" type="module"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
4
apps/ssb/lit-all.min.js
vendored
4
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
@ -1,13 +1,13 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
import * as tf_id_picker from './tf-id-picker.js';
|
|
||||||
import * as tf_app from './tf-app.js';
|
import * as tf_app from './tf-app.js';
|
||||||
import * as tf_message from './tf-message.js';
|
import * as tf_message from './tf-message.js';
|
||||||
import * as tf_user from './tf-user.js';
|
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_reactions_modal from './tf-reactions-modal.js';
|
||||||
import * as tf_tab_mentions from './tf-tab-mentions.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_news_feed from './tf-tab-news-feed.js';
|
||||||
|
@ -52,13 +52,15 @@ class TfElement extends LitElement {
|
|||||||
self.broadcasts = value;
|
self.broadcasts = value;
|
||||||
} else if (name === 'connections') {
|
} else if (name === 'connections') {
|
||||||
self.connections = value;
|
self.connections = value;
|
||||||
|
} else if (name === 'identity') {
|
||||||
|
self.whoami = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.initial_load();
|
this.initial_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initial_load() {
|
async initial_load() {
|
||||||
let whoami = await tfrpc.rpc.localStorageGet('whoami');
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||||
this.ids = ids;
|
this.ids = ids;
|
||||||
@ -107,7 +109,7 @@ class TfElement extends LitElement {
|
|||||||
let abouts = await tfrpc.rpc.query(
|
let abouts = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
messages.*
|
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM
|
FROM
|
||||||
messages,
|
messages,
|
||||||
json_each(?1) AS following
|
json_each(?1) AS following
|
||||||
@ -118,7 +120,7 @@ class TfElement extends LitElement {
|
|||||||
json_extract(messages.content, '$.type') = 'about'
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
UNION
|
UNION
|
||||||
SELECT
|
SELECT
|
||||||
messages.*
|
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM
|
FROM
|
||||||
messages,
|
messages,
|
||||||
json_each(?2) AS following
|
json_each(?2) AS following
|
||||||
@ -158,7 +160,7 @@ class TfElement extends LitElement {
|
|||||||
async fetch_new_message(id) {
|
async fetch_new_message(id) {
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.id = ?
|
WHERE messages.id = ?
|
||||||
@ -193,35 +195,12 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_id_picker() {
|
|
||||||
return html`
|
|
||||||
<div style="display: flex; gap: 8px">
|
|
||||||
<tf-id-picker
|
|
||||||
id="picker"
|
|
||||||
style="flex: 1 1 auto"
|
|
||||||
selected=${this.whoami}
|
|
||||||
.ids=${this.ids}
|
|
||||||
.users=${this.users}
|
|
||||||
@change=${this._handle_whoami_changed}
|
|
||||||
></tf-id-picker>
|
|
||||||
<button
|
|
||||||
class="w3-button w3-dark-grey w3-border"
|
|
||||||
style="flex: 0 0 auto"
|
|
||||||
@click=${this.create_identity}
|
|
||||||
id="create_identity"
|
|
||||||
>
|
|
||||||
Create Identity
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async load_recent_tags() {
|
async load_recent_tags() {
|
||||||
let start = new Date();
|
let start = new Date();
|
||||||
this.tags = await tfrpc.rpc.query(
|
this.tags = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH
|
WITH
|
||||||
recent AS (SELECT id, content FROM messages
|
recent AS (SELECT id, json(content) AS content FROM messages
|
||||||
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
||||||
ORDER BY timestamp DESC LIMIT 1024),
|
ORDER BY timestamp DESC LIMIT 1024),
|
||||||
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
||||||
@ -255,7 +234,15 @@ class TfElement extends LitElement {
|
|||||||
by_count.push({count: v.of, id: id});
|
by_count.push({count: v.of, id: id});
|
||||||
}
|
}
|
||||||
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
||||||
|
let start_time = new Date();
|
||||||
users = await this.fetch_about(Object.keys(following).sort(), users);
|
users = await this.fetch_about(Object.keys(following).sort(), users);
|
||||||
|
console.log(
|
||||||
|
'about took',
|
||||||
|
(new Date() - start_time) / 1000.0,
|
||||||
|
'seconds for',
|
||||||
|
Object.keys(users).length,
|
||||||
|
'users'
|
||||||
|
);
|
||||||
this.following = Object.keys(following);
|
this.following = Object.keys(following);
|
||||||
this.users = users;
|
this.users = users;
|
||||||
await tags;
|
await tags;
|
||||||
@ -277,6 +264,7 @@ class TfElement extends LitElement {
|
|||||||
hash=${this.hash}
|
hash=${this.hash}
|
||||||
.unread=${this.unread}
|
.unread=${this.unread}
|
||||||
@refresh=${() => (this.unread = [])}
|
@refresh=${() => (this.unread = [])}
|
||||||
|
?loading=${this.loading}
|
||||||
></tf-tab-news>
|
></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
@ -352,18 +340,20 @@ class TfElement extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let tabs = html`
|
let tabs = html`
|
||||||
<div class="w3-bar w3-black">
|
<div class="w3-bar w3-theme-l1">
|
||||||
${Object.entries(k_tabs).map(
|
${Object.entries(k_tabs).map(
|
||||||
([k, v]) => html`
|
([k, v]) => html`
|
||||||
<button
|
<button
|
||||||
title=${v}
|
title=${v}
|
||||||
class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
|
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
|
||||||
v
|
? 'w3-theme-l2'
|
||||||
? 'w3-red'
|
: 'w3-theme-l1'}"
|
||||||
: 'w3-black'}"
|
|
||||||
@click=${() => self.set_tab(v)}
|
@click=${() => self.set_tab(v)}
|
||||||
>
|
>
|
||||||
${k}
|
${k}
|
||||||
|
<span class=${self.tab == v ? '' : 'w3-hide-small'}
|
||||||
|
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@ -371,15 +361,27 @@ class TfElement extends LitElement {
|
|||||||
`;
|
`;
|
||||||
let contents = !this.loaded
|
let contents = !this.loaded
|
||||||
? this.loading
|
? this.loading
|
||||||
? html`<div>Loading...</div>`
|
? html`<div
|
||||||
|
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
|
||||||
|
>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
${this.render_tab()}`
|
||||||
: html`<div>Select or create an identity.</div>`
|
: html`<div>Select or create an identity.</div>`
|
||||||
: this.render_tab();
|
: this.render_tab();
|
||||||
return html`
|
return html`
|
||||||
${this.render_id_picker()} ${tabs}
|
<div
|
||||||
${this.tags.map(
|
style="width: 100vw; min-height: 100vh; height: 100%"
|
||||||
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
class="w3-theme-dark"
|
||||||
)}
|
>
|
||||||
${contents}
|
${tabs}
|
||||||
|
<div style="padding: 8px">
|
||||||
|
${this.tags.map(
|
||||||
|
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
||||||
|
)}
|
||||||
|
${contents}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.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';
|
||||||
@ -13,6 +13,7 @@ class TfComposeElement extends LitElement {
|
|||||||
branch: {type: String},
|
branch: {type: String},
|
||||||
apps: {type: Object},
|
apps: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
|
author: {type: String},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ class TfComposeElement extends LitElement {
|
|||||||
this.branch = undefined;
|
this.branch = undefined;
|
||||||
this.apps = undefined;
|
this.apps = undefined;
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
|
this.author = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
process_text(text) {
|
process_text(text) {
|
||||||
@ -64,7 +66,7 @@ class TfComposeElement extends LitElement {
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
this.requestUpdate();
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
return tfutils.markdown(text);
|
return tfutils.markdown(text);
|
||||||
}
|
}
|
||||||
@ -72,7 +74,7 @@ class TfComposeElement extends LitElement {
|
|||||||
input(event) {
|
input(event) {
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||||
let content_warning_preview = this.renderRoot.getElementById(
|
let content_warning_preview = this.renderRoot.getElementById(
|
||||||
'content_warning_preview'
|
'content_warning_preview'
|
||||||
@ -80,6 +82,10 @@ class TfComposeElement extends LitElement {
|
|||||||
if (content_warning && content_warning_preview) {
|
if (content_warning && content_warning_preview) {
|
||||||
content_warning_preview.innerText = content_warning.value;
|
content_warning_preview.innerText = content_warning.value;
|
||||||
}
|
}
|
||||||
|
let draft = this.get_draft();
|
||||||
|
draft.text = edit.innerText;
|
||||||
|
draft.content_warning = content_warning?.innerText;
|
||||||
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(draft) {
|
notify(draft) {
|
||||||
@ -95,14 +101,6 @@ class TfComposeElement extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
change() {
|
|
||||||
let draft = this.get_draft();
|
|
||||||
draft.text = this.renderRoot.getElementById('edit')?.value;
|
|
||||||
draft.content_warning =
|
|
||||||
this.renderRoot.getElementById('content_warning')?.value;
|
|
||||||
this.notify(draft);
|
|
||||||
}
|
|
||||||
|
|
||||||
convert_to_format(buffer, type, mime_type) {
|
convert_to_format(buffer, type, mime_type) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
let img = new Image();
|
let img = new Image();
|
||||||
@ -169,8 +167,7 @@ class TfComposeElement extends LitElement {
|
|||||||
size: buffer.length ?? buffer.byteLength,
|
size: buffer.length ?? buffer.byteLength,
|
||||||
};
|
};
|
||||||
let edit = self.renderRoot.getElementById('edit');
|
let edit = self.renderRoot.getElementById('edit');
|
||||||
edit.value += `\n`;
|
edit.innerText += `\n`;
|
||||||
self.change();
|
|
||||||
self.input();
|
self.input();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e?.message);
|
alert(e?.message);
|
||||||
@ -197,7 +194,7 @@ class TfComposeElement extends LitElement {
|
|||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let message = {
|
let message = {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: edit.value,
|
text: edit.innerText,
|
||||||
};
|
};
|
||||||
if (this.root || this.branch) {
|
if (this.root || this.branch) {
|
||||||
message.root = this.root;
|
message.root = this.root;
|
||||||
@ -225,8 +222,8 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
|
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
|
||||||
edit.value = '';
|
edit.innerText = '';
|
||||||
self.change();
|
self.input();
|
||||||
self.notify(undefined);
|
self.notify(undefined);
|
||||||
self.requestUpdate();
|
self.requestUpdate();
|
||||||
});
|
});
|
||||||
@ -236,17 +233,11 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
|
||||||
edit.value = '';
|
|
||||||
this.change();
|
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
|
||||||
preview.innerHTML = '';
|
|
||||||
this.notify(undefined);
|
this.notify(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function (event) {
|
input.onchange = function (event) {
|
||||||
@ -262,7 +253,7 @@ class TfComposeElement extends LitElement {
|
|||||||
try {
|
try {
|
||||||
let rows = await tfrpc.rpc.query(
|
let rows = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.content FROM messages_fts(?)
|
SELECT json(messages.content) FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
WHERE messages.content LIKE ?
|
WHERE messages.content LIKE ?
|
||||||
ORDER BY timestamp DESC LIMIT 10
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
@ -284,22 +275,38 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
|
let values = Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name ?? x[0],
|
||||||
|
value: x[0],
|
||||||
|
}));
|
||||||
|
if (this.author) {
|
||||||
|
values = [].concat(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: this.users[this.author]?.name,
|
||||||
|
value: this.author,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
collection: [
|
collection: [
|
||||||
{
|
{
|
||||||
values: Object.entries(this.users).map((x) => ({
|
values: values,
|
||||||
key: x[1].name,
|
|
||||||
value: x[0],
|
|
||||||
})),
|
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
return item
|
||||||
|
? `[@${item.original.key}](${item.original.value})`
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: '&',
|
trigger: '&',
|
||||||
values: this.autocomplete,
|
values: this.autocomplete,
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return ``;
|
return item
|
||||||
|
? ``
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -310,10 +317,10 @@ class TfComposeElement extends LitElement {
|
|||||||
updated() {
|
updated() {
|
||||||
super.updated();
|
super.updated();
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
if (this.last_updated_text !== edit.value) {
|
if (this.last_updated_text !== edit.innerText) {
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
this.last_updated_text = edit.value;
|
this.last_updated_text = edit.innerText;
|
||||||
}
|
}
|
||||||
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||||
if (encrypt) {
|
if (encrypt) {
|
||||||
@ -333,8 +340,7 @@ class TfComposeElement extends LitElement {
|
|||||||
remove_mention(id) {
|
remove_mention(id) {
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
delete draft.mentions[id];
|
delete draft.mentions[id];
|
||||||
this.notify(draft);
|
setTimeout(() => this.notify(), 0);
|
||||||
this.requestUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
@ -342,7 +348,7 @@ class TfComposeElement extends LitElement {
|
|||||||
return html` <div style="display: flex; flex-direction: row">
|
return html` <div style="display: flex; flex-direction: row">
|
||||||
<div style="align-self: center; margin: 0.5em">
|
<div style="align-self: center; margin: 0.5em">
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
title="Remove ${mention.name} mention"
|
title="Remove ${mention.name} mention"
|
||||||
@click=${() => self.remove_mention(mention.link)}
|
@click=${() => self.remove_mention(mention.link)}
|
||||||
>
|
>
|
||||||
@ -396,16 +402,16 @@ class TfComposeElement extends LitElement {
|
|||||||
if (this.apps) {
|
if (this.apps) {
|
||||||
return html`
|
return html`
|
||||||
<div class="w3-card-4 w3-margin w3-padding">
|
<div class="w3-card-4 w3-margin w3-padding">
|
||||||
<select id="select" class="w3-select w3-dark-grey">
|
<select id="select" class="w3-select w3-theme-d1">
|
||||||
${Object.keys(self.apps).map(
|
${Object.keys(self.apps).map(
|
||||||
(app) => html`<option value=${app}>${app}</option>`
|
(app) => html`<option value=${app}>${app}</option>`
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
|
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
|
||||||
Attach
|
Attach
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (this.apps = null)}
|
@click=${() => (this.apps = null)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@ -421,12 +427,12 @@ class TfComposeElement extends LitElement {
|
|||||||
self.apps = await tfrpc.rpc.apps();
|
self.apps = await tfrpc.rpc.apps();
|
||||||
}
|
}
|
||||||
if (!this.apps) {
|
if (!this.apps) {
|
||||||
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>
|
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
|
||||||
Attach App
|
Attach App
|
||||||
</button>`;
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<button
|
return html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (this.apps = null)}
|
@click=${() => (this.apps = null)}
|
||||||
>
|
>
|
||||||
Discard App
|
Discard App
|
||||||
@ -448,15 +454,15 @@ class TfComposeElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="w3-container w3-padding">
|
<div class="w3-container w3-padding">
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
</p>
|
</p>
|
||||||
<input type="text" class="w3-input w3-border w3-dark-grey" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
|
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -486,14 +492,14 @@ class TfComposeElement extends LitElement {
|
|||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
<label for="encrypt_to">🔐 To:</label>
|
<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="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${draft.encrypt_to.map(
|
${draft.encrypt_to.map(
|
||||||
(x) => html`
|
(x) => html`
|
||||||
<li>
|
<li>
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||||
</li>`
|
</li>`
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
@ -512,7 +518,7 @@ class TfComposeElement extends LitElement {
|
|||||||
let draft = self.get_draft();
|
let draft = self.get_draft();
|
||||||
let content_warning =
|
let content_warning =
|
||||||
draft.content_warning !== undefined
|
draft.content_warning !== undefined
|
||||||
? html`<div class="w3-panel w3-round-xlarge w3-blue">
|
? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
|
||||||
<p id="content_warning_preview">${draft.content_warning}</p>
|
<p id="content_warning_preview">${draft.content_warning}</p>
|
||||||
</div>`
|
</div>`
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -520,34 +526,31 @@ class TfComposeElement extends LitElement {
|
|||||||
draft.encrypt_to !== undefined
|
draft.encrypt_to !== undefined
|
||||||
? undefined
|
? undefined
|
||||||
: html`<button
|
: html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => this.set_encrypt([])}
|
@click=${() => this.set_encrypt([])}
|
||||||
>
|
>
|
||||||
🔐
|
🔐
|
||||||
</button>`;
|
</button>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
<div
|
<div
|
||||||
class="w3-card-4 w3-blue-grey w3-padding"
|
class="w3-card-4 w3-theme-d4 w3-padding-small"
|
||||||
style="box-sizing: border-box"
|
style="box-sizing: border-box"
|
||||||
>
|
>
|
||||||
${this.render_encrypt()}
|
${this.render_encrypt()}
|
||||||
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
|
<div class="w3-container w3-padding-small">
|
||||||
<div style="flex: 1 0 50%">
|
<div class="w3-half">
|
||||||
<p>
|
<span
|
||||||
<textarea
|
class="w3-input w3-theme-d1 w3-border"
|
||||||
class="w3-input w3-dark-grey w3-border"
|
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
|
||||||
style="resize: vertical"
|
placeholder="Write a post here."
|
||||||
placeholder="Write a post here."
|
id="edit"
|
||||||
id="edit"
|
@input=${this.input}
|
||||||
@input=${this.input}
|
@paste=${this.paste}
|
||||||
@change=${this.change}
|
contenteditable
|
||||||
@paste=${this.paste}
|
.innerText=${live(draft.text ?? '')}
|
||||||
>
|
></span>
|
||||||
${draft.text}</textarea
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1 0 50%">
|
<div class="w3-half w3-padding">
|
||||||
${content_warning}
|
${content_warning}
|
||||||
<div id="preview"></div>
|
<div id="preview"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -556,18 +559,14 @@ ${draft.text}</textarea
|
|||||||
self.render_mention(x)
|
self.render_mention(x)
|
||||||
)}
|
)}
|
||||||
${this.render_attach_app()} ${this.render_content_warning()}
|
${this.render_attach_app()} ${this.render_content_warning()}
|
||||||
<button
|
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
id="submit"
|
|
||||||
@click=${this.submit}
|
|
||||||
>
|
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.attach}>
|
<button class="w3-button w3-theme-d1" @click=${this.attach}>
|
||||||
Attach
|
Attach
|
||||||
</button>
|
</button>
|
||||||
${this.render_attach_app_button()} ${encrypt}
|
${this.render_attach_app_button()} ${encrypt}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.discard}>
|
<button class="w3-button w3-theme-d1" @click=${this.discard}>
|
||||||
Discard
|
Discard
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
import {styles} from './tf-styles.js';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Provide a list of IDs, and this lets the user pick one.
|
|
||||||
*/
|
|
||||||
class TfIdentityPickerElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
ids: {type: Array},
|
|
||||||
selected: {type: String},
|
|
||||||
users: {type: Object},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = styles;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.ids = [];
|
|
||||||
this.users = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
changed(event) {
|
|
||||||
this.selected = event.srcElement.value;
|
|
||||||
this.dispatchEvent(
|
|
||||||
new Event('change', {
|
|
||||||
srcElement: this,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<select
|
|
||||||
class="w3-select w3-dark-grey w3-padding w3-border"
|
|
||||||
@change=${this.changed}
|
|
||||||
style="max-width: 100%; overflow: hidden"
|
|
||||||
>
|
|
||||||
${(this.ids ?? []).map(
|
|
||||||
(id) =>
|
|
||||||
html`<option ?selected=${id == this.selected} value=${id}>
|
|
||||||
${this.users[id]?.name
|
|
||||||
? this.users[id]?.name + ' - '
|
|
||||||
: undefined}<small>${id}</small>
|
|
||||||
</option>`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
|
@ -1,4 +1,4 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.js';
|
||||||
import * as emojis from './emojis.js';
|
import * as emojis from './emojis.js';
|
||||||
@ -54,6 +54,12 @@ class TfMessageElement extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_reactions() {
|
||||||
|
let modal = document.getElementById('reactions_modal');
|
||||||
|
modal.users = this.users;
|
||||||
|
modal.votes = this.message?.votes || [];
|
||||||
|
}
|
||||||
|
|
||||||
render_votes() {
|
render_votes() {
|
||||||
function normalize_expression(expression) {
|
function normalize_expression(expression) {
|
||||||
if (expression === 'Like' || !expression) {
|
if (expression === 'Like' || !expression) {
|
||||||
@ -66,19 +72,21 @@ class TfMessageElement extends LitElement {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`<div>
|
if (this.message?.votes?.length) {
|
||||||
${(this.message.votes || []).map(
|
return html`<div class="w3-button" @click=${this.show_reactions}>
|
||||||
(vote) => html`
|
${(this.message.votes || []).map(
|
||||||
<span
|
(vote) => html`
|
||||||
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
<span
|
||||||
vote.timestamp
|
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
||||||
)}"
|
vote.timestamp
|
||||||
>
|
)}"
|
||||||
${normalize_expression(vote.content.vote.expression)}
|
>
|
||||||
</span>
|
${normalize_expression(vote.content.vote.expression)}
|
||||||
`
|
</span>
|
||||||
)}
|
`
|
||||||
</div>`;
|
)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_raw() {
|
render_raw() {
|
||||||
@ -125,7 +133,7 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
react(event) {
|
react(event) {
|
||||||
emojis.picker((x) => this.vote(x));
|
emojis.picker((x) => this.vote(x), null, this.whoami);
|
||||||
}
|
}
|
||||||
|
|
||||||
show_image(link) {
|
show_image(link) {
|
||||||
@ -239,9 +247,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
if (mentions.length) {
|
if (mentions.length) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<fieldset
|
<fieldset style="padding: 0.5em; border: 1px solid black">
|
||||||
style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"
|
|
||||||
>
|
|
||||||
<legend>Mentions</legend>
|
<legend>Mentions</legend>
|
||||||
${mentions.map((x) => self.render_mention(x))}
|
${mentions.map((x) => self.render_mention(x))}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -265,10 +271,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
new CustomEvent('tf-expand', {
|
new CustomEvent('tf-expand', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
|
||||||
id: (this.message.id || '') + (tag || ''),
|
|
||||||
expanded: expanded,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -285,14 +288,14 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
if (this.message.child_messages?.length) {
|
if (this.message.child_messages?.length) {
|
||||||
if (!this.expanded[this.message.id]) {
|
if (!this.expanded[this.message.id]) {
|
||||||
return html`<button
|
return html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => self.set_expanded(true)}
|
@click=${() => self.set_expanded(true)}
|
||||||
>
|
>
|
||||||
+ ${this.total_child_messages(this.message) + ' More'}
|
+ ${this.total_child_messages(this.message) + ' More'}
|
||||||
</button>`;
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<button
|
return html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => self.set_expanded(false)}
|
@click=${() => self.set_expanded(false)}
|
||||||
>
|
>
|
||||||
Collapse</button
|
Collapse</button
|
||||||
@ -334,20 +337,23 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
if (this.message?.decrypted?.type == 'post') {
|
if (this.message?.decrypted?.type == 'post') {
|
||||||
content = this.message.decrypted;
|
content = this.message.decrypted;
|
||||||
}
|
}
|
||||||
|
let class_background = this.message?.decrypted
|
||||||
|
? 'w3-pale-red'
|
||||||
|
: 'w3-theme-d4';
|
||||||
let self = this;
|
let self = this;
|
||||||
let raw_button;
|
let raw_button;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
if (content?.type == 'post' || content?.type == 'blog') {
|
if (content?.type == 'post' || content?.type == 'blog') {
|
||||||
raw_button = html`<button
|
raw_button = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (self.format = 'md')}
|
@click=${() => (self.format = 'md')}
|
||||||
>
|
>
|
||||||
Markdown
|
Markdown
|
||||||
</button>`;
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button
|
raw_button = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (self.format = 'message')}
|
@click=${() => (self.format = 'message')}
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
@ -356,7 +362,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
raw_button = html`<button
|
raw_button = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (self.format = 'message')}
|
@click=${() => (self.format = 'message')}
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
@ -364,7 +370,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
break;
|
break;
|
||||||
case 'decrypted':
|
case 'decrypted':
|
||||||
raw_button = html`<button
|
raw_button = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (self.format = 'raw')}
|
@click=${() => (self.format = 'raw')}
|
||||||
>
|
>
|
||||||
Raw
|
Raw
|
||||||
@ -373,14 +379,14 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
default:
|
default:
|
||||||
if (this.message.decrypted) {
|
if (this.message.decrypted) {
|
||||||
raw_button = html`<button
|
raw_button = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (self.format = 'decrypted')}
|
@click=${() => (self.format = 'decrypted')}
|
||||||
>
|
>
|
||||||
Decrypted
|
Decrypted
|
||||||
</button>`;
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button
|
raw_button = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => (self.format = 'raw')}
|
@click=${() => (self.format = 'raw')}
|
||||||
>
|
>
|
||||||
Raw
|
Raw
|
||||||
@ -392,8 +398,8 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
let body;
|
let body;
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="w3-card-4"
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
style="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"
|
<span style="padding-right: 8px"
|
||||||
@ -403,13 +409,24 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
>
|
>
|
||||||
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
${self.render_votes()}
|
${self.render_votes()}
|
||||||
|
${(self.message.child_messages || []).map(
|
||||||
|
(x) => html`
|
||||||
|
<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${self.whoami}
|
||||||
|
.users=${self.users}
|
||||||
|
.drafts=${self.drafts}
|
||||||
|
.expanded=${self.expanded}
|
||||||
|
></tf-message>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (this.message?.type === 'contact_group') {
|
if (this.message?.type === 'contact_group') {
|
||||||
return html` <div
|
return html` <div
|
||||||
class="w3-card-4"
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
>
|
>
|
||||||
${this.message.messages.map(
|
${this.message.messages.map(
|
||||||
(x) =>
|
(x) =>
|
||||||
@ -424,8 +441,8 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
</div>`;
|
</div>`;
|
||||||
} else if (this.message.placeholder) {
|
} else if (this.message.placeholder) {
|
||||||
return html` <div
|
return html` <div
|
||||||
class="w3-card-4"
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
>
|
>
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
||||||
(placeholder)
|
(placeholder)
|
||||||
@ -497,17 +514,15 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
<tf-compose
|
<tf-compose
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
root=${this.message.content.root || this.message.id}
|
root=${content.root || this.message.id}
|
||||||
branch=${this.message.id}
|
branch=${this.message.id}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}
|
@tf-discard=${this.discard_reply}
|
||||||
|
author=${this.message.author}
|
||||||
></tf-compose>
|
></tf-compose>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<button
|
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${this.show_reply}
|
|
||||||
>
|
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
@ -536,7 +551,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
}
|
}
|
||||||
let content_warning = html`
|
let content_warning = html`
|
||||||
<div
|
<div
|
||||||
class="w3-panel w3-round-xlarge w3-blue"
|
class="w3-panel w3-round-xlarge w3-theme-l4"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
@click=${(x) => this.toggle_expanded(':cw')}
|
@click=${(x) => this.toggle_expanded(':cw')}
|
||||||
>
|
>
|
||||||
@ -556,9 +571,6 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
let is_encrypted = this.message?.decrypted
|
let is_encrypted = this.message?.decrypted
|
||||||
? html`<span style="align-self: center">🔓</span>`
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
: undefined;
|
: 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 {
|
||||||
@ -575,8 +587,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div
|
||||||
class="w3-card-4"
|
class="w3-card-4 ${class_background} w3-border-theme"
|
||||||
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
style="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>
|
||||||
@ -591,7 +603,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
${payload} ${this.render_votes()}
|
${payload} ${this.render_votes()}
|
||||||
<p>
|
<p>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
React
|
React
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
@ -602,9 +614,6 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
let is_encrypted = this.message?.decrypted
|
let is_encrypted = this.message?.decrypted
|
||||||
? html`<span style="align-self: center">🔓</span>`
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
: undefined;
|
: 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 {
|
||||||
@ -621,8 +630,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div
|
||||||
class="w3-card-4"
|
class="w3-card-4 ${class_background} w3-border-theme"
|
||||||
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
style="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>
|
||||||
@ -636,7 +645,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
</div>
|
</div>
|
||||||
${content.text} ${this.render_votes()}
|
${content.text} ${this.render_votes()}
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
React
|
React
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
@ -684,17 +693,15 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
<tf-compose
|
<tf-compose
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
root=${this.message.content.root || this.message.id}
|
root=${content.root || this.message.id}
|
||||||
branch=${this.message.id}
|
branch=${this.message.id}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}
|
@tf-discard=${this.discard_reply}
|
||||||
|
author=${this.message.author}
|
||||||
></tf-compose>
|
></tf-compose>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<button
|
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${this.show_reply}
|
|
||||||
>
|
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
@ -714,8 +721,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div
|
||||||
class="w3-card-4"
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"
|
style="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>
|
||||||
@ -731,7 +738,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
<div>
|
<div>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
React
|
React
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -215,49 +215,49 @@ class TfProfileElement extends LitElement {
|
|||||||
let server_follow;
|
let server_follow;
|
||||||
if (this.server_follows_me === true) {
|
if (this.server_follows_me === true) {
|
||||||
server_follow = html`<button
|
server_follow = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => this.server_follow_me(false)}
|
@click=${() => this.server_follow_me(false)}
|
||||||
>
|
>
|
||||||
Server, Stop Following Me
|
Server, Stop Following Me
|
||||||
</button>`;
|
</button>`;
|
||||||
} else if (this.server_follows_me === false) {
|
} else if (this.server_follows_me === false) {
|
||||||
server_follow = html`<button
|
server_follow = html`<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => this.server_follow_me(true)}
|
@click=${() => this.server_follow_me(true)}
|
||||||
>
|
>
|
||||||
Server, Follow Me
|
Server, Follow Me
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
edit = html`
|
edit = html`
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>
|
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
|
||||||
Save Profile
|
Save Profile
|
||||||
</button>
|
</button>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
|
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
||||||
Discard
|
Discard
|
||||||
</button>
|
</button>
|
||||||
${server_follow}
|
${server_follow}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>
|
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
|
||||||
Edit Profile
|
Edit Profile
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami && this.following !== undefined) {
|
if (this.id !== this.whoami && this.following !== undefined) {
|
||||||
follow = this.following
|
follow = this.following
|
||||||
? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>
|
? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
|
||||||
Unfollow
|
Unfollow
|
||||||
</button>`
|
</button>`
|
||||||
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
|
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
|
||||||
Follow
|
Follow
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami && this.blocking !== undefined) {
|
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||||
block = this.blocking
|
block = this.blocking
|
||||||
? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>
|
? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
|
||||||
Unblock
|
Unblock
|
||||||
</button>`
|
</button>`
|
||||||
: html`<button class="w3-button w3-dark-grey" @click=${this.block}>
|
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
|
||||||
Block
|
Block
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
@ -267,16 +267,16 @@ class TfProfileElement extends LitElement {
|
|||||||
<div class="w3-container">
|
<div class="w3-container">
|
||||||
<div>
|
<div>
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||||
</div>
|
</div>
|
||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||||
<div>
|
<div>
|
||||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
|
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
68
apps/ssb/tf-reactions-modal.js
Normal file
68
apps/ssb/tf-reactions-modal.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfReactionsModalElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
users: {type: Object},
|
||||||
|
votes: {type: Array},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.votes = [];
|
||||||
|
this.users = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.votes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
return this.votes?.length
|
||||||
|
? html` <div
|
||||||
|
class="w3-modal w3-animate-opacity"
|
||||||
|
style="display: block; box-sizing: border-box"
|
||||||
|
>
|
||||||
|
<div class="w3-modal-content w3-card-4 w3-theme-d1">
|
||||||
|
<div class="w3-container w3-padding">
|
||||||
|
<header class="w3-container">
|
||||||
|
<h2>Reactions</h2>
|
||||||
|
<span class="w3-button w3-display-topright" @click=${this.clear}
|
||||||
|
>×</span
|
||||||
|
>
|
||||||
|
</header>
|
||||||
|
<ul class="w3-theme-dark w3-container w3-ul">
|
||||||
|
${this.votes.map(
|
||||||
|
(x) => html`
|
||||||
|
<li class="w3-bar">
|
||||||
|
<span class="w3-bar-item"
|
||||||
|
>${x?.content?.vote?.expression}</span
|
||||||
|
>
|
||||||
|
<tf-user
|
||||||
|
class="w3-bar-item"
|
||||||
|
id=${x.author}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
|
<span class="w3-bar-item w3-right"
|
||||||
|
>${new Date(x?.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<footer class="w3-container w3-padding">
|
||||||
|
<button class="w3-button" @click=${this.clear}>Close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-reactions-modal', TfReactionsModalElement);
|
@ -1,18 +1,6 @@
|
|||||||
import {css} from './lit-all.min.js';
|
import {css} from './lit-all.min.js';
|
||||||
|
|
||||||
const tf = css`
|
const tf = css`
|
||||||
a:link {
|
|
||||||
color: #bbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #ddf;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: min(640px, 100%);
|
max-width: min(640px, 100%);
|
||||||
max-height: min(480px, auto);
|
max-height: min(480px, auto);
|
||||||
@ -46,16 +34,12 @@ const tf = css`
|
|||||||
content: ' ±';
|
content: ' ±';
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
pre code {
|
||||||
background-color: #444;
|
display: block;
|
||||||
padding-left: 3px;
|
padding: 8px;
|
||||||
padding-right: 3px;
|
|
||||||
border: 1px dotted #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
background-color: #607d8b;
|
|
||||||
border-left: 4px solid #fff;
|
border-left: 4px solid #fff;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
@ -301,4 +285,30 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export let styles = [tf, w3];
|
// prettier-ignore
|
||||||
|
const w3_2016_riverside = css`
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#26364a !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
|
||||||
|
.w3-text-theme {color:#4c6a92 !important}
|
||||||
|
.w3-border-theme {border-color:#4c6a92 !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#4c6a92 !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export let styles = [tf, w3, w3_2016_riverside];
|
||||||
|
@ -7,9 +7,11 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
return {
|
return {
|
||||||
broadcasts: {type: Array},
|
broadcasts: {type: Array},
|
||||||
identities: {type: Array},
|
identities: {type: Array},
|
||||||
|
my_identities: {type: Array},
|
||||||
connections: {type: Array},
|
connections: {type: Array},
|
||||||
stored_connections: {type: Array},
|
stored_connections: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
|
server_identity: {type: String},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,22 +22,31 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
this.broadcasts = [];
|
this.broadcasts = [];
|
||||||
this.identities = [];
|
this.identities = [];
|
||||||
|
this.my_identities = [];
|
||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.stored_connections = [];
|
this.stored_connections = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
|
tfrpc.rpc.getIdentities().then(function (identities) {
|
||||||
|
self.my_identities = identities || [];
|
||||||
|
});
|
||||||
tfrpc.rpc.getAllIdentities().then(function (identities) {
|
tfrpc.rpc.getAllIdentities().then(function (identities) {
|
||||||
self.identities = identities || [];
|
self.identities = identities || [];
|
||||||
});
|
});
|
||||||
tfrpc.rpc.getStoredConnections().then(function (connections) {
|
tfrpc.rpc.getStoredConnections().then(function (connections) {
|
||||||
self.stored_connections = connections || [];
|
self.stored_connections = connections || [];
|
||||||
});
|
});
|
||||||
|
tfrpc.rpc.getServerIdentity().then(function (identity) {
|
||||||
|
self.server_identity = identity;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render_connection_summary(connection) {
|
render_connection_summary(connection) {
|
||||||
if (connection.address && connection.port) {
|
if (connection.address && connection.port) {
|
||||||
return html`(<small>${connection.address}:${connection.port}</small>)`;
|
return html`<div>
|
||||||
|
<small>${connection.address}:${connection.port}</small>
|
||||||
|
</div>`;
|
||||||
} else if (connection.tunnel) {
|
} else if (connection.tunnel) {
|
||||||
return html`(room peer)`;
|
return html`<div>room peer</div>`;
|
||||||
} else {
|
} else {
|
||||||
return JSON.stringify(connection);
|
return JSON.stringify(connection);
|
||||||
}
|
}
|
||||||
@ -61,7 +72,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
@ -73,15 +84,17 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_broadcast(connection) {
|
render_broadcast(connection) {
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
@click=${() => tfrpc.rpc.connect(connection)}
|
@click=${() => tfrpc.rpc.connect(connection)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<div class="w3-bar-item">
|
||||||
${this.render_connection_summary(connection)}
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
|
${this.render_connection_summary(connection)}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -92,9 +105,19 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_connection(connection) {
|
render_connection(connection) {
|
||||||
|
let requests = Object.values(
|
||||||
|
connection.requests.reduce(function (accumulator, value) {
|
||||||
|
let key = `${value.name}:${Math.sign(value.request_number)}`;
|
||||||
|
if (!accumulator[key]) {
|
||||||
|
accumulator[key] = Object.assign({count: 0}, value);
|
||||||
|
}
|
||||||
|
accumulator[key].count++;
|
||||||
|
return accumulator;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
@ -103,6 +126,20 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
${connection.tunnel !== undefined
|
${connection.tunnel !== undefined
|
||||||
? '🚇'
|
? '🚇'
|
||||||
: html`(${connection.host}:${connection.port})`}
|
: html`(${connection.host}:${connection.port})`}
|
||||||
|
<div>
|
||||||
|
${requests.map(
|
||||||
|
(x) => html`
|
||||||
|
<span class="w3-tag w3-small"
|
||||||
|
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
|
||||||
|
<span
|
||||||
|
class="w3-badge w3-white"
|
||||||
|
style=${x.count > 1 ? undefined : 'display: none'}
|
||||||
|
>${x.count}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections
|
${this.connections
|
||||||
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
||||||
@ -115,56 +152,74 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div class="w3-container">
|
<div class="w3-container" style="box-sizing: border-box">
|
||||||
<h2>New Connection</h2>
|
<h2>New Connection</h2>
|
||||||
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
|
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() =>
|
@click=${() =>
|
||||||
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
<h2>Broadcasts</h2>
|
<h2>Broadcasts</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.broadcasts
|
${this.broadcasts
|
||||||
.filter((x) => x.address)
|
.filter((x) => x.address)
|
||||||
.map((x) => self.render_broadcast(x))}
|
.map((x) => self.render_broadcast(x))}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.connections
|
${this.connections
|
||||||
.filter((x) => x.tunnel === undefined)
|
.filter((x) => x.tunnel === undefined)
|
||||||
.map((x) => html` <li>${this.render_connection(x)}</li> `)}
|
.map(
|
||||||
|
(x) => html`
|
||||||
|
<li class="w3-bar">${this.render_connection(x)}</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Stored Connections (WIP)</h2>
|
<h2>Stored Connections</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.stored_connections.map(
|
${this.stored_connections.map(
|
||||||
(x) => html`
|
(x) => html`
|
||||||
<li>
|
<li class="w3-bar">
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
@click=${() => self.forget_stored_connection(x)}
|
@click=${() => self.forget_stored_connection(x)}
|
||||||
>
|
>
|
||||||
Forget
|
Forget
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
@click=${() => tfrpc.rpc.connect(x)}
|
@click=${() => tfrpc.rpc.connect(x)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
${x.address}:${x.port}
|
<div class="w3-bar-item">
|
||||||
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||||
|
<div><small>${x.address}:${x.port}</small></div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Local Accounts</h2>
|
<h2>Local Accounts</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.identities.map(
|
${this.identities.map(
|
||||||
(x) =>
|
(x) =>
|
||||||
html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`
|
html`<li class="w3-bar">
|
||||||
|
${x == this.server_identity
|
||||||
|
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
|
||||||
|
>🖥 local server</span
|
||||||
|
>`
|
||||||
|
: undefined}
|
||||||
|
${this.my_identities.indexOf(x) != -1
|
||||||
|
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
|
||||||
|
>😎 you</span
|
||||||
|
>`
|
||||||
|
: undefined}
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
</li>`
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
console.log('Loading...', this.whoami);
|
console.log('Loading...', this.whoami);
|
||||||
let results = await tfrpc.rpc.query(
|
let results = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
@ -33,12 +33,12 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
if (this.hash.startsWith('#@')) {
|
if (this.hash.startsWith('#@')) {
|
||||||
let r = await tfrpc.rpc.query(
|
let r = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH mine AS (SELECT messages.*
|
WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE messages.author = ?
|
WHERE messages.author = ?
|
||||||
ORDER BY sequence DESC
|
ORDER BY sequence DESC
|
||||||
LIMIT 20)
|
LIMIT 20)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM mine
|
FROM mine
|
||||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
@ -51,11 +51,11 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
} else if (this.hash.startsWith('#%')) {
|
} else if (this.hash.startsWith('#%')) {
|
||||||
return await tfrpc.rpc.query(
|
return await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages JOIN messages_refs
|
FROM messages JOIN messages_refs
|
||||||
ON messages.id = messages_refs.message
|
ON messages.id = messages_refs.message
|
||||||
WHERE messages_refs.ref = ?1
|
WHERE messages_refs.ref = ?1
|
||||||
@ -69,17 +69,17 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
promises.push(
|
promises.push(
|
||||||
tfrpc.rpc.query(
|
tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
||||||
ORDER BY messages.timestamp DESC)
|
ORDER BY messages.timestamp DESC)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
@ -107,18 +107,18 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
||||||
let more = await tfrpc.rpc.query(
|
let more = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ?
|
WHERE messages.timestamp > ?
|
||||||
AND messages.timestamp <= ?
|
AND messages.timestamp <= ?
|
||||||
ORDER BY messages.timestamp DESC)
|
ORDER BY messages.timestamp DESC)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
@ -187,7 +187,7 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||||
more = html`
|
more = html`
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.load_more}>
|
<button class="w3-button w3-theme-d1" @click=${this.load_more}>
|
||||||
Load More
|
Load More
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
@ -12,6 +12,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
|
loading: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,10 +85,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
delete this.drafts[id];
|
delete this.drafts[id];
|
||||||
}
|
}
|
||||||
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
|
this.drafts = Object.assign({}, this.drafts);
|
||||||
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
|
|
||||||
this.drafts = Object.assign({}, this.drafts);
|
|
||||||
}
|
|
||||||
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,17 +114,31 @@ class TfTabNewsElement extends LitElement {
|
|||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
></tf-profile>`
|
></tf-profile>`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
let edit_profile;
|
||||||
|
if (
|
||||||
|
!this.loading &&
|
||||||
|
this.users[this.whoami]?.name === undefined &&
|
||||||
|
this.hash.substring(1) != this.whoami
|
||||||
|
) {
|
||||||
|
edit_profile = html` <div
|
||||||
|
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
|
||||||
|
>
|
||||||
|
ℹ️ Follow your identity link ☝️ above to edit your profile and set your
|
||||||
|
name.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<p class="w3-bar">
|
<p class="w3-bar">
|
||||||
<button
|
<button
|
||||||
class="w3-bar-item w3-button w3-dark-grey"
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
@click=${this.show_more}
|
@click=${this.show_more}
|
||||||
>
|
>
|
||||||
${this.new_messages_text()}
|
${this.new_messages_text()}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div class="w3-bar">
|
||||||
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
||||||
|
${edit_profile}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<tf-compose
|
<tf-compose
|
||||||
|
@ -110,14 +110,14 @@ class TfTabQueryElement extends LitElement {
|
|||||||
<textarea
|
<textarea
|
||||||
id="search"
|
id="search"
|
||||||
rows="8"
|
rows="8"
|
||||||
class="w3-input w3-dark-grey"
|
class="w3-input w3-theme-d1"
|
||||||
style="flex: 1; resize: vertical"
|
style="flex: 1; resize: vertical"
|
||||||
@keydown=${this.search_keydown}
|
@keydown=${this.search_keydown}
|
||||||
>
|
>
|
||||||
${this.query}</textarea
|
${this.query}</textarea
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-dark-grey"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${(event) =>
|
@click=${(event) =>
|
||||||
self.search(self.renderRoot.getElementById('search').value)}
|
self.search(self.renderRoot.getElementById('search').value)}
|
||||||
>
|
>
|
||||||
|
@ -35,7 +35,7 @@ class TfTabSearchElement extends LitElement {
|
|||||||
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
||||||
let results = await tfrpc.rpc.query(
|
let results = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
@ -78,8 +78,8 @@ class TfTabSearchElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: row; gap: 4px">
|
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||||
<input type="text" class="w3-input w3-dark-grey" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||||
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||||
</div>
|
</div>
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
||||||
`;
|
`;
|
||||||
|
@ -19,6 +19,11 @@ class TfUserElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let image = html`<span
|
||||||
|
class="w3-theme-light w3-circle"
|
||||||
|
style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
|
||||||
|
>?</span
|
||||||
|
>`;
|
||||||
let name = this.users?.[this.id]?.name;
|
let name = this.users?.[this.id]?.name;
|
||||||
name =
|
name =
|
||||||
name !== undefined
|
name !== undefined
|
||||||
@ -26,21 +31,20 @@ class TfUserElement extends LitElement {
|
|||||||
: html`<a target="_top" href=${'#' + this.id}>${this.id}</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_link = this.users[this.id].image;
|
||||||
image = typeof image == 'string' ? image : image?.link;
|
image_link =
|
||||||
return html` <div style="display: inline-block; font-weight: bold">
|
typeof image_link == 'string' ? image_link : image_link?.link;
|
||||||
<img
|
if (image_link !== undefined) {
|
||||||
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
|
image = html`<img
|
||||||
?hidden=${image === undefined}
|
class="w3-circle"
|
||||||
src="${image ? '/' + image + '/view' : undefined}"
|
style="width: 2em; height: 2em; vertical-align: middle"
|
||||||
/>
|
src="/${image_link}/view"
|
||||||
${name}
|
/>`;
|
||||||
</div>`;
|
}
|
||||||
} else {
|
|
||||||
return html` <div style="display: inline-block; font-weight: bold">
|
|
||||||
${name}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
return html` <div style="display: inline-block; font-weight: bold">
|
||||||
|
${image} ${name}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
|
||||||
import * as hashtagify from './commonmark-hashtag.js';
|
import * as hashtagify from './commonmark-hashtag.js';
|
||||||
|
|
||||||
|
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (
|
if (
|
||||||
node.firstChild?.type === 'text' &&
|
node.firstChild?.type === 'text' &&
|
||||||
@ -61,13 +62,32 @@ function image(node, entering) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function code(node) {
|
||||||
|
let attrs = this.attrs(node);
|
||||||
|
attrs.push(['class', k_code_classes]);
|
||||||
|
this.tag('code', attrs);
|
||||||
|
this.out(node.literal);
|
||||||
|
this.tag('/code');
|
||||||
|
}
|
||||||
|
|
||||||
|
function attrs(node) {
|
||||||
|
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
|
||||||
|
if (node.type == 'block_quote') {
|
||||||
|
result.push(['class', 'w3-theme-d1']);
|
||||||
|
} else if (node.type == 'code_block') {
|
||||||
|
result.push(['class', k_code_classes]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function markdown(md) {
|
export function markdown(md) {
|
||||||
let reader = new commonmark.Parser({safe: true});
|
let reader = new commonmark.Parser({safe: true});
|
||||||
let writer = new commonmark.HtmlRenderer();
|
let writer = new commonmark.HtmlRenderer();
|
||||||
writer.image = image;
|
writer.image = image;
|
||||||
|
writer.code = code;
|
||||||
|
writer.attrs = attrs;
|
||||||
let parsed = reader.parse(md || '');
|
let parsed = reader.parse(md || '');
|
||||||
parsed = hashtagify.transform(parsed);
|
parsed = hashtagify.transform(parsed);
|
||||||
parsed = linkify.transform(parsed);
|
|
||||||
let walker = parsed.walker();
|
let walker = parsed.walker();
|
||||||
let event, node;
|
let event, node;
|
||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "👋",
|
"emoji": "👋",
|
||||||
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
|
"previous": "&W5aJp2DgOW5rQ0AOIC9Ut3DpsahPrO6PjkJ1PQbNRdM=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
class="w3-button w3-black w3-padding-large"
|
class="w3-button w3-black w3-padding-large"
|
||||||
href="https://www.tildefriends.net/~cory/releases/"
|
href="https://dev.tildefriends.net/cory/tildefriends/releases"
|
||||||
><i class="fa fa-download"></i> Download</a
|
><i class="fa fa-download"></i> Download</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -63,6 +63,11 @@
|
|||||||
href="https://www.tildefriends.net/~cory/apps/"
|
href="https://www.tildefriends.net/~cory/apps/"
|
||||||
><i class="fa fa-link"></i> Try It</a
|
><i class="fa fa-link"></i> Try It</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
class="w3-button w3-black w3-padding-large"
|
||||||
|
href="https://dev.tildefriends.net/"
|
||||||
|
><i class="fa fa-mug-hot"></i> Code</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col l4 m6">
|
<div class="w3-col l4 m6">
|
||||||
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
|
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
|
||||||
@ -70,6 +75,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Getting Starting Section -->
|
||||||
|
<div class="w3-indigo w3-center">
|
||||||
|
<div class="w3-row-padding w3-padding-64">
|
||||||
|
<div class="w3-jumbo">
|
||||||
|
<i class="fa fa-rocket"></i> <b>Getting Started</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>First-time user checklist:</h2>
|
||||||
|
<ol type="1" style="text-align: left">
|
||||||
|
<li>
|
||||||
|
<a href="https://dev.tildefriends.net/cory/tildefriends/releases"
|
||||||
|
>Download</a
|
||||||
|
>
|
||||||
|
Tilde Friends and run your own instance, or use
|
||||||
|
<a href="https://www.tildefriends.net/"
|
||||||
|
>https://www.tildefriends.net/</a
|
||||||
|
>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Create an account to identify yourself with that instance by
|
||||||
|
username and password.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Create an SSB identity in the <b>ssb</b> app. This will generate a
|
||||||
|
keypair used to identify yourself to other users and sign your
|
||||||
|
messages so that they can be verified as from you.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Describe yourself in your profile in the <b>ssb</b> app. Give
|
||||||
|
yourself a name and an avatar if you like.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Connect to others. You will automatically discover peers on the
|
||||||
|
same instance and same network if there are any. Or use
|
||||||
|
<a href="https://github.com/staltz/ssb-room/blob/master/FAQ.md"
|
||||||
|
>rooms</a
|
||||||
|
>
|
||||||
|
and pubs to reach more distant users.
|
||||||
|
<a href="https://www.tildefriends.net/~cory/room/"
|
||||||
|
>tildefriends.net itself</a
|
||||||
|
>
|
||||||
|
operates as a room, so you can connect and see who else is online
|
||||||
|
and establish a connection.
|
||||||
|
</li>
|
||||||
|
<li>Follow people to grow your network.</li>
|
||||||
|
<li>
|
||||||
|
Use the <b>edit</b> link at the top of any page to start modifying
|
||||||
|
and making apps.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SSB Section -->
|
<!-- SSB Section -->
|
||||||
<div class="w3-light-grey">
|
<div class="w3-light-grey">
|
||||||
<div class="w3-row-padding w3-padding-64">
|
<div class="w3-row-padding w3-padding-64">
|
||||||
@ -199,7 +258,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w3-row" style="margin-top: 64px">
|
<div class="w3-row" style="margin-top: 64px">
|
||||||
<a href="https://codemirror.net/5/" class="w3-col s3">
|
<a href="https://codemirror.net/docs/changelog/" class="w3-col s3">
|
||||||
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
|
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
|
||||||
<p>CodeMirror</p>
|
<p>CodeMirror</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&/wl8HE2jZShRXTYEVYRrK3pjHwi41Wbxl9HoSJaQP6Y=.sha256"
|
"previous": "&DaYqKHRBKhjFGaOzbKZ1+/pLspJeEkDJYTF2B50tH6k=.sha256"
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import * as utils from './utils.js';
|
|||||||
let g_hash;
|
let g_hash;
|
||||||
let g_collection_notifies = {};
|
let g_collection_notifies = {};
|
||||||
|
|
||||||
|
tfrpc.register(async function getActiveIdentity() {
|
||||||
|
return ssb.getActiveIdentity();
|
||||||
|
});
|
||||||
tfrpc.register(async function getOwnerIdentities() {
|
tfrpc.register(async function getOwnerIdentities() {
|
||||||
return ssb.getOwnerIdentities();
|
return ssb.getOwnerIdentities();
|
||||||
});
|
});
|
||||||
@ -54,6 +57,9 @@ core.register('message', async function message_handler(message) {
|
|||||||
await tfrpc.rpc.hash_changed(message.hash);
|
await tfrpc.rpc.hash_changed(message.hash);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
core.register('setActiveIdentity', async function setActiveIdentityHandler(id) {
|
||||||
|
await tfrpc.rpc.setActiveIdentity(id);
|
||||||
|
});
|
||||||
|
|
||||||
tfrpc.register(function set_hash(hash) {
|
tfrpc.register(function set_hash(hash) {
|
||||||
if (g_hash != hash) {
|
if (g_hash != hash) {
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base target="_top" />
|
<base target="_top" />
|
||||||
|
<link rel="stylesheet" href="tildefriends.css" />
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body>
|
||||||
<tf-collections-app></tf-collections-app>
|
<tf-collections-app></tf-collections-app>
|
||||||
<script>
|
<script>
|
||||||
window.litDisableBundleWarning = true;
|
window.litDisableBundleWarning = true;
|
||||||
</script>
|
</script>
|
||||||
<script src="tf-collection.js" type="module"></script>
|
<script src="tf-collection.js" type="module"></script>
|
||||||
<script src="tf-id-picker.js" type="module"></script>
|
|
||||||
<script src="tf-wiki-doc.js" type="module"></script>
|
<script src="tf-wiki-doc.js" type="module"></script>
|
||||||
<script src="tf-wiki-app.js" type="module"></script>
|
<script src="tf-wiki-app.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
4
apps/wiki/lit-all.min.js
vendored
4
apps/wiki/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
@ -5,6 +5,7 @@ class TfCollectionElement extends LitElement {
|
|||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
whoami: {type: String},
|
whoami: {type: String},
|
||||||
|
category: {type: String},
|
||||||
collection: {type: Object},
|
collection: {type: Object},
|
||||||
selected_id: {type: String},
|
selected_id: {type: String},
|
||||||
is_creating: {type: Boolean},
|
is_creating: {type: Boolean},
|
||||||
@ -75,9 +76,10 @@ class TfCollectionElement extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<span style="display: inline-flex; flex-direction: row">
|
<link rel="stylesheet" href="tildefriends.css"/>
|
||||||
|
<span class="inline-flex-row">
|
||||||
<select @change=${this.on_selected} id="select" value=${this.selected_id}>
|
<select @change=${this.on_selected} id="select" value=${this.selected_id}>
|
||||||
<option value="" ?selected=${this.selected_id === ''} disabled hidden>(select)</option>
|
<option value="" ?selected=${this.selected_id === ''} disabled hidden>(select ${this.category})</option>
|
||||||
${Object.values(this.collection ?? {})
|
${Object.values(this.collection ?? {})
|
||||||
.sort((x, y) => x.name.localeCompare(y.name))
|
.sort((x, y) => x.name.localeCompare(y.name))
|
||||||
.map(
|
.map(
|
||||||
@ -91,22 +93,22 @@ class TfCollectionElement extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<span ?hidden=${!this.is_renaming || !this.whoami}>
|
<span ?hidden=${!this.is_renaming || !this.whoami}>
|
||||||
<span style="display: inline-flex; flex-direction: row; margin-left: 8px; margin-right: 8px">
|
<span class="inline-flex-row" style="margin-left: 8px; margin-right: 8px">
|
||||||
<label for="rename_name">🏷Rename to:</label>
|
<label for="rename_name">🏷Rename to:</label>
|
||||||
<input type="text" id="rename_name"></input>
|
<input type="text" id="rename_name"></input>
|
||||||
<button @click=${this.on_rename}>Rename ${this.type}</button>
|
<button @click=${this.on_rename}>Rename ${this.type}</button>
|
||||||
<button @click=${() => (self.is_renaming = false)}>x</button>
|
<button @click=${() => (self.is_renaming = false)}>x</button>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button @click=${() => (self.is_renaming = true)} ?disabled=${this.is_renaming || !this.selected_id} ?hidden=${!this.whoami}>🏷</button>
|
<button class="yellow" @click=${() => (self.is_renaming = true)} ?disabled=${this.is_renaming || !this.selected_id} ?hidden=${!this.whoami}>🏷</button>
|
||||||
<button @click=${self.on_tombstone} ?disabled=${!this.selected_id} ?hidden=${!this.whoami}>🪦</button>
|
<button class="red" @click=${self.on_tombstone} ?disabled=${!this.selected_id} ?hidden=${!this.whoami}>🪦</button>
|
||||||
<span ?hidden=${!this.is_creating || !this.whoami}>
|
<span ?hidden=${!this.is_creating || !this.whoami}>
|
||||||
<label for="create_name">New ${this.type} name:</label>
|
<label for="create_name">New ${this.type} name:</label>
|
||||||
<input type="text" id="create_name"></input>
|
<input type="text" id="create_name"></input>
|
||||||
<button @click=${this.on_create}>Create ${this.type}</button>
|
<button @click=${this.on_create}>Create ${this.type}</button>
|
||||||
<button @click=${() => (self.is_creating = false)}>x</button>
|
<button @click=${() => (self.is_creating = false)}>x</button>
|
||||||
</span>
|
</span>
|
||||||
<button @click=${() => (self.is_creating = true)} ?hidden=${this.is_creating || !this.whoami}>+</button>
|
<button class="green" @click=${() => (self.is_creating = true)} ?hidden=${this.is_creating || !this.whoami}>+</button>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Provide a list of IDs, and this lets the user pick one.
|
|
||||||
*/
|
|
||||||
class TfIdentityPickerElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
ids: {type: Array},
|
|
||||||
selected: {type: String},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.ids = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
changed(event) {
|
|
||||||
this.selected = event.srcElement.value;
|
|
||||||
this.dispatchEvent(
|
|
||||||
new Event('change', {
|
|
||||||
srcElement: this,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
|
||||||
${(this.ids ?? []).map(
|
|
||||||
(id) =>
|
|
||||||
html`<option ?selected=${id == this.selected} value=${id}>
|
|
||||||
${id}
|
|
||||||
</option>`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
|
@ -31,13 +31,16 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
tfrpc.register(function hash_changed(hash) {
|
tfrpc.register(function hash_changed(hash) {
|
||||||
self.notify_hash_changed(hash);
|
self.notify_hash_changed(hash);
|
||||||
});
|
});
|
||||||
|
tfrpc.register(function setActiveIdentity(id) {
|
||||||
|
self.whoami = id;
|
||||||
|
});
|
||||||
tfrpc.rpc.get_hash().then((hash) => self.notify_hash_changed(hash));
|
tfrpc.rpc.get_hash().then((hash) => self.notify_hash_changed(hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.ids = await tfrpc.rpc.getIdentities();
|
this.ids = await tfrpc.rpc.getIdentities();
|
||||||
this.owner_ids = await tfrpc.rpc.getOwnerIdentities();
|
this.owner_ids = await tfrpc.rpc.getOwnerIdentities();
|
||||||
this.whoami = await tfrpc.rpc.localStorageGet('collections_whoami');
|
this.whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
let ids = [...new Set([...this.owner_ids, this.whoami])].sort();
|
let ids = [...new Set([...this.owner_ids, this.whoami])].sort();
|
||||||
this.following = Object.keys(await tfrpc.rpc.following(ids, 1)).sort();
|
this.following = Object.keys(await tfrpc.rpc.following(ids, 1)).sort();
|
||||||
|
|
||||||
@ -255,23 +258,31 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
|
<link rel="stylesheet" href="tildefriends.css"/>
|
||||||
<style>
|
<style>
|
||||||
.toc:hover {
|
.toc-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toc-item:hover {
|
||||||
background-color: #0cc;
|
background-color: #0cc;
|
||||||
}
|
}
|
||||||
.toc.selected {
|
.toc-item.selected {
|
||||||
background-color: #088;
|
background-color: #088;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.table-of-contents {
|
||||||
|
flex: 0 0;
|
||||||
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div>
|
|
||||||
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
${keyed(
|
${keyed(
|
||||||
this.whoami,
|
this.whoami,
|
||||||
html`<tf-collection
|
html`<tf-collection
|
||||||
.collection=${this.wikis}
|
.collection=${this.wikis}
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
|
category="wiki"
|
||||||
selected_id=${this.wiki?.id}
|
selected_id=${this.wiki?.id}
|
||||||
@create=${this.on_wiki_create}
|
@create=${this.on_wiki_create}
|
||||||
@rename=${this.on_wiki_rename}
|
@rename=${this.on_wiki_rename}
|
||||||
@ -284,6 +295,7 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
html`<tf-collection
|
html`<tf-collection
|
||||||
.collection=${this.wiki_docs}
|
.collection=${this.wiki_docs}
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
|
category="document"
|
||||||
selected_id=${this.wiki_doc &&
|
selected_id=${this.wiki_doc &&
|
||||||
this.wiki_doc?.parent == this.wiki?.id
|
this.wiki_doc?.parent == this.wiki?.id
|
||||||
? this.wiki_doc?.id
|
? this.wiki_doc?.id
|
||||||
@ -298,9 +310,9 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
<div ?hidden=${!this.wiki?.editors || !this.expand_editors}>
|
<div ?hidden=${!this.wiki?.editors || !this.expand_editors}>
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
${this.wiki?.editors.map((id) => html`<li><button ?hidden=${id == this.whoami} @click=${() => self.on_remove_editor(id)}>x</button> ${id}</li>`)}
|
${this.wiki?.editors.map((id) => html`<li><button class="red" ?hidden=${id == this.whoami} @click=${() => self.on_remove_editor(id)}>x</button> ${id}</li>`)}
|
||||||
<li>
|
<li>
|
||||||
<button @click=${() => (self.adding_editor = true)} ?hidden=${this.wiki?.editors?.indexOf(this.whoami) == -1 || this.adding_editor}>+</button>
|
<button class="green" @click=${() => (self.adding_editor = true)} ?hidden=${this.wiki?.editors?.indexOf(this.whoami) == -1 || this.adding_editor}>+</button>
|
||||||
<div ?hidden=${!this.adding_editor}>
|
<div ?hidden=${!this.adding_editor}>
|
||||||
<label for="add_editor">Add Editor:</label>
|
<label for="add_editor">Add Editor:</label>
|
||||||
<input type="text" id="add_editor"></input>
|
<input type="text" id="add_editor"></input>
|
||||||
@ -312,18 +324,19 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div class="flex-row">
|
||||||
<div style="flex: 0 0">
|
<div class="box table-of-contents">
|
||||||
${Object.values(this.wikis || {})
|
${Object.values(this.wikis || {})
|
||||||
.sort((x, y) => x.name.localeCompare(y.name))
|
.sort((x, y) => x.name.localeCompare(y.name))
|
||||||
.map(
|
.map(
|
||||||
(wiki) => html`
|
(wiki) => html`
|
||||||
<div
|
<div
|
||||||
class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}"
|
class="toc-item ${self.wiki?.id === wiki.id
|
||||||
style="white-space: nowrap; cursor: pointer"
|
? 'selected'
|
||||||
|
: ''}"
|
||||||
@click=${() => self.on_wiki_changed({detail: {value: wiki}})}
|
@click=${() => self.on_wiki_changed({detail: {value: wiki}})}
|
||||||
>
|
>
|
||||||
${wiki.name}
|
${self.wiki?.id === wiki.id ? '' : '>'} ${wiki.name}
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${Object.values(self.wiki_docs || {})
|
${Object.values(self.wiki_docs || {})
|
||||||
@ -332,14 +345,12 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
.map(
|
.map(
|
||||||
(doc) => html`
|
(doc) => html`
|
||||||
<li
|
<li
|
||||||
class="toc ${self.wiki_doc?.id === doc.id
|
class="toc-item ${self.wiki_doc?.id === doc.id
|
||||||
? 'selected'
|
? 'selected'
|
||||||
: ''}"
|
: ''}"
|
||||||
style="white-space: nowrap; cursor: pointer; list-style: none; text-indent: -1rem"
|
style="list-style: none; text-indent: -1rem"
|
||||||
@click=${() =>
|
@click=${() =>
|
||||||
self.on_wiki_doc_changed({
|
self.on_wiki_doc_changed({detail: {value: doc}})}
|
||||||
detail: {value: doc},
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
${doc?.private ? '🔒' : '📄'} ${doc.name}
|
${doc?.private ? '🔒' : '📄'} ${doc.name}
|
||||||
</li>
|
</li>
|
||||||
|
@ -84,7 +84,11 @@ class TfWikiDocElement extends LitElement {
|
|||||||
|
|
||||||
async load_blob() {
|
async load_blob() {
|
||||||
let blob = await tfrpc.rpc.get_blob(this.value?.blob);
|
let blob = await tfrpc.rpc.get_blob(this.value?.blob);
|
||||||
if (blob.endsWith('.box')) {
|
if (!blob) {
|
||||||
|
console.warn(
|
||||||
|
"no blob found, we're going to assume the document is empty (load_blob())"
|
||||||
|
);
|
||||||
|
} else if (blob.endsWith('.box')) {
|
||||||
let d = await tfrpc.rpc.try_decrypt(this.whoami, blob);
|
let d = await tfrpc.rpc.try_decrypt(this.whoami, blob);
|
||||||
if (d) {
|
if (d) {
|
||||||
blob = d;
|
blob = d;
|
||||||
@ -253,85 +257,43 @@ class TfWikiDocElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let thumbnail_ref = this.thumbnail(this.blob);
|
let thumbnail_ref = this.thumbnail(this.blob);
|
||||||
return html`
|
return html`
|
||||||
|
<link rel="stylesheet" href="tildefriends.css"/>
|
||||||
<style>
|
<style>
|
||||||
a:link {
|
a:link { color: #268bd2 }
|
||||||
color: #268bd2;
|
a:visited { color: #6c71c4 }
|
||||||
}
|
a:hover { color: #859900 }
|
||||||
a:visited {
|
a:active { color: #2aa198 }
|
||||||
color: #6c71c4;
|
|
||||||
}
|
#editor-text-area {
|
||||||
a:hover {
|
background-color: #00000040;
|
||||||
color: #859900;
|
color: white;
|
||||||
}
|
style="flex: 1 1;
|
||||||
a:active {
|
min-height: 10em;
|
||||||
color: #2aa198;
|
font-size: larger;
|
||||||
}
|
${this.value?.private ? 'border: 4px solid #800' : ''}
|
||||||
</style>
|
</style>
|
||||||
<div style="display: inline-flex; flex-direction: row">
|
<div class="inline-flex-row">
|
||||||
<button
|
<button ?disabled=${!this.whoami || this.is_editing} @click=${() => (self.is_editing = true)}>Edit</button>
|
||||||
?disabled=${!this.whoami || this.is_editing}
|
<button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button>
|
||||||
@click=${() => (self.is_editing = true)}
|
<button ?disabled=${this.blob == this.blob_original && !this.value?.draft} @click=${this.on_publish}>Publish</button>
|
||||||
>
|
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>Discard</button>
|
||||||
Edit
|
<button ?disabled=${!this.is_editing} @click=${() => (self.value = Object.assign({}, self.value, {private: !self.value.private}))}>${this.value?.private ? 'Make Public' : 'Make Private'}</button>
|
||||||
</button>
|
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>Publish Blog</button>
|
||||||
<button
|
|
||||||
?disabled=${this.blob == this.blob_original}
|
|
||||||
@click=${this.on_save_draft}
|
|
||||||
>
|
|
||||||
Save Draft
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
?disabled=${this.blob == this.blob_original && !this.value?.draft}
|
|
||||||
@click=${this.on_publish}
|
|
||||||
>
|
|
||||||
Publish
|
|
||||||
</button>
|
|
||||||
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>
|
|
||||||
Discard
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
?disabled=${!this.is_editing}
|
|
||||||
@click=${() =>
|
|
||||||
(self.value = Object.assign({}, self.value, {
|
|
||||||
private: !self.value.private,
|
|
||||||
}))}
|
|
||||||
>
|
|
||||||
${this.value?.private ? 'Make Public' : 'Make Private'}
|
|
||||||
</button>
|
|
||||||
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>
|
|
||||||
Publish Blog
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div ?hidden=${!this.value?.private} style="color: #800">
|
<div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div>
|
||||||
🔒 document is private
|
<div class="flex-column" ${this.value?.private ? 'border-top: 4px solid #800' : ''}">
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="display: flex; flex-direction: row; ${this.value?.private
|
|
||||||
? 'border-top: 4px solid #800'
|
|
||||||
: ''}"
|
|
||||||
>
|
|
||||||
<textarea
|
<textarea
|
||||||
|
rows="25"
|
||||||
?hidden=${!this.is_editing}
|
?hidden=${!this.is_editing}
|
||||||
style="flex: 1 1; min-height: 10em; ${this.value?.private
|
id="editor-text-area"
|
||||||
? 'border: 4px solid #800'
|
|
||||||
: ''}"
|
|
||||||
@input=${this.on_edit}
|
@input=${this.on_edit}
|
||||||
@paste=${this.paste}
|
@paste=${this.paste}
|
||||||
.value=${this.blob ?? ''}
|
.value=${this.blob ?? ''}></textarea>
|
||||||
></textarea>
|
<div style="flex: 1 1; margin-top: 16px">
|
||||||
<div style="flex: 1 1">
|
<div ?hidden=${!this.is_editing} class="box">
|
||||||
<div
|
Summary
|
||||||
?hidden=${!this.is_editing}
|
<img ?hidden=${!thumbnail_ref} style="max-width: 128px; max-height: 128px; float: right" src="/${thumbnail_ref}/view">
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em"
|
<h1 ?hidden=${!this.title(this.blob)}>${unsafeHTML(this.markdown(this.title(this.blob)))}</h1>
|
||||||
>
|
|
||||||
<img
|
|
||||||
?hidden=${!thumbnail_ref}
|
|
||||||
style="max-width: 128px; max-height: 128px; float: right"
|
|
||||||
src="/${thumbnail_ref}/view"
|
|
||||||
/>
|
|
||||||
<h1 ?hidden=${!this.title(this.blob)}>
|
|
||||||
${unsafeHTML(this.markdown(this.title(this.blob)))}
|
|
||||||
</h1>
|
|
||||||
${unsafeHTML(this.markdown(this.summary(this.blob)))}
|
${unsafeHTML(this.markdown(this.summary(this.blob)))}
|
||||||
</div>
|
</div>
|
||||||
${unsafeHTML(this.markdown(this.blob))}
|
${unsafeHTML(this.markdown(this.blob))}
|
||||||
|
115
apps/wiki/tildefriends.css
Normal file
115
apps/wiki/tildefriends.css
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Tilde Friends core stylesheet
|
||||||
|
* This is a prototype; things may change based on feedback.
|
||||||
|
*
|
||||||
|
* This Software is an external library that is part of
|
||||||
|
* Tilde Friends and is shared under the MIT license.
|
||||||
|
*
|
||||||
|
* Inject this file in your app at tildefriends.css
|
||||||
|
* and use this tag to import it:
|
||||||
|
* <link rel="stylesheet" href="tildefriends.css"/>
|
||||||
|
*
|
||||||
|
* Revision 0 / 2024 M02 19
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
.button,
|
||||||
|
input[type='button'],
|
||||||
|
input[type='submit'],
|
||||||
|
select {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 4px;
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
background-color: #bd1e24;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
background-color: #18922d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blue {
|
||||||
|
background-color: #0067a7;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.yellow {
|
||||||
|
background-color: #ee9600;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: #268bd2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #6c71c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #859900;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: #2aa198;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border: 1px solid #ffffff40;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #ffffff20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-flex-row {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
background-color: #00000020;
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
@ -50,7 +50,7 @@ function new_message() {
|
|||||||
return g_new_message_promise;
|
return g_new_message_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssb.addEventListener('message', function (id) {
|
core.register('onMessage', function (id) {
|
||||||
let resolve = g_new_message_resolve;
|
let resolve = g_new_message_resolve;
|
||||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
@ -96,7 +96,7 @@ export async function collection(
|
|||||||
let rows = [];
|
let rows = [];
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
`
|
`
|
||||||
SELECT messages.id, author, content, timestamp
|
SELECT messages.id, author, json(content) AS content, timestamp
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?1) AS id ON messages.author = id.value
|
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||||
WHERE
|
WHERE
|
||||||
|
38
core/app.js
38
core/app.js
@ -1,4 +1,3 @@
|
|||||||
import * as auth from './auth.js';
|
|
||||||
import * as core from './core.js';
|
import * as core from './core.js';
|
||||||
|
|
||||||
let g_next_id = 1;
|
let g_next_id = 1;
|
||||||
@ -87,8 +86,7 @@ App.prototype.send = function (message) {
|
|||||||
function socket(request, response, client) {
|
function socket(request, response, client) {
|
||||||
let process;
|
let process;
|
||||||
let options = {};
|
let options = {};
|
||||||
let credentials = auth.query(request.headers);
|
let credentials = httpd.auth_query(request.headers);
|
||||||
let refresh = auth.makeRefresh(credentials);
|
|
||||||
|
|
||||||
response.onClose = async function () {
|
response.onClose = async function () {
|
||||||
if (process && process.task) {
|
if (process && process.task) {
|
||||||
@ -143,12 +141,21 @@ function socket(request, response, client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.send(
|
response.send(
|
||||||
JSON.stringify({
|
JSON.stringify(
|
||||||
action: 'session',
|
Object.assign(
|
||||||
credentials: credentials,
|
{
|
||||||
parentApp: parentApp,
|
action: 'session',
|
||||||
id: blobId,
|
credentials: credentials,
|
||||||
}),
|
parentApp: parentApp,
|
||||||
|
id: blobId,
|
||||||
|
},
|
||||||
|
await ssb.getIdentityInfo(
|
||||||
|
credentials?.session?.name,
|
||||||
|
packageOwner,
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
0x1
|
0x1
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -212,6 +219,10 @@ function socket(request, response, client) {
|
|||||||
if (process) {
|
if (process) {
|
||||||
process.resetPermission(message.permission);
|
process.resetPermission(message.permission);
|
||||||
}
|
}
|
||||||
|
} else if (message.action == 'setActiveIdentity') {
|
||||||
|
process.setActiveIdentity(message.identity);
|
||||||
|
} else if (message.action == 'createIdentity') {
|
||||||
|
process.createIdentity();
|
||||||
} else if (message.message == 'tfrpc') {
|
} else if (message.message == 'tfrpc') {
|
||||||
if (message.id && g_calls[message.id]) {
|
if (message.id && g_calls[message.id]) {
|
||||||
if (message.error !== undefined) {
|
if (message.error !== undefined) {
|
||||||
@ -241,14 +252,7 @@ function socket(request, response, client) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
response.upgrade(
|
response.upgrade(100, {});
|
||||||
100,
|
|
||||||
refresh
|
|
||||||
? {
|
|
||||||
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {socket, App};
|
export {socket, App};
|
||||||
|
@ -19,8 +19,11 @@
|
|||||||
Object.assign(app, g_data);
|
Object.assign(app, g_data);
|
||||||
|
|
||||||
class TfAuthElement extends LitElement {
|
class TfAuthElement extends LitElement {
|
||||||
static get_properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
code_of_conduct: {type: String},
|
||||||
|
error: {type: String},
|
||||||
|
have_administrator: {type: Boolean},
|
||||||
name: {type: String},
|
name: {type: String},
|
||||||
tab: {type: String},
|
tab: {type: String},
|
||||||
};
|
};
|
||||||
@ -31,11 +34,6 @@
|
|||||||
this.tab = 'login';
|
this.tab = 'login';
|
||||||
}
|
}
|
||||||
|
|
||||||
tab_changed(name) {
|
|
||||||
this.tab = name;
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
@ -83,16 +81,16 @@
|
|||||||
<h1 ?hidden=${this.name === undefined}>Welcome, ${this.name}.</h1>
|
<h1 ?hidden=${this.name === undefined}>Welcome, ${this.name}.</h1>
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<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>
|
<input type="radio" name="tab" id="login" value="Login" ?checked=${this.tab == 'login'} @change=${() => (self.tab = 'login')}></input>
|
||||||
<label for="login" id="login_label">Login</label>
|
<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>
|
<input type="radio" name="tab" id="register" value="Register" ?checked=${this.tab == 'register'} @change=${() => (self.tab = 'register')}></input>
|
||||||
<label for="register" id="register_label">Register</label>
|
<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>
|
<input type="radio" name="tab" id="guest" value="Guest" ?checked=${this.tab == 'guest'} @change=${() => (self.tab = 'guest')}></input>
|
||||||
<label for="guest" id="guest_label">Guest</label>
|
<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>
|
<input type="radio" name="tab" id="change" value="Change Password" ?checked=${this.tab == 'change'} @change=${() => (self.tab = 'change')}></input>
|
||||||
<label for="change" id="change_label">Change Password</label>
|
<label for="change" id="change_label">Change Password</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
421
core/auth.js
421
core/auth.js
@ -1,421 +0,0 @@
|
|||||||
import * as core from './core.js';
|
|
||||||
import * as form from './form.js';
|
|
||||||
|
|
||||||
let gDatabase = new Database('auth');
|
|
||||||
|
|
||||||
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a Base64 value URL safe
|
|
||||||
* @param {string} value
|
|
||||||
* @returns TODOC
|
|
||||||
*/
|
|
||||||
function b64url(value) {
|
|
||||||
value = value.replaceAll('+', '-').replaceAll('/', '_');
|
|
||||||
let equals = value.indexOf('=');
|
|
||||||
|
|
||||||
if (equals !== -1) {
|
|
||||||
return value.substring(0, equals);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODOC
|
|
||||||
* @param {string} value
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function unb64url(value) {
|
|
||||||
value = value.replaceAll('-', '+').replaceAll('_', '/');
|
|
||||||
let remainder = value.length % 4;
|
|
||||||
|
|
||||||
if (remainder == 3) {
|
|
||||||
return value + '=';
|
|
||||||
} else if (remainder == 2) {
|
|
||||||
return value + '==';
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a JSON Web Token
|
|
||||||
* @param {object} payload Object: {"name": "username"}
|
|
||||||
* @returns the JWT
|
|
||||||
*/
|
|
||||||
function makeJwt(payload) {
|
|
||||||
const ids = ssb.getIdentities(':auth');
|
|
||||||
let id;
|
|
||||||
|
|
||||||
if (ids?.length) {
|
|
||||||
id = ids[0];
|
|
||||||
} else {
|
|
||||||
id = ssb.createIdentity(':auth');
|
|
||||||
}
|
|
||||||
|
|
||||||
const final_payload = b64url(
|
|
||||||
base64Encode(
|
|
||||||
JSON.stringify(
|
|
||||||
Object.assign({}, payload, {
|
|
||||||
exp: new Date().valueOf() + kRefreshInterval,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const jwt = [
|
|
||||||
b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))),
|
|
||||||
final_payload,
|
|
||||||
b64url(ssb.hmacsha256sign(final_payload, ':auth', id)),
|
|
||||||
].join('.');
|
|
||||||
return jwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a JWT ?
|
|
||||||
* @param {*} session TODOC
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function readSession(session) {
|
|
||||||
let jwt_parts = session?.split('.');
|
|
||||||
|
|
||||||
if (jwt_parts?.length === 3) {
|
|
||||||
let [header, payload, signature] = jwt_parts;
|
|
||||||
header = JSON.parse(utf8Decode(base64Decode(unb64url(header))));
|
|
||||||
|
|
||||||
if (header.typ === 'JWT' && header.alg === 'HS256') {
|
|
||||||
signature = unb64url(signature);
|
|
||||||
let id = ssb.getIdentities(':auth');
|
|
||||||
|
|
||||||
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
|
||||||
const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
|
|
||||||
const now = new Date().valueOf();
|
|
||||||
|
|
||||||
if (now < result.exp) {
|
|
||||||
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
print(`JWT expired by ${(now - result.exp) / 1000} seconds.`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print('JWT verification failed.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print('Invalid JWT header.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the provided password matches the hash
|
|
||||||
* @param {string} password
|
|
||||||
* @param {string} hash bcrypt hash
|
|
||||||
* @returns true if the password matches the hash
|
|
||||||
*/
|
|
||||||
function verifyPassword(password, hash) {
|
|
||||||
return bCrypt.hashpw(password, hash) === hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hashes a password
|
|
||||||
* @param {string} password
|
|
||||||
* @returns {string} TODOC
|
|
||||||
*/
|
|
||||||
function hashPassword(password) {
|
|
||||||
let salt = bCrypt.gensalt(12);
|
|
||||||
return bCrypt.hashpw(password, salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if there is an administrator on the instance
|
|
||||||
* @returns TODOC
|
|
||||||
*/
|
|
||||||
function noAdministrator() {
|
|
||||||
return (
|
|
||||||
!core.globalSettings ||
|
|
||||||
!core.globalSettings.permissions ||
|
|
||||||
!Object.keys(core.globalSettings.permissions).some(function (name) {
|
|
||||||
return (
|
|
||||||
core.globalSettings.permissions[name].indexOf('administration') != -1
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a user an administrator
|
|
||||||
* @param {string} name the user's name
|
|
||||||
*/
|
|
||||||
function makeAdministrator(name) {
|
|
||||||
if (!core.globalSettings.permissions) {
|
|
||||||
core.globalSettings.permissions = {};
|
|
||||||
}
|
|
||||||
if (!core.globalSettings.permissions[name]) {
|
|
||||||
core.globalSettings.permissions[name] = [];
|
|
||||||
}
|
|
||||||
if (core.globalSettings.permissions[name].indexOf('administration') == -1) {
|
|
||||||
core.globalSettings.permissions[name].push('administration');
|
|
||||||
}
|
|
||||||
|
|
||||||
core.setGlobalSettings(core.globalSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODOC
|
|
||||||
* @param {*} headers most likely an object
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function getCookies(headers) {
|
|
||||||
let cookies = {};
|
|
||||||
|
|
||||||
if (headers.cookie) {
|
|
||||||
let parts = headers.cookie.split(/,|;/);
|
|
||||||
for (let i in parts) {
|
|
||||||
let equals = parts[i].indexOf('=');
|
|
||||||
let name = parts[i].substring(0, equals).trim();
|
|
||||||
let value = parts[i].substring(equals + 1).trim();
|
|
||||||
cookies[name] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cookies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a username
|
|
||||||
* @param {string} name
|
|
||||||
* @returns false | boolean[] ?
|
|
||||||
*/
|
|
||||||
function isNameValid(name) {
|
|
||||||
// TODO(tasiaiso): convert this into a regex
|
|
||||||
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')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request handler ?
|
|
||||||
* @param {*} request TODOC
|
|
||||||
* @param {*} response
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function handler(request, response) {
|
|
||||||
// TODO(tasiaiso): split this function
|
|
||||||
let session = getCookies(request.headers).session;
|
|
||||||
|
|
||||||
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 loginError;
|
|
||||||
|
|
||||||
if (request.method == 'POST' || formData.submit) {
|
|
||||||
sessionIsNew = true;
|
|
||||||
formData = form.decodeForm(utf8Decode(request.body), formData);
|
|
||||||
if (formData.submit == 'Login') {
|
|
||||||
let account = gDatabase.get('user:' + formData.name);
|
|
||||||
account = account ? JSON.parse(account) : account;
|
|
||||||
if (formData.register == '1') {
|
|
||||||
if (
|
|
||||||
!account &&
|
|
||||||
isNameValid(formData.name) &&
|
|
||||||
formData.password == formData.confirm
|
|
||||||
) {
|
|
||||||
let users = new Set();
|
|
||||||
let users_original = gDatabase.get('users');
|
|
||||||
try {
|
|
||||||
users = new Set(JSON.parse(users_original));
|
|
||||||
} catch {}
|
|
||||||
if (!users.has(formData.name)) {
|
|
||||||
users.add(formData.name);
|
|
||||||
}
|
|
||||||
users = JSON.stringify([...users].sort());
|
|
||||||
if (users !== users_original) {
|
|
||||||
gDatabase.set('users', users);
|
|
||||||
}
|
|
||||||
session = makeJwt({name: formData.name});
|
|
||||||
account = {password: hashPassword(formData.password)};
|
|
||||||
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
|
||||||
if (noAdministrator()) {
|
|
||||||
makeAdministrator(formData.name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 {
|
|
||||||
if (
|
|
||||||
account &&
|
|
||||||
account.password &&
|
|
||||||
verifyPassword(formData.password, account.password)
|
|
||||||
) {
|
|
||||||
session = makeJwt({name: formData.name});
|
|
||||||
if (noAdministrator()) {
|
|
||||||
makeAdministrator(formData.name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loginError = 'Invalid username or password.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Proceed as Guest
|
|
||||||
session = makeJwt({name: 'guest'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
|
|
||||||
let entry = readSession(session);
|
|
||||||
if (entry && formData.return) {
|
|
||||||
response.writeHead(303, {
|
|
||||||
Location: formData.return,
|
|
||||||
'Set-Cookie': cookie,
|
|
||||||
});
|
|
||||||
response.end();
|
|
||||||
} else {
|
|
||||||
File.readFile('core/auth.html')
|
|
||||||
.then(function (data) {
|
|
||||||
let html = utf8Decode(data);
|
|
||||||
let auth_data = {
|
|
||||||
session_is_new: sessionIsNew,
|
|
||||||
name: entry?.name,
|
|
||||||
error: loginError,
|
|
||||||
code_of_conduct: core.globalSettings.code_of_conduct,
|
|
||||||
have_administrator: !noAdministrator(),
|
|
||||||
};
|
|
||||||
html = utf8Encode(
|
|
||||||
html.replace('$AUTH_DATA', JSON.stringify(auth_data))
|
|
||||||
);
|
|
||||||
response.writeHead(200, {
|
|
||||||
'Content-Type': 'text/html; charset=utf-8',
|
|
||||||
'Set-Cookie': cookie,
|
|
||||||
'Content-Length': html.length,
|
|
||||||
});
|
|
||||||
response.end(html);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
response.writeHead(404, {
|
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
|
||||||
Connection: 'close',
|
|
||||||
});
|
|
||||||
response.end('404 File not found');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (request.uri == '/login/logout') {
|
|
||||||
response.writeHead(303, {
|
|
||||||
'Set-Cookie': `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`,
|
|
||||||
Location: '/login' + (request.query ? '?' + request.query : ''),
|
|
||||||
});
|
|
||||||
response.end();
|
|
||||||
} else {
|
|
||||||
response.writeHead(200, {
|
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
|
||||||
Connection: 'close',
|
|
||||||
});
|
|
||||||
response.end('Hello, ' + request.client.peerName + '.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a user's permissions based on it's session ?
|
|
||||||
* @param {*} session TODOC
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function getPermissions(session) {
|
|
||||||
let permissions;
|
|
||||||
let entry = readSession(session);
|
|
||||||
if (entry) {
|
|
||||||
permissions = getPermissionsForUser(entry.name);
|
|
||||||
permissions.authenticated = entry.name !== 'guest';
|
|
||||||
}
|
|
||||||
return permissions || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a user's permissions ?
|
|
||||||
* @param {string} userName TODOC
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function getPermissionsForUser(userName) {
|
|
||||||
let permissions = {};
|
|
||||||
if (
|
|
||||||
core.globalSettings &&
|
|
||||||
core.globalSettings.permissions &&
|
|
||||||
core.globalSettings.permissions[userName]
|
|
||||||
) {
|
|
||||||
for (let i in core.globalSettings.permissions[userName]) {
|
|
||||||
permissions[core.globalSettings.permissions[userName][i]] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return permissions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODOC
|
|
||||||
* @param {*} headers
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function query(headers) {
|
|
||||||
let session = getCookies(headers).session;
|
|
||||||
let entry;
|
|
||||||
let autologin = tildefriends.args.autologin;
|
|
||||||
if ((entry = autologin ? {name: autologin} : readSession(session))) {
|
|
||||||
return {
|
|
||||||
session: entry,
|
|
||||||
permissions: autologin
|
|
||||||
? getPermissionsForUser(autologin)
|
|
||||||
: getPermissions(session),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes a JWT ?
|
|
||||||
* @param {*} credentials TODOC
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function makeRefresh(credentials) {
|
|
||||||
if (credentials?.session?.name) {
|
|
||||||
return {
|
|
||||||
token: makeJwt({name: credentials.session.name}),
|
|
||||||
interval: kRefreshInterval,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {handler, query, makeRefresh};
|
|
251
core/client.js
251
core/client.js
@ -56,6 +56,9 @@ class TfNavigationElement extends LitElement {
|
|||||||
spark_lines: {type: Object},
|
spark_lines: {type: Object},
|
||||||
version: {type: Object},
|
version: {type: Object},
|
||||||
show_version: {type: Boolean},
|
show_version: {type: Boolean},
|
||||||
|
identity: {type: String},
|
||||||
|
identities: {type: Array},
|
||||||
|
names: {type: Object},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +68,8 @@ class TfNavigationElement extends LitElement {
|
|||||||
this.show_permissions = false;
|
this.show_permissions = false;
|
||||||
this.status = {};
|
this.status = {};
|
||||||
this.spark_lines = {};
|
this.spark_lines = {};
|
||||||
|
this.identities = [];
|
||||||
|
this.names = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,10 +102,10 @@ class TfNavigationElement extends LitElement {
|
|||||||
get_spark_line(key, options) {
|
get_spark_line(key, options) {
|
||||||
if (!this.spark_lines[key]) {
|
if (!this.spark_lines[key]) {
|
||||||
let spark_line = document.createElement('tf-sparkline');
|
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;
|
spark_line.title = key;
|
||||||
|
spark_line.classList.add('w3-bar-item');
|
||||||
|
spark_line.classList.add('w3-hide-small');
|
||||||
|
spark_line.style.paddingRight = '0';
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.max) {
|
if (options.max) {
|
||||||
spark_line.max = options.max;
|
spark_line.max = options.max;
|
||||||
@ -118,16 +123,108 @@ class TfNavigationElement extends LitElement {
|
|||||||
*/
|
*/
|
||||||
render_login() {
|
render_login() {
|
||||||
if (this?.credentials?.session?.name) {
|
if (this?.credentials?.session?.name) {
|
||||||
return html`<a id="login" href="/login/logout?return=${url() + hash()}"
|
return html`<a
|
||||||
|
class="w3-bar-item w3-right"
|
||||||
|
id="login"
|
||||||
|
href="/login/logout?return=${url() + hash()}"
|
||||||
>logout ${this.credentials.session.name}</a
|
>logout ${this.credentials.session.name}</a
|
||||||
>`;
|
>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<a id="login" href="/login?return=${url() + hash()}"
|
return html`<a
|
||||||
|
class="w3-bar-item w3-right"
|
||||||
|
id="login"
|
||||||
|
href="/login?return=${url() + hash()}"
|
||||||
>login</a
|
>login</a
|
||||||
>`;
|
>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_active_identity(id) {
|
||||||
|
send({action: 'setActiveIdentity', identity: id});
|
||||||
|
this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show');
|
||||||
|
}
|
||||||
|
|
||||||
|
create_identity(event) {
|
||||||
|
if (confirm('Are you sure you want to create a new identity?')) {
|
||||||
|
send({action: 'createIdentity'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_id_dropdown() {
|
||||||
|
this.renderRoot.getElementById('id_dropdown').classList.toggle('w3-show');
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_profile() {
|
||||||
|
window.location.href = '/~core/ssb/#' + this.identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_identity() {
|
||||||
|
let self = this;
|
||||||
|
if (this.identities?.length) {
|
||||||
|
return html`
|
||||||
|
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||||
|
<div class="w3-dropdown-click w3-right" style="max-width: 100%">
|
||||||
|
<button
|
||||||
|
class="w3-button w3-rest w3-cyan"
|
||||||
|
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
|
||||||
|
@click=${self.toggle_id_dropdown}
|
||||||
|
>
|
||||||
|
${self.names[this.identity]}${self.names[this.identity] ===
|
||||||
|
this.identity
|
||||||
|
? ''
|
||||||
|
: html` - ${this.identity}`}
|
||||||
|
▾
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
id="id_dropdown"
|
||||||
|
class="w3-dropdown-content w3-bar-block w3-card-4"
|
||||||
|
style="max-width: 100%"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-border"
|
||||||
|
@click=${() => (window.location.href = '/~core/identity')}
|
||||||
|
>
|
||||||
|
Manage Identities...
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-border"
|
||||||
|
@click=${self.edit_profile}
|
||||||
|
>
|
||||||
|
Edit Profile...
|
||||||
|
</button>
|
||||||
|
${this.identities.map(
|
||||||
|
(x) => html`
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button ${x === self.identity
|
||||||
|
? 'w3-cyan'
|
||||||
|
: ''}"
|
||||||
|
@click=${() => self.set_active_identity(x)}
|
||||||
|
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
|
||||||
|
>
|
||||||
|
${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (
|
||||||
|
this.credentials?.session?.name &&
|
||||||
|
this.credentials.session.name !== 'guest'
|
||||||
|
) {
|
||||||
|
return html`
|
||||||
|
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||||
|
<button
|
||||||
|
id="create_identity"
|
||||||
|
@click=${this.create_identity}
|
||||||
|
class="w3-button w3-mobile w3-blue w3-right"
|
||||||
|
>
|
||||||
|
Create an Identity
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
* @returns
|
* @returns
|
||||||
@ -145,11 +242,17 @@ class TfNavigationElement extends LitElement {
|
|||||||
<div>This app has the following permissions:</div>
|
<div>This app has the following permissions:</div>
|
||||||
${Object.keys(this.permissions).map(
|
${Object.keys(this.permissions).map(
|
||||||
(key) => html`
|
(key) => html`
|
||||||
<div>
|
<div>
|
||||||
<span>${key}</span>: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
|
<span>${key}</span>:
|
||||||
<button @click=${() => this.reset_permission(key)} class='w3-button w3-red">Reset</button>
|
${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
|
||||||
</div>
|
<button
|
||||||
`
|
@click=${() => this.reset_permission(key)}
|
||||||
|
class="w3-button w3-red"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@click=${() => (this.show_permissions = false)}
|
@click=${() => (this.show_permissions = false)}
|
||||||
@ -163,6 +266,10 @@ class TfNavigationElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear_error() {
|
||||||
|
this.status = {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
* @returns
|
* @returns
|
||||||
@ -170,10 +277,9 @@ class TfNavigationElement extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
|
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||||
<style>
|
<style>
|
||||||
/* prettier-ignore */
|
${k_global_style} .tooltip {
|
||||||
${k_global_style}
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: none;
|
display: none;
|
||||||
@ -187,17 +293,17 @@ class TfNavigationElement extends LitElement {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div class="w3-black w3-bar">
|
||||||
style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
|
class="w3-bar-item"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
@click=${() => (this.show_version = !this.show_version)}
|
@click=${() => (this.show_version = !this.show_version)}
|
||||||
>😎</span
|
>😎</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
?hidden=${!this.show_version}
|
class="w3-bar-item"
|
||||||
style="flex: 0 0; white-space: nowrap"
|
style=${'white-space: nowrap' +
|
||||||
|
(this.show_version ? '' : '; display: none')}
|
||||||
title=${this.version?.name +
|
title=${this.version?.name +
|
||||||
' ' +
|
' ' +
|
||||||
Object.entries(this.version || {})
|
Object.entries(this.version || {})
|
||||||
@ -206,6 +312,7 @@ class TfNavigationElement extends LitElement {
|
|||||||
>${this.version?.number}</span
|
>${this.version?.number}</span
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
class="w3-bar-item"
|
||||||
accesskey="h"
|
accesskey="h"
|
||||||
@mouseover=${set_access_key_title}
|
@mouseover=${set_access_key_title}
|
||||||
data-tip="Open home app."
|
data-tip="Open home app."
|
||||||
@ -214,6 +321,7 @@ class TfNavigationElement extends LitElement {
|
|||||||
>TF</a
|
>TF</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
class="w3-bar-item"
|
||||||
accesskey="a"
|
accesskey="a"
|
||||||
@mouseover=${set_access_key_title}
|
@mouseover=${set_access_key_title}
|
||||||
data-tip="Open apps list."
|
data-tip="Open apps list."
|
||||||
@ -221,6 +329,7 @@ class TfNavigationElement extends LitElement {
|
|||||||
>apps</a
|
>apps</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
class="w3-bar-item"
|
||||||
accesskey="e"
|
accesskey="e"
|
||||||
@mouseover=${set_access_key_title}
|
@mouseover=${set_access_key_title}
|
||||||
data-tip="Toggle the app editor."
|
data-tip="Toggle the app editor."
|
||||||
@ -229,6 +338,7 @@ class TfNavigationElement extends LitElement {
|
|||||||
>edit</a
|
>edit</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
class="w3-bar-item"
|
||||||
accesskey="p"
|
accesskey="p"
|
||||||
@mouseover=${set_access_key_title}
|
@mouseover=${set_access_key_title}
|
||||||
data-tip="View and change permissions."
|
data-tip="View and change permissions."
|
||||||
@ -236,27 +346,34 @@ class TfNavigationElement extends LitElement {
|
|||||||
@click=${() => (self.show_permissions = !self.show_permissions)}
|
@click=${() => (self.show_permissions = !self.show_permissions)}
|
||||||
>🎛️</a
|
>🎛️</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()}
|
${this.render_permissions()}
|
||||||
<span
|
${this.status?.message && !this.status.is_error
|
||||||
style="flex: 1 1; display: flex; flex-direction: row; white-space: nowrap; margin: 0; padding: 0"
|
? html`
|
||||||
>${Object.keys(this.spark_lines)
|
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||||
.sort()
|
<div
|
||||||
.map((x) => this.spark_lines[x])
|
class="w3-bar-item"
|
||||||
.map((x) => [
|
style="color: ${this.status.color ?? kStatusColor}"
|
||||||
html`<span style="font-size: xx-small">${x.dataset.emoji}</span>`,
|
>
|
||||||
x,
|
${this.status.message}
|
||||||
])}</span
|
</div>
|
||||||
>
|
`
|
||||||
<span style="flex: 0 0; white-space: nowrap"
|
: undefined}
|
||||||
>${this.render_login()}</span
|
${Object.keys(this.spark_lines)
|
||||||
>
|
.sort()
|
||||||
|
.map((x) => this.spark_lines[x])}
|
||||||
|
${this.render_login()} ${this.render_identity()}
|
||||||
</div>
|
</div>
|
||||||
|
${this.status?.is_error
|
||||||
|
? html`
|
||||||
|
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||||
|
<div class="w3-model w3-animate-top" style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1">
|
||||||
|
<dijv class="w3-modal-content w3-card-4" style="display: block; padding: 1em">
|
||||||
|
<span @click=${self.clear_error} class="w3-button w3-display-topright">×</span>
|
||||||
|
<div style="color: ${this.status.color ?? kErrorColor}"><b>ERROR:</b><p style="white-space: pre">${this.status.message}</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: undefined}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -580,13 +697,13 @@ class TfSparkLineElement extends LitElement {
|
|||||||
) / 10.0;
|
) / 10.0;
|
||||||
return html`
|
return html`
|
||||||
<svg
|
<svg
|
||||||
style="max-width: 7.5em; max-height: 1.5em; margin: 0; padding: 0; background: #000"
|
style="max-width: 7.5em; margin: 0; padding: 0; background: #000; height: 1em"
|
||||||
viewBox="0 0 50 10"
|
viewBox="0 0 50 10"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
${this.lines.map((x) => this.render_line(x))}
|
${this.lines.map((x) => this.render_line(x))}
|
||||||
<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">
|
<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">
|
||||||
${max}
|
${this.dataset.emoji}${max}
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
@ -1002,9 +1119,9 @@ function api_postMessage(message) {
|
|||||||
function api_error(error) {
|
function api_error(error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
if (typeof error == 'string') {
|
if (typeof error == 'string') {
|
||||||
setStatusMessage('⚠️ ' + error, '#f00');
|
setStatusMessage('⚠️ ' + error, kErrorColor);
|
||||||
} else {
|
} else {
|
||||||
setStatusMessage('⚠️ ' + error.message + '\n' + error.stack, '#f00');
|
setStatusMessage('⚠️ ' + error.message + '\n' + error.stack, kErrorColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
@ -1121,11 +1238,19 @@ function api_setHash(hash) {
|
|||||||
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);
|
||||||
document.getElementsByTagName('tf-navigation')[0].credentials =
|
let navigation = document.getElementsByTagName('tf-navigation')[0];
|
||||||
message.credentials;
|
navigation.credentials = message.credentials;
|
||||||
|
navigation.identities = message.identities;
|
||||||
|
navigation.identity = message.identity;
|
||||||
|
navigation.names = message.names;
|
||||||
} else if (message && message.action == 'permissions') {
|
} else if (message && message.action == 'permissions') {
|
||||||
document.getElementsByTagName('tf-navigation')[0].permissions =
|
let navigation = document.getElementsByTagName('tf-navigation')[0];
|
||||||
message.permissions ?? {};
|
navigation.permissions = message.permissions ?? {};
|
||||||
|
} else if (message && message.action == 'identities') {
|
||||||
|
let navigation = document.getElementsByTagName('tf-navigation')[0];
|
||||||
|
navigation.identities = message.identities;
|
||||||
|
navigation.identity = message.identity;
|
||||||
|
navigation.names = message.names;
|
||||||
} else if (message && message.action == 'ready') {
|
} else if (message && message.action == 'ready') {
|
||||||
setStatusMessage(null);
|
setStatusMessage(null);
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
@ -1213,6 +1338,7 @@ function setStatusMessage(message, color) {
|
|||||||
document.getElementsByTagName('tf-navigation')[0].status = {
|
document.getElementsByTagName('tf-navigation')[0].status = {
|
||||||
message: message,
|
message: message,
|
||||||
color: color,
|
color: color,
|
||||||
|
is_error: color == kErrorColor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1647,7 +1773,11 @@ async function sourcePretty() {
|
|||||||
let formatted = await prettier.format(source, {
|
let formatted = await prettier.format(source, {
|
||||||
parser: 'babel',
|
parser: 'babel',
|
||||||
plugins: [babel, estree],
|
plugins: [babel, estree],
|
||||||
|
trailingComma: 'es5',
|
||||||
useTabs: true,
|
useTabs: true,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
bracketSpacing: false,
|
||||||
});
|
});
|
||||||
if (source !== formatted) {
|
if (source !== formatted) {
|
||||||
gEditor.dispatch({
|
gEditor.dispatch({
|
||||||
@ -1660,6 +1790,31 @@ async function sourcePretty() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleVisibleWhitespace() {
|
||||||
|
let editor_style = document.getElementById('editor_style');
|
||||||
|
/*
|
||||||
|
* There is likely a better way to do this, but stomping on the CSS was
|
||||||
|
* the easiest to wrangle at the time.
|
||||||
|
*/
|
||||||
|
if (editor_style.innerHTML.length) {
|
||||||
|
editor_style.innerHTML = '';
|
||||||
|
window.localStorage.setItem('visible_whitespace', '0');
|
||||||
|
} else {
|
||||||
|
editor_style.innerHTML = css`
|
||||||
|
.cm-trailingSpace {
|
||||||
|
background-color: unset !important;
|
||||||
|
}
|
||||||
|
.cm-highlightTab {
|
||||||
|
background-image: unset !important;
|
||||||
|
}
|
||||||
|
.cm-highlightSpace:before {
|
||||||
|
content: unset !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
window.localStorage.setItem('visible_whitespace', '1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODOC
|
// TODOC
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
window.addEventListener('hashchange', hashChange);
|
window.addEventListener('hashchange', hashChange);
|
||||||
@ -1685,6 +1840,9 @@ window.addEventListener('load', function () {
|
|||||||
document
|
document
|
||||||
.getElementById('pretty')
|
.getElementById('pretty')
|
||||||
.addEventListener('click', () => sourcePretty());
|
.addEventListener('click', () => sourcePretty());
|
||||||
|
document
|
||||||
|
.getElementById('whitespace')
|
||||||
|
.addEventListener('click', () => toggleVisibleWhitespace());
|
||||||
document
|
document
|
||||||
.getElementById('trace_button')
|
.getElementById('trace_button')
|
||||||
.addEventListener('click', function (event) {
|
.addEventListener('click', function (event) {
|
||||||
@ -1698,4 +1856,7 @@ window.addEventListener('load', function () {
|
|||||||
} else {
|
} else {
|
||||||
closeEditor();
|
closeEditor();
|
||||||
}
|
}
|
||||||
|
if (window.localStorage.getItem('visible_whitespace') == '1') {
|
||||||
|
toggleVisibleWhitespace();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
223
core/core.js
223
core/core.js
@ -1,5 +1,4 @@
|
|||||||
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 form from './form.js';
|
||||||
import * as http from './http.js';
|
import * as http from './http.js';
|
||||||
|
|
||||||
@ -9,32 +8,6 @@ let gStatsTimer = false;
|
|||||||
const k_content_security_policy =
|
const k_content_security_policy =
|
||||||
'sandbox allow-downloads allow-top-navigation-by-user-activation';
|
'sandbox allow-downloads allow-top-navigation-by-user-activation';
|
||||||
|
|
||||||
const k_mime_types = {
|
|
||||||
css: 'text/css',
|
|
||||||
html: 'text/html',
|
|
||||||
js: 'text/javascript',
|
|
||||||
json: 'text/json',
|
|
||||||
map: 'application/json',
|
|
||||||
svg: 'image/svg+xml',
|
|
||||||
};
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
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'},
|
|
||||||
{bytes: [0x4d, 0x54, 0x68, 0x64], type: 'audio/midi'},
|
|
||||||
];
|
|
||||||
|
|
||||||
let k_static_files = [
|
let k_static_files = [
|
||||||
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
||||||
];
|
];
|
||||||
@ -161,6 +134,7 @@ function broadcastEvent(eventName, argv) {
|
|||||||
}
|
}
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
* @param {*} message
|
* @param {*} message
|
||||||
@ -182,6 +156,34 @@ function broadcast(message) {
|
|||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODOC
|
||||||
|
* @param {String} eventName
|
||||||
|
* @param {*} argv
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function broadcastAppEventToUser(
|
||||||
|
user,
|
||||||
|
packageOwner,
|
||||||
|
packageName,
|
||||||
|
eventName,
|
||||||
|
argv
|
||||||
|
) {
|
||||||
|
let promises = [];
|
||||||
|
for (let process of Object.values(gProcesses)) {
|
||||||
|
if (
|
||||||
|
process.credentials?.session?.name === user &&
|
||||||
|
process.packageOwner == packageOwner &&
|
||||||
|
process.packageName == packageName
|
||||||
|
) {
|
||||||
|
if (process.eventHandlers[eventName]) {
|
||||||
|
promises.push(invoke(process.eventHandlers[eventName], argv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
* @param {*} caller
|
* @param {*} caller
|
||||||
@ -277,6 +279,8 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
process.key = key;
|
process.key = key;
|
||||||
process.credentials = options.credentials || {};
|
process.credentials = options.credentials || {};
|
||||||
process.task = new Task();
|
process.task = new Task();
|
||||||
|
process.packageOwner = options.packageOwner;
|
||||||
|
process.packageName = options.packageName;
|
||||||
process.eventHandlers = {};
|
process.eventHandlers = {};
|
||||||
if (!options?.script || options?.script === 'app.js') {
|
if (!options?.script || options?.script === 'app.js') {
|
||||||
process.app = new app.App();
|
process.app = new app.App();
|
||||||
@ -425,6 +429,67 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
url: options?.url,
|
url: options?.url,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
process.sendIdentities = async function () {
|
||||||
|
process.app.send(
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
action: 'identities',
|
||||||
|
},
|
||||||
|
await ssb.getIdentityInfo(
|
||||||
|
process?.credentials?.session?.name,
|
||||||
|
options?.packageOwner,
|
||||||
|
options?.packageName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
process.setActiveIdentity = async function (identity) {
|
||||||
|
if (
|
||||||
|
process?.credentials?.session?.name &&
|
||||||
|
options.packageOwner &&
|
||||||
|
options.packageName
|
||||||
|
) {
|
||||||
|
await new Database(process?.credentials?.session?.name).set(
|
||||||
|
`id:${options.packageOwner}:${options.packageName}`,
|
||||||
|
identity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
process.sendIdentities();
|
||||||
|
broadcastAppEventToUser(
|
||||||
|
process?.credentials?.session?.name,
|
||||||
|
options.packageOwner,
|
||||||
|
options.packageName,
|
||||||
|
'setActiveIdentity',
|
||||||
|
[identity]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
process.createIdentity = async function () {
|
||||||
|
if (
|
||||||
|
process.credentials &&
|
||||||
|
process.credentials.session &&
|
||||||
|
process.credentials.session.name &&
|
||||||
|
process.credentials.session.name !== 'guest'
|
||||||
|
) {
|
||||||
|
let id = ssb.createIdentity(process.credentials.session.name);
|
||||||
|
await process.sendIdentities();
|
||||||
|
broadcastAppEventToUser(
|
||||||
|
process?.credentials?.session?.name,
|
||||||
|
options.packageOwner,
|
||||||
|
options.packageName,
|
||||||
|
'setActiveIdentity',
|
||||||
|
[
|
||||||
|
await ssb.getActiveIdentity(
|
||||||
|
process.credentials?.session?.name,
|
||||||
|
options.packageOwner,
|
||||||
|
options.packageName
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return id;
|
||||||
|
} else {
|
||||||
|
throw new Error('Must be signed-in to create an account.');
|
||||||
|
}
|
||||||
|
};
|
||||||
if (process.credentials?.permissions?.administration) {
|
if (process.credentials?.permissions?.administration) {
|
||||||
imports.core.globalSettingsDescriptions = function () {
|
imports.core.globalSettingsDescriptions = function () {
|
||||||
let settings = Object.assign({}, k_global_settings);
|
let settings = Object.assign({}, k_global_settings);
|
||||||
@ -494,15 +559,8 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
imports.ssb = Object.fromEntries(
|
imports.ssb = Object.fromEntries(
|
||||||
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
|
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
|
||||||
);
|
);
|
||||||
imports.ssb.createIdentity = function () {
|
imports.ssb.port = tildefriends.ssb_port;
|
||||||
if (
|
imports.ssb.createIdentity = () => process.createIdentity();
|
||||||
process.credentials &&
|
|
||||||
process.credentials.session &&
|
|
||||||
process.credentials.session.name
|
|
||||||
) {
|
|
||||||
return ssb.createIdentity(process.credentials.session.name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
imports.ssb.addIdentity = function (id) {
|
imports.ssb.addIdentity = function (id) {
|
||||||
if (
|
if (
|
||||||
process.credentials &&
|
process.credentials &&
|
||||||
@ -529,6 +587,13 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
|
||||||
|
imports.ssb.getActiveIdentity = () =>
|
||||||
|
ssb.getActiveIdentity(
|
||||||
|
process.credentials?.session?.name,
|
||||||
|
options.packageOwner,
|
||||||
|
options.packageName
|
||||||
|
);
|
||||||
imports.ssb.getOwnerIdentities = function () {
|
imports.ssb.getOwnerIdentities = function () {
|
||||||
if (options.packageOwner) {
|
if (options.packageOwner) {
|
||||||
return ssb.getIdentities(options.packageOwner);
|
return ssb.getIdentities(options.packageOwner);
|
||||||
@ -613,6 +678,9 @@ async function getProcessBlob(blobId, key, options) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
imports.ssb.addEventListener = undefined;
|
||||||
|
imports.ssb.removeEventListener = undefined;
|
||||||
|
imports.ssb.getIdentityInfo = undefined;
|
||||||
imports.fetch = function (url, options) {
|
imports.fetch = function (url, options) {
|
||||||
return http.fetch(url, options, gGlobalSettings.fetch_hosts);
|
return http.fetch(url, options, gGlobalSettings.fetch_hosts);
|
||||||
};
|
};
|
||||||
@ -765,29 +833,6 @@ function startsWithBytes(data, bytes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODOC
|
|
||||||
* @param {*} path
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function guessTypeFromName(path) {
|
|
||||||
let extension = path.split('.').pop();
|
|
||||||
return k_mime_types[extension];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODOC
|
|
||||||
* @param {*} data
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function guessTypeFromMagicBytes(data) {
|
|
||||||
for (let magic of k_magic_bytes) {
|
|
||||||
if (startsWithBytes(data, magic.bytes)) {
|
|
||||||
return magic.type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
* @param {*} response
|
* @param {*} response
|
||||||
@ -803,7 +848,9 @@ function sendData(response, data, type, headers, status_code) {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
'Content-Type':
|
'Content-Type':
|
||||||
type || guessTypeFromMagicBytes(data) || 'application/binary',
|
type ||
|
||||||
|
httpd.mime_type_from_magic_bytes(data) ||
|
||||||
|
'application/binary',
|
||||||
'Content-Length': data.byteLength,
|
'Content-Length': data.byteLength,
|
||||||
},
|
},
|
||||||
headers || {}
|
headers || {}
|
||||||
@ -882,7 +929,7 @@ async function useAppHandler(
|
|||||||
},
|
},
|
||||||
respond: do_resolve,
|
respond: do_resolve,
|
||||||
},
|
},
|
||||||
credentials: auth.query(headers),
|
credentials: httpd.auth_query(headers),
|
||||||
packageOwner: packageOwner,
|
packageOwner: packageOwner,
|
||||||
packageName: packageName,
|
packageName: packageName,
|
||||||
}
|
}
|
||||||
@ -1013,7 +1060,7 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||||
let user = match[1];
|
let user = match[1];
|
||||||
let appName = match[2];
|
let appName = match[2];
|
||||||
let credentials = auth.query(request.headers);
|
let credentials = httpd.auth_query(request.headers);
|
||||||
if (
|
if (
|
||||||
credentials &&
|
credentials &&
|
||||||
credentials.session &&
|
credentials.session &&
|
||||||
@ -1076,7 +1123,7 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||||
let user = match[1];
|
let user = match[1];
|
||||||
let appName = match[2];
|
let appName = match[2];
|
||||||
let credentials = auth.query(request.headers);
|
let credentials = httpd.auth_query(request.headers);
|
||||||
if (
|
if (
|
||||||
credentials &&
|
credentials &&
|
||||||
credentials.session &&
|
credentials.session &&
|
||||||
@ -1172,7 +1219,9 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
'Content-Security-Policy': k_content_security_policy,
|
'Content-Security-Policy': k_content_security_policy,
|
||||||
};
|
};
|
||||||
data = await getBlobOrContent(id);
|
data = await getBlobOrContent(id);
|
||||||
let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
|
let type =
|
||||||
|
httpd.mime_type_from_extension(uri) ||
|
||||||
|
httpd.mime_type_from_magic_bytes(data);
|
||||||
sendData(response, data, type, headers);
|
sendData(response, data, type, headers);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1181,6 +1230,10 @@ async function blobHandler(request, response, blobId, uri) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssb.addEventListener('message', function () {
|
||||||
|
broadcastEvent('onMessage', [...arguments]);
|
||||||
|
});
|
||||||
|
|
||||||
ssb.addEventListener('broadcasts', function () {
|
ssb.addEventListener('broadcasts', function () {
|
||||||
broadcastEvent('onBroadcastsChanged', []);
|
broadcastEvent('onBroadcastsChanged', []);
|
||||||
});
|
});
|
||||||
@ -1249,39 +1302,10 @@ loadSettings()
|
|||||||
if (tildefriends.https_port && gGlobalSettings.http_redirect) {
|
if (tildefriends.https_port && gGlobalSettings.http_redirect) {
|
||||||
httpd.set_http_redirect(gGlobalSettings.http_redirect);
|
httpd.set_http_redirect(gGlobalSettings.http_redirect);
|
||||||
}
|
}
|
||||||
httpd.all('/login', auth.handler);
|
|
||||||
httpd.all('/login/logout', auth.handler);
|
|
||||||
httpd.all('/app/socket', app.socket);
|
httpd.all('/app/socket', app.socket);
|
||||||
httpd.all('', function default_http_handler(request, response) {
|
httpd.all('', function default_http_handler(request, response) {
|
||||||
let match;
|
let match;
|
||||||
if (request.uri === '/' || request.uri === '') {
|
if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) {
|
||||||
let host = request.headers['x-forwarded-host'] ?? request.headers.host;
|
|
||||||
try {
|
|
||||||
for (let line of (gGlobalSettings.index_map || '').split('\n')) {
|
|
||||||
let parts = line.split('=');
|
|
||||||
if (parts.length == 2 && host.match(new RegExp(parts[0], 'i'))) {
|
|
||||||
response.writeHead(303, {
|
|
||||||
Location:
|
|
||||||
(request.client.tls ? 'https://' : 'http://') +
|
|
||||||
host +
|
|
||||||
parts[1],
|
|
||||||
'Content-Length': '0',
|
|
||||||
});
|
|
||||||
return response.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
response.writeHead(303, {
|
|
||||||
Location:
|
|
||||||
(request.client.tls ? 'https://' : 'http://') +
|
|
||||||
host +
|
|
||||||
gGlobalSettings.index,
|
|
||||||
'Content-Length': '0',
|
|
||||||
});
|
|
||||||
return response.end();
|
|
||||||
} else if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) {
|
|
||||||
return blobHandler(request, response, match[1], match[2]);
|
return blobHandler(request, response, match[1], match[2]);
|
||||||
} else if (
|
} else if (
|
||||||
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
|
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
|
||||||
@ -1321,8 +1345,15 @@ loadSettings()
|
|||||||
async function start_tls() {
|
async function start_tls() {
|
||||||
const kCertificatePath = 'data/httpd/certificate.pem';
|
const kCertificatePath = 'data/httpd/certificate.pem';
|
||||||
const kPrivateKeyPath = 'data/httpd/privatekey.pem';
|
const kPrivateKeyPath = 'data/httpd/privatekey.pem';
|
||||||
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
let privateKey;
|
||||||
let certificate = utf8Decode(await File.readFile(kCertificatePath));
|
let certificate;
|
||||||
|
try {
|
||||||
|
privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
||||||
|
certificate = utf8Decode(await File.readFile(kCertificatePath));
|
||||||
|
} catch (e) {
|
||||||
|
print(`TLS disabled (${e.message}).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let context = new TlsContext();
|
let context = new TlsContext();
|
||||||
context.setPrivateKey(privateKey);
|
context.setPrivateKey(privateKey);
|
||||||
context.setCertificate(certificate);
|
context.setCertificate(certificate);
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
class="w3-bar-item w3-button w3-blue"
|
class="w3-bar-item w3-button w3-blue"
|
||||||
id="icon"
|
id="icon"
|
||||||
name="icon"
|
name="icon"
|
||||||
accesskey="i"
|
accesskey="j"
|
||||||
onmouseover="set_access_key_title(event)"
|
onmouseover="set_access_key_title(event)"
|
||||||
data-tip="Set an icon/emoji for the app"
|
data-tip="Set an icon/emoji for the app"
|
||||||
>
|
>
|
||||||
@ -93,6 +93,16 @@
|
|||||||
>
|
>
|
||||||
🧼
|
🧼
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="whitespace"
|
||||||
|
name="whitespace"
|
||||||
|
accesskey="w"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Toggle visible whitespace"
|
||||||
|
>
|
||||||
|
✨
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
class="w3-bar-item w3-input w3-border w3-blue"
|
class="w3-bar-item w3-input w3-border w3-blue"
|
||||||
type="text"
|
type="text"
|
||||||
@ -125,6 +135,7 @@
|
|||||||
<tf-files-pane style="overflow: auto"></tf-files-pane>
|
<tf-files-pane style="overflow: auto"></tf-files-pane>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1 1; overflow: auto">
|
<div style="flex: 1 1; overflow: auto">
|
||||||
|
<style id="editor_style"></style>
|
||||||
<div id="editor" style="width: 100%; height: 100%"></div>
|
<div id="editor" style="width: 100%; height: 100%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,22 +15,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:link {
|
|
||||||
color: #268bd2;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: #6c71c4;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #859900;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:active {
|
|
||||||
color: #2aa198;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo {
|
#logo {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
68
default.nix
Normal file
68
default.nix
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# How to upgrade to a newer version
|
||||||
|
# - Comment `src.hash`
|
||||||
|
# - Change `version`
|
||||||
|
# - Run `$ nix build`
|
||||||
|
# This will fetch the source code
|
||||||
|
# Since `hash` is not provided, nix will stop building and throw an error:
|
||||||
|
#
|
||||||
|
# error: hash mismatch in fixed-output derivation '/nix/store/fghi3ljs6fhz8pwm3dh73j5fwjpq5wbz-source.drv':
|
||||||
|
# specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
# got: sha256-+uthA1w8CmZfW+WOK9wYGl2fUl/k10ufOc8W+Pwa9iQ=
|
||||||
|
# error: 1 dependencies of derivation '/nix/store/imcwsw5r74vkd8r0qa2k7cys2xfgraaz-tildefriends-0.0.18.drv' failed to build
|
||||||
|
#
|
||||||
|
# - Change `src.hash` to the new one, ie `sha256-+uthA1w8CmZfW+WOK9wYGl2fUl/k10ufOc8W+Pwa9iQ=`
|
||||||
|
# - Uncomment `src.hash`
|
||||||
|
# - Build again, this time it should work.
|
||||||
|
# - Check the release notes, if there's a new dependency or a change to `GNUMakefile`, this file might need to be changed too.
|
||||||
|
# For more details, contact tasiaiso @ https://tilde.club/~tasiaiso/
|
||||||
|
#
|
||||||
|
# WARNING: currently it is pinned to `47838d5e482cb4aac40190fa0414f08b8cf94d40`. I couldn't get v0.0.18 to work for some reason.
|
||||||
|
# I'll change this in the next release - tasiaiso
|
||||||
|
{
|
||||||
|
pkgs ? import <nixpkgs> {},
|
||||||
|
lib ? import <nixpkgs/lib>,
|
||||||
|
}:
|
||||||
|
pkgs.stdenv.mkDerivation rec {
|
||||||
|
pname = "tildefriends";
|
||||||
|
version = "0.0.19-wip";
|
||||||
|
|
||||||
|
src = pkgs.fetchFromGitea {
|
||||||
|
domain = "dev.tildefriends.net";
|
||||||
|
owner = "cory";
|
||||||
|
repo = "tildefriends";
|
||||||
|
# rev = "v${version}";
|
||||||
|
rev = "47838d5e482cb4aac40190fa0414f08b8cf94d40";
|
||||||
|
hash = "sha256-mb5KYvWPIqgV64FOaXKHm2ownBJiiSRtdH8+YWiXwvE="; # 47838d5e482cb4aac40190fa0414f08b8cf94d40
|
||||||
|
fetchSubmodules = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
gnumake
|
||||||
|
openssl
|
||||||
|
which
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
openssl
|
||||||
|
which
|
||||||
|
];
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
make -j $NIX_BUILD_CORES release
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp -r out/release/tildefriends $out/bin
|
||||||
|
'';
|
||||||
|
|
||||||
|
doCheck = false;
|
||||||
|
|
||||||
|
meta = with pkgs; {
|
||||||
|
homepage = "https://tildefriends.net";
|
||||||
|
description = "Make apps and friends from the comfort of your web browser.";
|
||||||
|
mainProgram = "tildefriends";
|
||||||
|
license = with lib.licenses; [mit];
|
||||||
|
platforms = lib.platforms.all;
|
||||||
|
};
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user