Compare commits
399 Commits
70a3e7fc7d
...
v0.0.22
Author | SHA1 | Date | |
---|---|---|---|
3c499c834b | |||
17d6cc7d46 | |||
646bd7dc38 | |||
56e483782d | |||
e1b9066b26 | |||
7114ce2516 | |||
9240c6570a | |||
f80a44ccd7 | |||
e6f5eb244e | |||
ab62e83110 | |||
aeefb9e536 | |||
ee0efa536a | |||
2523130fdc | |||
c024777184 | |||
5951d7cd2d | |||
011670c70b | |||
6cebd6c769 | |||
546ae5cbf1 | |||
f543cc642e | |||
8ac3c5ea22 | |||
63918f0680 | |||
bfb3d8b8a2 | |||
e38ff99607 | |||
b0e3d922c8 | |||
a15bb8e994 | |||
6f487100cd | |||
0693a2315f | |||
f360e886ff | |||
6ea08cc5dc | |||
347c706d6f | |||
5f5e6616c7 | |||
657bcadc7e | |||
107666cc60 | |||
b37669184a | |||
163a01f224 | |||
3d58094199 | |||
463951a4f1 | |||
34804d5162 | |||
3895c33915 | |||
17f4eb1a56 | |||
0abdffdea6 | |||
d32999f178 | |||
f621feb843 | |||
8d277f029d | |||
1788a02338 | |||
ba0800d16c | |||
4008c7d8f6 | |||
610a2e2afc | |||
6f3715d1eb | |||
b78ecaa814 | |||
e6f5399d53 | |||
0e5806cadd | |||
68c9d4afa7 | |||
f0ea38fe49 | |||
b0332f923e | |||
8a76c25394 | |||
fd96126e3e | |||
ff3fbedc18 | |||
8791419f8e | |||
5447b247a0 | |||
aabbb10564 | |||
3ccd6c9a3e | |||
c290240de7 | |||
8e799b174b | |||
a9c3a93989 | |||
3ef8698f42 | |||
fa4e843c30 | |||
9a4d11f4d9 | |||
eed2b8d618 | |||
13f02c2aca | |||
d50f8fbc8b | |||
155238a516 | |||
427fcdbdca | |||
ca05d402a7 | |||
c5a80b68ca | |||
c1fb15b135 | |||
4b2c131836 | |||
9ca1e69b3c | |||
082d041d44 | |||
221f276c4b | |||
24cec21465 | |||
9f71ec6194 | |||
bb36afc390 | |||
b53bf0ff64 | |||
3ebc6f2436 | |||
2eef6778a6 | |||
81fabec810 | |||
dc6e7924b5 | |||
48dec5a2c8 | |||
9b500e1da9 | |||
a038820112 | |||
70a15973b6 | |||
09b6a00731 | |||
883c3cf0e9 | |||
a46bb8183c | |||
d5d5a7b012 | |||
a120efdc91 | |||
d48f4b06eb | |||
f078912736 | |||
63b0f0dedd | |||
84c22dbf5f | |||
b8cd1232be | |||
a518ab07f4 | |||
9e5a1ee975 | |||
95bf3f0316 | |||
d69dd513bc | |||
525cdf571a | |||
9cfe0a8804 | |||
50b54599ef | |||
ed6bef6d24 | |||
71268636df | |||
568729ecd6 | |||
9139725be6 | |||
969a8da6bf
|
|||
2338b26329 | |||
d4df206740 | |||
8a93cdd33c | |||
92b31de4a9 | |||
5452f3f623 | |||
256614dbaf | |||
049449b213 | |||
85b46336b1 | |||
590afa7b01 | |||
574292b798 | |||
21cf503a59 | |||
3630cdbfe0 | |||
0f3be229e6 | |||
8e5a024d3d | |||
410bb7c09d | |||
9de8b0f449 | |||
d47c3a1222 | |||
df99b3aa90 | |||
0090850e10 | |||
9efd64bd18 | |||
b16c37e48b | |||
3ee2c00726 | |||
d5a7e19f1a | |||
9b52415b35 | |||
dbe24494d9 | |||
3eab5a5f70 | |||
548febfb22 | |||
b40f72443a | |||
2c03496373 | |||
b6a937c954 | |||
63776d40bd | |||
cb3c7afade | |||
991022adfc | |||
2bc71a18a6 | |||
57ca864fbb | |||
a09edfb612 | |||
7997a739ab | |||
248b258413 | |||
0423ed7fb4 | |||
c29378c2f8 | |||
163fbd85e7 | |||
58bb86ebe1 | |||
c5140ee8e8 | |||
6270fd8118 | |||
3fff706848 | |||
c259defab5 | |||
e5fee5c306 | |||
9d35b4bdfb | |||
9497d7cf64 | |||
c7d3e602cb | |||
0076eb4ed4 | |||
6070bde413 | |||
c7a6d426f0 | |||
f66cf0f802 | |||
e4b6c81024 | |||
44d784cd04 | |||
0394201113 | |||
e270c16516 | |||
4c10538632
|
|||
71329c5532 | |||
feb4bf9e87 | |||
5d5567e94c | |||
684e6fb9cb | |||
ee21fa6d03
|
|||
7a2974e54f | |||
f4dfc1dd98 | |||
2eebfa9a7a | |||
10097ffeb8 | |||
cbe1f54a2a | |||
4d8f081a59 | |||
29e79c9484 | |||
ba35869b0a | |||
580688381e | |||
e63d69a440 | |||
be64fe04fb | |||
801ab20723 | |||
d974a5e044 | |||
1be94ae0be | |||
b883e6a485 | |||
a0210379ae | |||
e56dc207d1 | |||
523c9c9ad2 | |||
74bb2151c1 | |||
f79d7b35a4 | |||
3b36496dac
|
|||
4ebd6c24a9
|
|||
05451d98b3
|
|||
22a4bce3c8
|
|||
76d499f00b | |||
f0772f9b99
|
|||
46e711f0a5 | |||
abffac3f82 | |||
27b275548e | |||
93ce253d1e | |||
a5af312b39 | |||
4b5e8e8a43 | |||
443dd4d168 | |||
907479df84 | |||
9887a78e98 | |||
f669371349 | |||
24c720c79a | |||
4485234980
|
|||
b6871c0b1f
|
|||
47838d5e48 | |||
69fccd56d3 | |||
ca00c4fb5d | |||
427ca3f265 | |||
c1a80e50e7 | |||
52962f3a5e | |||
b3f095b61f | |||
a5004c8ba9 | |||
7d9b1b508b | |||
5e265dfc83 | |||
3a43d6f8ac | |||
11a6649847 | |||
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 | |||
2d3e108fd9 | |||
7822b30dcb | |||
e361c3f975 | |||
1d5cdf9607 | |||
a4bf3542e0 | |||
df82cfe66b | |||
53f9547cc5 |
40
.fdroid.yml
Normal file
40
.fdroid.yml
Normal file
@ -0,0 +1,40 @@
|
||||
Categories:
|
||||
- Internet
|
||||
License: MIT
|
||||
|
||||
AutoName: tildefriends
|
||||
AuthorName: Cory McWilliams
|
||||
AuthorEmail: cory@tildefriends.net
|
||||
|
||||
RepoType: git
|
||||
Repo: https://dev.tildefriends.net/cory/tildefriends.git
|
||||
|
||||
Builds:
|
||||
- versionName: 0.0.21-wip
|
||||
versionCode: 22
|
||||
commit: 09b6a00731d45fa160b23a2c44be6def98d92d6a
|
||||
subdir: src/android
|
||||
submodules: true
|
||||
sudo:
|
||||
- apt-get update
|
||||
- apt-get install -y ant make zip
|
||||
androidupdate:
|
||||
- no
|
||||
scandelete:
|
||||
- deps/libuv/docs/src/static/diagrams.key/Index.zip
|
||||
- deps/openssl_src/cloudflare-quiche/*
|
||||
- deps/openssl_src/fuzz/*
|
||||
- deps/openssl_src/gost-engine/*
|
||||
- deps/openssl_src/test/*
|
||||
- deps/openssl_src/tlslite-ng/*
|
||||
prebuild:
|
||||
- sdkmanager "platforms;android-34" "build-tools;34.0.0"
|
||||
build:
|
||||
- mkdir bin/
|
||||
- ANDROID_SDK=$$SDK$$ ANDROID_NDK=$$NDK$$ ANDROID_NDK_ROOT=$$NDK$$ make -C ../../ -j`nproc` fdroid
|
||||
ndk: r26d
|
||||
|
||||
AutoUpdateMode: Version ^v[0-9\.]+$
|
||||
UpdateCheckMode: Tags
|
||||
CurrentVersion: 0.0.21-wip
|
||||
CurrentVersionCode: 22
|
15
.gitea/workflows/build.yaml
Normal file
15
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
name: Build Tilde Friends
|
||||
run-name: ${{ gitea.actor }} running 🚀
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Build-All:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64
|
||||
- run: make all -j`nproc` docs
|
||||
- run: docker build .
|
15
.gitignore
vendored
15
.gitignore
vendored
@ -1,8 +1,17 @@
|
||||
**/node_modules
|
||||
.keys
|
||||
.zsign_cache/
|
||||
build/
|
||||
*.core
|
||||
db.*
|
||||
deps/ios_toolchain/
|
||||
deps/openssl/
|
||||
dist/
|
||||
.keys
|
||||
logs/
|
||||
**/node_modules
|
||||
out
|
||||
repo/
|
||||
result
|
||||
*.swo
|
||||
*.swp
|
||||
tmp/
|
||||
unsigned/
|
||||
.zsign_cache/
|
||||
|
28
.gitmodules
vendored
Normal file
28
.gitmodules
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
[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
|
||||
[submodule "deps/openssl_src"]
|
||||
path = deps/openssl_src
|
||||
url = https://github.com/openssl/openssl.git
|
||||
shallow = true
|
||||
[submodule "deps/c-ares"]
|
||||
path = deps/c-ares
|
||||
url = https://github.com/c-ares/c-ares.git
|
@ -2,6 +2,7 @@ node_modules
|
||||
src
|
||||
deps
|
||||
.clang-format
|
||||
flake.lock
|
||||
|
||||
# Minified files
|
||||
**/*.min.css
|
||||
|
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`.
|
372
GNUmakefile
372
GNUmakefile
@ -3,9 +3,12 @@
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
VERSION_CODE := 16
|
||||
VERSION_NUMBER := 0.0.16-wip
|
||||
VERSION_NAME := Medium English breakfast tea.
|
||||
VERSION_CODE := 26
|
||||
VERSION_NUMBER := 0.0.22
|
||||
VERSION_NAME := Get born soon.
|
||||
|
||||
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
|
||||
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
|
||||
|
||||
PROJECT = tildefriends
|
||||
BUILD_DIR ?= out
|
||||
@ -13,6 +16,12 @@ UNAME_S := $(shell uname -s)
|
||||
UNAME_M := $(shell uname -m)
|
||||
|
||||
ANDROID_SDK ?= ~/Android/Sdk
|
||||
BUNDLETOOL = out/bundletool.jar
|
||||
|
||||
HAVE_WIN := 0
|
||||
|
||||
export SOURCE_DATE_EPOCH=1
|
||||
export TZ=UTC
|
||||
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
||||
@ -36,7 +45,6 @@ LDFLAGS += \
|
||||
-lc++abi
|
||||
HAVE_ANDROID := 0
|
||||
HAVE_LINUX_IOS := 0
|
||||
HAVE_WIN := 0
|
||||
else
|
||||
$(error Unexpected host platform $(UNAME_S).)
|
||||
endif
|
||||
@ -46,18 +54,22 @@ CFLAGS += \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unknown-warning-option \
|
||||
-MMD \
|
||||
-MP \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-fno-exceptions \
|
||||
-g
|
||||
LDFLAGS += \
|
||||
-Wno-attributes \
|
||||
-flto=auto
|
||||
|
||||
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_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.3.11579264
|
||||
|
||||
ANDROID_ARMV7A_TARGETS := \
|
||||
out/androiddebug-armv7a/tildefriends \
|
||||
@ -86,7 +98,7 @@ BUILD_TYPES += \
|
||||
androidrelease-x86 \
|
||||
androiddebug-x86_64 \
|
||||
androidrelease-x86_64
|
||||
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
|
||||
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk out/TildeFriends-release.fdroid.apk
|
||||
endif
|
||||
|
||||
WINDOWS_TARGETS := \
|
||||
@ -146,19 +158,24 @@ ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
||||
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
|
||||
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
|
||||
DEADSTRIP_TARGETS := $(filter-out $(ANDROID_TARGETS),$(NONMACOS_TARGETS))
|
||||
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
|
||||
|
||||
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||
$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
||||
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
||||
$(ANDROID_TARGETS): CFLAGS += \
|
||||
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||
-fPIC \
|
||||
-fdebug-compilation-dir . \
|
||||
-fomit-frame-pointer \
|
||||
-fno-asynchronous-unwind-tables \
|
||||
-funwind-tables
|
||||
-funwind-tables \
|
||||
-Wno-unknown-warning-option
|
||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
||||
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
||||
$(RELEASE_TARGETS): CFLAGS += \
|
||||
-DNDEBUG \
|
||||
-flto
|
||||
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
||||
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
|
||||
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
|
||||
@ -201,7 +218,7 @@ $(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
|
||||
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
|
||||
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
|
||||
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
|
||||
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
|
||||
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
|
||||
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
|
||||
@ -209,14 +226,14 @@ $(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/li
|
||||
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
ifneq ($(UNAME_S),Haiku)
|
||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(UNAME_M),aarch64)
|
||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||
endif
|
||||
|
||||
get_objs = \
|
||||
@ -234,6 +251,8 @@ APP_SOURCES_ios := $(wildcard src/*.m)
|
||||
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||
$(APP_OBJS): CFLAGS += \
|
||||
-Ideps/base64c/include \
|
||||
-Ideps/c-ares/include \
|
||||
-Ideps/c-ares_config \
|
||||
-Ideps/crypt_blowfish \
|
||||
-Ideps/libbacktrace \
|
||||
-Ideps/libsodium \
|
||||
@ -245,7 +264,6 @@ $(APP_OBJS): CFLAGS += \
|
||||
-Ideps/quickjs \
|
||||
-Ideps/sqlite \
|
||||
-Ideps/valgrind \
|
||||
-Ideps/xopt \
|
||||
-Wdouble-promotion \
|
||||
-Werror
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
@ -253,6 +271,105 @@ $(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_O
|
||||
-fanalyzer
|
||||
endif
|
||||
|
||||
ARES_SOURCES := \
|
||||
deps/c-ares/src/lib/ares_platform.c \
|
||||
deps/c-ares/src/lib/record/ares_dns_mapping.c \
|
||||
deps/c-ares/src/lib/record/ares_dns_parse.c \
|
||||
deps/c-ares/src/lib/record/ares_dns_write.c \
|
||||
deps/c-ares/src/lib/record/ares_dns_name.c \
|
||||
deps/c-ares/src/lib/record/ares_dns_record.c \
|
||||
deps/c-ares/src/lib/record/ares_dns_multistring.c \
|
||||
deps/c-ares/src/lib/ares_destroy.c \
|
||||
deps/c-ares/src/lib/ares_data.c \
|
||||
deps/c-ares/src/lib/ares_sysconfig.c \
|
||||
deps/c-ares/src/lib/ares_cancel.c \
|
||||
deps/c-ares/src/lib/ares_metrics.c \
|
||||
deps/c-ares/src/lib/ares_getnameinfo.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_txt_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_naptr_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_create_query.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_mx_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_srv_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_ptr_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_caa_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_aaaa_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_expand_name.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_uri_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_a_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_expand_string.c \
|
||||
deps/c-ares/src/lib/legacy/ares_fds.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_ns_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_parse_soa_reply.c \
|
||||
deps/c-ares/src/lib/legacy/ares_getsock.c \
|
||||
deps/c-ares/src/lib/windows_port.c \
|
||||
deps/c-ares/src/lib/ares_qcache.c \
|
||||
deps/c-ares/src/lib/ares_update_servers.c \
|
||||
deps/c-ares/src/lib/ares_process.c \
|
||||
deps/c-ares/src/lib/ares_getenv.c \
|
||||
deps/c-ares/src/lib/ares_gethostbyname.c \
|
||||
deps/c-ares/src/lib/ares_send.c \
|
||||
deps/c-ares/src/lib/dsa/ares__slist.c \
|
||||
deps/c-ares/src/lib/dsa/ares__htable.c \
|
||||
deps/c-ares/src/lib/dsa/ares__llist.c \
|
||||
deps/c-ares/src/lib/dsa/ares__htable_szvp.c \
|
||||
deps/c-ares/src/lib/dsa/ares__htable_asvp.c \
|
||||
deps/c-ares/src/lib/dsa/ares__htable_vpvp.c \
|
||||
deps/c-ares/src/lib/dsa/ares__htable_strvp.c \
|
||||
deps/c-ares/src/lib/dsa/ares__array.c \
|
||||
deps/c-ares/src/lib/ares__socket.c \
|
||||
deps/c-ares/src/lib/event/ares_event_poll.c \
|
||||
deps/c-ares/src/lib/event/ares_event_thread.c \
|
||||
deps/c-ares/src/lib/event/ares_event_select.c \
|
||||
deps/c-ares/src/lib/event/ares_event_kqueue.c \
|
||||
deps/c-ares/src/lib/event/ares_event_configchg.c \
|
||||
deps/c-ares/src/lib/event/ares_event_epoll.c \
|
||||
deps/c-ares/src/lib/event/ares_event_wake_pipe.c \
|
||||
deps/c-ares/src/lib/event/ares_event_win32.c \
|
||||
deps/c-ares/src/lib/ares_search.c \
|
||||
deps/c-ares/src/lib/ares__parse_into_addrinfo.c \
|
||||
deps/c-ares/src/lib/ares__hosts_file.c \
|
||||
deps/c-ares/src/lib/ares_getaddrinfo.c \
|
||||
deps/c-ares/src/lib/ares__addrinfo2hostent.c \
|
||||
deps/c-ares/src/lib/ares_freeaddrinfo.c \
|
||||
deps/c-ares/src/lib/ares_strerror.c \
|
||||
deps/c-ares/src/lib/ares_version.c \
|
||||
deps/c-ares/src/lib/ares_gethostbyaddr.c \
|
||||
deps/c-ares/src/lib/ares__addrinfo_localhost.c \
|
||||
deps/c-ares/src/lib/ares_free_hostent.c \
|
||||
deps/c-ares/src/lib/ares__close_sockets.c \
|
||||
deps/c-ares/src/lib/ares_free_string.c \
|
||||
deps/c-ares/src/lib/ares_init.c \
|
||||
deps/c-ares/src/lib/ares_options.c \
|
||||
deps/c-ares/src/lib/str/ares_strcasecmp.c \
|
||||
deps/c-ares/src/lib/str/ares__buf.c \
|
||||
deps/c-ares/src/lib/str/ares_strsplit.c \
|
||||
deps/c-ares/src/lib/str/ares_str.c \
|
||||
deps/c-ares/src/lib/ares_sysconfig_mac.c \
|
||||
deps/c-ares/src/lib/ares__sortaddrinfo.c \
|
||||
deps/c-ares/src/lib/ares_sysconfig_files.c \
|
||||
deps/c-ares/src/lib/util/ares__iface_ips.c \
|
||||
deps/c-ares/src/lib/util/ares__timeval.c \
|
||||
deps/c-ares/src/lib/util/ares_math.c \
|
||||
deps/c-ares/src/lib/util/ares_rand.c \
|
||||
deps/c-ares/src/lib/util/ares__threads.c \
|
||||
deps/c-ares/src/lib/ares_query.c \
|
||||
deps/c-ares/src/lib/ares_cookie.c \
|
||||
deps/c-ares/src/lib/inet_net_pton.c \
|
||||
deps/c-ares/src/lib/inet_ntop.c \
|
||||
deps/c-ares/src/lib/ares_library_init.c \
|
||||
deps/c-ares/src/lib/ares_android.c \
|
||||
deps/c-ares/src/lib/ares_sysconfig_win.c \
|
||||
deps/c-ares/src/lib/ares_timeout.c
|
||||
ARES_OBJS := $(call get_objs,ARES_SOURCES)
|
||||
$(ARES_OBJS): CFLAGS += \
|
||||
-Ideps/c-ares/include \
|
||||
-Ideps/c-ares/src/lib \
|
||||
-Ideps/c-ares_config/ \
|
||||
-D_GNU_SOURCE \
|
||||
-Wno-unused-function \
|
||||
-Wno-deprecated-declarations \
|
||||
-Wno-unused-result
|
||||
|
||||
BLOWFISH_SOURCES := \
|
||||
deps/crypt_blowfish/crypt_blowfish.c \
|
||||
deps/crypt_blowfish/crypt_gensalt.c \
|
||||
@ -384,6 +501,8 @@ $(UV_OBJS): CFLAGS += \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-unused-result \
|
||||
-Wno-unused-variable
|
||||
$(UV_OBJS): CFLAGS += -fno-lto
|
||||
$(filter out/win%,$(UV_OBJS)): CFLAGS += -Wno-cast-function-type
|
||||
ifeq ($(UNAME_S),Linux)
|
||||
$(UV_OBJS): CFLAGS += \
|
||||
-D_GNU_SOURCE
|
||||
@ -459,6 +578,7 @@ ifneq ($(UNAME_S),OpenBSD)
|
||||
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
|
||||
-DHAVE_ALLOCA_H
|
||||
endif
|
||||
$(SODIUM_OBJS): CFLAGS := $(filter-out -flto,$(CFLAGS))
|
||||
|
||||
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||
@ -497,18 +617,6 @@ $(SQLITE_OBJS): CFLAGS += \
|
||||
-Wno-unused-function \
|
||||
-Wno-unused-variable
|
||||
|
||||
XOPT_SOURCES := deps/xopt/xopt.c
|
||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
||||
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
||||
-DHAVE_SNPRINTF \
|
||||
-DHAVE_VSNPRINTF \
|
||||
-DHAVE_VASNPRINTF \
|
||||
-DHAVE_VASPRINTF \
|
||||
-Dvsnprintf=rpl_vsnprintf
|
||||
$(XOPT_OBJS): CFLAGS += \
|
||||
-Wno-implicit-const-int-float-conversion \
|
||||
-Wno-pointer-to-int-cast
|
||||
|
||||
QUICKJS_SOURCES := \
|
||||
deps/quickjs/cutils.c \
|
||||
deps/quickjs/libbf.c \
|
||||
@ -588,7 +696,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
|
||||
LDFLAGS += \
|
||||
-pthread \
|
||||
-lm
|
||||
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||
-lssl \
|
||||
-lcrypto
|
||||
ifneq ($(UNAME_S),Haiku)
|
||||
@ -630,6 +738,7 @@ all: $(BUILD_TYPES)
|
||||
|
||||
ALL_APP_OBJS := \
|
||||
$(APP_OBJS) \
|
||||
$(ARES_OBJS) \
|
||||
$(BLOWFISH_OBJS) \
|
||||
$(LIBBACKTRACE_OBJS) \
|
||||
$(MINIUNZIP_OBJS) \
|
||||
@ -637,8 +746,7 @@ ALL_APP_OBJS := \
|
||||
$(QUICKJS_OBJS) \
|
||||
$(SODIUM_OBJS) \
|
||||
$(SQLITE_OBJS) \
|
||||
$(UV_OBJS) \
|
||||
$(XOPT_OBJS)
|
||||
$(UV_OBJS)
|
||||
|
||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||
-include $(DEPS)
|
||||
@ -695,20 +803,37 @@ out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
||||
|
||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||
@mkdir -p $(dir $@)
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
||||
@echo [aapt2 link] res.apk
|
||||
@mkdir -p out/apk/
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
|
||||
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||
--manifest src/android/AndroidManifest.xml \
|
||||
-o out/apk/res.apk \
|
||||
--java out/gen/
|
||||
|
||||
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||
@echo [aapt2 link] res.fdroid.apk
|
||||
@mkdir -p out/apk/
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
|
||||
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||
--rename-manifest-package com.unprompted.tildefriends.fdroid \
|
||||
--manifest src/android/AndroidManifest.xml \
|
||||
-o out/apk/res.fdroid.apk \
|
||||
--java out/gen_fdroid/
|
||||
|
||||
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||
|
||||
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||
@echo "[javac] $(CLASS_FILES)"
|
||||
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
||||
@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)
|
||||
@mkdir -p $(dir $@)
|
||||
@echo "[d8] $@"
|
||||
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||
@$(ANDROID_BUILD_TOOLS)/d8 --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||
|
||||
PACKAGE_DIRS := \
|
||||
apps/ \
|
||||
@ -717,52 +842,140 @@ PACKAGE_DIRS := \
|
||||
deps/prettier/ \
|
||||
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 := $(sort $(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-release.unsigned.apk: BUILD_TYPE := release
|
||||
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
|
||||
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
|
||||
out/apk/TildeFriends-release.fdroid.unsigned.apk: BUILD_TYPE := release
|
||||
|
||||
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||
out/apk/TildeFriends-release.fdroid.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.fdroid.apk
|
||||
|
||||
$(BUNDLETOOL):
|
||||
@echo [curl] $(BUNDLETOOL_URL) TO $@
|
||||
@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL)
|
||||
|
||||
out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGETS)) $(RAW_FILES) out/apk/res.apk src/android/AndroidManifest.xml $(BUNDLETOOL)
|
||||
@rm -rf out/aab/staging/
|
||||
@mkdir -p out/aab/staging
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
|
||||
-I $(ANDROID_PLATFORM)/android.jar \
|
||||
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||
--manifest src/android/AndroidManifest.xml \
|
||||
-R out/res/layout_activity_main.xml.flat \
|
||||
-R out/res/drawable_icon.xml.flat \
|
||||
--auto-add-overlay
|
||||
@unzip out/aab/temporary.apk -d out/aab/staging/
|
||||
@mkdir -p out/aab/staging/root/deps
|
||||
@mkdir -p out/aab/staging/classes
|
||||
@mkdir -p out/aab/staging/dex
|
||||
@mkdir -p out/aab/staging/manifest
|
||||
@mv out/aab/staging/AndroidManifest.xml out/aab/staging/manifest/AndroidManifest.xml
|
||||
@cp out/apk/classes.dex out/aab/staging/dex/
|
||||
@rm -fv out/base.zip
|
||||
@mkdir -p out/aab/staging/lib/arm64-v8a out/aab/staging/lib/armeabi-v7a out/aab/staging/lib/x86_64 out/aab/staging/lib/x86
|
||||
@cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
|
||||
@cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/libtildefriends.so
|
||||
@cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/libtildefriends.so
|
||||
@cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/arm64-v8a/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/armeabi-v7a/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86_64/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86/libtildefriends.so
|
||||
@cp -r apps/ out/aab/staging/root/
|
||||
@rm -rf out/aab/staging/root/apps/welcome*
|
||||
@cp -r core/ out/aab/staging/root/
|
||||
@cp -r deps/prettier/ out/aab/staging/root/deps/
|
||||
@cp -r deps/lit/ out/aab/staging/root/deps/
|
||||
@cp -r deps/codemirror/ out/aab/staging/root/deps/
|
||||
@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../
|
||||
@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
|
||||
@jarsigner -keystore .keys/android.jks $@ androidKey -storepass android
|
||||
|
||||
aab: out/TildeFriends.aab
|
||||
.PHONY: aab
|
||||
|
||||
out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL)
|
||||
@java -jar $(BUNDLETOOL) build-apks --bundle out/TildeFriends.aab --overwrite --output $@ --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android
|
||||
|
||||
aabgo: out/TildeFriends.apks $(BUNDLETOOL)
|
||||
@java -jar $(BUNDLETOOL) install-apks --apks out/TildeFriends.apks
|
||||
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||
|
||||
out/apk/TildeFriends-arm-%.unsigned.apk:
|
||||
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||
@echo "[aapt] $@"
|
||||
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||
@cp out/apk/res.apk $@
|
||||
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||
@cp out/apk/res.apk $@.zip
|
||||
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
||||
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||
@zip -u $@ -q -9 $(RAW_FILES)
|
||||
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||
|
||||
out/apk/TildeFriends-x86-%.unsigned.apk:
|
||||
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||
@echo "[aapt] $@"
|
||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||
@cp out/apk/res.apk $@
|
||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||
@cp out/apk/res.apk $@.zip
|
||||
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
||||
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||
@zip -u $@ -q -9 $(RAW_FILES)
|
||||
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||
|
||||
out/apk/TildeFriends-%.fdroid.unsigned.apk:
|
||||
@rm -rf out/apk-fdroid-$(BUILD_TYPE) out/apk-fdroid-$(BUILD_TYPE)-raw
|
||||
@mkdir -p $(dir $@) out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/ out/apk-fdroid-$(BUILD_TYPE)/lib/x86/ out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||
@echo "[aapt] $@"
|
||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||
@cp out/apk/res.fdroid.apk $@.zip
|
||||
@cp out/apk/classes.dex out/apk-fdroid-$(BUILD_TYPE)/classes.dex
|
||||
@touch -d @0 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
|
||||
@chmod 755 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
|
||||
@cd out/apk-fdroid-$(BUILD_TYPE) && zip -X -u ../../$@.zip -q classes.dex lib/*/libtildefriends.so && cd ../../
|
||||
@mkdir out/apk-fdroid-$(BUILD_TYPE)-raw
|
||||
@for i in $(RAW_FILES); do mkdir -p $$(dirname out/apk-fdroid-$(BUILD_TYPE)-raw/$$i) && cp $$i out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && touch -d @0 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && chmod 644 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i; done
|
||||
@cd out/apk-fdroid-$(BUILD_TYPE)-raw && zip -X -u ../../$@.zip -q $(RAW_FILES) && cd ../../
|
||||
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||
|
||||
out/%.apk: out/apk/%.unsigned.apk
|
||||
@echo "[apksigner] $(notdir $@)"
|
||||
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
||||
@$(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
|
||||
|
||||
apkgo: out/TildeFriends-arm-debug.apk
|
||||
@adb install -r $<
|
||||
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||
.PHONY: apkgo
|
||||
|
||||
releaseapkgo: out/TildeFriends-arm-release.apk
|
||||
@adb install -r $<
|
||||
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
||||
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||
.PHONY: releaseapkgo
|
||||
|
||||
# iOS Support
|
||||
@ -773,10 +986,10 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
|
||||
@mkdir -p $(dir $@)
|
||||
@cp -v $< $@
|
||||
|
||||
out/%/data.zip: $(RAW_FILES)
|
||||
out/data.zip: $(RAW_FILES)
|
||||
@zip -u $@ -q -9 $(RAW_FILES)
|
||||
|
||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip
|
||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
|
||||
@mkdir -p $(dir $@)
|
||||
@cp -v $< $@
|
||||
ifeq ($(HAVE_LINUX_IOS),1)
|
||||
@ -791,6 +1004,16 @@ out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
||||
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
||||
@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
|
||||
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
||||
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
||||
@ -813,12 +1036,10 @@ apklog:
|
||||
.PHONY: apklog
|
||||
|
||||
fetchdeps:
|
||||
@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 -d deps/libuv/ || (mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
|
||||
@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 -d deps/sqlite/ || (mkdir -p deps/sqlite/ && unzip -qDj -d deps/sqlite/ 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/ && 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"
|
||||
@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
|
||||
@ -828,7 +1049,7 @@ fetchdeps:
|
||||
|
||||
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
|
||||
$(ANDROID_DEPS):
|
||||
+@tools/ssl-android
|
||||
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
|
||||
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
|
||||
|
||||
ifeq ($(HAVE_WIN),1)
|
||||
@ -839,7 +1060,7 @@ $(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
|
||||
endif
|
||||
|
||||
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):
|
||||
+@tools/ssl-ios
|
||||
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
|
||||
@ -849,13 +1070,12 @@ clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
.PHONY: clean
|
||||
|
||||
dist: release-apk iosrelease-ipa
|
||||
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk
|
||||
@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
|
||||
@rm -rf 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 \
|
||||
--exclude=apps/gg* \
|
||||
--exclude=apps/welcome* \
|
||||
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
||||
--exclude=deps/libsodium/builds/msvc/vs* \
|
||||
@ -870,14 +1090,19 @@ dist: release-apk iosrelease-ipa
|
||||
--exclude=deps/sqlite/shell.c \
|
||||
--exclude=deps/zlib/contrib/vstudio \
|
||||
--exclude=deps/zlib/doc \
|
||||
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz out/tildefriends-$(VERSION_NUMBER)
|
||||
#@rm -rf out/tildefriends-$(VERSION_NUMBER)
|
||||
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \
|
||||
-C out/ \
|
||||
tildefriends-$(VERSION_NUMBER)
|
||||
@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"
|
||||
@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"
|
||||
@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
|
||||
@cp out/TildeFriends.aab dist/TildeFriends-$(VERSION_NUMBER).aab
|
||||
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
|
||||
.PHONY: dist
|
||||
|
||||
dist-test: dist
|
||||
@ -891,6 +1116,13 @@ format:
|
||||
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
|
||||
.PHONY: format
|
||||
|
||||
prettier:
|
||||
@npm run prettier
|
||||
.PHONY: prettier
|
||||
|
||||
docs:
|
||||
@doxygen
|
||||
.PHONY: docs
|
||||
|
||||
fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk
|
||||
.PHONY: fdroid
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🎛"
|
||||
"emoji": "🎛",
|
||||
"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
|
||||
}
|
||||
|
@ -4,9 +4,38 @@
|
||||
<script>
|
||||
const g_data = $data;
|
||||
</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>
|
||||
<body style="color: #fff; width: 100%">
|
||||
<h1>Tilde Friends Administration</h1>
|
||||
<body class="w3-theme-l4">
|
||||
<header class="w3-row w3-padding w3-header w3-theme-l1">
|
||||
<h1>Tilde Friends Administration</h1>
|
||||
</header>
|
||||
</body>
|
||||
<script type="module" src="script.js"></script>
|
||||
</html>
|
||||
|
@ -27,64 +27,84 @@ function global_settings_set(key, value) {
|
||||
});
|
||||
}
|
||||
|
||||
function title_case(name) {
|
||||
return name.split('_').map(x => x.charAt(0).toUpperCase() + x.substring(1)).join(' ');
|
||||
}
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
const permission_template = (permission) => html` <code>${permission}</code>`;
|
||||
function input_template(key, description) {
|
||||
if (description.type === 'boolean') {
|
||||
return html`
|
||||
<div style="margin-top: 1em">
|
||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||
<div>
|
||||
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
||||
<div>${description.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<li class="w3-row">
|
||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
|
||||
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
|
||||
</li>
|
||||
`;
|
||||
} else if (description.type === 'textarea') {
|
||||
return html`
|
||||
<div style="margin-top: 1em"">
|
||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||
<div style="width: 100%; padding: 0; margin: 0">
|
||||
<div style="width: 90%; padding: 0 margin: 0">
|
||||
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
|
||||
</div>
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
|
||||
<div>${description.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<li class="w3-row">
|
||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
|
||||
>${title_case(key)}</label
|
||||
>
|
||||
<div class="w3-rest w3-padding">${description.description}</div>
|
||||
<textarea
|
||||
class="w3-input"
|
||||
style="vertical-align: top; resize: vertical"
|
||||
id=${'gs_' + key}
|
||||
>
|
||||
${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 {
|
||||
return html`
|
||||
<div style="margin-top: 1em">
|
||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||
<div>
|
||||
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||
<div>${description.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<li class="w3-row">
|
||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
}
|
||||
const user_template = (user, permissions) => html`
|
||||
<li>
|
||||
<button @click=${(e) => delete_user(user)}>Delete</button>
|
||||
<li class="w3-card w3-margin">
|
||||
<button
|
||||
class="w3-button w3-theme-action"
|
||||
@click=${(e) => delete_user(user)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
${user}: ${permissions.map((x) => permission_template(x))}
|
||||
</li>
|
||||
`;
|
||||
const users_template = (users) =>
|
||||
html`<h2>Users</h2>
|
||||
<ul>
|
||||
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
|
||||
<ul class="w3-ul">
|
||||
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
|
||||
</ul>`;
|
||||
const page_template = (data) =>
|
||||
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||
<h2>Global Settings</h2>
|
||||
<div>
|
||||
${Object.keys(data.settings)
|
||||
.sort()
|
||||
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
|
||||
<div class="w3-container">
|
||||
<ul class="w3-ul">
|
||||
${Object.keys(data.settings)
|
||||
.sort()
|
||||
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||
</ul>
|
||||
</div>
|
||||
${users_template(data.users)}
|
||||
</div> `;
|
||||
|
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",
|
||||
"emoji": "💻",
|
||||
"previous": "&RdVEsVscZm3aWzcMrEZS8mskO5tUmvaEUihex2MMfZQ=.sha256"
|
||||
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
|
||||
}
|
||||
|
@ -28,10 +28,10 @@ async function fetch_shared_apps() {
|
||||
|
||||
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"')
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
ORDER BY timestamp
|
||||
ORDER BY messages.timestamp
|
||||
`,
|
||||
[],
|
||||
function (row) {
|
||||
|
44
apps/blog/lit-all.min.js
vendored
44
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,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",
|
||||
"emoji": "🪪",
|
||||
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
||||
"previous": "&de7q4A59auHP/34bXgeNH05JZoxsGr5TjwXPvehWH30=.sha256"
|
||||
}
|
||||
|
@ -19,7 +19,36 @@ tfrpc.register(async function reload() {
|
||||
async function main() {
|
||||
let ids = await ssb.getIdentities();
|
||||
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 type="module">
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
@ -27,7 +56,8 @@ async function main() {
|
||||
let id = event.srcElement.dataset.id;
|
||||
let element = document.createElement('textarea');
|
||||
element.value = await tfrpc.rpc.get_private_key(id);
|
||||
element.style = 'width: 100%; read-only: true';
|
||||
element.style = 'width: 100%; height: auto; read-only: true; resize: none';
|
||||
element.classList.add('w3-input');
|
||||
element.readOnly = true;
|
||||
event.srcElement.parentElement.appendChild(element);
|
||||
event.srcElement.onclick = event => handler.hide_id(event, element);
|
||||
@ -48,7 +78,7 @@ async function main() {
|
||||
alert('Successfully created: ' + id);
|
||||
await tfrpc.rpc.reload();
|
||||
} catch (e) {
|
||||
alert('Error creating identity: ' + e);
|
||||
alert('Error creating identity: ' + e.message);
|
||||
}
|
||||
}
|
||||
handler.hide_id = function hide_id(event, element) {
|
||||
@ -69,23 +99,36 @@ async function main() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h1>SSB Identity Management</h1>
|
||||
<h2>Create a new identity</h2>
|
||||
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
|
||||
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
||||
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
||||
<h2>Identities</h2>
|
||||
<ul>` +
|
||||
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
|
||||
<div class="w3-card-4 w3-margin">
|
||||
<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header>
|
||||
<footer class="w3-padding">
|
||||
<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button>
|
||||
</footer>
|
||||
</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
|
||||
.map(
|
||||
(id) => `<li>
|
||||
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
||||
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
||||
${id}
|
||||
</li>`
|
||||
(
|
||||
id
|
||||
) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
|
||||
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
|
||||
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
|
||||
${id}
|
||||
</li>`
|
||||
)
|
||||
.join('\n') +
|
||||
` </ul>
|
||||
</div>
|
||||
</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",
|
||||
"emoji": "🦟",
|
||||
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
||||
"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256"
|
||||
}
|
||||
|
@ -67,9 +67,6 @@ tfrpc.register(function getHash(id, message) {
|
||||
tfrpc.register(function setHash(hash) {
|
||||
return app.setHash(hash);
|
||||
});
|
||||
ssb.addEventListener('message', async function (id) {
|
||||
await tfrpc.rpc.notifyNewMessage(id);
|
||||
});
|
||||
tfrpc.register(async function store_blob(blob) {
|
||||
if (Array.isArray(blob)) {
|
||||
blob = Uint8Array.from(blob);
|
||||
@ -85,13 +82,18 @@ tfrpc.register(async function store_message(message) {
|
||||
tfrpc.register(function apps() {
|
||||
return core.apps();
|
||||
});
|
||||
tfrpc.register(function getActiveIdentity() {
|
||||
return ssb.getActiveIdentity();
|
||||
});
|
||||
tfrpc.register(async function try_decrypt(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());
|
||||
});
|
||||
|
||||
core.register('onConnectionsChanged', async function () {
|
||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||
});
|
||||
|
44
apps/issues/lit-all.min.js
vendored
44
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';
|
||||
|
||||
class TfIdPickerElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
ids: {type: Array},
|
||||
selected: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.selected = await tfrpc.rpc.localStorageGet('whoami');
|
||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||
}
|
||||
|
||||
changed(event) {
|
||||
this.selected = event.srcElement.value;
|
||||
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.ids) {
|
||||
return html`
|
||||
<select @change=${this.changed} style="max-width: 100%">
|
||||
${this.ids.map(
|
||||
(id) =>
|
||||
html`<option ?selected=${id == this.selected} value=${id}>
|
||||
${id}
|
||||
</option>`
|
||||
)}
|
||||
</select>
|
||||
`;
|
||||
} else {
|
||||
return html`<div>Loading...</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define('tf-id-picker', TfIdPickerElement);
|
||||
|
||||
class TfComposeElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
@ -105,10 +63,10 @@ class TfIssuesAppElement extends LitElement {
|
||||
let issues = {};
|
||||
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
|
||||
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
|
||||
messages.id = messages_refs.message
|
||||
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
||||
@ -206,7 +164,7 @@ class TfIssuesAppElement extends LitElement {
|
||||
if (
|
||||
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, {
|
||||
type: 'issue-edit',
|
||||
issues: [
|
||||
@ -221,7 +179,7 @@ class TfIssuesAppElement extends LitElement {
|
||||
}
|
||||
|
||||
async create_issue(event) {
|
||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||
await tfrpc.rpc.appendMessage(whoami, {
|
||||
type: 'issue',
|
||||
project: k_project,
|
||||
@ -231,7 +189,7 @@ class TfIssuesAppElement extends LitElement {
|
||||
}
|
||||
|
||||
async reply_to_issue(event) {
|
||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||
await tfrpc.rpc.appendMessage(whoami, {
|
||||
type: 'post',
|
||||
text: event.detail.value,
|
||||
@ -249,10 +207,7 @@ class TfIssuesAppElement extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
let header = html`
|
||||
<h1>Tilde Friends Issues</h1>
|
||||
<tf-id-picker id="picker"></tf-id-picker>
|
||||
`;
|
||||
let header = html` <h1>Tilde Friends Issues</h1> `;
|
||||
if (this.selected) {
|
||||
return html`
|
||||
${header}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📝",
|
||||
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
||||
"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ function new_message() {
|
||||
return g_new_message_promise;
|
||||
}
|
||||
|
||||
ssb.addEventListener('message', function (id) {
|
||||
core.register('onMessage', function (id) {
|
||||
let resolve = g_new_message_resolve;
|
||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||
g_new_message_resolve = resolve;
|
||||
|
44
apps/journal/lit-all.min.js
vendored
44
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",
|
||||
"emoji": "👟"
|
||||
"emoji": "👟",
|
||||
"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256"
|
||||
}
|
||||
|
44
apps/sneaker/lit-all.min.js
vendored
44
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) {
|
||||
const k_flag_sequence_before_author = 1;
|
||||
let out = {
|
||||
previous: message.previous ?? null,
|
||||
};
|
||||
if (message.sequence_before_author) {
|
||||
if (message.flags & k_flag_sequence_before_author) {
|
||||
out.sequence = message.sequence;
|
||||
out.author = message.author;
|
||||
} else {
|
||||
@ -188,7 +189,12 @@ class TfSneakerAppElement extends LitElement {
|
||||
)[0].total;
|
||||
while (true) {
|
||||
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]
|
||||
);
|
||||
if (messages?.length) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🐌",
|
||||
"previous": "&DUxMMCJcuhm6S9jg/eKgEyWodkITu6Tg9g5I5wgLWFU=.sha256"
|
||||
"previous": "&xsmsLytB3VvoHphiFHZGGEvrCfTEVGXrGwobGTIYFPQ=.sha256"
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
|
||||
tfrpc.register(function setHash(hash) {
|
||||
return app.setHash(hash);
|
||||
});
|
||||
ssb.addEventListener('message', async function (id) {
|
||||
core.register('onMessage', async function (id) {
|
||||
await tfrpc.rpc.notifyNewMessage(id);
|
||||
});
|
||||
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) {
|
||||
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());
|
||||
});
|
||||
|
||||
core.register('onConnectionsChanged', async function () {
|
||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||
});
|
||||
core.register('setActiveIdentity', async function (id) {
|
||||
await tfrpc.rpc.set('identity', id);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
if (typeof database !== 'undefined') {
|
||||
|
@ -1,90 +1,94 @@
|
||||
function textNode(text) {
|
||||
const node = new commonmark.Node("text", undefined);
|
||||
node.literal = text;
|
||||
return node;
|
||||
const node = new commonmark.Node('text', undefined);
|
||||
node.literal = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function linkNode(text, link) {
|
||||
const linkNode = new commonmark.Node("link", undefined);
|
||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||
linkNode.appendChild(textNode(text));
|
||||
return linkNode;
|
||||
const linkNode = new commonmark.Node('link', undefined);
|
||||
if (link.startsWith('#')) {
|
||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||
} else {
|
||||
linkNode.destination = link;
|
||||
}
|
||||
linkNode.appendChild(textNode(text));
|
||||
return linkNode;
|
||||
}
|
||||
|
||||
function splitMatches(text, regexp) {
|
||||
// Regexp must be sticky.
|
||||
regexp = new RegExp(regexp, "gm");
|
||||
// Regexp must be sticky.
|
||||
regexp = new RegExp(regexp, 'gm');
|
||||
|
||||
let i = 0;
|
||||
const result = [];
|
||||
let i = 0;
|
||||
const result = [];
|
||||
|
||||
let match = regexp.exec(text);
|
||||
while (match) {
|
||||
const matchText = match[0];
|
||||
let match = regexp.exec(text);
|
||||
while (match) {
|
||||
const matchText = match[0];
|
||||
|
||||
if (match.index > i) {
|
||||
result.push([text.substring(i, match.index), false]);
|
||||
}
|
||||
if (match.index > i) {
|
||||
result.push([text.substring(i, match.index), false]);
|
||||
}
|
||||
|
||||
result.push([matchText, true]);
|
||||
i = match.index + matchText.length;
|
||||
result.push([matchText, true]);
|
||||
i = match.index + matchText.length;
|
||||
|
||||
match = regexp.exec(text);
|
||||
}
|
||||
match = regexp.exec(text);
|
||||
}
|
||||
|
||||
if (i < text.length) {
|
||||
result.push([text.substring(i, text.length), false]);
|
||||
}
|
||||
if (i < text.length) {
|
||||
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) {
|
||||
const text = textNodes.map(n => n.literal).join("");
|
||||
const parts = splitMatches(text, regex);
|
||||
const text = textNodes.map((n) => n.literal).join('');
|
||||
const parts = splitMatches(text, regex);
|
||||
|
||||
return parts.map(part => {
|
||||
if (part[1]) {
|
||||
return linkNode(part[0], part[0]);
|
||||
} else {
|
||||
return textNode(part[0]);
|
||||
}
|
||||
});
|
||||
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;
|
||||
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) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
let nodes = [];
|
||||
while ((event = walker.next())) {
|
||||
const node = event.node;
|
||||
if (event.entering && node.type === 'text') {
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length > 0) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach((newNode) => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
|
||||
nodes.forEach(n => n.unlink());
|
||||
nodes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
nodes.forEach((n) => n.unlink());
|
||||
nodes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
nodes.forEach(n => n.unlink());
|
||||
}
|
||||
if (nodes.length > 0) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach((newNode) => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
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,7 @@
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {html, render} from './lit-all.min.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
let g_emojis;
|
||||
|
||||
function get_emojis() {
|
||||
@ -10,105 +14,151 @@ function get_emojis() {
|
||||
});
|
||||
}
|
||||
|
||||
export function picker(callback, anchor) {
|
||||
get_emojis().then(function (json) {
|
||||
let div = document.createElement('div');
|
||||
div.id = 'emoji_picker';
|
||||
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();
|
||||
});
|
||||
async function get_recent(author) {
|
||||
let recent = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
||||
FROM messages
|
||||
WHERE author = ? AND
|
||||
content ->> '$.type' = 'vote'
|
||||
ORDER BY timestamp DESC LIMIT 10
|
||||
`,
|
||||
[author]
|
||||
);
|
||||
return recent.map((x) => x.value);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
console.log('emoji cleanup');
|
||||
div.parentElement.removeChild(div);
|
||||
window.removeEventListener('keydown', key_down);
|
||||
console.log('removing click');
|
||||
document.body.removeEventListener('mousedown', cleanup);
|
||||
}
|
||||
export async function picker(callback, anchor, author) {
|
||||
let json = await get_emojis();
|
||||
let recent = await get_recent(author);
|
||||
|
||||
function key_down(event) {
|
||||
if (event.key == 'Escape') {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
let div = document.createElement('div');
|
||||
div.id = 'emoji_picker';
|
||||
div.style.color = '#000';
|
||||
div.style.background = '#fff';
|
||||
div.style.border = '1px solid #000';
|
||||
div.style.display = 'block';
|
||||
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) {
|
||||
console.log(event.srcElement.innerText);
|
||||
callback(event.srcElement.innerText);
|
||||
function key_down(event) {
|
||||
if (event.key == 'Escape') {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
while (list.firstChild) {
|
||||
list.removeChild(list.firstChild);
|
||||
}
|
||||
let search = input.value.toLowerCase();
|
||||
let any_at_all = false;
|
||||
for (let row of Object.entries(json)) {
|
||||
let header = document.createElement('div');
|
||||
header.appendChild(document.createTextNode(row[0]));
|
||||
list.appendChild(header);
|
||||
let any = false;
|
||||
for (let entry of Object.entries(row[1])) {
|
||||
if (
|
||||
search &&
|
||||
search.length &&
|
||||
entry[0].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 = entry[0];
|
||||
emoji.appendChild(document.createTextNode(entry[1]));
|
||||
list.appendChild(emoji);
|
||||
any = true;
|
||||
any_at_all = true;
|
||||
}
|
||||
if (!any) {
|
||||
list.removeChild(header);
|
||||
function chosen(event) {
|
||||
console.log(event.srcElement.innerText);
|
||||
callback(event.srcElement.innerText);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
while (list.firstChild) {
|
||||
list.removeChild(list.firstChild);
|
||||
}
|
||||
let search = input.value.toLowerCase();
|
||||
let any_at_all = false;
|
||||
if (recent) {
|
||||
let emoji_to_name = {};
|
||||
for (let row of Object.values(json)) {
|
||||
for (let entry of Object.entries(row)) {
|
||||
emoji_to_name[entry[1]] = entry[0];
|
||||
}
|
||||
}
|
||||
if (!any_at_all) {
|
||||
list.appendChild(document.createTextNode('No matches found.'));
|
||||
let header = document.createElement('div');
|
||||
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();
|
||||
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);
|
||||
});
|
||||
for (let row of Object.entries(json)) {
|
||||
let header = document.createElement('div');
|
||||
header.appendChild(document.createTextNode(row[0]));
|
||||
list.appendChild(header);
|
||||
let any = false;
|
||||
for (let entry of Object.entries(row[1])) {
|
||||
if (
|
||||
search &&
|
||||
search.length &&
|
||||
entry[0].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 = 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;
|
||||
let modal = html`
|
||||
<style>
|
||||
${styles}
|
||||
</style>
|
||||
<div class="w3-modal" style="display: block">
|
||||
<div class="w3-modal-content w3-card-4">${div}</div>
|
||||
</div>
|
||||
`;
|
||||
let parent = document.createElement('div');
|
||||
document.body.appendChild(parent);
|
||||
function cleanup() {
|
||||
parent.parentElement.removeChild(parent);
|
||||
window.removeEventListener('keydown', key_down);
|
||||
document.body.removeEventListener('mousedown', cleanup);
|
||||
}
|
||||
render(modal, parent);
|
||||
input.focus();
|
||||
document.body.addEventListener('mousedown', cleanup);
|
||||
window.addEventListener('keydown', key_down);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html style="color: #fff">
|
||||
<html>
|
||||
<head>
|
||||
<title>Tilde Friends</title>
|
||||
<base target="_top" />
|
||||
@ -10,14 +10,14 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #223a5e">
|
||||
<tf-app class="w3-deep-purple" />
|
||||
<body style="margin: 0; padding: 0">
|
||||
<tf-app></tf-app>
|
||||
<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
|
||||
<script>
|
||||
window.litDisableBundleWarning = true;
|
||||
</script>
|
||||
<script src="filesaver.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="script.js" type="module"></script>
|
||||
</body>
|
||||
|
44
apps/ssb/lit-all.min.js
vendored
44
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 * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
import * as tf_id_picker from './tf-id-picker.js';
|
||||
import * as tf_app from './tf-app.js';
|
||||
import * as tf_message from './tf-message.js';
|
||||
import * as tf_user from './tf-user.js';
|
||||
import * as tf_compose from './tf-compose.js';
|
||||
import * as tf_news from './tf-news.js';
|
||||
import * as tf_profile from './tf-profile.js';
|
||||
import * as tf_reactions_modal from './tf-reactions-modal.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_feed from './tf-tab-news-feed.js';
|
||||
|
@ -52,13 +52,15 @@ class TfElement extends LitElement {
|
||||
self.broadcasts = value;
|
||||
} else if (name === 'connections') {
|
||||
self.connections = value;
|
||||
} else if (name === 'identity') {
|
||||
self.whoami = value;
|
||||
}
|
||||
});
|
||||
this.initial_load();
|
||||
}
|
||||
|
||||
async initial_load() {
|
||||
let whoami = await tfrpc.rpc.localStorageGet('whoami');
|
||||
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||
this.ids = ids;
|
||||
@ -107,7 +109,7 @@ class TfElement extends LitElement {
|
||||
let abouts = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT
|
||||
messages.*
|
||||
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||
FROM
|
||||
messages,
|
||||
json_each(?1) AS following
|
||||
@ -118,7 +120,7 @@ class TfElement extends LitElement {
|
||||
json_extract(messages.content, '$.type') = 'about'
|
||||
UNION
|
||||
SELECT
|
||||
messages.*
|
||||
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||
FROM
|
||||
messages,
|
||||
json_each(?2) AS following
|
||||
@ -158,7 +160,7 @@ class TfElement extends LitElement {
|
||||
async fetch_new_message(id) {
|
||||
let messages = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.*
|
||||
SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||
FROM messages
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
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() {
|
||||
let start = new Date();
|
||||
this.tags = await tfrpc.rpc.query(
|
||||
`
|
||||
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'
|
||||
ORDER BY timestamp DESC LIMIT 1024),
|
||||
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});
|
||||
}
|
||||
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);
|
||||
console.log(
|
||||
'about took',
|
||||
(new Date() - start_time) / 1000.0,
|
||||
'seconds for',
|
||||
Object.keys(users).length,
|
||||
'users'
|
||||
);
|
||||
this.following = Object.keys(following);
|
||||
this.users = users;
|
||||
await tags;
|
||||
@ -277,6 +264,7 @@ class TfElement extends LitElement {
|
||||
hash=${this.hash}
|
||||
.unread=${this.unread}
|
||||
@refresh=${() => (this.unread = [])}
|
||||
?loading=${this.loading}
|
||||
></tf-tab-news>
|
||||
`;
|
||||
} else if (this.tab === 'connections') {
|
||||
@ -352,18 +340,20 @@ class TfElement extends LitElement {
|
||||
};
|
||||
|
||||
let tabs = html`
|
||||
<div class="w3-bar w3-black">
|
||||
<div class="w3-bar w3-theme-l1">
|
||||
${Object.entries(k_tabs).map(
|
||||
([k, v]) => html`
|
||||
<button
|
||||
title=${v}
|
||||
class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
|
||||
v
|
||||
? 'w3-red'
|
||||
: 'w3-black'}"
|
||||
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
|
||||
? 'w3-theme-l2'
|
||||
: 'w3-theme-l1'}"
|
||||
@click=${() => self.set_tab(v)}
|
||||
>
|
||||
${k}
|
||||
<span class=${self.tab == v ? '' : 'w3-hide-small'}
|
||||
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
|
||||
>
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
@ -371,15 +361,27 @@ class TfElement extends LitElement {
|
||||
`;
|
||||
let contents = !this.loaded
|
||||
? 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>`
|
||||
: this.render_tab();
|
||||
return html`
|
||||
${this.render_id_picker()} ${tabs}
|
||||
${this.tags.map(
|
||||
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
||||
)}
|
||||
${contents}
|
||||
<div
|
||||
style="width: 100vw; min-height: 100vh; height: 100%"
|
||||
class="w3-theme-dark"
|
||||
>
|
||||
${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 tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
@ -13,6 +13,7 @@ class TfComposeElement extends LitElement {
|
||||
branch: {type: String},
|
||||
apps: {type: Object},
|
||||
drafts: {type: Object},
|
||||
author: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
@ -25,6 +26,7 @@ class TfComposeElement extends LitElement {
|
||||
this.branch = undefined;
|
||||
this.apps = undefined;
|
||||
this.drafts = {};
|
||||
this.author = undefined;
|
||||
}
|
||||
|
||||
process_text(text) {
|
||||
@ -64,7 +66,7 @@ class TfComposeElement extends LitElement {
|
||||
updated = true;
|
||||
}
|
||||
if (updated) {
|
||||
this.requestUpdate();
|
||||
setTimeout(() => this.notify(draft), 0);
|
||||
}
|
||||
return tfutils.markdown(text);
|
||||
}
|
||||
@ -72,14 +74,12 @@ class TfComposeElement extends LitElement {
|
||||
input(event) {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
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_preview = this.renderRoot.getElementById(
|
||||
'content_warning_preview'
|
||||
);
|
||||
if (content_warning && content_warning_preview) {
|
||||
content_warning_preview.innerText = content_warning.value;
|
||||
}
|
||||
let draft = this.get_draft();
|
||||
draft.text = edit.innerText;
|
||||
draft.content_warning = content_warning?.value;
|
||||
setTimeout(() => this.notify(draft), 0);
|
||||
}
|
||||
|
||||
notify(draft) {
|
||||
@ -95,14 +95,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) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let img = new Image();
|
||||
@ -169,8 +161,7 @@ class TfComposeElement extends LitElement {
|
||||
size: buffer.length ?? buffer.byteLength,
|
||||
};
|
||||
let edit = self.renderRoot.getElementById('edit');
|
||||
edit.value += `\n`;
|
||||
self.change();
|
||||
edit.innerText += `\n`;
|
||||
self.input();
|
||||
} catch (e) {
|
||||
alert(e?.message);
|
||||
@ -197,7 +188,7 @@ class TfComposeElement extends LitElement {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let message = {
|
||||
type: 'post',
|
||||
text: edit.value,
|
||||
text: edit.innerText,
|
||||
};
|
||||
if (this.root || this.branch) {
|
||||
message.root = this.root;
|
||||
@ -224,29 +215,19 @@ class TfComposeElement extends LitElement {
|
||||
console.log('encrypted as', message);
|
||||
}
|
||||
try {
|
||||
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
|
||||
edit.value = '';
|
||||
self.change();
|
||||
self.notify(undefined);
|
||||
self.requestUpdate();
|
||||
});
|
||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||
self.notify(undefined);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
discard() {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
edit.value = '';
|
||||
this.change();
|
||||
let preview = this.renderRoot.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
this.notify(undefined);
|
||||
}
|
||||
|
||||
attach() {
|
||||
let self = this;
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.onchange = function (event) {
|
||||
@ -262,9 +243,9 @@ class TfComposeElement extends LitElement {
|
||||
try {
|
||||
let rows = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.content FROM messages_fts(?)
|
||||
SELECT json(messages.content) AS content FROM messages_fts(?)
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
WHERE messages.content LIKE ?
|
||||
WHERE json(messages.content) LIKE ?
|
||||
ORDER BY timestamp DESC LIMIT 10
|
||||
`,
|
||||
['"' + text.replace('"', '""') + '"', `%%`]
|
||||
@ -284,22 +265,39 @@ class TfComposeElement extends LitElement {
|
||||
}
|
||||
|
||||
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({
|
||||
iframe: this.shadowRoot,
|
||||
collection: [
|
||||
{
|
||||
values: Object.entries(this.users).map((x) => ({
|
||||
key: x[1].name,
|
||||
value: x[0],
|
||||
})),
|
||||
values: values,
|
||||
selectTemplate: function (item) {
|
||||
return `[@${item.original.key}](${item.original.value})`;
|
||||
return item
|
||||
? `[@${item.original.key}](${item.original.value})`
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: '&',
|
||||
values: this.autocomplete,
|
||||
selectTemplate: function (item) {
|
||||
return ``;
|
||||
return item
|
||||
? ``
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -310,14 +308,15 @@ class TfComposeElement extends LitElement {
|
||||
updated() {
|
||||
super.updated();
|
||||
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');
|
||||
preview.innerHTML = this.process_text(edit.value);
|
||||
this.last_updated_text = edit.value;
|
||||
preview.innerHTML = this.process_text(edit.innerText);
|
||||
this.last_updated_text = edit.innerText;
|
||||
}
|
||||
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||
if (encrypt) {
|
||||
let tribute = new Tribute({
|
||||
iframe: this.shadowRoot,
|
||||
values: Object.entries(this.users).map((x) => ({
|
||||
key: x[1].name,
|
||||
value: x[0],
|
||||
@ -333,8 +332,7 @@ class TfComposeElement extends LitElement {
|
||||
remove_mention(id) {
|
||||
let draft = this.get_draft();
|
||||
delete draft.mentions[id];
|
||||
this.notify(draft);
|
||||
this.requestUpdate();
|
||||
setTimeout(() => this.notify(), 0);
|
||||
}
|
||||
|
||||
render_mention(mention) {
|
||||
@ -342,7 +340,7 @@ class TfComposeElement extends LitElement {
|
||||
return html` <div style="display: flex; flex-direction: row">
|
||||
<div style="align-self: center; margin: 0.5em">
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
title="Remove ${mention.name} mention"
|
||||
@click=${() => self.remove_mention(mention.link)}
|
||||
>
|
||||
@ -396,16 +394,16 @@ class TfComposeElement extends LitElement {
|
||||
if (this.apps) {
|
||||
return html`
|
||||
<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(
|
||||
(app) => html`<option value=${app}>${app}</option>`
|
||||
)}
|
||||
</select>
|
||||
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
|
||||
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
|
||||
Attach
|
||||
</button>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (this.apps = null)}
|
||||
>
|
||||
Cancel
|
||||
@ -421,12 +419,12 @@ class TfComposeElement extends LitElement {
|
||||
self.apps = await tfrpc.rpc.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
|
||||
</button>`;
|
||||
} else {
|
||||
return html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (this.apps = null)}
|
||||
>
|
||||
Discard App
|
||||
@ -448,15 +446,15 @@ class TfComposeElement extends LitElement {
|
||||
return html`
|
||||
<div class="w3-container w3-padding">
|
||||
<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>
|
||||
</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=${self.input} value=${draft.content_warning}></input>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
@ -486,14 +484,14 @@ class TfComposeElement extends LitElement {
|
||||
<div style="display: flex; flex-direction: row; width: 100%">
|
||||
<label for="encrypt_to">🔐 To:</label>
|
||||
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||
</div>
|
||||
<ul>
|
||||
${draft.encrypt_to.map(
|
||||
(x) => html`
|
||||
<li>
|
||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||
<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
@ -512,7 +510,7 @@ class TfComposeElement extends LitElement {
|
||||
let draft = self.get_draft();
|
||||
let content_warning =
|
||||
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>
|
||||
</div>`
|
||||
: undefined;
|
||||
@ -520,34 +518,31 @@ class TfComposeElement extends LitElement {
|
||||
draft.encrypt_to !== undefined
|
||||
? undefined
|
||||
: html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.set_encrypt([])}
|
||||
>
|
||||
🔐
|
||||
</button>`;
|
||||
let result = html`
|
||||
<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"
|
||||
>
|
||||
${this.render_encrypt()}
|
||||
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
|
||||
<div style="flex: 1 0 50%">
|
||||
<p>
|
||||
<textarea
|
||||
class="w3-input w3-dark-grey w3-border"
|
||||
style="resize: vertical"
|
||||
placeholder="Write a post here."
|
||||
id="edit"
|
||||
@input=${this.input}
|
||||
@change=${this.change}
|
||||
@paste=${this.paste}
|
||||
>
|
||||
${draft.text}</textarea
|
||||
>
|
||||
</p>
|
||||
<div class="w3-container w3-padding-small">
|
||||
<div class="w3-half">
|
||||
<span
|
||||
class="w3-input w3-theme-d1 w3-border"
|
||||
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
|
||||
placeholder="Write a post here."
|
||||
id="edit"
|
||||
@input=${this.input}
|
||||
@paste=${this.paste}
|
||||
contenteditable="plaintext-only"
|
||||
.innerText=${live(draft.text ?? '')}
|
||||
></span>
|
||||
</div>
|
||||
<div style="flex: 1 0 50%">
|
||||
<div class="w3-half w3-padding">
|
||||
${content_warning}
|
||||
<div id="preview"></div>
|
||||
</div>
|
||||
@ -556,18 +551,14 @@ ${draft.text}</textarea
|
||||
self.render_mention(x)
|
||||
)}
|
||||
${this.render_attach_app()} ${this.render_content_warning()}
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
id="submit"
|
||||
@click=${this.submit}
|
||||
>
|
||||
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
|
||||
Submit
|
||||
</button>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.attach}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.attach}>
|
||||
Attach
|
||||
</button>
|
||||
${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
|
||||
</button>
|
||||
</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 tfutils from './tf-utils.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() {
|
||||
function normalize_expression(expression) {
|
||||
if (expression === 'Like' || !expression) {
|
||||
@ -66,19 +72,21 @@ class TfMessageElement extends LitElement {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
return html`<div>
|
||||
${(this.message.votes || []).map(
|
||||
(vote) => html`
|
||||
<span
|
||||
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
||||
vote.timestamp
|
||||
)}"
|
||||
>
|
||||
${normalize_expression(vote.content.vote.expression)}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>`;
|
||||
if (this.message?.votes?.length) {
|
||||
return html`<div class="w3-button" @click=${this.show_reactions}>
|
||||
${(this.message.votes || []).map(
|
||||
(vote) => html`
|
||||
<span
|
||||
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
||||
vote.timestamp
|
||||
)}"
|
||||
>
|
||||
${normalize_expression(vote.content.vote.expression)}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
render_raw() {
|
||||
@ -125,7 +133,7 @@ class TfMessageElement extends LitElement {
|
||||
}
|
||||
|
||||
react(event) {
|
||||
emojis.picker((x) => this.vote(x));
|
||||
emojis.picker((x) => this.vote(x), null, this.whoami);
|
||||
}
|
||||
|
||||
show_image(link) {
|
||||
@ -164,7 +172,7 @@ class TfMessageElement extends LitElement {
|
||||
event.srcElement.classList.contains('img_caption')
|
||||
) {
|
||||
let next = event.srcElement.nextSibling;
|
||||
if (next.style.display == 'block') {
|
||||
if (next.style.display != 'none') {
|
||||
next.style.display = 'none';
|
||||
} else {
|
||||
next.style.display = 'block';
|
||||
@ -239,9 +247,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
if (mentions.length) {
|
||||
let self = this;
|
||||
return html`
|
||||
<fieldset
|
||||
style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"
|
||||
>
|
||||
<fieldset style="padding: 0.5em; border: 1px solid black">
|
||||
<legend>Mentions</legend>
|
||||
${mentions.map((x) => self.render_mention(x))}
|
||||
</fieldset>
|
||||
@ -282,14 +288,14 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
if (this.message.child_messages?.length) {
|
||||
if (!this.expanded[this.message.id]) {
|
||||
return html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self.set_expanded(true)}
|
||||
>
|
||||
+ ${this.total_child_messages(this.message) + ' More'}
|
||||
</button>`;
|
||||
} else {
|
||||
return html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self.set_expanded(false)}
|
||||
>
|
||||
Collapse</button
|
||||
@ -331,20 +337,23 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
if (this.message?.decrypted?.type == 'post') {
|
||||
content = this.message.decrypted;
|
||||
}
|
||||
let class_background = this.message?.decrypted
|
||||
? 'w3-pale-red'
|
||||
: 'w3-theme-d4';
|
||||
let self = this;
|
||||
let raw_button;
|
||||
switch (this.format) {
|
||||
case 'raw':
|
||||
if (content?.type == 'post' || content?.type == 'blog') {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'md')}
|
||||
>
|
||||
Markdown
|
||||
</button>`;
|
||||
} else {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'message')}
|
||||
>
|
||||
Message
|
||||
@ -353,7 +362,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
break;
|
||||
case 'md':
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'message')}
|
||||
>
|
||||
Message
|
||||
@ -361,7 +370,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
break;
|
||||
case 'decrypted':
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'raw')}
|
||||
>
|
||||
Raw
|
||||
@ -370,14 +379,14 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
default:
|
||||
if (this.message.decrypted) {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'decrypted')}
|
||||
>
|
||||
Decrypted
|
||||
</button>`;
|
||||
} else {
|
||||
raw_button = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (self.format = 'raw')}
|
||||
>
|
||||
Raw
|
||||
@ -389,8 +398,8 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
let body;
|
||||
return html`
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
||||
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
||||
>
|
||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||
<span style="padding-right: 8px"
|
||||
@ -400,13 +409,24 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
>
|
||||
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
if (this.message?.type === 'contact_group') {
|
||||
return html` <div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
>
|
||||
${this.message.messages.map(
|
||||
(x) =>
|
||||
@ -421,8 +441,8 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
</div>`;
|
||||
} else if (this.message.placeholder) {
|
||||
return html` <div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
>
|
||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
||||
(placeholder)
|
||||
@ -494,17 +514,15 @@ ${JSON.stringify(mention, null, 2)}</pre
|
||||
<tf-compose
|
||||
whoami=${this.whoami}
|
||||
.users=${this.users}
|
||||
root=${this.message.content.root || this.message.id}
|
||||
root=${content.root || this.message.id}
|
||||
branch=${this.message.id}
|
||||
.drafts=${this.drafts}
|
||||
@tf-discard=${this.discard_reply}
|
||||
author=${this.message.author}
|
||||
></tf-compose>
|
||||
`
|
||||
: html`
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
@click=${this.show_reply}
|
||||
>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||
Reply
|
||||
</button>
|
||||
`;
|
||||
@ -533,7 +551,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
}
|
||||
let content_warning = html`
|
||||
<div
|
||||
class="w3-panel w3-round-xlarge w3-blue"
|
||||
class="w3-panel w3-round-xlarge w3-theme-l4"
|
||||
style="cursor: pointer"
|
||||
@click=${(x) => this.toggle_expanded(':cw')}
|
||||
>
|
||||
@ -553,9 +571,6 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
let is_encrypted = this.message?.decrypted
|
||||
? html`<span style="align-self: center">🔓</span>`
|
||||
: undefined;
|
||||
let style_background = this.message?.decrypted
|
||||
? 'rgba(255, 0, 0, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.1)';
|
||||
return html`
|
||||
<style>
|
||||
code {
|
||||
@ -572,8 +587,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
||||
class="w3-card-4 ${class_background} w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px"
|
||||
>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
@ -588,7 +603,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
${payload} ${this.render_votes()}
|
||||
<p>
|
||||
${reply}
|
||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</p>
|
||||
@ -599,9 +614,6 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
let is_encrypted = this.message?.decrypted
|
||||
? html`<span style="align-self: center">🔓</span>`
|
||||
: undefined;
|
||||
let style_background = this.message?.decrypted
|
||||
? 'rgba(255, 0, 0, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.1)';
|
||||
return html`
|
||||
<style>
|
||||
code {
|
||||
@ -618,8 +630,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
||||
class="w3-card-4 ${class_background} w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px"
|
||||
>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
@ -633,7 +645,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
</div>
|
||||
${content.text} ${this.render_votes()}
|
||||
<p>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</p>
|
||||
@ -681,17 +693,15 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
<tf-compose
|
||||
whoami=${this.whoami}
|
||||
.users=${this.users}
|
||||
root=${this.message.content.root || this.message.id}
|
||||
root=${content.root || this.message.id}
|
||||
branch=${this.message.id}
|
||||
.drafts=${this.drafts}
|
||||
@tf-discard=${this.discard_reply}
|
||||
author=${this.message.author}
|
||||
></tf-compose>
|
||||
`
|
||||
: html`
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
@click=${this.show_reply}
|
||||
>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||
Reply
|
||||
</button>
|
||||
`;
|
||||
@ -711,8 +721,8 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="w3-card-4"
|
||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"
|
||||
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px"
|
||||
>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
@ -728,7 +738,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
||||
${this.render_mentions()}
|
||||
<div>
|
||||
${reply}
|
||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</div>
|
||||
|
@ -188,6 +188,10 @@ class TfProfileElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
copy_id() {
|
||||
navigator.clipboard.writeText(this.id);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (
|
||||
this.id == this.whoami &&
|
||||
@ -215,49 +219,49 @@ class TfProfileElement extends LitElement {
|
||||
let server_follow;
|
||||
if (this.server_follows_me === true) {
|
||||
server_follow = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.server_follow_me(false)}
|
||||
>
|
||||
Server, Stop Following Me
|
||||
</button>`;
|
||||
} else if (this.server_follows_me === false) {
|
||||
server_follow = html`<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.server_follow_me(true)}
|
||||
>
|
||||
Server, Follow Me
|
||||
</button>`;
|
||||
}
|
||||
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
|
||||
</button>
|
||||
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
||||
Discard
|
||||
</button>
|
||||
${server_follow}
|
||||
`;
|
||||
} 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
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
if (this.id !== this.whoami && this.following !== undefined) {
|
||||
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
|
||||
</button>`
|
||||
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
|
||||
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
|
||||
Follow
|
||||
</button>`;
|
||||
}
|
||||
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||
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
|
||||
</button>`
|
||||
: html`<button class="w3-button w3-dark-grey" @click=${this.block}>
|
||||
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
|
||||
Block
|
||||
</button>`;
|
||||
}
|
||||
@ -267,16 +271,16 @@ class TfProfileElement extends LitElement {
|
||||
<div class="w3-container">
|
||||
<div>
|
||||
<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><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>
|
||||
<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>
|
||||
<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>`
|
||||
@ -287,6 +291,14 @@ class TfProfileElement extends LitElement {
|
||||
let description = this.editing?.description ?? profile.description;
|
||||
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
||||
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||
<div class="w3-row">
|
||||
<div class="w3-col s1 w3-container w3-right">
|
||||
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
|
||||
</div>
|
||||
<div class="w3-rest w3-container">
|
||||
<input type="text" class="w3-theme-d1" style="width: 100%; vertical-align: middle" readonly value=${this.id}></input>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||
${edit_profile}
|
||||
<div style="flex: 1 0 50%">
|
||||
|
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);
|
File diff suppressed because it is too large
Load Diff
@ -7,35 +7,52 @@ class TfTabConnectionsElement extends LitElement {
|
||||
return {
|
||||
broadcasts: {type: Array},
|
||||
identities: {type: Array},
|
||||
my_identities: {type: Array},
|
||||
connections: {type: Array},
|
||||
stored_connections: {type: Array},
|
||||
users: {type: Object},
|
||||
server_identity: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
static k_broadcast_emojis = {
|
||||
discovery: '🏓',
|
||||
room: '🚪',
|
||||
peer_exchange: '🕸',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.broadcasts = [];
|
||||
this.identities = [];
|
||||
this.my_identities = [];
|
||||
this.connections = [];
|
||||
this.stored_connections = [];
|
||||
this.users = {};
|
||||
tfrpc.rpc.getIdentities().then(function (identities) {
|
||||
self.my_identities = identities || [];
|
||||
});
|
||||
tfrpc.rpc.getAllIdentities().then(function (identities) {
|
||||
self.identities = identities || [];
|
||||
});
|
||||
tfrpc.rpc.getStoredConnections().then(function (connections) {
|
||||
self.stored_connections = connections || [];
|
||||
});
|
||||
tfrpc.rpc.getServerIdentity().then(function (identity) {
|
||||
self.server_identity = identity;
|
||||
});
|
||||
}
|
||||
|
||||
render_connection_summary(connection) {
|
||||
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) {
|
||||
return html`(room peer)`;
|
||||
return html`<div>room peer</div>`;
|
||||
} else {
|
||||
return JSON.stringify(connection);
|
||||
}
|
||||
@ -61,7 +78,7 @@ class TfTabConnectionsElement extends LitElement {
|
||||
return html`
|
||||
<li>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
||||
>
|
||||
Connect
|
||||
@ -73,15 +90,18 @@ class TfTabConnectionsElement extends LitElement {
|
||||
|
||||
render_broadcast(connection) {
|
||||
return html`
|
||||
<li>
|
||||
<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-bar-item w3-button w3-theme-d1"
|
||||
@click=${() => tfrpc.rpc.connect(connection)}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||
${this.render_connection_summary(connection)}
|
||||
<div class="w3-bar-item">
|
||||
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
|
||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||
${this.render_connection_summary(connection)}
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
@ -92,9 +112,19 @@ class TfTabConnectionsElement extends LitElement {
|
||||
}
|
||||
|
||||
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`
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||
>
|
||||
Close
|
||||
@ -103,6 +133,20 @@ class TfTabConnectionsElement extends LitElement {
|
||||
${connection.tunnel !== undefined
|
||||
? '🚇'
|
||||
: 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>
|
||||
${this.connections
|
||||
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
||||
@ -115,56 +159,74 @@ class TfTabConnectionsElement extends LitElement {
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
<div class="w3-container">
|
||||
<div class="w3-container" style="box-sizing: border-box">
|
||||
<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
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() =>
|
||||
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<h2>Broadcasts</h2>
|
||||
<ul>
|
||||
<ul class="w3-ul w3-border">
|
||||
${this.broadcasts
|
||||
.filter((x) => x.address)
|
||||
.map((x) => self.render_broadcast(x))}
|
||||
</ul>
|
||||
<h2>Connections</h2>
|
||||
<ul>
|
||||
<ul class="w3-ul w3-border">
|
||||
${this.connections
|
||||
.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>
|
||||
<h2>Stored Connections (WIP)</h2>
|
||||
<ul>
|
||||
<h2>Stored Connections</h2>
|
||||
<ul class="w3-ul w3-border">
|
||||
${this.stored_connections.map(
|
||||
(x) => html`
|
||||
<li>
|
||||
<li class="w3-bar">
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-bar-item w3-button w3-theme-d1"
|
||||
@click=${() => self.forget_stored_connection(x)}
|
||||
>
|
||||
Forget
|
||||
</button>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-bar-item w3-button w3-theme-d1"
|
||||
@click=${() => tfrpc.rpc.connect(x)}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
${x.address}:${x.port}
|
||||
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||
<div class="w3-bar-item">
|
||||
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||
<div><small>${x.address}:${x.port}</small></div>
|
||||
</div>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
<h2>Local Accounts</h2>
|
||||
<ul>
|
||||
<ul class="w3-ul w3-border">
|
||||
${this.identities.map(
|
||||
(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>
|
||||
</div>
|
||||
|
@ -29,7 +29,7 @@ class TfTabMentionsElement extends LitElement {
|
||||
console.log('Loading...', this.whoami);
|
||||
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(?)
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
|
@ -33,12 +33,12 @@ class TfTabNewsFeedElement extends LitElement {
|
||||
if (this.hash.startsWith('#@')) {
|
||||
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
|
||||
WHERE messages.author = ?
|
||||
ORDER BY sequence DESC
|
||||
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
|
||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
@ -51,11 +51,11 @@ class TfTabNewsFeedElement extends LitElement {
|
||||
} else if (this.hash.startsWith('#%')) {
|
||||
return await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.*
|
||||
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||
FROM messages
|
||||
WHERE id = ?1
|
||||
UNION
|
||||
SELECT messages.*
|
||||
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||
FROM messages JOIN messages_refs
|
||||
ON messages.id = messages_refs.message
|
||||
WHERE messages_refs.ref = ?1
|
||||
@ -69,17 +69,17 @@ class TfTabNewsFeedElement extends LitElement {
|
||||
promises.push(
|
||||
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
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
||||
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
|
||||
JOIN messages_refs ON news.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
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
|
||||
JOIN messages_refs ON news.id = messages_refs.message
|
||||
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;
|
||||
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
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.timestamp > ?
|
||||
AND messages.timestamp <= ?
|
||||
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
|
||||
JOIN messages_refs ON news.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
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
|
||||
JOIN messages_refs ON news.id = messages_refs.message
|
||||
JOIN messages ON messages_refs.ref = messages.id
|
||||
@ -187,7 +187,7 @@ class TfTabNewsFeedElement extends LitElement {
|
||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||
more = html`
|
||||
<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
|
||||
</button>
|
||||
</p>
|
||||
|
@ -12,6 +12,7 @@ class TfTabNewsElement extends LitElement {
|
||||
following: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
loading: {type: Boolean},
|
||||
};
|
||||
}
|
||||
|
||||
@ -84,10 +85,7 @@ class TfTabNewsElement extends LitElement {
|
||||
} else {
|
||||
delete this.drafts[id];
|
||||
}
|
||||
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
|
||||
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
|
||||
this.drafts = Object.assign({}, this.drafts);
|
||||
}
|
||||
this.drafts = Object.assign({}, this.drafts);
|
||||
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||
}
|
||||
|
||||
@ -116,17 +114,31 @@ class TfTabNewsElement extends LitElement {
|
||||
.users=${this.users}
|
||||
></tf-profile>`
|
||||
: 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`
|
||||
<p class="w3-bar">
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-dark-grey"
|
||||
class="w3-bar-item w3-button w3-theme-d1"
|
||||
@click=${this.show_more}
|
||||
>
|
||||
${this.new_messages_text()}
|
||||
</button>
|
||||
</p>
|
||||
<div>
|
||||
<div class="w3-bar">
|
||||
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
||||
${edit_profile}
|
||||
</div>
|
||||
<div>
|
||||
<tf-compose
|
||||
|
@ -110,14 +110,14 @@ class TfTabQueryElement extends LitElement {
|
||||
<textarea
|
||||
id="search"
|
||||
rows="8"
|
||||
class="w3-input w3-dark-grey"
|
||||
class="w3-input w3-theme-d1"
|
||||
style="flex: 1; resize: vertical"
|
||||
@keydown=${this.search_keydown}
|
||||
>
|
||||
${this.query}</textarea
|
||||
>
|
||||
<button
|
||||
class="w3-button w3-dark-grey"
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${(event) =>
|
||||
self.search(self.renderRoot.getElementById('search').value)}
|
||||
>
|
||||
|
@ -35,7 +35,7 @@ class TfTabSearchElement extends LitElement {
|
||||
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(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(?)
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
@ -78,8 +78,8 @@ class TfTabSearchElement extends LitElement {
|
||||
let self = this;
|
||||
return html`
|
||||
<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>
|
||||
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||
<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-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||
</div>
|
||||
<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() {
|
||||
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;
|
||||
name =
|
||||
name !== undefined
|
||||
@ -26,21 +31,20 @@ class TfUserElement extends LitElement {
|
||||
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
||||
|
||||
if (this.users[this.id]) {
|
||||
let image = this.users[this.id].image;
|
||||
image = typeof image == 'string' ? image : image?.link;
|
||||
return html` <div style="display: inline-block; font-weight: bold">
|
||||
<img
|
||||
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
|
||||
?hidden=${image === undefined}
|
||||
src="${image ? '/' + image + '/view' : undefined}"
|
||||
/>
|
||||
${name}
|
||||
</div>`;
|
||||
} else {
|
||||
return html` <div style="display: inline-block; font-weight: bold">
|
||||
${name}
|
||||
</div>`;
|
||||
let image_link = this.users[this.id].image;
|
||||
image_link =
|
||||
typeof image_link == 'string' ? image_link : image_link?.link;
|
||||
if (image_link !== undefined) {
|
||||
image = html`<img
|
||||
class="w3-circle"
|
||||
style="width: 2em; height: 2em; vertical-align: middle"
|
||||
src="/${image_link}/view"
|
||||
/>`;
|
||||
}
|
||||
}
|
||||
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';
|
||||
|
||||
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
|
||||
|
||||
function image(node, entering) {
|
||||
if (
|
||||
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) {
|
||||
let reader = new commonmark.Parser({safe: true});
|
||||
let writer = new commonmark.HtmlRenderer();
|
||||
writer.image = image;
|
||||
writer.code = code;
|
||||
writer.attrs = attrs;
|
||||
let parsed = reader.parse(md || '');
|
||||
parsed = hashtagify.transform(parsed);
|
||||
parsed = linkify.transform(parsed);
|
||||
let walker = parsed.walker();
|
||||
let event, node;
|
||||
while ((event = walker.next())) {
|
||||
|
@ -482,16 +482,7 @@ class TributeRange {
|
||||
}
|
||||
|
||||
getDocument() {
|
||||
let iframe;
|
||||
if (this.tribute.current.collection) {
|
||||
iframe = this.tribute.current.collection.iframe;
|
||||
}
|
||||
|
||||
if (!iframe) {
|
||||
return document
|
||||
}
|
||||
|
||||
return iframe.contentWindow.document
|
||||
return document;
|
||||
}
|
||||
|
||||
positionMenuAtCaret(scrollTo) {
|
||||
@ -653,8 +644,8 @@ class TributeRange {
|
||||
}
|
||||
|
||||
getWindowSelection() {
|
||||
if (this.tribute.collection.iframe) {
|
||||
return this.tribute.collection.iframe.contentWindow.getSelection()
|
||||
if (this.tribute.collection[0].iframe?.getSelection) {
|
||||
return this.tribute.collection[0].iframe.getSelection()
|
||||
}
|
||||
|
||||
return window.getSelection()
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "👋",
|
||||
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
|
||||
"previous": "&W5aJp2DgOW5rQ0AOIC9Ut3DpsahPrO6PjkJ1PQbNRdM=.sha256"
|
||||
}
|
||||
|
@ -55,7 +55,7 @@
|
||||
</p>
|
||||
<a
|
||||
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
|
||||
>
|
||||
<a
|
||||
@ -63,6 +63,11 @@
|
||||
href="https://www.tildefriends.net/~cory/apps/"
|
||||
><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 class="w3-col l4 m6">
|
||||
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
|
||||
@ -70,6 +75,60 @@
|
||||
</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 -->
|
||||
<div class="w3-light-grey">
|
||||
<div class="w3-row-padding w3-padding-64">
|
||||
@ -199,7 +258,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p>CodeMirror</p>
|
||||
</a>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📝",
|
||||
"previous": "&/wl8HE2jZShRXTYEVYRrK3pjHwi41Wbxl9HoSJaQP6Y=.sha256"
|
||||
"previous": "&DaYqKHRBKhjFGaOzbKZ1+/pLspJeEkDJYTF2B50tH6k=.sha256"
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import * as utils from './utils.js';
|
||||
let g_hash;
|
||||
let g_collection_notifies = {};
|
||||
|
||||
tfrpc.register(async function getActiveIdentity() {
|
||||
return ssb.getActiveIdentity();
|
||||
});
|
||||
tfrpc.register(async function getOwnerIdentities() {
|
||||
return ssb.getOwnerIdentities();
|
||||
});
|
||||
@ -54,6 +57,9 @@ core.register('message', async function message_handler(message) {
|
||||
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) {
|
||||
if (g_hash != hash) {
|
||||
|
@ -2,14 +2,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<base target="_top" />
|
||||
<link rel="stylesheet" href="tildefriends.css" />
|
||||
</head>
|
||||
<body style="color: #fff">
|
||||
<body>
|
||||
<tf-collections-app></tf-collections-app>
|
||||
<script>
|
||||
window.litDisableBundleWarning = true;
|
||||
</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-app.js" type="module"></script>
|
||||
</body>
|
||||
|
44
apps/wiki/lit-all.min.js
vendored
44
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() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
category: {type: String},
|
||||
collection: {type: Object},
|
||||
selected_id: {type: String},
|
||||
is_creating: {type: Boolean},
|
||||
@ -75,9 +76,10 @@ class TfCollectionElement extends LitElement {
|
||||
render() {
|
||||
let self = this;
|
||||
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}>
|
||||
<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 ?? {})
|
||||
.sort((x, y) => x.name.localeCompare(y.name))
|
||||
.map(
|
||||
@ -91,22 +93,22 @@ class TfCollectionElement extends LitElement {
|
||||
)}
|
||||
</select>
|
||||
<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>
|
||||
<input type="text" id="rename_name"></input>
|
||||
<button @click=${this.on_rename}>Rename ${this.type}</button>
|
||||
<button @click=${() => (self.is_renaming = false)}>x</button>
|
||||
</span>
|
||||
</span>
|
||||
<button @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="yellow" @click=${() => (self.is_renaming = true)} ?disabled=${this.is_renaming || !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}>
|
||||
<label for="create_name">New ${this.type} name:</label>
|
||||
<input type="text" id="create_name"></input>
|
||||
<button @click=${this.on_create}>Create ${this.type}</button>
|
||||
<button @click=${() => (self.is_creating = false)}>x</button>
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
@ -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) {
|
||||
self.notify_hash_changed(hash);
|
||||
});
|
||||
tfrpc.register(function setActiveIdentity(id) {
|
||||
self.whoami = id;
|
||||
});
|
||||
tfrpc.rpc.get_hash().then((hash) => self.notify_hash_changed(hash));
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.ids = await tfrpc.rpc.getIdentities();
|
||||
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();
|
||||
this.following = Object.keys(await tfrpc.rpc.following(ids, 1)).sort();
|
||||
|
||||
@ -255,23 +258,31 @@ class TfCollectionsAppElement extends LitElement {
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
<link rel="stylesheet" href="tildefriends.css"/>
|
||||
<style>
|
||||
.toc:hover {
|
||||
.toc-item {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toc-item:hover {
|
||||
background-color: #0cc;
|
||||
}
|
||||
.toc.selected {
|
||||
.toc-item.selected {
|
||||
background-color: #088;
|
||||
font-weight: bold;
|
||||
}
|
||||
.table-of-contents {
|
||||
flex: 0 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
</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>
|
||||
${keyed(
|
||||
this.whoami,
|
||||
html`<tf-collection
|
||||
.collection=${this.wikis}
|
||||
whoami=${this.whoami}
|
||||
category="wiki"
|
||||
selected_id=${this.wiki?.id}
|
||||
@create=${this.on_wiki_create}
|
||||
@rename=${this.on_wiki_rename}
|
||||
@ -284,6 +295,7 @@ class TfCollectionsAppElement extends LitElement {
|
||||
html`<tf-collection
|
||||
.collection=${this.wiki_docs}
|
||||
whoami=${this.whoami}
|
||||
category="document"
|
||||
selected_id=${this.wiki_doc &&
|
||||
this.wiki_doc?.parent == this.wiki?.id
|
||||
? this.wiki_doc?.id
|
||||
@ -298,9 +310,9 @@ class TfCollectionsAppElement extends LitElement {
|
||||
<div ?hidden=${!this.wiki?.editors || !this.expand_editors}>
|
||||
<div>
|
||||
<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>
|
||||
<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}>
|
||||
<label for="add_editor">Add Editor:</label>
|
||||
<input type="text" id="add_editor"></input>
|
||||
@ -312,18 +324,19 @@ class TfCollectionsAppElement extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<div style="flex: 0 0">
|
||||
<div class="flex-row">
|
||||
<div class="box table-of-contents">
|
||||
${Object.values(this.wikis || {})
|
||||
.sort((x, y) => x.name.localeCompare(y.name))
|
||||
.map(
|
||||
(wiki) => html`
|
||||
<div
|
||||
class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}"
|
||||
style="white-space: nowrap; cursor: pointer"
|
||||
class="toc-item ${self.wiki?.id === wiki.id
|
||||
? 'selected'
|
||||
: ''}"
|
||||
@click=${() => self.on_wiki_changed({detail: {value: wiki}})}
|
||||
>
|
||||
${wiki.name}
|
||||
${self.wiki?.id === wiki.id ? '' : '>'} ${wiki.name}
|
||||
</div>
|
||||
<ul>
|
||||
${Object.values(self.wiki_docs || {})
|
||||
@ -332,10 +345,10 @@ class TfCollectionsAppElement extends LitElement {
|
||||
.map(
|
||||
(doc) => html`
|
||||
<li
|
||||
class="toc ${self.wiki_doc?.id === doc.id
|
||||
class="toc-item ${self.wiki_doc?.id === doc.id
|
||||
? 'selected'
|
||||
: ''}"
|
||||
style="white-space: nowrap; cursor: pointer; list-style: none; text-indent: -1rem"
|
||||
style="list-style: none; text-indent: -1rem"
|
||||
@click=${() =>
|
||||
self.on_wiki_doc_changed({detail: {value: doc}})}
|
||||
>
|
||||
|
@ -84,7 +84,11 @@ class TfWikiDocElement extends LitElement {
|
||||
|
||||
async load_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);
|
||||
if (d) {
|
||||
blob = d;
|
||||
@ -253,85 +257,43 @@ class TfWikiDocElement extends LitElement {
|
||||
let self = this;
|
||||
let thumbnail_ref = this.thumbnail(this.blob);
|
||||
return html`
|
||||
<link rel="stylesheet" href="tildefriends.css"/>
|
||||
<style>
|
||||
a:link {
|
||||
color: #268bd2;
|
||||
}
|
||||
a:visited {
|
||||
color: #6c71c4;
|
||||
}
|
||||
a:hover {
|
||||
color: #859900;
|
||||
}
|
||||
a:active {
|
||||
color: #2aa198;
|
||||
}
|
||||
a:link { color: #268bd2 }
|
||||
a:visited { color: #6c71c4 }
|
||||
a:hover { color: #859900 }
|
||||
a:active { color: #2aa198 }
|
||||
|
||||
#editor-text-area {
|
||||
background-color: #00000040;
|
||||
color: white;
|
||||
style="flex: 1 1;
|
||||
min-height: 10em;
|
||||
font-size: larger;
|
||||
${this.value?.private ? 'border: 4px solid #800' : ''}
|
||||
</style>
|
||||
<div style="display: inline-flex; flex-direction: row">
|
||||
<button
|
||||
?disabled=${!this.whoami || this.is_editing}
|
||||
@click=${() => (self.is_editing = true)}
|
||||
>
|
||||
Edit
|
||||
</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 class="inline-flex-row">
|
||||
<button ?disabled=${!this.whoami || this.is_editing} @click=${() => (self.is_editing = true)}>Edit</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 ?hidden=${!this.value?.private} style="color: #800">
|
||||
🔒 document is private
|
||||
</div>
|
||||
<div
|
||||
style="display: flex; flex-direction: row; ${this.value?.private
|
||||
? 'border-top: 4px solid #800'
|
||||
: ''}"
|
||||
>
|
||||
<div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div>
|
||||
<div class="flex-column" ${this.value?.private ? 'border-top: 4px solid #800' : ''}">
|
||||
<textarea
|
||||
rows="25"
|
||||
?hidden=${!this.is_editing}
|
||||
style="flex: 1 1; min-height: 10em; ${this.value?.private
|
||||
? 'border: 4px solid #800'
|
||||
: ''}"
|
||||
id="editor-text-area"
|
||||
@input=${this.on_edit}
|
||||
@paste=${this.paste}
|
||||
.value=${this.blob ?? ''}
|
||||
></textarea>
|
||||
<div style="flex: 1 1">
|
||||
<div
|
||||
?hidden=${!this.is_editing}
|
||||
style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em"
|
||||
>
|
||||
<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>
|
||||
.value=${this.blob ?? ''}></textarea>
|
||||
<div style="flex: 1 1; margin-top: 16px">
|
||||
<div ?hidden=${!this.is_editing} class="box">
|
||||
Summary
|
||||
<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)))}
|
||||
</div>
|
||||
${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;
|
||||
}
|
||||
|
||||
ssb.addEventListener('message', function (id) {
|
||||
core.register('onMessage', function (id) {
|
||||
let resolve = g_new_message_resolve;
|
||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||
g_new_message_resolve = resolve;
|
||||
@ -96,7 +96,7 @@ export async function collection(
|
||||
let rows = [];
|
||||
await ssb.sqlAsync(
|
||||
`
|
||||
SELECT messages.id, author, content, timestamp
|
||||
SELECT messages.id, author, json(content) AS content, timestamp
|
||||
FROM messages
|
||||
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||
WHERE
|
||||
|
44
core/app.js
44
core/app.js
@ -1,4 +1,3 @@
|
||||
import * as auth from './auth.js';
|
||||
import * as core from './core.js';
|
||||
|
||||
let g_next_id = 1;
|
||||
@ -84,11 +83,10 @@ App.prototype.send = function (message) {
|
||||
* @param {*} response
|
||||
* @param {*} client
|
||||
*/
|
||||
function socket(request, response, client) {
|
||||
async function socket(request, response, client) {
|
||||
let process;
|
||||
let options = {};
|
||||
let credentials = auth.query(request.headers);
|
||||
let refresh = auth.makeRefresh(credentials);
|
||||
let credentials = await httpd.auth_query(request.headers);
|
||||
|
||||
response.onClose = async function () {
|
||||
if (process && process.task) {
|
||||
@ -143,12 +141,21 @@ function socket(request, response, client) {
|
||||
}
|
||||
}
|
||||
response.send(
|
||||
JSON.stringify({
|
||||
action: 'session',
|
||||
credentials: credentials,
|
||||
parentApp: parentApp,
|
||||
id: blobId,
|
||||
}),
|
||||
JSON.stringify(
|
||||
Object.assign(
|
||||
{
|
||||
action: 'session',
|
||||
credentials: credentials,
|
||||
parentApp: parentApp,
|
||||
id: blobId,
|
||||
},
|
||||
await ssb.getIdentityInfo(
|
||||
credentials?.session?.name,
|
||||
packageOwner,
|
||||
packageName
|
||||
)
|
||||
)
|
||||
),
|
||||
0x1
|
||||
);
|
||||
|
||||
@ -204,14 +211,14 @@ function socket(request, response, client) {
|
||||
if (process && process.timeout > 0) {
|
||||
setTimeout(ping, process.timeout);
|
||||
}
|
||||
} else if (message.action == 'enableStats') {
|
||||
if (process) {
|
||||
core.enableStats(process, message.enabled);
|
||||
}
|
||||
} else if (message.action == 'resetPermission') {
|
||||
if (process) {
|
||||
process.resetPermission(message.permission);
|
||||
}
|
||||
} else if (message.action == 'setActiveIdentity') {
|
||||
process.setActiveIdentity(message.identity);
|
||||
} else if (message.action == 'createIdentity') {
|
||||
await process.createIdentity();
|
||||
} else if (message.message == 'tfrpc') {
|
||||
if (message.id && g_calls[message.id]) {
|
||||
if (message.error !== undefined) {
|
||||
@ -241,14 +248,7 @@ function socket(request, response, client) {
|
||||
}
|
||||
};
|
||||
|
||||
response.upgrade(
|
||||
100,
|
||||
refresh
|
||||
? {
|
||||
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
||||
}
|
||||
: {}
|
||||
);
|
||||
response.upgrade(100, {});
|
||||
}
|
||||
|
||||
export {socket, App};
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<title>Tilde Friends Sign-in</title>
|
||||
<link type="text/css" rel="stylesheet" href="/static/style.css" />
|
||||
<link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
|
||||
<link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
@ -19,8 +19,11 @@
|
||||
Object.assign(app, g_data);
|
||||
|
||||
class TfAuthElement extends LitElement {
|
||||
static get_properties() {
|
||||
static get properties() {
|
||||
return {
|
||||
code_of_conduct: {type: String},
|
||||
error: {type: String},
|
||||
have_administrator: {type: Boolean},
|
||||
name: {type: String},
|
||||
tab: {type: String},
|
||||
};
|
||||
@ -31,11 +34,6 @@
|
||||
this.tab = 'login';
|
||||
}
|
||||
|
||||
tab_changed(name) {
|
||||
this.tab = name;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
@ -83,16 +81,16 @@
|
||||
<h1 ?hidden=${this.name === undefined}>Welcome, ${this.name}.</h1>
|
||||
|
||||
<div style="display: flex; flex-direction: row; width: 100%">
|
||||
<input type="radio" name="tab" id="login" value="Login" ?checked=${this.tab == 'login'} @change=${() => self.tab_changed('login')}></input>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
420
core/auth.js
420
core/auth.js
@ -1,420 +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};
|
274
core/client.js
274
core/client.js
@ -10,6 +10,7 @@ let gEditor;
|
||||
let gOriginalInput;
|
||||
|
||||
let kErrorColor = '#dc322f';
|
||||
let kDisconnectColor = '#f00';
|
||||
let kStatusColor = '#fff';
|
||||
|
||||
// Functions that server-side app code can call through the app object.
|
||||
@ -56,6 +57,9 @@ class TfNavigationElement extends LitElement {
|
||||
spark_lines: {type: Object},
|
||||
version: {type: Object},
|
||||
show_version: {type: Boolean},
|
||||
identity: {type: String},
|
||||
identities: {type: Array},
|
||||
names: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
@ -65,6 +69,8 @@ class TfNavigationElement extends LitElement {
|
||||
this.show_permissions = false;
|
||||
this.status = {};
|
||||
this.spark_lines = {};
|
||||
this.identities = [];
|
||||
this.names = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,10 +103,10 @@ class TfNavigationElement extends LitElement {
|
||||
get_spark_line(key, options) {
|
||||
if (!this.spark_lines[key]) {
|
||||
let spark_line = document.createElement('tf-sparkline');
|
||||
spark_line.style.display = 'flex';
|
||||
spark_line.style.flexDirection = 'row';
|
||||
spark_line.style.flex = '0 50 5em';
|
||||
spark_line.title = key;
|
||||
spark_line.classList.add('w3-bar-item');
|
||||
spark_line.classList.add('w3-hide-small');
|
||||
spark_line.style.paddingRight = '0';
|
||||
if (options) {
|
||||
if (options.max) {
|
||||
spark_line.max = options.max;
|
||||
@ -112,17 +118,122 @@ class TfNavigationElement extends LitElement {
|
||||
return this.spark_lines[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @returns
|
||||
*/
|
||||
render_login() {
|
||||
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;
|
||||
}
|
||||
|
||||
logout() {
|
||||
window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`;
|
||||
}
|
||||
|
||||
render_identity() {
|
||||
let self = this;
|
||||
|
||||
if (this?.credentials?.session?.name) {
|
||||
return html`<a id="login" href="/login/logout?return=${url() + hash()}"
|
||||
>logout ${this.credentials.session.name}</a
|
||||
>`;
|
||||
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%"
|
||||
id="identity"
|
||||
@click=${self.toggle_id_dropdown}
|
||||
>
|
||||
${self.names[this.identity]}▾
|
||||
</button>
|
||||
<div
|
||||
id="id_dropdown"
|
||||
class="w3-dropdown-content w3-bar-block w3-card-4"
|
||||
style="max-width: 100%; right: 0"
|
||||
>
|
||||
<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>
|
||||
`
|
||||
)}
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-border"
|
||||
id="logout"
|
||||
@click=${self.logout}
|
||||
>
|
||||
Logout ${this.credentials.session.name}
|
||||
</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
|
||||
class="w3-bar-item w3-button w3-right w3-cyan"
|
||||
id="logout"
|
||||
@click=${self.logout}
|
||||
>
|
||||
Logout ${this.credentials.session.name}
|
||||
</button>
|
||||
<button
|
||||
id="create_identity"
|
||||
@click=${this.create_identity}
|
||||
class="w3-button w3-mobile w3-red w3-right"
|
||||
>
|
||||
Create an Identity
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-right w3-cyan"
|
||||
id="logout"
|
||||
@click=${self.logout}
|
||||
>
|
||||
Logout ${this.credentials.session.name}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
return html`<a id="login" href="/login?return=${url() + hash()}"
|
||||
return html`<a
|
||||
class="w3-bar-item w3-cyan w3-right"
|
||||
id="login"
|
||||
href="/login?return=${url() + hash()}"
|
||||
>login</a
|
||||
>`;
|
||||
}
|
||||
@ -145,11 +256,17 @@ class TfNavigationElement extends LitElement {
|
||||
<div>This app has the following permissions:</div>
|
||||
${Object.keys(this.permissions).map(
|
||||
(key) => html`
|
||||
<div>
|
||||
<span>${key}</span>: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
|
||||
<button @click=${() => this.reset_permission(key)} class='w3-button w3-red">Reset</button>
|
||||
</div>
|
||||
`
|
||||
<div>
|
||||
<span>${key}</span>:
|
||||
${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
|
||||
<button
|
||||
@click=${() => this.reset_permission(key)}
|
||||
class="w3-button w3-red"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<button
|
||||
@click=${() => (this.show_permissions = false)}
|
||||
@ -163,6 +280,10 @@ class TfNavigationElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
clear_error() {
|
||||
this.status = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @returns
|
||||
@ -170,6 +291,7 @@ class TfNavigationElement extends LitElement {
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||
<style>
|
||||
${k_global_style} .tooltip {
|
||||
position: absolute;
|
||||
@ -185,17 +307,17 @@ class TfNavigationElement extends LitElement {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center"
|
||||
>
|
||||
<div class="w3-black w3-bar">
|
||||
<span
|
||||
class="w3-bar-item"
|
||||
style="cursor: pointer"
|
||||
@click=${() => (this.show_version = !this.show_version)}
|
||||
>😎</span
|
||||
>
|
||||
<span
|
||||
?hidden=${!this.show_version}
|
||||
style="flex: 0 0; white-space: nowrap"
|
||||
class="w3-bar-item"
|
||||
style=${'white-space: nowrap' +
|
||||
(this.show_version ? '' : '; display: none')}
|
||||
title=${this.version?.name +
|
||||
' ' +
|
||||
Object.entries(this.version || {})
|
||||
@ -204,6 +326,7 @@ class TfNavigationElement extends LitElement {
|
||||
>${this.version?.number}</span
|
||||
>
|
||||
<a
|
||||
class="w3-bar-item"
|
||||
accesskey="h"
|
||||
@mouseover=${set_access_key_title}
|
||||
data-tip="Open home app."
|
||||
@ -212,6 +335,7 @@ class TfNavigationElement extends LitElement {
|
||||
>TF</a
|
||||
>
|
||||
<a
|
||||
class="w3-bar-item"
|
||||
accesskey="a"
|
||||
@mouseover=${set_access_key_title}
|
||||
data-tip="Open apps list."
|
||||
@ -219,6 +343,7 @@ class TfNavigationElement extends LitElement {
|
||||
>apps</a
|
||||
>
|
||||
<a
|
||||
class="w3-bar-item"
|
||||
accesskey="e"
|
||||
@mouseover=${set_access_key_title}
|
||||
data-tip="Toggle the app editor."
|
||||
@ -227,6 +352,7 @@ class TfNavigationElement extends LitElement {
|
||||
>edit</a
|
||||
>
|
||||
<a
|
||||
class="w3-bar-item"
|
||||
accesskey="p"
|
||||
@mouseover=${set_access_key_title}
|
||||
data-tip="View and change permissions."
|
||||
@ -234,27 +360,34 @@ class TfNavigationElement extends LitElement {
|
||||
@click=${() => (self.show_permissions = !self.show_permissions)}
|
||||
>🎛️</a
|
||||
>
|
||||
<span
|
||||
style="display: inline-block; vertical-align: top; white-space: pre; color: ${this
|
||||
.status.color ?? kErrorColor}"
|
||||
>${this.status.message}</span
|
||||
>
|
||||
<span id="requests"></span>
|
||||
${this.render_permissions()}
|
||||
<span
|
||||
style="flex: 1 1; display: flex; flex-direction: row; white-space: nowrap; margin: 0; padding: 0"
|
||||
>${Object.keys(this.spark_lines)
|
||||
.sort()
|
||||
.map((x) => this.spark_lines[x])
|
||||
.map((x) => [
|
||||
html`<span style="font-size: xx-small">${x.dataset.emoji}</span>`,
|
||||
x,
|
||||
])}</span
|
||||
>
|
||||
<span style="flex: 0 0; white-space: nowrap"
|
||||
>${this.render_login()}</span
|
||||
>
|
||||
${this.status?.message && !this.status.is_error
|
||||
? html`
|
||||
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||
<div
|
||||
class="w3-bar-item"
|
||||
style="color: ${this.status.color ?? kStatusColor}"
|
||||
>
|
||||
${this.status.message}
|
||||
</div>
|
||||
`
|
||||
: undefined}
|
||||
${Object.keys(this.spark_lines)
|
||||
.sort()
|
||||
.map((x) => this.spark_lines[x])}
|
||||
${this.render_identity()}
|
||||
</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}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -578,13 +711,13 @@ class TfSparkLineElement extends LitElement {
|
||||
) / 10.0;
|
||||
return html`
|
||||
<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"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
${this.lines.map((x) => this.render_line(x))}
|
||||
<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">
|
||||
${max}
|
||||
${this.dataset.emoji}${max}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
@ -1000,9 +1133,9 @@ function api_postMessage(message) {
|
||||
function api_error(error) {
|
||||
if (error) {
|
||||
if (typeof error == 'string') {
|
||||
setStatusMessage('⚠️ ' + error, '#f00');
|
||||
setStatusMessage('⚠️ ' + error, kErrorColor);
|
||||
} else {
|
||||
setStatusMessage('⚠️ ' + error.message + '\n' + error.stack, '#f00');
|
||||
setStatusMessage('⚠️ ' + error.message + '\n' + error.stack, kErrorColor);
|
||||
}
|
||||
}
|
||||
console.log('error', error);
|
||||
@ -1119,11 +1252,19 @@ function api_setHash(hash) {
|
||||
function _receive_websocket_message(message) {
|
||||
if (message && message.action == 'session') {
|
||||
setStatusMessage('🟢 Executing...', kStatusColor);
|
||||
document.getElementsByTagName('tf-navigation')[0].credentials =
|
||||
message.credentials;
|
||||
let navigation = document.getElementsByTagName('tf-navigation')[0];
|
||||
navigation.credentials = message.credentials;
|
||||
navigation.identities = message.identities;
|
||||
navigation.identity = message.identity;
|
||||
navigation.names = message.names;
|
||||
} else if (message && message.action == 'permissions') {
|
||||
document.getElementsByTagName('tf-navigation')[0].permissions =
|
||||
message.permissions ?? {};
|
||||
let navigation = document.getElementsByTagName('tf-navigation')[0];
|
||||
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') {
|
||||
setStatusMessage(null);
|
||||
if (window.location.hash) {
|
||||
@ -1133,7 +1274,6 @@ function _receive_websocket_message(message) {
|
||||
document.getElementById('viewPane').style.display = message.edit_only
|
||||
? 'none'
|
||||
: 'flex';
|
||||
send({action: 'enableStats', enabled: true});
|
||||
} else if (message && message.action == 'ping') {
|
||||
send({action: 'pong'});
|
||||
} else if (message && message.action == 'stats') {
|
||||
@ -1211,6 +1351,7 @@ function setStatusMessage(message, color) {
|
||||
document.getElementsByTagName('tf-navigation')[0].status = {
|
||||
message: message,
|
||||
color: color,
|
||||
is_error: color == kErrorColor,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1420,7 +1561,7 @@ function connectSocket(path) {
|
||||
};
|
||||
setStatusMessage(
|
||||
'🔴 Closed: ' + (k_codes[event.code] || event.code),
|
||||
kErrorColor
|
||||
kDisconnectColor
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -1662,6 +1803,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
|
||||
window.addEventListener('load', function () {
|
||||
window.addEventListener('hashchange', hashChange);
|
||||
@ -1687,6 +1853,9 @@ window.addEventListener('load', function () {
|
||||
document
|
||||
.getElementById('pretty')
|
||||
.addEventListener('click', () => sourcePretty());
|
||||
document
|
||||
.getElementById('whitespace')
|
||||
.addEventListener('click', () => toggleVisibleWhitespace());
|
||||
document
|
||||
.getElementById('trace_button')
|
||||
.addEventListener('click', function (event) {
|
||||
@ -1700,4 +1869,7 @@ window.addEventListener('load', function () {
|
||||
} else {
|
||||
closeEditor();
|
||||
}
|
||||
if (window.localStorage.getItem('visible_whitespace') == '1') {
|
||||
toggleVisibleWhitespace();
|
||||
}
|
||||
});
|
||||
|
504
core/core.js
504
core/core.js
@ -1,5 +1,4 @@
|
||||
import * as app from './app.js';
|
||||
import * as auth from './auth.js';
|
||||
import * as form from './form.js';
|
||||
import * as http from './http.js';
|
||||
|
||||
@ -9,120 +8,6 @@ let gStatsTimer = false;
|
||||
const k_content_security_policy =
|
||||
'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',
|
||||
};
|
||||
|
||||
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 = [
|
||||
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
||||
];
|
||||
|
||||
const k_global_settings = {
|
||||
index: {
|
||||
type: 'string',
|
||||
@ -138,13 +23,18 @@ const k_global_settings = {
|
||||
room: {
|
||||
type: 'boolean',
|
||||
default_value: true,
|
||||
description: 'Whether this instance should behave as a room.',
|
||||
description: 'Enable peers to tunnel through this instance as a room.',
|
||||
},
|
||||
room_name: {
|
||||
type: 'string',
|
||||
default_value: 'tilde friends tunnel',
|
||||
description: 'Name of the room.',
|
||||
},
|
||||
replicator: {
|
||||
type: 'boolean',
|
||||
default_value: true,
|
||||
description: 'Enable message and blob replication.',
|
||||
},
|
||||
code_of_conduct: {
|
||||
type: 'textarea',
|
||||
default_value: undefined,
|
||||
@ -179,6 +69,21 @@ const k_global_settings = {
|
||||
: undefined,
|
||||
description: 'Blobs older than this will be automatically deleted.',
|
||||
},
|
||||
seeds_host: {
|
||||
type: 'string',
|
||||
default_value: 'seeds.tildefriends.net',
|
||||
description: 'Hostname for seed connections.',
|
||||
},
|
||||
peer_exchange: {
|
||||
type: 'boolean',
|
||||
default_value: false,
|
||||
description: 'Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.',
|
||||
},
|
||||
account_registration: {
|
||||
type: 'boolean',
|
||||
default_value: true,
|
||||
description: 'Allow registration of new accounts.',
|
||||
},
|
||||
};
|
||||
|
||||
let gGlobalSettings = {
|
||||
@ -245,6 +150,7 @@ function broadcastEvent(eventName, argv) {
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} message
|
||||
@ -266,6 +172,34 @@ function broadcast(message) {
|
||||
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
|
||||
* @param {*} caller
|
||||
@ -288,7 +222,7 @@ function getUser(caller, process) {
|
||||
* @param {*} process
|
||||
* @returns
|
||||
*/
|
||||
function getApps(user, process) {
|
||||
async function getApps(user, process) {
|
||||
if (
|
||||
process.credentials &&
|
||||
process.credentials.session &&
|
||||
@ -303,10 +237,12 @@ function getApps(user, process) {
|
||||
if (user) {
|
||||
let db = new Database(user);
|
||||
try {
|
||||
let names = JSON.parse(db.get('apps'));
|
||||
return Object.fromEntries(
|
||||
names.map((name) => [name, db.get('path:' + name)])
|
||||
);
|
||||
let names = JSON.parse(await db.get('apps'));
|
||||
let result = {};
|
||||
for (let name of names) {
|
||||
result[name] = await db.get('path:' + name);
|
||||
}
|
||||
return result;
|
||||
} catch {}
|
||||
}
|
||||
return {};
|
||||
@ -361,6 +297,8 @@ async function getProcessBlob(blobId, key, options) {
|
||||
process.key = key;
|
||||
process.credentials = options.credentials || {};
|
||||
process.task = new Task();
|
||||
process.packageOwner = options.packageOwner;
|
||||
process.packageName = options.packageName;
|
||||
process.eventHandlers = {};
|
||||
if (!options?.script || options?.script === 'app.js') {
|
||||
process.app = new app.App();
|
||||
@ -368,7 +306,6 @@ async function getProcessBlob(blobId, key, options) {
|
||||
process.lastActive = Date.now();
|
||||
process.lastPing = null;
|
||||
process.timeout = options.timeout;
|
||||
process.stats = false;
|
||||
process.ready = new Promise(function (resolve, reject) {
|
||||
resolveReady = resolve;
|
||||
rejectReady = reject;
|
||||
@ -400,9 +337,9 @@ async function getProcessBlob(blobId, key, options) {
|
||||
}
|
||||
},
|
||||
user: getUser(process, process),
|
||||
users: function () {
|
||||
users: async function () {
|
||||
try {
|
||||
return JSON.parse(new Database('auth').get('users'));
|
||||
return JSON.parse(await new Database('auth').get('users'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@ -509,6 +446,67 @@ async function getProcessBlob(blobId, key, options) {
|
||||
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 = await 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) {
|
||||
imports.core.globalSettingsDescriptions = function () {
|
||||
let settings = Object.assign({}, k_global_settings);
|
||||
@ -522,31 +520,27 @@ async function getProcessBlob(blobId, key, options) {
|
||||
imports.core.globalSettingsGet = function (key) {
|
||||
return gGlobalSettings[key];
|
||||
};
|
||||
imports.core.globalSettingsSet = function (key, value) {
|
||||
imports.core.globalSettingsSet = async function (key, value) {
|
||||
print('Setting', key, value);
|
||||
await loadSettings();
|
||||
gGlobalSettings[key] = value;
|
||||
setGlobalSettings(gGlobalSettings);
|
||||
print('Done.');
|
||||
};
|
||||
imports.core.deleteUser = function (user) {
|
||||
return Promise.resolve(
|
||||
imports.core.permissionTest('delete_user')
|
||||
).then(function () {
|
||||
let db = new Database('auth');
|
||||
|
||||
db.remove('user:' + user);
|
||||
|
||||
let users = new Set();
|
||||
let users_original = db.get('users');
|
||||
try {
|
||||
users = new Set(JSON.parse(users_original));
|
||||
} catch {}
|
||||
users.delete(user);
|
||||
users = JSON.stringify([...users].sort());
|
||||
if (users !== users_original) {
|
||||
db.set('users', users);
|
||||
}
|
||||
});
|
||||
imports.core.deleteUser = async function (user) {
|
||||
await imports.core.permissionTest('delete_user');
|
||||
let db = new Database('auth');
|
||||
db.remove('user:' + user);
|
||||
let users = new Set();
|
||||
let users_original = await db.get('users');
|
||||
try {
|
||||
users = new Set(JSON.parse(users_original));
|
||||
} catch {}
|
||||
users.delete(user);
|
||||
users = JSON.stringify([...users].sort());
|
||||
if (users !== users_original) {
|
||||
await db.set('users', users);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (options.api) {
|
||||
@ -578,15 +572,8 @@ async function getProcessBlob(blobId, key, options) {
|
||||
imports.ssb = Object.fromEntries(
|
||||
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
|
||||
);
|
||||
imports.ssb.createIdentity = function () {
|
||||
if (
|
||||
process.credentials &&
|
||||
process.credentials.session &&
|
||||
process.credentials.session.name
|
||||
) {
|
||||
return ssb.createIdentity(process.credentials.session.name);
|
||||
}
|
||||
};
|
||||
imports.ssb.port = tildefriends.ssb_port;
|
||||
imports.ssb.createIdentity = () => process.createIdentity();
|
||||
imports.ssb.addIdentity = function (id) {
|
||||
if (
|
||||
process.credentials &&
|
||||
@ -613,6 +600,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 () {
|
||||
if (options.packageOwner) {
|
||||
return ssb.getIdentities(options.packageOwner);
|
||||
@ -697,6 +691,9 @@ async function getProcessBlob(blobId, key, options) {
|
||||
);
|
||||
}
|
||||
};
|
||||
imports.ssb.addEventListener = undefined;
|
||||
imports.ssb.removeEventListener = undefined;
|
||||
imports.ssb.getIdentityInfo = undefined;
|
||||
imports.fetch = function (url, options) {
|
||||
return http.fetch(url, options, gGlobalSettings.fetch_hosts);
|
||||
};
|
||||
@ -768,7 +765,7 @@ async function getProcessBlob(blobId, key, options) {
|
||||
};
|
||||
process.task.setImports(imports);
|
||||
process.task.activate();
|
||||
let source = await getBlobOrContent(blobId);
|
||||
let source = await ssb.blobGet(blobId);
|
||||
let appSourceName = blobId;
|
||||
let appSource = utf8Decode(source);
|
||||
try {
|
||||
@ -776,7 +773,7 @@ async function getProcessBlob(blobId, key, options) {
|
||||
if (appObject.type == 'tildefriends-app') {
|
||||
appSourceName = options?.script ?? 'app.js';
|
||||
let id = appObject.files[appSourceName];
|
||||
let blob = await getBlobOrContent(id);
|
||||
let blob = await ssb.blobGet(id);
|
||||
appSource = utf8Decode(blob);
|
||||
await process.task.loadFile([
|
||||
'/tfrpc.js',
|
||||
@ -786,7 +783,7 @@ async function getProcessBlob(blobId, key, options) {
|
||||
Object.keys(appObject.files).map(async function (f) {
|
||||
await process.task.loadFile([
|
||||
f,
|
||||
await getBlobOrContent(appObject.files[f]),
|
||||
await ssb.blobGet(appObject.files[f]),
|
||||
]);
|
||||
})
|
||||
);
|
||||
@ -801,6 +798,10 @@ async function getProcessBlob(blobId, key, options) {
|
||||
}
|
||||
await process.task.execute({name: appSourceName, source: appSource});
|
||||
resolveReady(process);
|
||||
if (!gStatsTimer) {
|
||||
gStatsTimer = true;
|
||||
sendStats();
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.app) {
|
||||
if (process?.task?.onError) {
|
||||
@ -822,56 +823,15 @@ async function getProcessBlob(blobId, key, options) {
|
||||
* @param {*} settings
|
||||
* @returns
|
||||
*/
|
||||
function setGlobalSettings(settings) {
|
||||
async function setGlobalSettings(settings) {
|
||||
gGlobalSettings = settings;
|
||||
try {
|
||||
return new Database('core').set('settings', JSON.stringify(settings));
|
||||
return await new Database('core').set('settings', JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
print('Error storing settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} data
|
||||
* @param {*} bytes
|
||||
* @returns
|
||||
*/
|
||||
function startsWithBytes(data, bytes) {
|
||||
if (data.byteLength >= bytes.length) {
|
||||
let dataBytes = new Uint8Array(data.slice(0, bytes.length));
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {*} response
|
||||
@ -887,7 +847,9 @@ function sendData(response, data, type, headers, status_code) {
|
||||
Object.assign(
|
||||
{
|
||||
'Content-Type':
|
||||
type || guessTypeFromMagicBytes(data) || 'application/binary',
|
||||
type ||
|
||||
httpd.mime_type_from_magic_bytes(data) ||
|
||||
'application/binary',
|
||||
'Content-Length': data.byteLength,
|
||||
},
|
||||
headers || {}
|
||||
@ -909,21 +871,6 @@ function sendData(response, data, type, headers, status_code) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} id
|
||||
* @returns
|
||||
*/
|
||||
async function getBlobOrContent(id) {
|
||||
if (!id) {
|
||||
return;
|
||||
} else if (id.startsWith('&')) {
|
||||
return ssb.blobGet(id);
|
||||
} else if (id.startsWith('%')) {
|
||||
return ssb.messageContentGet(id);
|
||||
}
|
||||
}
|
||||
|
||||
let g_handler_index = 0;
|
||||
|
||||
/**
|
||||
@ -966,7 +913,7 @@ async function useAppHandler(
|
||||
},
|
||||
respond: do_resolve,
|
||||
},
|
||||
credentials: auth.query(headers),
|
||||
credentials: await httpd.auth_query(headers),
|
||||
packageOwner: packageOwner,
|
||||
packageName: packageName,
|
||||
}
|
||||
@ -991,34 +938,6 @@ async function useAppHandler(
|
||||
* @returns
|
||||
*/
|
||||
async function blobHandler(request, response, blobId, uri) {
|
||||
// TODO(tasiaiso): break this down ?
|
||||
for (let i in k_static_files) {
|
||||
if (uri === k_static_files[i].uri && k_static_files[i].path) {
|
||||
let stat = await File.stat('core/' + k_static_files[i].path);
|
||||
let id = `${stat.mtime}_${stat.size}`;
|
||||
|
||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||
response.writeHead(304, {'Content-Length': '0'});
|
||||
response.end();
|
||||
} else {
|
||||
let data = await File.readFile('core/' + k_static_files[i].path);
|
||||
response.writeHead(
|
||||
200,
|
||||
Object.assign(
|
||||
{
|
||||
'Content-Type': k_static_files[i].type,
|
||||
'Content-Length': data.byteLength,
|
||||
etag: '"' + id + '"',
|
||||
},
|
||||
k_static_files[i].headers || {}
|
||||
)
|
||||
);
|
||||
response.end(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!uri) {
|
||||
response.writeHead(303, {
|
||||
Location:
|
||||
@ -1051,7 +970,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
data = await getBlobOrContent(id);
|
||||
data = await ssb.blobGet(id);
|
||||
if (match[3]) {
|
||||
let appObject = JSON.parse(data);
|
||||
data = appObject.files[match[3]];
|
||||
@ -1083,7 +1002,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
data = await getBlobOrContent(blobId);
|
||||
data = await ssb.blobGet(blobId);
|
||||
sendData(
|
||||
response,
|
||||
data,
|
||||
@ -1097,7 +1016,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||
let user = match[1];
|
||||
let appName = match[2];
|
||||
let credentials = auth.query(request.headers);
|
||||
let credentials = await httpd.auth_query(request.headers);
|
||||
if (
|
||||
credentials &&
|
||||
credentials.session &&
|
||||
@ -1107,7 +1026,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
let database = new Database(user);
|
||||
|
||||
let app_object = JSON.parse(utf8Decode(request.body));
|
||||
let previous_id = database.get('path:' + appName);
|
||||
let previous_id = await database.get('path:' + appName);
|
||||
if (previous_id) {
|
||||
try {
|
||||
let previous_object = JSON.parse(
|
||||
@ -1128,7 +1047,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
let newBlobId = await ssb.blobStore(JSON.stringify(app_object));
|
||||
|
||||
let apps = new Set();
|
||||
let apps_original = database.get('apps');
|
||||
let apps_original = await database.get('apps');
|
||||
try {
|
||||
apps = new Set(JSON.parse(apps_original));
|
||||
} catch {}
|
||||
@ -1137,9 +1056,9 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
}
|
||||
apps = JSON.stringify([...apps].sort());
|
||||
if (apps != apps_original) {
|
||||
database.set('apps', apps);
|
||||
await database.set('apps', apps);
|
||||
}
|
||||
database.set('path:' + appName, newBlobId);
|
||||
await database.set('path:' + appName, newBlobId);
|
||||
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
|
||||
response.end('/' + newBlobId);
|
||||
} else {
|
||||
@ -1160,7 +1079,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
|
||||
let user = match[1];
|
||||
let appName = match[2];
|
||||
let credentials = auth.query(request.headers);
|
||||
let credentials = await httpd.auth_query(request.headers);
|
||||
if (
|
||||
credentials &&
|
||||
credentials.session &&
|
||||
@ -1170,10 +1089,10 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
let database = new Database(user);
|
||||
let apps = new Set();
|
||||
try {
|
||||
apps = new Set(JSON.parse(database.get('apps')));
|
||||
apps = new Set(JSON.parse(await database.get('apps')));
|
||||
} catch {}
|
||||
if (apps.delete(appName)) {
|
||||
database.set('apps', JSON.stringify([...apps].sort()));
|
||||
await database.set('apps', JSON.stringify([...apps].sort()));
|
||||
}
|
||||
database.remove('path:' + appName);
|
||||
} else {
|
||||
@ -1199,9 +1118,9 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
app_id = await db.get('path:' + match[2]);
|
||||
}
|
||||
|
||||
let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id)));
|
||||
id = app_object.files[uri.substring(1)];
|
||||
if (!id && app_object.files['handler.js']) {
|
||||
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
|
||||
id = app_object?.files[uri.substring(1)];
|
||||
if (!id && app_object?.files['handler.js']) {
|
||||
let answer;
|
||||
try {
|
||||
answer = await useAppHandler(
|
||||
@ -1255,8 +1174,10 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy': k_content_security_policy,
|
||||
};
|
||||
data = await getBlobOrContent(id);
|
||||
let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
|
||||
data = await ssb.blobGet(id);
|
||||
let type =
|
||||
httpd.mime_type_from_extension(uri) ||
|
||||
httpd.mime_type_from_magic_bytes(data);
|
||||
sendData(response, data, type, headers);
|
||||
}
|
||||
} else {
|
||||
@ -1265,6 +1186,10 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
}
|
||||
}
|
||||
|
||||
ssb.addEventListener('message', function () {
|
||||
broadcastEvent('onMessage', [...arguments]);
|
||||
});
|
||||
|
||||
ssb.addEventListener('broadcasts', function () {
|
||||
broadcastEvent('onBroadcastsChanged', []);
|
||||
});
|
||||
@ -1279,7 +1204,7 @@ ssb.addEventListener('connections', function () {
|
||||
async function loadSettings() {
|
||||
let data = {};
|
||||
try {
|
||||
let settings = new Database('core').get('settings');
|
||||
let settings = await new Database('core').get('settings');
|
||||
if (settings) {
|
||||
data = JSON.parse(settings);
|
||||
}
|
||||
@ -1299,7 +1224,7 @@ async function loadSettings() {
|
||||
*/
|
||||
function sendStats() {
|
||||
let apps = Object.values(gProcesses)
|
||||
.filter((process) => process.app && process.stats)
|
||||
.filter((process) => process.app)
|
||||
.map((process) => process.app);
|
||||
if (apps.length) {
|
||||
let stats = getStats();
|
||||
@ -1312,19 +1237,6 @@ function sendStats() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
* @param {*} process
|
||||
* @param {*} enabled
|
||||
*/
|
||||
function enableStats(process, enabled) {
|
||||
process.stats = enabled;
|
||||
if (!gStatsTimer) {
|
||||
gStatsTimer = true;
|
||||
sendStats();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOC
|
||||
*/
|
||||
@ -1333,39 +1245,10 @@ loadSettings()
|
||||
if (tildefriends.https_port && 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('', function default_http_handler(request, response) {
|
||||
let match;
|
||||
if (request.uri === '/' || 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))) {
|
||||
if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) {
|
||||
return blobHandler(request, response, match[1], match[2]);
|
||||
} else if (
|
||||
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
|
||||
@ -1405,8 +1288,15 @@ loadSettings()
|
||||
async function start_tls() {
|
||||
const kCertificatePath = 'data/httpd/certificate.pem';
|
||||
const kPrivateKeyPath = 'data/httpd/privatekey.pem';
|
||||
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
||||
let certificate = utf8Decode(await File.readFile(kCertificatePath));
|
||||
let privateKey;
|
||||
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();
|
||||
context.setPrivateKey(privateKey);
|
||||
context.setCertificate(certificate);
|
||||
@ -1462,8 +1352,6 @@ function storePermission(user, packageOwner, packageName, permission, allow) {
|
||||
|
||||
export {
|
||||
gGlobalSettings as globalSettings,
|
||||
setGlobalSettings,
|
||||
enableStats,
|
||||
invoke,
|
||||
getSessionProcessBlob,
|
||||
};
|
||||
|
BIN
core/favicon.png
BIN
core/favicon.png
Binary file not shown.
Before Width: | Height: | Size: 320 B |
161
core/index.html
161
core/index.html
@ -1,46 +1,167 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tilde Friends</title>
|
||||
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
||||
<link type="text/css" rel="stylesheet" href="/static/w3.css">
|
||||
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link type="text/css" rel="stylesheet" href="/static/style.css" />
|
||||
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||
<link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script>
|
||||
function set_access_key_title(event) {
|
||||
if (!event.srcElement.title) {
|
||||
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || ('⌨️' + event.srcElement.accessKey)).toUpperCase()}]`;
|
||||
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || '⌨️' + event.srcElement.accessKey).toUpperCase()}]`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body style="display: flex; flex-flow: column; width: 100vw; height: 100vh; position: absolute; max-width: 100%; max-height: 100%">
|
||||
<body
|
||||
style="
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
"
|
||||
>
|
||||
<noscript>
|
||||
<div class="w3-container">
|
||||
<div class="w3-panel w3-red w3-padding w3-card-4">
|
||||
<h1>TildeFriends requires JavaScript.</h1>
|
||||
<p>
|
||||
It looks like JavaScript is disabled or unsupported. This isn't
|
||||
going to work.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<tf-navigation></tf-navigation>
|
||||
<div id="content" class="hbox" style="flex: 1 0; overflow: auto">
|
||||
<div id="editPane" class="vbox" style="flex: 0 1 100%; display: none; overflow: auto">
|
||||
<div
|
||||
id="editPane"
|
||||
class="vbox"
|
||||
style="flex: 0 1 100%; display: none; overflow: auto"
|
||||
>
|
||||
<div class="navigation w3-bar" style="display: flex">
|
||||
<button class="w3-bar-item w3-button w3-blue" id="closeEditor" name="closeEditor" accesskey="c" onmouseover="set_access_key_title(event)" data-tip="Close the editor">Close</button>
|
||||
<button class="w3-bar-item w3-button w3-blue" id="save" name="save" accesskey="s" onmouseover="set_access_key_title(event)" data-tip="Save the app under the given path">Save</button>
|
||||
<button class="w3-bar-item w3-button w3-blue" id="icon" name="icon" accesskey="i" onmouseover="set_access_key_title(event)" data-tip="Set an icon/emoji for the app">📦</button>
|
||||
<button class="w3-bar-item w3-button w3-blue" id="export" name="export" accesskey="e" onmouseover="set_access_key_title(event)" data-tip="Export app to .zip file">Export</button>
|
||||
<button class="w3-bar-item w3-button w3-blue" id="import" name="import" accesskey="i" onmouseover="set_access_key_title(event)" data-tip="Import app from .zip file">Import</button>
|
||||
<button class="w3-bar-item w3-button w3-blue" id="pretty" name="pretty" accesskey="p" onmouseover="set_access_key_title(event)" data-tip="Clean up source formatting">🧼</button>
|
||||
<input class="w3-bar-item w3-input w3-border w3-blue" type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
|
||||
<button class="w3-bar-item w3-button w3-blue" id="delete" name="delete" accesskey="d" onmouseover="set_access_key_title(event)" data-tip="Delete the app">Delete</button>
|
||||
<button class="w3-bar-item w3-button w3-blue" id="trace_button" accesskey="t" onmouseover="set_access_key_title(event)" data-tip="Open a performance trace for the server">Trace</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="closeEditor"
|
||||
name="closeEditor"
|
||||
accesskey="c"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Close the editor"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="save"
|
||||
name="save"
|
||||
accesskey="s"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Save the app under the given path"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="icon"
|
||||
name="icon"
|
||||
accesskey="j"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Set an icon/emoji for the app"
|
||||
>
|
||||
📦
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="export"
|
||||
name="export"
|
||||
accesskey="e"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Export app to .zip file"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="import"
|
||||
name="import"
|
||||
accesskey="i"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Import app from .zip file"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="pretty"
|
||||
name="pretty"
|
||||
accesskey="p"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Clean up source formatting"
|
||||
>
|
||||
🧼
|
||||
</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
|
||||
class="w3-bar-item w3-input w3-border w3-blue"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
style="flex: 1 1; min-width: 1em"
|
||||
/>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="delete"
|
||||
name="delete"
|
||||
accesskey="d"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Delete the app"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="trace_button"
|
||||
accesskey="t"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Open a performance trace for the server"
|
||||
>
|
||||
Trace
|
||||
</button>
|
||||
</div>
|
||||
<div class="hbox" style="flex: 1 1; overflow: auto">
|
||||
<div style="overflow: auto">
|
||||
<tf-files-pane style="overflow: auto"></tf-files-pane>
|
||||
</div>
|
||||
<div style="flex: 1 1; overflow: auto"><div id="editor" style="width: 100%; height: 100%"></div></div>
|
||||
<div style="flex: 1 1; overflow: auto">
|
||||
<style id="editor_style"></style>
|
||||
<div id="editor" style="width: 100%; height: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="viewPane" class="vbox" style="flex: 0 1 100%; overflow: auto">
|
||||
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
|
||||
<iframe
|
||||
id="document"
|
||||
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
|
||||
style="width: 100%; height: 100%; border: 0"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<script>window.litDisableBundleWarning = true;</script>
|
||||
<script>
|
||||
window.litDisableBundleWarning = true;
|
||||
</script>
|
||||
<script src="/static/client.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -15,22 +15,6 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #268bd2;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #6c71c4;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #859900;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #2aa198;
|
||||
}
|
||||
|
||||
#logo {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
88
core/tildefriends.svg
Normal file
88
core/tildefriends.svg
Normal file
@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="65"
|
||||
height="65"
|
||||
viewBox="0 0 61 65"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg910"
|
||||
sodipodi:docname="tildefriends.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs914" />
|
||||
<sodipodi:namedview
|
||||
id="namedview912"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="18.369231"
|
||||
inkscape:cx="32.472781"
|
||||
inkscape:cy="32.5"
|
||||
inkscape:window-width="2256"
|
||||
inkscape:window-height="1447"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg910" />
|
||||
<path
|
||||
style="fill:#0af;stroke-width:.712717;fill-opacity:1"
|
||||
d="M6 0h49a8 8 45 0 1 8 8v49a8 8 135 0 1-8 8H6a8 8 45 0 1-8-8V8a8 8 135 0 1 8-8Z"
|
||||
id="path886" />
|
||||
<g
|
||||
aria-label="~"
|
||||
id="text890"
|
||||
style="font-size:40px;line-height:1.25;fill:#000000">
|
||||
<path
|
||||
d="m 1.6762187,36.689095 v -4.003907 q 2.0703125,-2.34375 5.4296875,-2.34375 1.171875,0 2.4609375,0.351563 1.2890623,0.332031 3.6718753,1.347656 1.347656,0.566406 2.011718,0.742188 0.683594,0.175781 1.367188,0.175781 1.269531,0 2.617187,-0.761719 1.367188,-0.761719 2.421875,-1.914062 v 4.140625 q -1.25,1.171875 -2.539062,1.699218 -1.269531,0.527344 -2.871094,0.527344 -1.171875,0 -2.246094,-0.273437 -1.054687,-0.273438 -3.378906,-1.308594 -2.3046873,-1.035156 -3.847656,-1.035156 -1.25,0 -2.3632813,0.546875 -1.09375,0.527343 -2.734375,2.109375 z"
|
||||
style="font-family:Arial;-inkscape-font-specification:'Arial, Normal'"
|
||||
id="path1704" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(16.213 5.975) scale(.72923)"
|
||||
id="g896">
|
||||
<circle
|
||||
cx="36"
|
||||
cy="36"
|
||||
r="23"
|
||||
fill="#fcea2b"
|
||||
id="circle892" />
|
||||
<path
|
||||
fill="#3f3f3f"
|
||||
d="M45.331 38.564c3.963 0 7.178-2.862 7.178-6.389 0-1.765.448-3.53-.852-4.685-1.299-1.156-4.345-1.704-6.326-1.704-2.357 0-5.143.143-6.451 1.704-.894 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389zM25.738 38.564c3.963 0 7.179-2.862 7.179-6.389 0-1.765.447-3.53-.852-4.685-1.3-1.156-4.345-1.704-6.327-1.704-2.356 0-5.142.143-6.451 1.704-.893 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389z"
|
||||
id="path894" />
|
||||
</g>
|
||||
<g
|
||||
stroke="#000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
transform="translate(16.213 5.975) scale(.72923)"
|
||||
id="g908">
|
||||
<circle
|
||||
cx="35.887"
|
||||
cy="36.056"
|
||||
r="23"
|
||||
id="circle898" />
|
||||
<path
|
||||
d="M45.702 44.862c-6.574 3.525-14.045 3.658-19.63 0M18.883 30.464s-.953 8.55 6.86 7.918c2.62-.212 7.817-.65 7.867-8.342.005-.698-.007-1.6-.81-2.63-1.065-1.367-3.572-1.971-9.945-1.422 0 0-3.446-.1-3.972 4.476z"
|
||||
id="path900" />
|
||||
<path
|
||||
d="m18.953 29.931-.433-3.372 3.833-.527M52.741 30.464s.953 8.55-6.86 7.918c-2.62-.212-7.817-.65-7.868-8.342-.004-.698.008-1.6.811-2.63 1.065-1.367 3.572-1.971 9.945-1.422 0 0 3.446-.1 3.972 4.476z"
|
||||
id="path902" />
|
||||
<path
|
||||
d="M31.505 26.416s4.124 2.534 8.657 0M33.536 31.318s2.202-3.751 4.536 0M52.664 29.933l.433-3.371-3.833-.528"
|
||||
id="path904" />
|
||||
<path
|
||||
d="M33.955 30.027s1.795-3.75 3.699 0"
|
||||
id="path906" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
66
default.nix
Normal file
66
default.nix
Normal file
@ -0,0 +1,66 @@
|
||||
# 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/
|
||||
{
|
||||
pkgs ? import <nixpkgs> {},
|
||||
lib ? import <nixpkgs/lib>,
|
||||
}:
|
||||
pkgs.stdenv.mkDerivation rec {
|
||||
pname = "tildefriends";
|
||||
version = "0.0.22";
|
||||
|
||||
src = pkgs.fetchFromGitea {
|
||||
domain = "dev.tildefriends.net";
|
||||
owner = "cory";
|
||||
repo = "tildefriends";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-0x4LJXtMUxBsWEOoWAaQlStTfvqUf4Qs/5vJZ2EvBpY=";
|
||||
fetchSubmodules = true;
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
glibc
|
||||
gnumake
|
||||
openssl
|
||||
which
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
glibc
|
||||
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;
|
||||
};
|
||||
}
|
1
deps/c-ares
vendored
Submodule
1
deps/c-ares
vendored
Submodule
Submodule deps/c-ares added at caffa5ffb3
177
deps/c-ares_config/ares_build.h
vendored
Normal file
177
deps/c-ares_config/ares_build.h
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
#ifndef __CARES_BUILD_H
|
||||
#define __CARES_BUILD_H
|
||||
/*
|
||||
* Copyright (C) The c-ares project and its contributors
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#define CARES_STATICLIB
|
||||
|
||||
#ifdef CARES_HAVE_SYS_TYPES_H
|
||||
# include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#ifdef CARES_HAVE_SYS_SOCKET_H
|
||||
# include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
#ifdef CARES_HAVE_SYS_SELECT_H
|
||||
# include <sys/select.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#undef HAVE_REGISTERWAITFORSINGLEOBJECT
|
||||
#define CARES_HAVE_WINSOCK2_H
|
||||
#define CARES_HAVE_WINDOWS_H
|
||||
#define CARES_HAVE_WS2TCPIP_H
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#define CARES_TYPEOF_ARES_SOCKLEN_T int
|
||||
#define CARES_TYPEOF_ARES_SSIZE_T ssize_t
|
||||
#else
|
||||
#define CARES_TYPEOF_ARES_SOCKLEN_T socklen_t
|
||||
#define CARES_TYPEOF_ARES_SSIZE_T ssize_t
|
||||
#endif
|
||||
|
||||
#if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(_WIN32) && !defined(__OpenBSD__) && !defined(__HAIKU__)
|
||||
#define GETSERVBYNAME_R_ARGS 6
|
||||
#define GETSERVBYPORT_R_ARGS 6
|
||||
#define HAVE_GETSERVBYNAME_R 1
|
||||
#define HAVE_GETSERVBYPORT_R 1
|
||||
#endif
|
||||
|
||||
#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__OpenBSD__) && !defined(__HAIKU__)
|
||||
#define HAVE_PIPE2 1
|
||||
#endif
|
||||
|
||||
#if defined(__OpenBSD__) || defined(__HAIKU__)
|
||||
#define GETSERVBYNAME_R_ARGS 4
|
||||
#define GETSERVBYPORT_R_ARGS 4
|
||||
#endif
|
||||
|
||||
#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__OpenBSD__) && !defined(__HAIKU__)
|
||||
#define HAVE_MALLOC_H 1
|
||||
#define HAVE_EPOLL 1
|
||||
#define HAVE_SYS_EPOLL_H 1
|
||||
#define HAVE_SYS_RANDOM_H 1
|
||||
#endif
|
||||
|
||||
#if !defined(__WIN32)
|
||||
#undef AC_APPLE_UNIVERSAL_BUILD
|
||||
#undef ETC_INET
|
||||
#define GETHOSTNAME_TYPE_ARG2 size_t
|
||||
#define GETNAMEINFO_QUAL_ARG1
|
||||
#define GETNAMEINFO_TYPE_ARG1 struct sockaddr *
|
||||
#define GETNAMEINFO_TYPE_ARG2 socklen_t
|
||||
#define GETNAMEINFO_TYPE_ARG46 socklen_t
|
||||
#define GETNAMEINFO_TYPE_ARG7 int
|
||||
#define HAVE_AF_INET6 1
|
||||
#define HAVE_ARPA_INET_H 1
|
||||
#define HAVE_ARPA_NAMESER_COMPAT_H 1
|
||||
#define HAVE_ARPA_NAMESER_H 1
|
||||
#define HAVE_ASSERT_H 1
|
||||
#define HAVE_CLOCK_GETTIME_MONOTONIC 1
|
||||
#define HAVE_CONNECT 1
|
||||
#define HAVE_DLFCN_H 1
|
||||
#define HAVE_ERRNO_H 1
|
||||
#define HAVE_POLL_H 1
|
||||
#define HAVE_POLL 1
|
||||
#define HAVE_PIPE 1
|
||||
#define HAVE_FCNTL 1
|
||||
#define HAVE_FCNTL_H 1
|
||||
#define HAVE_FCNTL_O_NONBLOCK 1
|
||||
#define HAVE_FREEADDRINFO 1
|
||||
#define HAVE_GETADDRINFO 1
|
||||
#define HAVE_GETENV 1
|
||||
#define HAVE_GETHOSTNAME 1
|
||||
#define HAVE_GETNAMEINFO 1
|
||||
#if !defined(__HAIKU__)
|
||||
#define HAVE_GETRANDOM 1
|
||||
#endif
|
||||
#define HAVE_GETTIMEOFDAY 1
|
||||
#define HAVE_IF_INDEXTONAME 1
|
||||
#define HAVE_IF_NAMETOINDEX 1
|
||||
#define HAVE_INET_NTOP 1
|
||||
#define HAVE_INET_PTON 1
|
||||
#define HAVE_INTTYPES_H 1
|
||||
#define HAVE_IOCTL 1
|
||||
#define HAVE_IOCTL_FIONBIO 1
|
||||
#define HAVE_IOCTL_SIOCGIFADDR 1
|
||||
#define HAVE_LIMITS_H 1
|
||||
#define HAVE_LONGLONG 1
|
||||
#define HAVE_MEMORY_H 1
|
||||
#define HAVE_MSG_NOSIGNAL 1
|
||||
#define HAVE_NETDB_H 1
|
||||
#define HAVE_NETINET_IN_H 1
|
||||
#define HAVE_NETINET_TCP_H 1
|
||||
#define HAVE_NET_IF_H 1
|
||||
#define HAVE_PF_INET6 1
|
||||
#define HAVE_RECV 1
|
||||
#define HAVE_RECVFROM 1
|
||||
#define HAVE_SEND 1
|
||||
#define HAVE_SETSOCKOPT 1
|
||||
#define HAVE_SIGNAL_H 1
|
||||
#define HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID 1
|
||||
#define HAVE_SOCKET 1
|
||||
#define HAVE_STDBOOL_H 1
|
||||
#define HAVE_STDINT_H 1
|
||||
#define HAVE_STDLIB_H 1
|
||||
#define HAVE_STRCASECMP 1
|
||||
#define HAVE_STRDUP 1
|
||||
#define HAVE_STRINGS_H 1
|
||||
#define HAVE_STRING_H 1
|
||||
#define HAVE_STRNCASECMP 1
|
||||
#define HAVE_STRUCT_ADDRINFO 1
|
||||
#define HAVE_STRUCT_IN6_ADDR 1
|
||||
#define HAVE_STRUCT_SOCKADDR_IN6 1
|
||||
#define HAVE_STRUCT_SOCKADDR_STORAGE 1
|
||||
#define HAVE_STRUCT_TIMEVAL 1
|
||||
#define HAVE_SYS_IOCTL_H 1
|
||||
#define HAVE_SYS_PARAM_H 1
|
||||
#define HAVE_SYS_SELECT_H 1
|
||||
#define HAVE_SYS_SOCKET_H 1
|
||||
#define HAVE_SYS_STAT_H 1
|
||||
#define HAVE_SYS_TIME_H 1
|
||||
#define HAVE_SYS_TYPES_H 1
|
||||
#define HAVE_SYS_UIO_H 1
|
||||
#define HAVE_TIME_H 1
|
||||
#define HAVE_IFADDRS_H 1
|
||||
#define HAVE_UNISTD_H 1
|
||||
#define HAVE_WRITEV 1
|
||||
#if defined(__ANDROID__) || defined(__APPLE__) || defined(__OpenBSD__)
|
||||
#define HAVE_ARC4RANDOM_BUF 1
|
||||
#else
|
||||
#undef HAVE_ARC4RANDOM_BUF
|
||||
#endif
|
||||
#define HAVE_GETIFADDRS 1
|
||||
#define HAVE_STAT 1
|
||||
#define CARES_RANDOM_FILE "/dev/urandom"
|
||||
#define RECVFROM_QUAL_ARG5
|
||||
#define RECVFROM_TYPE_ARG1 int
|
||||
#define RECVFROM_TYPE_ARG2 void *
|
||||
#define RECVFROM_TYPE_ARG2_IS_VOID 0
|
||||
#define RECVFROM_TYPE_ARG3 size_t
|
||||
#define RECVFROM_TYPE_ARG4 int
|
||||
#define RECVFROM_TYPE_ARG5 struct sockaddr *
|
||||
#define RECVFROM_TYPE_ARG5_IS_VOID 0
|
||||
#define RECVFROM_TYPE_ARG6 socklen_t *
|
||||
#define RECVFROM_TYPE_ARG6_IS_VOID 0
|
||||
#define RECVFROM_TYPE_RETV ssize_t
|
||||
#define RECV_TYPE_ARG1 int
|
||||
#define RECV_TYPE_ARG2 void *
|
||||
#define RECV_TYPE_ARG3 size_t
|
||||
#define RECV_TYPE_ARG4 int
|
||||
#define RECV_TYPE_RETV ssize_t
|
||||
#define SEND_TYPE_ARG1 int
|
||||
#define SEND_TYPE_ARG2 const void *
|
||||
#define SEND_TYPE_ARG3 size_t
|
||||
#define SEND_TYPE_ARG4 int
|
||||
#define SEND_TYPE_RETV ssize_t
|
||||
#undef USE_BLOCKING_SOCKETS
|
||||
#undef WIN32_LEAN_AND_MEAN
|
||||
#define HAVE_PTHREAD_H 1
|
||||
#define CARES_THREADS 1
|
||||
#endif
|
||||
|
||||
#endif /* __CARES_BUILD_H */
|
2
deps/codemirror/cm6.js
vendored
2
deps/codemirror/cm6.js
vendored
File diff suppressed because one or more lines are too long
825
deps/codemirror_src/package-lock.json
generated
vendored
825
deps/codemirror_src/package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user