Compare commits

...

112 Commits

Author SHA1 Message Date
72369ab745 Let's release 0.0.15.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4814 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 23:35:28 +00:00
b62a05f627 Latest picohttpparser 4e7bc76.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4813 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 21:32:12 +00:00
03eb8e7fae Change that mentions icon.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4812 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 21:11:24 +00:00
a5a00b6987 Make garbage collecting blobs ease up on my phone.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4811 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 21:08:08 +00:00
542162c78a One editor at a time.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4810 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 20:59:46 +00:00
8cfe0fb7d2 -Os => -Oz for android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4809 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 13:54:51 +00:00
49c831cb62 Fix seeding files for a new app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4808 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 13:09:31 +00:00
2c79e03094 A little paranoia as I stare at this code and some analyzer nonsense.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4807 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 12:50:47 +00:00
21e6cf10b6 Sigh. Linked list bugs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4806 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 21:53:57 +00:00
dc655bb359 Prefer tf_resize_vec many places over tf_realloc.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4805 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 21:29:06 +00:00
b9987580ee Now all the tests run clean.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4804 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 21:01:10 +00:00
cb2dfc696d Fixed a few more good leaks. Now there are just some unclean shutdown issues.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4803 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 18:26:01 +00:00
7f0643f9c0 Stop leaking the TLS context.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4802 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 17:27:56 +00:00
14a4117aff Don't leak the http handlers.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4801 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 17:11:24 +00:00
55fb5dce1a Whoa, leaked messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4800 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 16:37:22 +00:00
923d6f9835 I think that's all the leaks accounted for though not yet fixed.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4799 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 15:45:51 +00:00
08b5ade8ec Getting closer on lifetime issues.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4798 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 14:44:17 +00:00
91f41c7497 Fix the windows build.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4797 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 13:51:08 +00:00
fa06282ff9 Make it so we don't have to wait ages for a timer to be able to shutdown with ^C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4796 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 13:48:16 +00:00
48b967f5b6 Tryingn to button down websocket lifetime issues.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4795 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 02:36:08 +00:00
f479165aac Fixes 'tildefriends test'.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4794 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 01:47:51 +00:00
2f83ecc1ac At least one legit memory leak, but also add a SIGTERM handler that attempts a clean shutdown so that I can ensure that it succeeds. It currently does not.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4793 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-27 01:25:30 +00:00
01efc215fd Update codemirror.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4792 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-26 02:47:47 +00:00
00ba74a6c4 This simplifies upgrading an HTTP request to a websocket, I believe, and fixes sending refresh auth tokens.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4791 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-25 18:00:23 +00:00
44b5ba1a9a Fix excessive scroll bars.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4790 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-24 03:11:49 +00:00
843e172e56 Allow pasting non-image files into the wiki editor.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4789 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-24 02:58:53 +00:00
a0df336abe Latest libsodium-1.0.19-stable.tar.gz
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4788 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-23 02:18:59 +00:00
0db4bb06c9 zlib 1.3.1.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4787 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-23 02:14:27 +00:00
ad2b49b838 App import/export from the editor.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4786 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-21 23:56:36 +00:00
ab519342e8 Styling the files pane.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4785 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-20 16:05:00 +00:00
1f0b9012e3 Use some w3.css in the core HTML.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4784 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-19 02:32:55 +00:00
1ddad6be93 Null check around my apps change.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4783 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-19 02:17:09 +00:00
cf311003c0 Fix some weird spacing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4782 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-19 02:12:17 +00:00
64249976a8 Fix https requests redirecting to http.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4781 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-19 00:48:42 +00:00
6ecb3ccd08 Some var => let.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4780 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-18 00:38:38 +00:00
4867bacb72 Remove the docs app. Will figure out how to do this all via the wiki app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4779 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-18 00:37:49 +00:00
7d029d3d7a Remove the appstore app. apps does most of what it used to do, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4778 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-17 23:36:08 +00:00
455befc18f List shared apps in the apps app. App.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4777 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-17 22:43:32 +00:00
6e57845512 sqlite-amalgamation-3450000.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4776 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-15 20:58:15 +00:00
1335a6e1e5 Fixed the create account falling off the screen.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4775 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-15 02:13:21 +00:00
1eab44464c More style.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4774 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-15 02:01:36 +00:00
c3e2da3d51 Oops, this is the rebuild of cm6 with that last change.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4773 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-15 01:57:37 +00:00
1716f71c12 Make the editor theme background a bit darker.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4772 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-15 01:56:43 +00:00
b52e99c958 Fix indentation?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4771 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-14 03:07:08 +00:00
86531bfd7e Fix some sizes.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4770 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-14 03:06:59 +00:00
874a22325e Gotta highlight that whitespace.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4769 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-14 02:58:46 +00:00
2380b65853 Man, CSS.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4768 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-14 02:37:15 +00:00
f72e8cbd91 CodeMirror 6.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4767 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-13 17:40:47 +00:00
24e418344e Make malloc_usable_size() go away with CFLAGS.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4766 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-13 13:19:08 +00:00
2b7077ca70 quickjs-2024-01-13.tar.xz
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4765 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-13 13:04:19 +00:00
10d438e723 Eh? Windows fix.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4764 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-13 13:00:39 +00:00
331846ee2e Fiddling with buttons.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4763 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-13 03:03:03 +00:00
dc0e58afc1 w3.css-ified ssb more.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4762 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-13 02:55:52 +00:00
18e9252998 speedscope 1.20.0.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4761 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-13 02:34:41 +00:00
b2e3c04036 I did some CSS, and it was kind of OK.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4760 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-12 04:23:31 +00:00
4fd155e68a Make haiku compile again, though I'm not happy about its lack of INADDR_ANY support.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4759 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-12 00:11:03 +00:00
59ac0b5f20 Print a colored result at the end of autotest.py.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4758 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-11 23:57:02 +00:00
f4979c841a Cleanup of some minor old cruft in the js code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4757 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-11 02:11:24 +00:00
74eb74deb1 Playing with pahole.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4756 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-11 01:38:30 +00:00
9e5e7b70d4 Let's try only showing my own blog posts.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4755 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-11 01:02:47 +00:00
2384fc9fa9 Improve some more blog links.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4754 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-11 00:50:12 +00:00
576e58b1e3 Make blogs semi-navigable.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4753 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-11 00:33:53 +00:00
a0af058f5e Don't leak promises.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4752 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-10 02:49:44 +00:00
b40457d774 Disable storing messages for disconnection debug by default, and add another environment variable for logging SSB RPC messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4751 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-10 02:41:28 +00:00
2353b43514 Attempt to release sqlite memory.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4750 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-10 02:36:27 +00:00
b11d5192c2 Fiddling with blog links.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4749 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-10 02:23:40 +00:00
d38c58ce1d lit 3.1.1
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4748 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-10 00:00:14 +00:00
a0f390b7dc Fix a memory leak in httpd.js.c.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4747 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-09 17:22:39 +00:00
cb12799111 Add audio/midi mime type.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4746 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-09 17:22:09 +00:00
86fb5c53a1 Fix wiki links within the standalone pages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4745 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-08 17:42:56 +00:00
29fc728509 These look like potential leaks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4744 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-08 02:30:08 +00:00
0fb341f378 Enable memory tracking on an environment variable.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4743 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-08 02:18:10 +00:00
8a1a182479 Fix mingw build?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4742 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-07 21:35:51 +00:00
49907bc8ee Oops.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4741 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-07 21:08:37 +00:00
21d4a9b328 Appease gcc 12's analyzer.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4740 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-07 21:08:20 +00:00
d5ede43a13 Update the welcome links to all go to pages with versions for my own convenience.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4739 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-07 15:23:09 +00:00
b73f5011cf Continuing trying to crunch android openssl sizes.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4738 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-06 21:33:20 +00:00
32ebfa78cd Some automation for the identity app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4737 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-06 19:52:14 +00:00
39c942a205 Support deleting identities, too.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4736 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-06 19:22:49 +00:00
ebc4533b10 Minor identity interface improvements.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4735 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-06 18:40:57 +00:00
4e5f9c86a8 Fix the app emoji button.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4734 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-06 15:47:14 +00:00
d89a7a5556 Looks like I can round-trip an SSB identity, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4733 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-04 01:17:30 +00:00
8ab53f2da3 Some plumbing to export an SSB identity from Tilde Friends.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4732 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-04 00:21:15 +00:00
4c8eab2692 Set more button tooltips.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4731 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-03 23:52:05 +00:00
08989f54d9 Wiki link colors, and determine the thumbnail better.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4730 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-03 23:24:24 +00:00
c78753f3ff Expose bip39 to script, and fix some things around base64 so that I can round trip it properly.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4729 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-03 17:25:34 +00:00
34a87d8b3b Minor cleanup. Make http.c trace its callbacks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4728 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-03 02:14:17 +00:00
7516524d69 Implement the rest of the endpoints that were already mostly C in C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4727 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-02 23:26:42 +00:00
549d7ffec8 Minor blog changes I've apparently been sitting on.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4726 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-02 22:21:13 +00:00
ccafc23d3c Adding bip39 so I can use it to move around private keys.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4725 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-02 20:25:11 +00:00
709b57d84f Move /trace and /mem to C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4724 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-02 15:43:17 +00:00
9ef909c9a1 Reimplement http -> https redirects. Remove request phases. With just a little extra storage, it wasn't needed.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4723 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-02 15:02:47 +00:00
d7c0ffaac4 speedscope 1.19.0.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4722 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-02 01:09:05 +00:00
e4cd5312f1 Oops, fix websockets.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4721 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-01 22:22:03 +00:00
197fca6d3b Fix/cleanup around a crash I'm seeing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4720 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-01 22:14:27 +00:00
04af1f0053 I think it we ask for AF_INET6, we get 4+6. Let's do that.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4719 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-31 03:42:07 +00:00
93d9b1ed93 I think we can assume curl on all platforms for tests.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4718 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-31 03:24:20 +00:00
2d73116bc0 Don't free an undefined JSValue.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4717 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-31 03:09:32 +00:00
f2f6d78790 Fine, whatever.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4716 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-31 03:09:15 +00:00
797509fc11 Fix a crash processing TLS while a session is closing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4715 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-31 03:05:52 +00:00
6920504762 Work around this test failure. Dunno.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4714 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-31 02:41:16 +00:00
9d1476a760 Slight memcpy paranoia.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4713 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 21:41:48 +00:00
c1890775dc Fixes for fragmented websocket messages. Android is happy, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4712 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 21:35:53 +00:00
72e5fe5b8f Allow receiving fragmented websocket messages. I thought this was what was breaking me on Android, but it's not.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4711 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 20:35:03 +00:00
c81ec214e2 Missing thread busy indicator.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4710 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 20:34:35 +00:00
0dcc879eb1 Delete httpd.js.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4709 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 19:47:36 +00:00
4f3f4295ea Some HTTP fixes.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4708 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 19:18:09 +00:00
d02f17a8cf I think the new HTTP implementation is basically working, now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4707 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 18:59:02 +00:00
2f6a92168e Implement connection activity timeouts.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4706 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 16:52:05 +00:00
b6a3923b27 Some quick http refactors to make websockets less magic.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4705 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 16:29:16 +00:00
d556cbc835 Let's start 0.0.15.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4704 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-30 16:08:15 +00:00
f186806117 wiki size fix and allow replying/reacting to blog posts in the ssb app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4703 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-12-29 20:13:35 +00:00
266 changed files with 20055 additions and 8715 deletions

View File

@ -3,9 +3,9 @@
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 14
VERSION_NUMBER := 0.0.14
VERSION_NAME := Served on apple cider dressed winter greens.
VERSION_CODE := 15
VERSION_NUMBER := 0.0.15
VERSION_NAME := Medium English breakfast tea.
PROJECT = tildefriends
BUILD_DIR ?= out
@ -159,7 +159,7 @@ $(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
$(WINDOWS_TARGETS): AS = $(CC)
$(WINDOWS_TARGETS): CFLAGS += \
@ -213,6 +213,11 @@ debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
endif
ifeq ($(UNAME_M),aarch64)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
get_objs = \
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
@ -500,7 +505,8 @@ $(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
-DHAVE_VASPRINTF \
-Dvsnprintf=rpl_vsnprintf
$(XOPT_OBJS): CFLAGS += \
-Wno-implicit-const-int-float-conversion
-Wno-implicit-const-int-float-conversion \
-Wno-pointer-to-int-cast
QUICKJS_SOURCES := \
deps/quickjs/cutils.c \
@ -521,6 +527,12 @@ $(QUICKJS_OBJS): CFLAGS += \
-Wno-unused-variable
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
ifeq ($(UNAME_S),Haiku)
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
else ifeq ($(UNAME_S),OpenBSD)
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
endif
LIBBACKTRACE_SOURCES := \
deps/libbacktrace/atomic.c \
deps/libbacktrace/backtrace.c \

View File

@ -1,4 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💻"
"emoji": "💻",
"previous": "&33ngNe0YrH3JScss6krlCwddZcXl8C5szonp7DYy4qA=.sha256"
}

View File

@ -8,9 +8,42 @@ async function fetch_info(apps) {
return result;
}
async function fetch_shared_apps() {
let messages = {};
await ssb.sqlAsync(`
SELECT messages.*
FROM messages_fts('"application/tildefriends"')
JOIN messages ON messages.rowid = messages_fts.rowid
ORDER BY timestamp
`,
[],
function(row) {
let content = JSON.parse(row.content);
for (let mention of content.mentions) {
if (mention?.type === 'application/tildefriends') {
messages[JSON.stringify([row.author, mention.name])] = {
message: row,
blob: mention.link,
name: mention.name,
};
}
}
});
let result = {};
for (let app of Object.values(messages).sort((x, y) => y.message.timestamp - x.message.timestamp)) {
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
if (app_object) {
app_object.blob_id = app.blob;
result[app.name] = app_object;
}
}
return result;
}
async function main() {
var apps = await fetch_info(await core.apps());
var core_apps = await fetch_info(await core.apps('core'));
let shared_apps = await fetch_shared_apps();
var doc = `<!DOCTYPE html>
<html>
<head>
@ -40,6 +73,8 @@ async function main() {
<body style="background: #888">
<h1 id="apps_title">Apps</h1>
<div id="apps" class="container"></div>
<h1>Shared Apps</h1>
<div id="shared_apps" class="container"></div>
<h1>Core Apps</h1>
<div id="core_apps" class="container"></div>
</body>
@ -50,18 +85,19 @@ async function main() {
let div = list.appendChild(document.createElement('div'));
div.classList.add('app');
let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
let icon_a = document.createElement('a');
let icon = document.createElement('div');
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
icon.style.fontSize = 'xxx-large';
icon_a.appendChild(icon);
icon_a.href = '/~' + name + '/' + app + '/';
icon_a.href = href;
icon_a.target = '_top';
div.appendChild(icon_a);
let a = document.createElement('a');
a.appendChild(document.createTextNode(app));
a.href = '/~' + name + '/' + app + '/';
a.href = href;
a.target = '_top';
div.appendChild(a);
}
@ -69,6 +105,7 @@ async function main() {
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
</script>
</html>`;
app.setDocument(doc);

View File

@ -1,4 +0,0 @@
{
"type": "tildefriends-app",
"emoji": "🛍"
}

View File

@ -1,55 +0,0 @@
async function get_apps() {
let results = {};
await ssb.sqlAsync(`
SELECT messages.*
FROM messages_fts('"application/tildefriends"')
JOIN messages ON messages.rowid = messages_fts.rowid
ORDER BY timestamp
`,
[],
function(row) {
let content = JSON.parse(row.content);
for (let mention of content.mentions) {
if (mention?.type === 'application/tildefriends') {
results[JSON.stringify([row.author, mention.name])] = {
message: row,
blob: mention.link,
name: mention.name,
};
}
}
});
return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
}
function render_app(app) {
return `
<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
<a href="/~cory/ssb/#${app.message.author}">@</a>
<a href="/~cory/ssb/#${app.message.id}">%</a>
<a href="/${app.blob}/">${app.name}</a>
</div>
`;
}
async function main() {
let apps = await get_apps();
app.setDocument(`
<html>
<head>
<base target="_top">
<style>
a:link { color: #bbf; }
a:visited { color: #ddd; }
a:hover { color: #ddf; }
</style>
</head>
<body style="color: #fff">
<h1>${apps.length} apps</h1>
${apps.map(render_app).join('\n')}
</body>
</html>
`);
}
main();

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪵",
"previous": "&YHBylHM7DlDDiGfuNj+g95Bf7NFxTZs/IKG14TbWnhs=.sha256"
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
}

View File

@ -8,7 +8,40 @@ function escapeAttribute(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;');
}
function markdown(md) {
export async function get_blog_message(id) {
let message;
await ssb.sqlAsync(
'SELECT author, timestamp, content FROM messages WHERE id = ?',
[id],
function(row) {
let content = JSON.parse(row.content);
message = {
author: row.author,
timestamp: row.timestamp,
blog: content?.blog,
title: content?.title,
};
});
if (message) {
await ssb.sqlAsync(
`
SELECT json_extract(content, '$.name') AS name
FROM messages
WHERE author = ?
AND json_extract(content, '$.type') = 'about'
AND json_extract(content, '$.about') = author
AND name IS NOT NULL
ORDER BY sequence DESC LIMIT 1
`,
[message.author],
function(row) {
message.name = row.name;
});
}
return message;
}
export function markdown(md) {
let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer();
let parsed = reader.parse(md || '');
@ -17,10 +50,10 @@ function markdown(md) {
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
if (node.destination?.startsWith('&')) {
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
} else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) {
node.destination = '/~core/ssb/#' + escape(node.destination);
}
}
}
@ -31,7 +64,12 @@ export async function render_blog_post_html(blog_post) {
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
return `<!DOCTYPE html>
<html>
<head>
<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title>
<base target="_top">
</head>
<body>
<h1><a href="./">🪵Tilde Friends Blog</a></h1>
<div>
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
<div>${markdown(blob)}</div>
@ -44,7 +82,7 @@ export async function render_blog_post_html(blog_post) {
function render_blog_post(blog_post) {
return `
<div>
<h2><a href="../ssb/#${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
<h2><a href="/~${core.app.owner}/${core.app.name}/${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
<div>${markdown(blog_post.summary)}</div>
</div>
@ -55,18 +93,18 @@ export function render_html(blogs) {
return `<!DOCTYPE html>
<html>
<head>
<title>🪵Tilde Blog</title>
<title>🪵Tilde Friends Blog</title>
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
<style>
html {
background-color: #ccc;
}
</style>
<base target="_blank">
<base target="_top">
</head>
<body>
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
<h1>🪵Tilde Blog</h1>
<h1>🪵Tilde Friends Blog</h1>
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
</div>
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
@ -77,7 +115,7 @@ export function render_html(blogs) {
function render_blog_post_atom(blog_post) {
return `<entry>
<title>${escape(blog_post.title)}</title>
<link href="https://tildefriends.net/~cory/ssb/#${blog_post.id}" />
<link href="/~cory/ssb/#${blog_post.id}" />
<id>${blog_post.id}</id>
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
<summary>${escape(blog_post.summary)}</summary>
@ -93,9 +131,9 @@ export function render_atom(blogs) {
<feed xmlns="http://www.w3.org/2005/Atom">
<title>🪵Tilde Blog</title>
<subtitle>A subtitle.</subtitle>
<link href="https://tildefriends.net/~cory/blog/atom" rel="self"/>
<link href="https://tildefriends.net/~cory/blog/"/>
<id>https://www.tildefriends.net/~cory/blog/</id>
<link href="${core.url}/atom" rel="self"/>
<link href="${core.url}"/>
<id>${core.url}</id>
<updated>${new Date().toString()}</updated>
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
</feed>`;
@ -103,6 +141,7 @@ export function render_atom(blogs) {
export async function get_posts() {
let blogs = [];
let ids = await ssb.getIdentities();
await ssb.sqlAsync(`
WITH
blogs AS (
@ -139,10 +178,11 @@ export async function get_posts() {
name IS NOT NULL)
WHERE author_rank = 1)
SELECT blogs.*, names.name FROM blogs
JOIN json_each(?) AS self ON self.value = blogs.author
JOIN public ON public.author = blogs.author
LEFT OUTER JOIN names ON names.author = blogs.author
ORDER BY blogs.timestamp DESC LIMIT 20
`, [], function(row) {
`, [JSON.stringify(ids)], function(row) {
blogs.push(row);
});
return blogs;

View File

@ -1,17 +1,26 @@
import * as blog from './blog.js';
async function main() {
let blogs = await blog.get_posts();
for (let blog_post of blogs) {
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
if (request.path === title) {
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
return;
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path;
let message = await blog.get_blog_message(id);
if (message) {
respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'});
} else {
respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'});
}
}
if (request.path == 'atom') {
} else if (request.path == 'atom') {
let blogs = await blog.get_posts();
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
} else {
let blogs = await blog.get_posts();
for (let blog_post of blogs) {
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
if (request.path === title) {
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
return;
}
}
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +0,0 @@
{
"type": "tildefriends-app",
"emoji": "📚"
}

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
# Tilde Friends Developer's Guide
[Back to index](#index)
A Tilde Friends application runs on the server. To make an interesting
application that interacts with the client, it's necessary to understand
how the parts work together.
## Hello, world!
A simple starting point. Presents `Hello, world!` in the browser when
visited.
**app.js**:
```
app.setDocument('<h1>Hello, world!</h1>');
```

View File

@ -1,12 +0,0 @@
# Tilde Friends Documentation
Tilde Friends is a participating member of a greater social
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
adding a way to safely and securely write, share,
and run code in the form of server-side web applications.
- [Tilde Friends Vision](#vision)
- [Secure Scuttlebutt from Scratch](#ssb)
- [Structure](#structure)
- [Guide](#guide)
- [TODO](#todo)

View File

@ -1,41 +0,0 @@
# Secure Scuttlebutt from Scratch
[Back to index](#index)
This aims to be the missing reference for those who wish to create a Secure
Scuttlebutt client from scratch.
## Discovery
A good way to get started is to participate in local network discovery with a known working
client on the same network. The
[Scuttlebutt Programming Guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#local-network)
is a good start, here, with a few things to note:
1. Some clients advertise multiple addresses separated by semicolons (`;`).
2. Some clients advertise alternative protocols than `shs` and use hostnames instead of
IPv4 addresses.
So be prepared to accept variations.
There also an undocumented "new" style of discovery message.
## Secret Handshake, Box Stream, and RPC Protocol
Now that two clients are aware of eachother, they need to complete a secret handshake.
The [programming guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake)
is once again a good reference.
The box stream and RPC protocol can both be implemented from the
[same documentation](https://ssbc.github.io/scuttlebutt-protocol-guide/#box-stream)
without surprises.
## Synchronizing Data
... `ebt.replicate` or `createHistoryStream` ...
## Rooms
TODO
## References
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
* [https://dev.planetary.social/](https://dev.planetary.social/)
* [https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints](https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints)

View File

@ -1,65 +0,0 @@
# Tilde Friends Structure
[Back to index](#index)
Tilde Friends is a mostly-self-contained executable written in C.
In combines the following key components:
- A Secure Scuttlebutt (SSB) client/server. This talks with other SSB
instances, storing messages and blobs for anyone visible to local
users as they are encountered and sharing anything published locally
as appropriate.
- An sqlite database. This is where the SSB instance stores its data.
The general schema involves a `messages` table, storing mostly JSON,
a `blobs` table storing arbitrary blob data, and a `properties` table,
storing arbitrary state gleaned from `messages` and `blobs`, generally
updated on demand and incrementally.
- A QuickJS runtime. The core process runs stock scripts and has access
and permission to use all resources. All other processes, which
includes everything which runs untrusted code created by Tilde Friends
users, are strictly sandboxed in ways similar to how web browsers run
untrusted code. All attempts to access potentially sensitive resources
are mediated through the core process.
When run with no arguments, it starts a web server on
[http://localhost:12345/](http://localhost:12345/) and an SSB node.
## Web Interface
The Tilde Friends web server provides access to Tilde Friends applications,
which are arbitrary user-defined web applications.
At the top left, in addition to some basic navigation links, is an `edit`
link. Anyone can view, modify, and run in-place the code to any Tilde
Friends application by using the in-browser editor.
At the top right, one can `login` (to save work in their own space)
or `logout` (proceeding as a guest).
The rest of the page is an iframe belonging to the application.
## Special Paths
- `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user`
is a username of a Tilde Friends account, and `app` is an arbitrary name
of an application saved by the given user.
- `/~user/app/file` - A raw file in an app.
- `/&blobid.ed25519` - A raw blob. Content-Type is inferred for at least
a few common image types.
## Communication Channels
Web Browser <-> Core <-> Sandbox
Visiting an application path delivers stock HTML and JavaScript which
establishes a WebSocket connection back to the server.
At this point, a new sandbox process is started in Tilde Friends, much
as a new sandboxed process might be started for a new tab in a web
browser. This process has a custom RPC connection to the core process
which holds the WebSocket connection to the browser.
The custom RPC communication between the sandbox process and the core
process facilitates passing and calling functions remotely. Calling a
function in another process returns a `Promise`.
An application will typically call `app.setDocument()` at startup to
populate the app's iframe in the web browser with its own client web
application resources.

View File

@ -1,63 +0,0 @@
# Tilde Friends TODO
[Back to index](#index)
## MVP3
- Sync status (problem feeds, messages/seconds stats, ...)
- app: wiki
- app: public blog
- Content-Disposition: download
- remove SSB credentials
- export SSB credentials
- initial: better empty news screen
- initial: remembered wrong user across login/logout
- initial: bad experience when following nobody
- make a cool independent app
- indicate when workspace differs from installed
- / => Something good.
- update docs
- audit + document API exposed to apps
- fix weird HTTP warnings
- channels
- placeholder/missing images
- no denial of service
- package standalone executable
- editor without app iframe
- sequence_before_author -> flags
- linkify ssb: links
- perfect rooms support
- connections 2.0
- make a better connections API
## Maybe Done
- blob_wants 2.0
- image downsample
- app: todo
- app: build archive
- update README
- administrators config
- apps name characters
- initial: can't switch to account when there is only one
- get tarball under 5MB
- rooms
- initial: doesn't refresh when create identity
- tf account timeout why
- ssb don't overflow boxes
- jwt for session tokens
- linkify https://...
- emoji reaction picker
- expose loads of stats
- confirm posting all new messages
- multiple identities per user, in database
- auto-populate data on initial launch
- make the docker image good / test it / use it
- leaking imports / exports
- file upload widget
- keep working on good error feedback
- build for windows
- installable apps (bring back an app message?)
- sqlStream => sqlExec or something
- !ssb from child process?
## Done
- update LICENSE
- logging to browser

View File

@ -1,62 +0,0 @@
# Tilde Friends Vision
[Back to index](#index)
Tilde Friends is a tool for making and sharing.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, and an environment for creating and running web applications.
## Why
This is a thing that I wanted to exist and wanted to work on. No other reason.
There is not a business model. I believe it is interesting and unique.
## Goals
1. Make it **easy and fun** to run all sorts of web applications.
2. Provide **security** that is easy to understand and protects your data.
3. Make **creating and sharing** web applications accessible to anyone with a
browser.
## Ways to Use Tilde Friends
1. **Social Network User**: This is a social network first. You are just here,
because your friends are. Or you like how we limit your message length or
short videos or whatever the trend is. If you are ambitious, you click links
and see interactive experiences (apps) that you wouldn't see elsewhere.
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
lists, wiki pages, ..., and you interact with them as though they were
cloud-hosted by a megacorporation. They just work, and you don't think twice.
3. **Group leader**: You host or use a small public instance, installing apps for
a group of friends to use as web visitors.
4. **Developer**: You like to write code and make or improve apps for fun or to
solve problems. When you encounter a Tilde Friends app on a strange server,
you know you can trivially modify it or download it to your own instance.
## Future Goals / Endgame
1. Mobile apps. This can run on your old phone. Maybe you won't be hosting
the web interface publicly, but you can sync, install and edit apps, and
otherwise get the full experience from a tiny touch screen.
2. The universal application runtime. The web browser is the universal
platform, but even for the simplest application that you might want to host
for your friends, cloud hosting, containers, and complicated dependencies might
all enter the mix. Tilde Friends, though it is yet another thing to host,
includes everything you need out of the box to run a vast variety of interesting
apps.
Tilde Friends will be built out, gradually providing safe access to host
resources and client resources the same way web browsers extended access to
resources like GPU, persistent storage, cameras, ... over the years.
Not much effort has been put forward yet to having a robust, long-lasting API,
but since the client side longevity is already handled by web browsers, it
seems possible that the server-side API can be managed in a similar way.
3. An awesome development environment. Right now it runs JavaScript from the
first embeddable text editor I could poorly configure enough to edit code,
but it could incorporate a debugger, source control integration a la ssb-git,
merge tools, and transpiling from all sorts of different languages.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
apps/identity.json Normal file
View File

@ -0,0 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪪",
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
}

87
apps/identity/app.js Normal file
View File

@ -0,0 +1,87 @@
import * as tfrpc from '/tfrpc.js';
tfrpc.register(async function get_private_key(id) {
return bip39Words(await ssb.getPrivateKey(id));
});
tfrpc.register(async function create_id(id) {
return await ssb.createIdentity();
});
tfrpc.register(async function add_id(id) {
return await ssb.addIdentity(bip39Bytes(id));
});
tfrpc.register(async function delete_id(id) {
return await ssb.deleteIdentity(id);
});
tfrpc.register(async function reload() {
await main();
});
async function main() {
let ids = await ssb.getIdentities();
await app.setDocument(`<body style="color: #fff">
<script>const handler = {};</script>
<script type="module">
import * as tfrpc from '/static/tfrpc.js';
handler.export_id = async function export_id(event) {
let id = event.srcElement.dataset.id;
let element = document.createElement('textarea');
element.value = await tfrpc.rpc.get_private_key(id);
element.style = 'width: 100%; read-only: true';
element.readOnly = true;
event.srcElement.parentElement.appendChild(element);
event.srcElement.onclick = event => handler.hide_id(event, element);
}
handler.add_id = async function add_id(event) {
let id = document.getElementById('add_id').value;
try {
let new_id = await tfrpc.rpc.add_id(id);
alert('Successfully imported: ' + new_id);
await tfrpc.rpc.reload();
} catch (e) {
alert('Error importing identity: ' + e);
}
}
handler.create_id = async function create_id(event) {
try {
let id = await tfrpc.rpc.create_id();
alert('Successfully created: ' + id);
await tfrpc.rpc.reload();
} catch (e) {
alert('Error creating identity: ' + e);
}
}
handler.hide_id = function hide_id(event, element) {
element.parentNode.removeChild(element);
event.srcElement.onclick = handler.export_id;
}
handler.delete_id = async function delete_id(event) {
let id = event.srcElement.dataset.id;
try {
if (prompt('Are you sure you want to delete "' + id + '"? It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') {
if (await tfrpc.rpc.delete_id(id)) {
alert('Successfully deleted ID: ' + id);
}
await tfrpc.rpc.reload();
}
} catch (e) {
alert('Error deleting ID: ' + e);
}
}
</script>
<h1>SSB Identity Management</h1>
<h2>Create a new identity</h2>
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
<h2>Identities</h2>
<ul>`+
ids.map(id => `<li>
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
${id}
</li>`).join('\n')+
` </ul>
</body>`);
}
main();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🐌",
"previous": "&dO6ckMIPVv9QvSc+0TOg0S59qe+rirPo2a6p9xSHj9M=.sha256"
"previous": "&h+PXCrnUHtHHfKyUaLW+Y1dP/JpWwG9cbRNjxOCVqw0=.sha256"
}

View File

@ -3,15 +3,15 @@
<head>
<title>Tilde Friends</title>
<base target="_top">
<link rel="stylesheet" href="tribute.css" />
<link rel="stylesheet" href="tribute.css"/>
<style>
.tribute-container {
color: #000;
}
</style>
</head>
<body>
<tf-app/>
<body style="background-color: #223a5e">
<tf-app class="w3-deep-purple"/>
<script>window.litDisableBundleWarning = true;</script>
<script src="filesaver.min.js"></script>
<script src="commonmark.min.js"></script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -184,8 +184,10 @@ class TfElement extends LitElement {
render_id_picker() {
return html`
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} .users=${this.users} @change=${this._handle_whoami_changed}></tf-id-picker>
<button @click=${this.create_identity} id="create_identity">Create Identity</button>
<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>
`;
}
@ -283,13 +285,19 @@ class TfElement extends LitElement {
});
}
const k_tabs = {
'📰': 'news',
'📡': 'connections',
'@': 'mentions',
'🔍': 'search',
'👩‍💻': 'query',
};
let tabs = html`
<div>
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
<input type="button" class="tab" value="Mentions" ?disabled=${self.tab == 'mentions'} @click=${() => self.set_tab('mentions')}></input>
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
<input type="button" class="tab" value="Query" ?disabled=${self.tab == 'query'} @click=${() => self.set_tab('query')}></input>
<div class="w3-bar w3-black">
${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'}" @click=${() => self.set_tab(v)}>${k}</button>
`)}
</div>
`;
let contents =

View File

@ -314,7 +314,7 @@ class TfComposeElement extends LitElement {
return html`
<div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em">
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
<button class="w3-button w3-dark-grey" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}>🚮</button>
</div>
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
@ -357,12 +357,12 @@ class TfComposeElement extends LitElement {
if (this.apps) {
return html`
<div>
<select id="select">
<div class="w3-card-4 w3-margin w3-padding">
<select id="select" class="w3-select w3-dark-grey">
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
</select>
<input type="button" value="Attach" @click=${attach_selected_app}></input>
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>Attach</button>
<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Cancel</button>
</div>
`;
}
@ -374,9 +374,9 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.apps();
}
if (!this.apps) {
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>Attach App</button>`;
} else {
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
return html`<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Discard App</button>`;
}
}
@ -392,15 +392,17 @@ class TfComposeElement extends LitElement {
let draft = this.get_draft();
if (draft.content_warning !== undefined) {
return html`
<div>
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
<label for="cw">CW</label>
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
<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>
<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>
</div>
`;
} else {
return html`
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
<label for="cw">CW</label>
`;
}
@ -430,13 +432,13 @@ 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>
<input type="button" value="🚮" @click=${() => this.set_encrypt(undefined)}></input>
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
</div>
<ul>
${draft.encrypt_to.map(x => html`
<li>
<tf-user id=${x} .users=${this.users}></tf-user>
<input type="button" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
</li>`)}
</ul>
`;
@ -454,28 +456,34 @@ class TfComposeElement extends LitElement {
let draft = self.get_draft();
let content_warning =
draft.content_warning !== undefined ?
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
html`<div class="w3-panel w3-round-xlarge w3-blue">
<p id="content_warning_preview">${draft.content_warning}</p>
</div>` :
undefined;
let encrypt = draft.encrypt_to !== undefined ?
undefined :
html`<input type="button" value="🔐" @click=${() => this.set_encrypt([])}></input>`;
html`<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt([])}>🔐</button>`;
let result = html`
${this.render_encrypt()}
<div style="display: flex; flex-direction: row; width: 100%">
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
<div style="flex: 1 0 50%">
${content_warning}
<div id="preview"></div>
<div class="w3-card-4 w3-blue-grey w3-padding" 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>
<div style="flex: 1 0 50%">
${content_warning}
<div id="preview"></div>
</div>
</div>
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
${this.render_attach_app()}
${this.render_content_warning()}
<button class="w3-button w3-dark-grey" id="submit" @click=${this.submit}>Submit</button>
<button class="w3-button w3-dark-grey" @click=${this.attach}>Attach</button>
${this.render_attach_app_button()}
${encrypt}
<button class="w3-button w3-dark-grey" @click=${this.discard}>Discard</button>
</div>
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
${this.render_content_warning()}
${this.render_attach_app()}
<input type="button" id="submit" value="Submit" @click=${this.submit}></input>
<input type="button" value="Attach" @click=${this.attach}></input>
${this.render_attach_app_button()}
${encrypt}
<input type="button" value="Discard" @click=${this.discard}></input>
`;
return result;
}

View File

@ -1,5 +1,6 @@
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.
@ -13,6 +14,8 @@ class TfIdentityPickerElement extends LitElement {
};
}
static styles = styles;
constructor() {
super();
this.ids = [];
@ -28,7 +31,7 @@ class TfIdentityPickerElement extends LitElement {
render() {
return html`
<select @change=${this.changed} style="max-width: 100%">
<select class="w3-select w3-dark-grey w3-padding w3-border" @change=${this.changed} style="max-width: 100%; overflow: hidden">
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${this.users[id]?.name ? (this.users[id]?.name + ' - ') : undefined}<small>${id}</small></option>`)}
</select>
`;

View File

@ -213,9 +213,9 @@ class TfMessageElement extends LitElement {
let self = this;
if (this.message.child_messages?.length) {
if (!this.expanded[this.message.id]) {
return html`<input type="button" value=${this.total_child_messages(this.message) + ' More'} @click=${() => self.set_expanded(true)}></input>`;
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(true)}>+ ${this.total_child_messages(this.message) + ' More'}</button>`;
} else {
return html`<input type="button" value="Collapse" @click=${() => self.set_expanded(false)}></input>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`;
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(false)}>Collapse</button>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`;
}
}
}
@ -250,29 +250,29 @@ class TfMessageElement extends LitElement {
switch (this.format) {
case 'raw':
if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<input type="button" value="Markdown" @click=${() => self.format = 'md'}></input>`;
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`;
} else {
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
}
break;
case 'md':
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
break;
case 'decrypted':
raw_button = html`<input type="button" value="Raw" @click=${() => self.format = 'raw'}></input>`;
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
break;
default:
if (this.message.decrypted) {
raw_button = html`<input type="button" value="Decrypted" @click=${() => self.format = 'decrypted'}></input>`;
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'decrypted'}>Decrypted</button>`;
} else {
raw_button = html`<input type="button" value="Raw" @click=${() => self.format = 'raw'}></input>`;
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
}
break;
}
function small_frame(inner) {
let body;
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
<div class="w3-card-4" style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
${raw_button}
@ -283,14 +283,14 @@ class TfMessageElement extends LitElement {
}
if (this.message?.type === 'contact_group') {
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
${this.message.messages.map(x =>
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
)}
</div>`;
} else if (this.message.placeholder) {
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
<div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(x => html`
@ -351,7 +351,7 @@ class TfMessageElement extends LitElement {
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}></tf-compose>
` : html`
<input type="button" value="Reply" @click=${this.show_reply}></input>
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
`;
let self = this;
let body;
@ -367,7 +367,7 @@ class TfMessageElement extends LitElement {
break;
}
let content_warning = html`
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
<div class="w3-panel w3-round-xlarge w3-blue" style="cursor: pointer" @click=${x => this.toggle_expanded(':cw')}><p>${content.contentWarning}</p></div>
`;
let content_html =
html`
@ -401,7 +401,7 @@ class TfMessageElement extends LitElement {
display: block;
}
</style>
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
@ -411,10 +411,10 @@ class TfMessageElement extends LitElement {
</div>
${payload}
${this.render_votes()}
<div>
<p>
${reply}
<input type="button" value="React" @click=${this.react}></input>
</div>
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
</p>
${this.render_children()}
</div>
`;
@ -436,7 +436,7 @@ class TfMessageElement extends LitElement {
display: block;
}
</style>
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
@ -446,9 +446,9 @@ class TfMessageElement extends LitElement {
</div>
${content.text}
${this.render_votes()}
<div>
<input type="button" value="React" @click=${this.react}></input>
</div>
<p>
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
</p>
${this.render_children()}
</div>
`;
@ -484,6 +484,17 @@ class TfMessageElement extends LitElement {
`;
break;
}
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${this.message.content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}></tf-compose>
` : html`
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
`;
return html`
<style>
code {
@ -499,7 +510,7 @@ class TfMessageElement extends LitElement {
display: block;
}
</style>
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span>
@ -509,7 +520,12 @@ class TfMessageElement extends LitElement {
<div>${body}</div>
${this.render_mentions()}
<div>
${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
</div>
${this.render_votes()}
${this.render_children()}
</div>
`;
} else if (content.type === 'pub') {

View File

@ -184,47 +184,49 @@ class TfProfileElement extends LitElement {
if (this.editing) {
let server_follow;
if (this.server_follows_me === true) {
server_follow = html`<input type="button" value="Server, Stop Following Me" @click=${() => this.server_follow_me(false)}></input>`;
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(false)}>Server, Stop Following Me</button>`;
} else if (this.server_follows_me === false) {
server_follow = html`<input type="button" value="Server, Follow Me" @click=${() => this.server_follow_me(true)}></input>`;
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(true)}>Server, Follow Me</button>`;
}
edit = html`
<input type="button" value="Save Profile" @click=${this.save_edits}></input>
<input type="button" value="Discard" @click=${this.discard_edits}></input>
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>Save Profile</button>
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>Discard</button>
${server_follow}
`;
} else {
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`;
}
}
if (this.id !== this.whoami &&
this.following !== undefined) {
follow =
this.following ?
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>Unfollow</button>` :
html`<button class="w3-button w3-dark-grey" @click=${this.follow}>Follow</button>`;
}
if (this.id !== this.whoami &&
this.blocking !== undefined) {
block =
this.blocking ?
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
html`<input type="button" value="Block" @click=${this.block}></input>`;
html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>Unblock</button>` :
html`<button class="w3-button w3-dark-grey" @click=${this.block}>Block</button>`;
}
let edit_profile = this.editing ? html`
<div style="flex: 1 0 50%; display: flex; flex-direction: column">
<div>
<label for="name">Name:</label>
<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
</div>
<div><label for="description">Description:</label></div>
<textarea style="flex: 1 0" 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 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>
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
<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>
</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>
<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>
</div>
<div>
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
</div>
</div>
</div>` : null;
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;

View File

@ -1,6 +1,6 @@
import {css} from './lit-all.min.js';
export let styles = css`
const tf = css`
a:link {
color: #bbf;
}
@ -46,9 +46,258 @@ div.img_caption::after {
content: ' ±';
}
blockquote {
border-left: 4px solid #fff;
margin-left: 0px;
padding-left: 8px;
code {
background-color: #444;
padding-left: 3px;
padding-right: 3px;
border: 1px dotted #fff;
border-radius: 4px;
}
`;
blockquote {
background-color: #607d8b;
border-left: 4px solid #fff;
padding: 8px;
padding-left: 12px;
}
`;
const w3 = css`
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent}a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
`;
export let styles = [tf, w3];

View File

@ -1,5 +1,6 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabConnectionsElement extends LitElement {
static get properties() {
@ -12,6 +13,8 @@ class TfTabConnectionsElement extends LitElement {
};
}
static styles = styles;
constructor() {
super();
let self = this;
@ -55,7 +58,7 @@ class TfTabConnectionsElement extends LitElement {
let self = this;
return html`
<li>
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
<button class="w3-button w3-dark-grey" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}>Connect</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
</li>
`;
@ -64,7 +67,7 @@ class TfTabConnectionsElement extends LitElement {
render_broadcast(connection) {
return html`
<li>
<input type="button" @click=${() => tfrpc.rpc.connect(connection)} value="Connect"></input>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(connection)}>Connect</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</li>
@ -78,7 +81,7 @@ class TfTabConnectionsElement extends LitElement {
render_connection(connection) {
return html`
<input type="button" @click=${() => tfrpc.rpc.closeConnection(connection.id)} value="Close"></input>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</button>
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
<ul>
@ -91,35 +94,35 @@ class TfTabConnectionsElement extends LitElement {
render() {
let self = this;
return html`
<h2>New Connection</h2>
<div style="display: flex; flex-direction: column">
<textarea id="code"></textarea>
<div class="w3-container">
<h2>New Connection</h2>
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}>Connect</button>
<h2>Broadcasts</h2>
<ul>
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
</ul>
<h2>Connections</h2>
<ul>
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
<li>${this.render_connection(x)}</li>
`)}
</ul>
<h2>Stored Connections (WIP)</h2>
<ul>
${this.stored_connections.map(x => html`
<li>
<button class="w3-button w3-dark-grey" @click=${() => self.forget_stored_connection(x)}>Forget</button>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(x)}>Connect</button>
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
</li>
`)}
</ul>
<h2>Local Accounts</h2>
<ul>
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
</ul>
</div>
<input type="button" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} value="Connect"></input>
<h2>Broadcasts</h2>
<ul>
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
</ul>
<h2>Connections</h2>
<ul>
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
<li>${this.render_connection(x)}</li>
`)}
</ul>
<h2>Stored Connections (WIP)</h2>
<ul>
${this.stored_connections.map(x => html`
<li>
<input type="button" @click=${() => self.forget_stored_connection(x)} value="Forget"></input>
<input type="button" @click=${() => tfrpc.rpc.connect(x)} value="Connect"></input>
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
</li>
`)}
</ul>
<h2>Local Accounts</h2>
<ul>
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
</ul>
`;
}
}

View File

@ -183,7 +183,9 @@ class TfTabNewsFeedElement extends LitElement {
let more;
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
more = html`
<input type="button" value="Load More" @click=${this.load_more}></input>
<p>
<button class="w3-button w3-dark-grey" @click=${this.load_more}>Load More</button>
</p>
`;
}
return html`

View File

@ -66,7 +66,7 @@ class TfTabNewsElement extends LitElement {
}
counts[type] = (counts[type] || 0) + 1;
}
return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
}
draft(event) {
@ -106,8 +106,9 @@ class TfTabNewsElement extends LitElement {
let profile = this.hash.startsWith('#@') ?
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
return html`
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
<p class="w3-bar">
<button class="w3-bar-item w3-button w3-dark-grey" @click=${this.show_more}>${this.new_messages_text()}</button>
</p>
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
${profile}

View File

@ -99,9 +99,9 @@ class TfTabQueryElement extends LitElement {
}
let self = this;
return html`
<div style="display: flex; flex-direction: row">
<textarea id="search" rows=8 style="flex: 1" @keydown=${this.search_keydown}>${this.query}</textarea>
<input type="button" value="Execute" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
<div style="display: flex; flex-direction: row; gap: 4px">
<textarea id="search" rows=8 class="w3-input w3-dark-grey" style="flex: 1; resize: vertical" @keydown=${this.search_keydown}>${this.query}</textarea>
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Execute</button>
</div>
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
<div ?hidden=${this.duration !== undefined}>Executing...</div>

View File

@ -75,9 +75,9 @@ class TfTabSearchElement extends LitElement {
}
let self = this;
return html`
<div style="display: flex; flex-direction: row">
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
<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>
</div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
`;

View File

@ -46,14 +46,14 @@ function image(node, entering) {
}
export function markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer();
writer.image = image;
var parsed = reader.parse(md || '');
let parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
parsed = hashtagify.transform(parsed);
var walker = parsed.walker();
var event, node;
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&xc5LDQt9bWp6wrPxZhvSGiMXjweSK1YbO2DReM7/2ic=.sha256"
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
}

View File

@ -35,7 +35,7 @@
<i class="fa-brands fa-windows w3-xlarge"></i>
</p>
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/releases/"><i class="fa fa-download"></i> Download</a>
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/apps/"><i class="fa fa-link"></i> Try Some Apps</a>
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/apps/"><i class="fa fa-link"></i> Try It</a>
</div>
<div class="w3-col l4 m6">
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small">
@ -120,7 +120,7 @@
<i class="fa fa-database w3-text-red w3-jumbo"></i>
<p>SQLite</p>
</a>
<a href="https://libuv.org/" class="w3-col s3">
<a href="https://github.com/libuv/libuv" class="w3-col s3">
<i class="fa fa-bolt w3-text-yellow w3-jumbo"></i>
<p>libuv</p>
</a>
@ -150,11 +150,11 @@
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
<p>CodeMirror</p>
</a>
<a href="https://speedscope.app/" class="w3-col s3">
<a href="https://github.com/jlfwong/speedscope/" class="w3-col s3">
<i class="fa fa-microscope w3-text-orange w3-jumbo"></i>
<p>Speedscope</p>
</a>
<a href="https://lit.dev/" class="w3-col s3">
<a href="https://github.com/lit/lit/" class="w3-col s3">
<i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
<p>Lit</p>
</a>

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📝",
"previous": "&JHopifgZn2TsiMCQY8HUTlDqHEDDviiu2ifvr8HHNwo=.sha256"
"previous": "&qrcrBeaWg89ikgql9hXdr68krkg+5NZkmwTbpodEW4U=.sha256"
}

View File

@ -13,7 +13,7 @@ function markdown(md) {
if (node.type === 'link') {
if (node.destination.indexOf(':') == -1 &&
node.destination.indexOf('/') == -1) {
node.destination = `#${this.wiki?.name}/${node.destination}`;
node.destination = `${node.destination}`;
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -292,13 +292,14 @@ class TfCollectionsAppElement extends LitElement {
<div class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_changed({detail: {value: wiki}})}>${wiki.name}</div>
<ul>
${Object.values(self.wiki_docs || {}).filter(doc => doc.parent === wiki?.id).sort((x, y) => x.name.localeCompare(y.name)).map(doc => html`
<li class="toc ${self.wiki_doc?.id === doc.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_doc_changed({detail: {value: doc}})}>${doc.name}</li>
<li class="toc ${self.wiki_doc?.id === doc.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer; list-style: none; text-indent: -1rem" @click=${() => self.on_wiki_doc_changed({detail: {value: doc}})}>${doc?.private ? '🔒' : '📄'} ${doc.name}</li>
`)}
</ul>
`)}
</div>
${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html`
<tf-wiki-doc
style="width: 100%"
whoami=${this.whoami}
.wiki=${this.wiki}
.value=${this.wiki_doc}></tf-wiki-doc>

View File

@ -72,9 +72,7 @@ class TfWikiDocElement extends LitElement {
}
thumbnail(md) {
//let m = md ? md.match(/\!\[image:[^\]]+]\((\&.{44}\.sha256)\)/) : undefined;
let m = md ? md.match(/.*\((\&.{44}\.sha256)\).*/) : undefined;
console.log('thumb', m);
let m = md ? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/) : undefined;
return m ? m[1] : undefined;
}
@ -171,11 +169,22 @@ class TfWikiDocElement extends LitElement {
});
}
humanSize(value) {
let units = ['B', 'kB', 'MB', 'GB'];
let i = 0;
while (i < units.length - 1 && value >= 1024) {
value /= 1024;
i++;
}
return `${Math.round(value * 10) / 10} ${units[i]}`;
}
async add_file(editor, file) {
try {
let self = this;
let buffer = await file.arrayBuffer();
let type = file.type;
let insert;
if (type.startsWith('image/')) {
let best_buffer;
let best_type;
@ -188,12 +197,16 @@ class TfWikiDocElement extends LitElement {
}
buffer = best_buffer;
type = best_type;
let id = await tfrpc.rpc.store_blob(buffer);
let name = type.split('/')[0] + ':' + file.name;
insert = `\n![${name}](${id})`;
} else {
buffer = Array.from(new Uint8Array(buffer));
let id = await tfrpc.rpc.store_blob(buffer);
let name = file.name;
insert = `\n[${name}](${id}) (${this.humanSize(buffer.length)})`;
}
let id = await tfrpc.rpc.store_blob(buffer);
let name = type.split('/')[0] + ':' + file.name;
editor.value += `\n![${name}](${id})`;
document.execCommand('insertText', false, insert);
self.on_edit({srcElement: editor});
} catch(e) {
alert(e?.message);
@ -203,13 +216,10 @@ class TfWikiDocElement extends LitElement {
paste(event) {
let self = this;
for (let item of event.clipboardData.items) {
if (item.type?.startsWith('image/')) {
let file = item.getAsFile();
if (!file) {
continue;
}
let file = item.getAsFile();
if (file) {
self.add_file(event.srcElement, file);
break;
event.preventDefault();
}
}
}
@ -225,6 +235,12 @@ class TfWikiDocElement extends LitElement {
let self = this;
let thumbnail_ref = this.thumbnail(this.blob);
return html`
<style>
a:link { color: #268bd2 }
a:visited { color: #6c71c4 }
a:hover { color: #859900 }
a:active { color: #2aa198 }
</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>

View File

@ -67,11 +67,8 @@ function socket(request, response, client) {
if (process && process.task) {
process.task.kill();
}
}
response.onError = async function(error) {
if (process && process.task) {
process.task.kill();
if (process) {
process.timeout = 0;
}
}
@ -157,7 +154,7 @@ function socket(request, response, client) {
process.lastPing = now;
}
if (again) {
if (again && process.timeout) {
setTimeout(ping, process.timeout);
}
}
@ -202,11 +199,9 @@ function socket(request, response, client) {
}
}
if (refresh) {
return {
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
};
}
response.upgrade(100, refresh ? {
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
} : {});
}
export { socket, App };

View File

@ -45,12 +45,12 @@ function readSession(session) {
let jwt_parts = session?.split('.');
if (jwt_parts?.length === 3) {
let [header, payload, signature] = jwt_parts;
header = JSON.parse(base64Decode(unb64url(header)));
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)) {
let result = JSON.parse(base64Decode(unb64url(payload)));
let result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
let now = new Date().valueOf()
if (now < result.exp) {
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
@ -64,8 +64,6 @@ function readSession(session) {
} else {
print('Invalid JWT header.');
}
} else {
print('No session JWT.');
}
}

View File

@ -1,5 +1,6 @@
import {LitElement, html, css, svg} from '/static/lit/lit-all.min.js';
let cm6;
let gSocket;
let gCurrentFile;
@ -104,28 +105,23 @@ class TfNavigationElement extends LitElement {
render_permissions() {
if (this.show_permissions) {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css">
<div style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%">
<div style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff">
<div>This app has the following permissions:</div>
${Object.keys(this.permissions).map(key => html`
<div>
<span>${key}</span>: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
<button @click=${() => this.reset_permission(key)} style='min-width: 3em; min-height: 3em">Reset</button>
<button @click=${() => this.reset_permission(key)} class='w3-button w3-red">Reset</button>
</div>
`)}
<button @click=${() => this.show_permissions = false} style="min-width: 3em; min-height: 3em">Close</button>
<button @click=${() => this.show_permissions = false} class="w3-button w3-blue">Close</button>
</div>
</div>
`;
}
}
set_access_key_title(event) {
if (!event.srcElement.title) {
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || event.srcElement.accessKey).toUpperCase()}]`;
}
}
render() {
let self = this;
return html`
@ -148,10 +144,10 @@ class TfNavigationElement extends LitElement {
<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center">
<span style="cursor: pointer" @click=${() => this.show_version = !this.show_version}>😎</span>
<span ?hidden=${!this.show_version} style="flex: 0 0; white-space: nowrap" title=${this.version?.name + ' ' + Object.entries(this.version || {}).filter(x => ['name', 'number'].indexOf(x[0]) == -1).map(x => `\n* ${x[0]}: ${x[1]}`)}>${this.version?.number}</span>
<a accesskey="h" @mouseover=${this.set_access_key_title} data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
<a accesskey="a" @mouseover=${this.set_access_key_title} data-tip="Open apps list." href="/~core/apps/">apps</a>
<a accesskey="e" @mouseover=${this.set_access_key_title} data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
<a accesskey="p" @mouseover=${this.set_access_key_title} data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
<a accesskey="h" @mouseover=${set_access_key_title} data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
<a accesskey="a" @mouseover=${set_access_key_title} data-tip="Open apps list." href="/~core/apps/">apps</a>
<a accesskey="e" @mouseover=${set_access_key_title} data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
<a accesskey="p" @mouseover=${set_access_key_title} data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
<span id="requests"></span>
${this.render_permissions()}
@ -207,7 +203,7 @@ class TfFilesElement extends LitElement {
let buffer = await file.arrayBuffer();
let text = new TextDecoder('latin1').decode(buffer);
gFiles[file.name] = {
doc: new CodeMirror.Doc(text, guessMode(file.name)),
doc: new cm6.EditorState.create({doc: text, extensions: cm6.extensions}),
buffer: buffer,
generation: -1,
isNew: true,
@ -288,32 +284,20 @@ class TfFilesPaneElement extends LitElement {
render() {
let self = this;
let expander = this.expanded ?
html`<span @click=${() => self.set_expanded(false)} class="expander">«</span>` :
html`<span @click=${() => self.set_expanded(true)} class="expander">»</span>`;
html`<div class="w3-button w3-bar-item w3-blue" style="flex: 0 0 auto; display: flex; flex-direction: row" @click=${() => self.set_expanded(false)}>
<span style="flex: 1 1" font-weight: bold; text-align: center; flex: 1">Files</span>
<span style="flex: 0 0">«</span>
</div>` :
html`<div class="w3-button w3-bar-item w3-blue" @click=${() => self.set_expanded(true)}>»</div>`;
let content = html`
<div id="files_content">
<tf-files .files=${self.files} current=${self.current} @file_click=${event => openFile(event.detail.file)}></tf-files>
<br>
<div><button @click=${() => newFile()}>New File</button></div>
<div><button @click=${() => removeFile()}>Remove File</button></div>
</div>
<tf-files style="flex: 1 1; overflow: auto" .files=${self.files} current=${self.current} @file_click=${event => openFile(event.detail.file)}></tf-files>
<div><button class="w3-bar-item w3-button w3-blue" style="width: 100%; flex: 0 0" @click=${() => newFile()} accesskey="n" @mouseover=${set_access_key_title} data-tip="Add a new, empty file to the app">📄 New File</button></div>
<div><button class="w3-bar-item w3-button w3-blue" style="width: 100%; flex: 0 0" @click=${() => removeFile()} accesskey="r" @mouseover=${set_access_key_title} data-tip="Remove the selected file from the app">🚮 Remove File</button></div>
`;
return html`
<style>
.expander {
font-weight: bold;
width: 100%;
right: 0;
flex: 0;
padding: 0.25em;
cursor: pointer;
}
</style>
<div>
<div style="display: flex; flex-direction: row">
${this.expanded ? html`<span style="font-weight: bold; text-align: center; flex: 1">Files</span>` : undefined}
${expander}
</div>
<link type="text/css" rel="stylesheet" href="/static/w3.css">
<div style="display: flex; flex-direction: column; height: 100%">
${expander}
${this.expanded ? content : undefined}
</div>
`;
@ -441,7 +425,7 @@ function is_edit_only() {
return window.location.search == '?editonly=1' || window.innerWidth < 1024;
}
function edit() {
async function edit() {
if (editing()) {
return;
}
@ -450,33 +434,17 @@ function edit() {
document.getElementById("editPane").style.display = 'flex';
document.getElementById('viewPane').style.display = is_edit_only() ? 'none' : 'flex';
ensureLoaded([
{tagName: "script", attributes: {src: "/codemirror/codemirror.min.js"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/base16-dark.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/matchesonscrollbar.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/dialog.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/codemirror.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/lint.css"}},
{tagName: "script", attributes: {src: "/codemirror/trailingspace.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/dialog.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/search.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/searchcursor.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/jump-to-line.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/matchesonscrollbar.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/annotatescrollbar.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/javascript.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/css.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/xml.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/htmlmixed.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/lint.js"}},
{tagName: "script", attributes: {src: "/codemirror/jshint.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/javascript-lint.min.js"}},
], function() {
load().catch(function(error) {
alert(error);
closeEditor();
});
});
try {
if (!gEditor) {
cm6 = await import('/codemirror/cm6.js');
gEditor = cm6.TildeFriendsEditorView(document.getElementById("editor"));
}
gEditor.onDocChange = updateFiles;
await load();
} catch (error) {
alert(`${error.message}\n\n${error.stack}`);
closeEditor();
}
}
function trace() {
@ -497,73 +465,49 @@ function loadFile(name, id) {
}
return response.text();
}).then(function(text) {
gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name));
gFiles[name].doc = cm6.EditorState.create({doc: text, extensions: cm6.extensions});
gFiles[name].original = gFiles[name].doc.doc.toString();
if (!Object.values(gFiles).some(x => !x.doc)) {
openFile(Object.keys(gFiles).sort()[0]);
}
});
}
function load(path) {
return fetch((path || url()) + 'view').then(function(response) {
if (!response.ok) {
if (response.status == 404) {
return null;
} else {
throw new Error(response.status + ' ' + response.statusText);
}
}
return response.json();
}).then(function(json) {
if (!gEditor) {
gEditor = CodeMirror.fromTextArea(document.getElementById("editor"), {
'theme': 'base16-dark',
'lineNumbers': true,
'tabSize': 4,
'indentUnit': 4,
'indentWithTabs': true,
'showTrailingSpace': true,
'gutters': ['CodeMirror-lint-markers'],
'mode': {'js': 'javascript'}[(path || url()).split('.').pop()],
'lint': {
'options': {
esversion: 2021,
varstmt: true,
},
},
});
gEditor.on('changes', function() {
updateFiles();
});
}
gFiles = {};
let isApp = false;
let promises = [];
async function load(path) {
let response = await fetch((path || url()) + 'view');
let json;
if (response.ok) {
json = await response.json();
} else if (response.status != 404) {
throw new Error(response.status + ' ' + response.statusText);
}
gFiles = {};
let isApp = false;
let promises = [];
if (json && json['type'] == 'tildefriends-app') {
isApp = true;
Object.keys(json['files']).forEach(function(name) {
gFiles[name] = {};
promises.push(loadFile(name, json['files'][name]));
});
if (Object.keys(json['files']).length == 0) {
document.getElementById("editPane").style.display = 'flex';
}
gApp = json;
gApp.emoji = gApp.emoji || '📦';
document.getElementById('icon').value = gApp.emoji;
}
if (!isApp) {
if (json && json['type'] == 'tildefriends-app') {
isApp = true;
Object.keys(json['files']).forEach(function(name) {
gFiles[name] = {};
promises.push(loadFile(name, json['files'][name]));
});
if (Object.keys(json['files']).length == 0) {
document.getElementById("editPane").style.display = 'flex';
let text = '// New script.\n';
gCurrentFile = 'app.js';
gFiles[gCurrentFile] = {
doc: new CodeMirror.Doc(text, guessMode(gCurrentFile)),
};
openFile(gCurrentFile);
}
return Promise.all(promises);
});
gApp = json;
gApp.emoji = gApp.emoji || '📦';
document.getElementById('icon').innerHTML = gApp.emoji;
}
if (!isApp) {
document.getElementById("editPane").style.display = 'flex';
let text = '// New script.\n';
gCurrentFile = 'app.js';
gFiles[gCurrentFile] = {
doc: cm6.EditorState.create({doc: text, extensions: cm6.extensions}),
};
openFile(gCurrentFile);
}
return Promise.all(promises);
}
function closeEditor() {
@ -579,8 +523,8 @@ function explodePath() {
function save(save_to) {
document.getElementById("save").disabled = true;
if (gCurrentFile) {
gFiles[gCurrentFile].doc = gEditor.getDoc();
if (!gFiles[gCurrentFile].isNew && !gFiles[gCurrentFile].doc.isClean(gFiles[gCurrentFile].doc.changeGeneration())) {
gFiles[gCurrentFile].doc = gEditor.state;
if (!gFiles[gCurrentFile].isNew && !gFiles[gCurrentFile].doc.doc.toString() == gFiles[gCurrentFile].original) {
delete gFiles[gCurrentFile].buffer;
}
}
@ -598,7 +542,7 @@ function save(save_to) {
let promises = [];
for (let name of Object.keys(gFiles)) {
let file = gFiles[name];
if (!file.isNew && file.doc.isClean(file.generation)) {
if (!file.isNew && file.doc.doc.toString() == file.original) {
continue;
}
@ -609,7 +553,7 @@ function save(save_to) {
headers: {
'Content-Type': 'application/binary',
},
body: file.buffer ?? file.doc.getValue(),
body: file.buffer ?? file.doc.doc.toString(),
}).then(function(response) {
if (!response.ok) {
throw new Error('Saving "' + name + '": ' + response.status + ' ' + response.statusText);
@ -654,7 +598,7 @@ function save(save_to) {
}).finally(function() {
document.getElementById("save").disabled = false;
Object.values(gFiles).forEach(function(file) {
file.generation = file.doc.changeGeneration();
file.original = file.doc.doc.toString();
});
updateFiles();
});
@ -664,7 +608,7 @@ function changeIcon() {
let value = prompt('Enter a new app icon emoji:');
if (value !== undefined) {
gApp.emoji = value || '📦';
document.getElementById('icon').value = gApp.emoji;
document.getElementById('icon').innerHTML = gApp.emoji;
}
}
@ -752,6 +696,8 @@ function api_requestPermission(permission, id) {
let check = document.createElement('input');
check.id = 'permissions_remember_check';
check.type = 'checkbox';
check.classList.add('w3-check');
check.classList.add('w3-blue');
div.appendChild(check);
let label = document.createElement('label');
label.htmlFor = check.id;
@ -777,8 +723,8 @@ function api_requestPermission(permission, id) {
div = document.createElement('div');
for (let option of k_options) {
let button = document.createElement('button');
button.style.minWidth = '3em';
button.style.minHeight = '3em';
button.classList.add('w3-button');
button.classList.add('w3-blue');
button.innerText = option.text;
button.id = option.id;
button.onclick = function() {
@ -1035,11 +981,13 @@ function connectSocket(path) {
}
function openFile(name) {
let newDoc = (name && gFiles[name]) ? gFiles[name].doc : new CodeMirror.Doc("", guessMode(name));
let oldDoc = gEditor.swapDoc(newDoc);
let newDoc = (name && gFiles[name]) ? gFiles[name].doc : cm6.EditorState.create({doc: "", extensions: cm6.extensions});
let oldDoc = gEditor.state;
gEditor.setState(newDoc);
if (gFiles[gCurrentFile]) {
gFiles[gCurrentFile].doc = oldDoc;
if (!gFiles[gCurrentFile].isNew && gFiles[gCurrentFile].doc.isClean(oldDoc.generation)) {
if (!gFiles[gCurrentFile].isNew && gFiles[gCurrentFile].doc.doc.toString() == oldDoc.doc.toString()) {
delete gFiles[gCurrentFile].buffer;
}
}
@ -1052,7 +1000,7 @@ function updateFiles() {
let files = document.getElementsByTagName("tf-files-pane")[0];
if (files) {
files.files = Object.fromEntries(Object.keys(gFiles).map(file => [file, {
clean: gFiles[file].doc.isClean(gFiles[file].generation),
clean: (file == gCurrentFile ? gEditor.state.doc.toString() : gFiles[file].doc.doc.toString()) == gFiles[file].original,
}]));
files.current = gCurrentFile;
}
@ -1061,7 +1009,7 @@ function updateFiles() {
function makeNewFile(name) {
gFiles[name] = {
doc: new CodeMirror.Doc("", guessMode(name)),
doc: cm6.EditorState.create({extensions: cm6.extensions}),
generation: -1,
};
openFile(name);
@ -1081,6 +1029,97 @@ function removeFile() {
}
}
async function appExport() {
let JsZip = (await import('/static/jszip.min.js')).default;
let owner = window.location.pathname.split('/')[1].replace('~', '');
let name = window.location.pathname.split('/')[2];
let zip = new JsZip();
zip.file(`${name}.json`, JSON.stringify({
type: "tildefriends-app",
emoji: gApp.emoji || '📦',
}));
for (let file of Object.keys(gFiles)) {
zip.file(`${name}/${file}`, gFiles[file].buffer ?? gFiles[file].doc.doc.toString());
}
let content = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
});
let a = document.createElement('a');
a.href = URL.createObjectURL(content);
a.download = `${owner}_${name}.zip`;
a.click();
}
async function save_file_to_blob_id(name, file) {
console.log(`Saving ${name}.`);
let response = await fetch('/save', {
method: 'POST',
headers: {
'Content-Type': 'application/binary',
},
body: file,
});
if (!response.ok) {
throw new Error('Saving "' + name + '": ' + response.status + ' ' + response.statusText);
}
let blob_id = await response.text();
if (blob_id.charAt(0) == '/') {
blob_id = blob_id.substr(1);
}
return blob_id;
}
async function appImport() {
let JsZip = (await import('/static/jszip.min.js')).default;
let input = document.createElement('input');
input.type = 'file';
input.click();
input.onchange = async function() {
try {
for (let file of input.files) {
if (file.type != 'application/zip') {
console.log('This does not look like a .zip.');
continue;
}
let buffer = await file.arrayBuffer();
let zip = new JsZip();
await zip.loadAsync(buffer);
let app_object;
let app_name;
for (let [name, object] of Object.entries(zip.files)) {
if (name.endsWith('.json') && name.indexOf('/') == -1) {
try {
let parsed = JSON.parse(await object.async('text'));
if (parsed.type == 'tildefriends-app') {
app_object = parsed;
app_name = name.substring(0, name.length - '.json'.length);
break;
}
} catch (e) {
console.log(e);
}
}
}
if (app_object) {
app_object.files = {};
for (let [name, object] of Object.entries(zip.files)) {
if (!name.startsWith(app_name + '/') || name.endsWith('/')) {
continue;
}
app_object.files[name.substring(app_name.length + '/'.length)] = await save_file_to_blob_id(name, await object.async('arrayBuffer'));
}
let path = '/' + await save_file_to_blob_id(`${app_name}.json`, JSON.stringify(app_object)) + '/';
console.log('Redirecting to:', path);
window.location.pathname = path;
}
}
} catch (e) {
alert(e.toString());
}
}
}
window.addEventListener("load", function() {
window.addEventListener("hashchange", hashChange);
window.addEventListener("focus", focus);
@ -1092,6 +1131,8 @@ window.addEventListener("load", function() {
document.getElementById('save').addEventListener('click', () => save());
document.getElementById('icon').addEventListener('click', () => changeIcon());
document.getElementById('delete').addEventListener('click', () => deleteApp());
document.getElementById('export').addEventListener('click', () => appExport());
document.getElementById('import').addEventListener('click', () => appImport());
document.getElementById('trace_button').addEventListener('click', function(event) {
event.preventDefault();
trace();

View File

@ -2,9 +2,7 @@ import * as app from './app.js';
import * as auth from './auth.js';
import * as form from './form.js';
import * as http from './http.js';
import * as httpd from './httpd.js';
let gProcessIndex = 0;
let gProcesses = {};
let gStatsTimer = false;
@ -30,15 +28,18 @@ const k_magic_bytes = [
{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'},
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
{uri: '/favicon.png', type: 'image/png'},
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
{uri: '/favicon.png', type: 'image/png'},
{uri: '/jszip.min.js', type: 'text/javascript; charset=UTF-8'},
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
{uri: '/w3.css', type: 'text/css; charset=UTF-8'},
];
const k_global_settings = {
@ -125,8 +126,7 @@ function invoke(handlers, argv) {
function broadcastEvent(eventName, argv) {
let promises = [];
for (let i in gProcesses) {
let process = gProcesses[i];
for (let process of Object.values(gProcesses)) {
if (process.eventHandlers[eventName]) {
promises.push(invoke(process.eventHandlers[eventName], argv));
}
@ -137,8 +137,7 @@ function broadcastEvent(eventName, argv) {
function broadcast(message) {
let sender = this;
let promises = [];
for (let i in gProcesses) {
let process = gProcesses[i];
for (let process of Object.values(gProcesses)) {
if (process != sender
&& process.packageOwner == sender.packageOwner
&& process.packageName == sender.packageName) {
@ -151,9 +150,7 @@ function broadcast(message) {
function getUser(caller, process) {
return {
name: process.userName,
key: process.key,
index: process.index,
packageOwner: process.packageOwner,
packageName: process.packageName,
credentials: process.credentials,
@ -208,8 +205,6 @@ async function getProcessBlob(blobId, key, options) {
print("Creating task for " + blobId + " " + key);
process = {};
process.key = key;
process.index = gProcessIndex++;
process.userName = 'user' + process.index;
process.credentials = options.credentials || {};
process.task = new Task();
process.eventHandlers = {};
@ -320,6 +315,10 @@ async function getProcessBlob(blobId, key, options) {
throw Error(`Permission denied: ${permission}.`);
}
},
app: {
owner: options?.packageOwner,
name: options?.packageName,
},
url: options?.url,
}
};
@ -396,6 +395,24 @@ async function getProcessBlob(blobId, key, options) {
return ssb.createIdentity(process.credentials.session.name);
}
};
imports.ssb.addIdentity = function(id) {
if (process.credentials &&
process.credentials.session &&
process.credentials.session.name) {
return Promise.resolve(imports.core.permissionTest('ssb_id_add')).then(function() {
return ssb.addIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.deleteIdentity = function(id) {
if (process.credentials &&
process.credentials.session &&
process.credentials.session.name) {
return Promise.resolve(imports.core.permissionTest('ssb_id_delete')).then(function() {
return ssb.deleteIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.getOwnerIdentities = function() {
if (options.packageOwner) {
return ssb.getIdentities(options.packageOwner);
@ -408,6 +425,15 @@ async function getProcessBlob(blobId, key, options) {
return ssb.getIdentities(process.credentials.session.name);
}
};
imports.ssb.getPrivateKey = function(id) {
if (process.credentials &&
process.credentials.session &&
process.credentials.session.name) {
return Promise.resolve(imports.core.permissionTest('ssb_id_export')).then(function() {
return ssb.getPrivateKey(process.credentials.session.name, id);
});
}
};
imports.ssb.appendMessageWithIdentity = function(id, message) {
if (process.credentials &&
process.credentials.session &&
@ -952,9 +978,12 @@ function stringResponse(response, data) {
}
loadSettings().then(function() {
let httpd_impl = (tildefriends.args.httpdc ? httpdc : httpd);
httpd_impl.all("/login", auth.handler);
httpd_impl.all("", function(request, response) {
if (tildefriends.https_port && gGlobalSettings.http_redirect) {
httpd.set_http_redirect(gGlobalSettings.http_redirect);
}
httpd.all("/login", auth.handler);
httpd.all("/app/socket", app.socket);
httpd.all("", function default_http_handler(request, response) {
let match;
if (request.uri === "/" || request.uri === "") {
try {
@ -986,16 +1015,6 @@ loadSettings().then(function() {
return staticFileHandler(request, response, null, request.uri);
} else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/trace$/.exec(request.uri)) {
return stringResponse(response, trace());
} else if (match = /^\/disconnections$/.exec(request.uri)) {
return stringResponse(response, JSON.stringify(disconnectionsDebug(), null, 2));
} else if (match = /^\/debug$/.exec(request.uri)) {
return stringResponse(response, JSON.stringify(getDebug(), null, 2));
} else if (match = /^\/hitches$/.exec(request.uri)) {
return stringResponse(response, JSON.stringify(getHitches(), null, 2));
} else if (match = /^\/mem$/.exec(request.uri)) {
return stringResponse(response, JSON.stringify(getAllocations(), null, 2));
} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
return wellKnownHandler(request, response, match[1]);
} else {
@ -1004,8 +1023,29 @@ loadSettings().then(function() {
return response.end(data);
}
});
httpd_impl.registerSocketHandler("/app/socket", app.socket);
httpd_impl.start(tildefriends.http_port);
let port = httpd.start(tildefriends.http_port);
if (tildefriends.args.out_http_port_file) {
print("Writing the port file.");
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
print("Wrote the port file:", tildefriends.args.out_http_port_file, r);
}).catch(function() {
print("Failed to write the port file.");
});
}
if (tildefriends.https_port) {
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 context = new TlsContext();
context.setPrivateKey(privateKey);
context.setCertificate(certificate);
httpd.start(tildefriends.https_port, context);
}
start_tls();
}
}).catch(function(error) {
print('Failed to load settings.');
printError({print: print}, error);

View File

@ -1,598 +0,0 @@
import * as core from './core.js';
let gHandlers = [];
let gSocketHandlers = [];
let gBadRequests = {};
const kRequestTimeout = 5000;
const kStallTimeout = 60000;
function logError(error) {
print("ERROR " + error);
if (error.stackTrace) {
print(error.stackTrace);
}
}
function addHandler(handler) {
let added = false;
for (let i in gHandlers) {
if (gHandlers[i].path == handler.path) {
gHandlers[i] = handler;
added = true;
break;
}
}
if (!added) {
gHandlers.push(handler);
added = true;
}
}
function all(prefix, handler) {
addHandler({
owner: this,
path: prefix,
invoke: handler,
});
}
function registerSocketHandler(prefix, handler) {
gSocketHandlers.push({
owner: this,
path: prefix,
invoke: handler,
});
}
function Request(method, uri, version, headers, body, client) {
this.method = method;
let index = uri.indexOf("?");
if (index != -1) {
this.uri = uri.slice(0, index);
this.query = uri.slice(index + 1);
} else {
this.uri = uri;
this.query = undefined;
}
this.version = version || '';
this.headers = headers;
this.client = {peerName: client.peerName, tls: client.tls};
this.body = body;
return this;
}
function findHandler(request) {
let matchedHandler = null;
for (let name in gHandlers) {
let handler = gHandlers[name];
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
matchedHandler = handler;
break;
}
}
return matchedHandler;
}
function findSocketHandler(request) {
let matchedHandler = null;
for (let name in gSocketHandlers) {
let handler = gSocketHandlers[name];
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
matchedHandler = handler;
break;
}
}
return matchedHandler;
}
function Response(request, client) {
let kStatusText = {
101: "Switching Protocols",
200: 'OK',
303: 'See other',
304: 'Not Modified',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'File not found',
500: 'Internal server error',
};
let _started = false;
let _finished = false;
let _keepAlive = false;
let _chunked = false;
return {
writeHead: function(status) {
if (_started) {
throw new Error("Response.writeHead called multiple times.");
}
let reason;
let headers;
if (arguments.length == 3) {
reason = arguments[1];
headers = arguments[2];
} else {
reason = kStatusText[status];
headers = arguments[1];
}
let lowerHeaders = {};
let requestVersion = request.version.split("/")[1].split(".");
let responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0";
let headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n";
headers['Server'] = `Tilde Friends/${version().number}`;
for (let i in headers) {
headerString += i + ": " + headers[i] + "\r\n";
lowerHeaders[i.toLowerCase()] = headers[i];
}
if ("connection" in lowerHeaders) {
_keepAlive = lowerHeaders["connection"].toLowerCase() == "keep-alive";
} else {
_keepAlive = ((request.version == "HTTP/1.0" && ("connection" in lowerHeaders && lowerHeaders["connection"].toLowerCase() == "keep-alive")) ||
(request.version == "HTTP/1.1" && (!("connection" in lowerHeaders) || lowerHeaders["connection"].toLowerCase() != "close")));
headerString += "Connection: " + (_keepAlive ? "keep-alive" : "close") + "\r\n";
}
_chunked = _keepAlive && !("content-length" in lowerHeaders);
if (_chunked) {
headerString += "Transfer-Encoding: chunked\r\n";
}
headerString += "\r\n";
_started = true;
client.write(headerString).catch(function() {});
},
end: function(data) {
if (_finished) {
throw new Error("Response.end called multiple times.");
}
if (data) {
if (_chunked) {
client.write(data.length.toString(16) + "\r\n" + data + "\r\n" + "0\r\n\r\n").catch(function() {});
} else {
client.write(data).catch(function() {});
}
} else if (_chunked) {
client.write("0\r\n\r\n").catch(function() {});
}
_finished = true;
if (!_keepAlive) {
client.shutdown().catch(function() {});
}
},
reportError: function(error) {
if (!_started) {
client.write("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n").catch(function() {});
}
if (!_finished) {
client.write("500 Internal Server Error\r\n\r\n" + error?.stackTrace).catch(function() {});
}
logError(client.peerName + " - - [" + new Date() + "] " + error);
},
isConnected: function() { return client.isConnected; },
};
}
function handleRequest(request, response) {
let handler = findHandler(request);
print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\"");
if (handler) {
try {
Promise.resolve(handler.invoke(request, response)).catch(function(error) {
response.reportError(error);
request.client.close();
});
} catch (error) {
response.reportError(error);
request.client.close();
}
} else {
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
response.end("No handler found for request: " + request.uri);
}
}
function handleWebSocketRequest(request, response, client) {
let buffer = new Uint8Array(0);
let frame;
let frameOpCode = 0x0;
let handler = findSocketHandler(request);
if (!handler) {
client.close();
return;
}
if (client) {
response.send = function(message, opCode) {
if (opCode === undefined) {
opCode = 0x2;
}
if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) {
message = utf8Encode(message);
}
let fin = true;
let packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
let mask = false;
if (message.length < 126) {
packet.push((mask ? (1 << 7) : 0) | message.length);
} else if (message.length < (1 << 16)) {
packet.push((mask ? (1 << 7) : 0) | 126);
packet.push((message.length >> 8) & 0xff);
packet.push(message.length & 0xff);
} else {
let high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
let low = message.length & 0xffffffff;
packet.push((mask ? (1 << 7) : 0) | 127);
packet.push((high >> 24) & 0xff);
packet.push((high >> 16) & 0xff);
packet.push((high >> 8) & 0xff);
packet.push((high >> 0) & 0xff);
packet.push((low >> 24) & 0xff);
packet.push((low >> 16) & 0xff);
packet.push((low >> 8) & 0xff);
packet.push(low & 0xff);
}
let array = new Uint8Array(packet.length + message.length);
array.set(packet, 0);
array.set(message, packet.length);
try {
return client.write(array);
} catch (error) {
client.close();
throw error;
}
}
}
response.onMessage = null;
let extra_headers = handler.invoke(request, response);
if (client) {
client.read(function(data) {
if (data) {
let newBuffer = new Uint8Array(buffer.length + data.length);
newBuffer.set(buffer, 0);
newBuffer.set(data, buffer.length);
buffer = newBuffer;
while (buffer.length >= 2) {
let bits0 = buffer[0];
let bits1 = buffer[1];
if (bits1 & (1 << 7) == 0) {
// Unmasked message.
client.close();
}
let opCode = bits0 & 0xf;
let fin = bits0 & (1 << 7);
let payloadLength = bits1 & 0x7f;
let maskStart = 2;
if (payloadLength == 126) {
payloadLength = 0;
for (let i = 0; i < 2; i++) {
payloadLength <<= 8;
payloadLength |= buffer[2 + i];
}
maskStart = 4;
} else if (payloadLength == 127) {
payloadLength = 0;
for (let i = 0; i < 8; i++) {
payloadLength <<= 8;
payloadLength |= buffer[2 + i];
}
maskStart = 10;
}
let havePayload = buffer.length >= payloadLength + 2 + 4;
if (havePayload) {
let mask =
buffer[maskStart + 0] |
buffer[maskStart + 1] << 8 |
buffer[maskStart + 2] << 16 |
buffer[maskStart + 3] << 24;
let dataStart = maskStart + 4;
let payload = buffer.slice(dataStart, dataStart + payloadLength);
let decoded = maskBytes(payload, mask);
buffer = buffer.slice(dataStart + payloadLength);
if (frame) {
let newBuffer = new Uint8Array(frame.length + decoded.length);
newBuffer.set(frame, 0);
newBuffer.set(decoded, frame.length);
frame = newBuffer;
} else {
frame = decoded;
}
if (opCode) {
frameOpCode = opCode;
}
if (fin) {
if (response.onMessage) {
response.onMessage({
data: frameOpCode == 0x1 ? utf8Decode(frame) : frame,
opCode: frameOpCode,
});
}
frame = undefined;
}
} else {
break;
}
}
} else {
response.onClose();
client.close();
}
});
client.onError(function(error) {
logError(client.peerName + " - - [" + new Date() + "] " + error);
response.onError(error);
});
}
let headers = {
"Upgrade": "websocket",
"Connection": "Upgrade",
"Sec-WebSocket-Accept": webSocketAcceptResponse(request.headers["sec-websocket-key"]),
};
if (request.headers["sec-websocket-version"] != "13") {
headers["Sec-WebSocket-Version"] = "13";
}
response.writeHead(101, Object.assign({}, headers, extra_headers));
}
function webSocketAcceptResponse(key) {
let kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
return base64Encode(sha1Digest(key + kMagic));
}
function badRequest(client, reason) {
let now = new Date();
let count = 0;
let old = gBadRequests[client.peerName];
if (!old) {
gBadRequests[client.peerName] = {
expire: new Date(now.getTime() + 1 * 60 * 1000),
count: 1,
reason: reason,
};
count = 1;
} else {
old.count++;
old.reason = reason;
count = old.count;
}
new Response({version: '1.0'}, client).reportError(reason + ': ' + count);
client.close();
}
function allowRequest(client) {
let old = gBadRequests[client.peerName];
if (old) {
let now = new Date();
if (old.expire < now) {
delete gBadRequests[client.peerName];
return true;
} else {
return old.count < 3;
}
} else {
return true;
}
}
function handleConnection(client) {
if (!allowRequest(client)) {
print('Rejecting client for too many bad requests: ', client.peerName, gBadRequests[client.peerName].reason);
client.info = 'rejected';
client.close();
return;
}
client.info = 'accepted';
let inputBuffer = new Uint8Array(0);
let request;
let headers = {};
let parsing_header = true;
let bodyToRead = -1;
let body;
let readCount = 0;
let isWebsocket = false;
client.setActivityTimeout(kRequestTimeout);
function reset() {
request = undefined;
headers = {};
parsing_header = true;
bodyToRead = -1;
body = undefined;
client.info = 'reset';
client.setActivityTimeout(kRequestTimeout);
}
function finish() {
client.info = 'finishing';
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
let response = new Response(requestObject, client);
try {
handleRequest(requestObject, response)
if (client.isConnected) {
reset();
}
} catch (error) {
response.reportError(error);
client.close();
}
}
client.onError(function(error) {
logError(client.peerName + " - - [" + new Date() + "] " + error);
});
client.read(function(data) {
readCount++;
if (data) {
let newBuffer = new Uint8Array(inputBuffer.length + data.length);
newBuffer.set(inputBuffer, 0);
newBuffer.set(data, inputBuffer.length);
inputBuffer = newBuffer;
if (parsing_header)
{
let result = parseHttpRequest(inputBuffer, inputBuffer.length - data.length);
if (result) {
if (typeof result === 'number') {
if (result == -2) {
/* More. */
} else {
badRequest(client, `Bad request(parse=${result}, length=${inputBuffer.length - data.length}).`);
return;
}
} else if (typeof result === 'object') {
client.setActivityTimeout(kStallTimeout);
request = [
result.method,
result.path,
`HTTP/1.${result.minor_version}`,
];
headers = Object.fromEntries(Object.entries(result.headers).map(x => [x[0].toLowerCase(), x[1]]));
parsing_header = false;
inputBuffer = inputBuffer.slice(result.bytes_parsed);
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect && !result.path.startsWith('/.well-known/')) {
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
let response = new Response(requestObject, client);
response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"});
response.end();
return;
}
if (headers["content-length"] != undefined) {
bodyToRead = parseInt(headers["content-length"]);
if (bodyToRead > 16 * 1024 * 1024) {
badRequest(client, 'Request too large: ' + bodyToRead + '.');
return;
}
body = new Uint8Array(bodyToRead);
client.info = 'waiting for body';
} else if (headers["connection"]
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
&& headers["upgrade"]
&& headers["upgrade"].toLowerCase() == "websocket") {
isWebsocket = true;
client.info = 'websocket';
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
let response = new Response(requestObject, client);
handleWebSocketRequest(requestObject, response, client);
/* Prevent the timeout from disconnecting us. */
client.setActivityTimeout();
} else {
finish();
}
}
}
}
if (!parsing_header && inputBuffer.length)
{
let offset = body.length - bodyToRead;
let length = Math.min(inputBuffer.length, body.length - offset);
if (inputBuffer.length > body.length - offset) {
body.set(inputBuffer.slice(0, length), offset);
inputBuffer = inputBuffer.slice(length);
} else {
body.set(inputBuffer, offset);
inputBuffer = inputBuffer.slice(inputBuffer.length);
}
bodyToRead -= length;
if (bodyToRead <= 0) {
finish();
}
}
} else {
client.info = 'EOF';
client.close();
}
});
}
let kBacklog = 8;
let kHost = platform() == 'haiku' ? 'localhost' : '::';
function start() {
let socket = new Socket();
socket.bind(kHost, tildefriends.http_port).then(function(port) {
print("bound to", port);
print("checking", tildefriends.args.out_http_port_file);
if (tildefriends.args.out_http_port_file) {
print("going to write the file");
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
print("wrote port file", tildefriends.args.out_http_port_file, r);
}).catch(function() {
print("failed to write port file");
});
}
let listenResult = socket.listen(kBacklog, async function() {
try {
let client = await socket.accept();
client.noDelay = true;
handleConnection(client);
} catch (error) {
logError("[" + new Date() + "] accept error " + error);
}
});
}).catch(function(error) {
logError("[" + new Date() + "] bind error " + error);
});
if (tildefriends.https_port) {
let tls = {};
let secureSocket = new Socket();
secureSocket.bind(kHost, tildefriends.https_port).then(function() {
return secureSocket.listen(kBacklog, async function() {
try {
let client = await secureSocket.accept();
client.noDelay = true;
client.tls = true;
const kCertificatePath = "data/httpd/certificate.pem";
const kPrivateKeyPath = "data/httpd/privatekey.pem";
let stat = await Promise.all([
await File.stat(kCertificatePath),
await File.stat(kPrivateKeyPath),
]);
if (!tls.context ||
tls.certStat.mtime != stat[0].mtime ||
tls.certStat.size != stat[0].size ||
tls.keyStat.mtime != stat[1].mtime ||
tls.keyStat.size != stat[1].size) {
print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath);
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
let certificate = utf8Decode(await File.readFile(kCertificatePath));
tls.context = new TlsContext();
tls.context.setPrivateKey(privateKey);
tls.context.setCertificate(certificate);
tls.certStat = stat[0];
tls.keyStat = stat[1];
}
let result = client.startTls(tls.context);
handleConnection(client);
return result;
} catch (error) {
logError("[" + new Date() + "] " + error);
}
});
}).catch(function(error) {
logError("[" + new Date() + "] bind error " + error);
});
}
}
export { all, start, registerSocketHandler };

View File

@ -3,31 +3,39 @@
<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">
<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()}]`;
}
}
</script>
</head>
<body style="display: flex; flex-flow: column">
<body style="display: flex; flex-flow: column; width: 100vw; height: 100vh; position: absolute; max-width: 100%; max-height: 100%">
<tf-navigation></tf-navigation>
<div id="content" class="hbox" style="flex: 1 1; width: 100%">
<div id="editPane" class="vbox" style="flex: 1 1; display: none">
<div class="navigation hbox">
<input type="button" id="closeEditor" name="closeEditor" value="Close">
<input type="button" id="save" name="save" value="Save">
<input type="button" id="icon" name="icon" value="📦">
<input type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
<input type="button" id="delete" name="delete" value="Delete">
<input type="button" id="trace_button" value="Trace">
<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 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>
<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>
</div>
<div class="hbox" style="height: 100%">
<tf-files-pane></tf-files-pane>
<div id="docPane" style="display: flex; flex: 1 1 50%; flex-flow: column">
<div style="flex: 1 1 50%; position: relative">
<textarea id="editor" class="main"></textarea>
</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>
</div>
<div id="viewPane" class="vbox" style="flex: 1 1; overflow: auto">
<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>
</div>
</div>

1
core/jszip.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -64,14 +64,8 @@ a:active {
color: #eee8d5;
}
.CodeMirror {
.cm-editor {
height: 100%;
padding: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
position: absolute;
}
.cm-tab {

235
core/w3.css Normal file
View 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}

View File

@ -1 +0,0 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(t){"use strict";function e(t,e){function i(t){clearTimeout(n.doRedraw),n.doRedraw=setTimeout(function(){n.redraw()},t)}this.cm=t,this.options=e,this.buttonHeight=e.scrollButtonHeight||t.getOption("scrollButtonHeight"),this.annotations=[],this.doRedraw=this.doUpdate=null,this.div=t.getWrapperElement().appendChild(document.createElement("div")),this.div.style.cssText="position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none",this.computeScale();var n=this;t.on("refresh",this.resizeHandler=function(){clearTimeout(n.doUpdate),n.doUpdate=setTimeout(function(){n.computeScale()&&i(20)},100)}),t.on("markerAdded",this.resizeHandler),t.on("markerCleared",this.resizeHandler),!1!==e.listenForChanges&&t.on("changes",this.changeHandler=function(){i(250)})}t.defineExtension("annotateScrollbar",function(t){return new e(this,t="string"==typeof t?{className:t}:t)}),t.defineOption("scrollButtonHeight",0),e.prototype.computeScale=function(){var t=this.cm,t=(t.getWrapperElement().clientHeight-t.display.barHeight-2*this.buttonHeight)/t.getScrollerElement().scrollHeight;if(t!=this.hScale)return this.hScale=t,!0},e.prototype.update=function(t){this.annotations=t,this.redraw()},e.prototype.redraw=function(t){!1!==t&&this.computeScale();var n=this.cm,e=this.hScale,i=document.createDocumentFragment(),o=this.annotations,r=n.getOption("lineWrapping"),a=r&&1.5*n.defaultTextHeight(),s=null,h=null;function l(t,e){var i;return s!=t.line&&(s=t.line,h=n.getLineHandle(t.line),(i=n.getLineHandleVisualStart(h))!=h&&(s=n.getLineNumber(i),h=i)),h.widgets&&h.widgets.length||r&&h.height>a?n.charCoords(t,"local")[e?"top":"bottom"]:n.heightAtLine(h,"local")+(e?0:h.height)}var d=n.lastLine();if(n.display.barWidth)for(var c,p=0;p<o.length;p++){var u=o[p];if(!(u.to.line>d)){for(var m,f,g=c||l(u.from,!0)*e,H=l(u.to,!1)*e;p<o.length-1&&!(o[p+1].to.line>d)&&!(H+.9<(c=l(o[p+1].from,!0)*e));)H=l((u=o[++p]).to,!1)*e;H!=g&&(m=Math.max(H-g,3),(f=i.appendChild(document.createElement("div"))).style.cssText="position: absolute; right: 0px; width: "+Math.max(n.display.barWidth-1,2)+"px; top: "+(g+this.buttonHeight)+"px; height: "+m+"px",f.className=this.options.className,u.id&&f.setAttribute("annotation-id",u.id))}}this.div.textContent="",this.div.appendChild(i)},e.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler),this.cm.off("markerAdded",this.resizeHandler),this.cm.off("markerCleared",this.resizeHandler),this.changeHandler&&this.cm.off("changes",this.changeHandler),this.div.parentNode.removeChild(this.div)}});

View File

@ -1 +0,0 @@
.cm-s-base16-dark.CodeMirror{background:#151515;color:#e0e0e0}.cm-s-base16-dark div.CodeMirror-selected{background:#303030}.cm-s-base16-dark .CodeMirror-line::selection,.cm-s-base16-dark .CodeMirror-line>span::selection,.cm-s-base16-dark .CodeMirror-line>span>span::selection{background:rgba(48,48,48,.99)}.cm-s-base16-dark .CodeMirror-line::-moz-selection,.cm-s-base16-dark .CodeMirror-line>span::-moz-selection,.cm-s-base16-dark .CodeMirror-line>span>span::-moz-selection{background:rgba(48,48,48,.99)}.cm-s-base16-dark .CodeMirror-gutters{background:#151515;border-right:0}.cm-s-base16-dark .CodeMirror-guttermarker{color:#ac4142}.cm-s-base16-dark .CodeMirror-guttermarker-subtle{color:#505050}.cm-s-base16-dark .CodeMirror-linenumber{color:#505050}.cm-s-base16-dark .CodeMirror-cursor{border-left:1px solid #b0b0b0}.cm-s-base16-dark.cm-fat-cursor .CodeMirror-cursor{background-color:#8e8d8875!important}.cm-s-base16-dark .cm-animate-fat-cursor{background-color:#8e8d8875!important}.cm-s-base16-dark span.cm-comment{color:#8f5536}.cm-s-base16-dark span.cm-atom{color:#aa759f}.cm-s-base16-dark span.cm-number{color:#aa759f}.cm-s-base16-dark span.cm-attribute,.cm-s-base16-dark span.cm-property{color:#90a959}.cm-s-base16-dark span.cm-keyword{color:#ac4142}.cm-s-base16-dark span.cm-string{color:#f4bf75}.cm-s-base16-dark span.cm-variable{color:#90a959}.cm-s-base16-dark span.cm-variable-2{color:#6a9fb5}.cm-s-base16-dark span.cm-def{color:#d28445}.cm-s-base16-dark span.cm-bracket{color:#e0e0e0}.cm-s-base16-dark span.cm-tag{color:#ac4142}.cm-s-base16-dark span.cm-link{color:#aa759f}.cm-s-base16-dark span.cm-error{background:#ac4142;color:#b0b0b0}.cm-s-base16-dark .CodeMirror-activeline-background{background:#202020}.cm-s-base16-dark .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important}

1
deps/codemirror/cm6.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.CodeMirror-dialog{position:absolute;left:0;right:0;background:inherit;z-index:15;padding:.1em .8em;overflow:hidden;color:inherit}.CodeMirror-dialog-top{border-bottom:1px solid #eee;top:0}.CodeMirror-dialog-bottom{border-top:1px solid #eee;bottom:0}.CodeMirror-dialog input{border:none;outline:0;background:0 0;width:20em;color:inherit;font-family:monospace}.CodeMirror-dialog button{font-size:70%}

View File

@ -1 +0,0 @@
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(s){function f(e,o,n){var e=e.getWrapperElement(),t=e.appendChild(document.createElement("div"));return t.className=n?"CodeMirror-dialog CodeMirror-dialog-bottom":"CodeMirror-dialog CodeMirror-dialog-top","string"==typeof o?t.innerHTML=o:t.appendChild(o),s.addClass(e,"dialog-opened"),t}function p(e,o){e.state.currentNotificationClose&&e.state.currentNotificationClose(),e.state.currentNotificationClose=o}s.defineExtension("openDialog",function(e,o,n){n=n||{},p(this,null);var t=f(this,e,n.bottom),i=!1,r=this;function u(e){"string"==typeof e?l.value=e:i||(i=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),r.focus(),n.onClose&&n.onClose(t))}var l=t.getElementsByTagName("input")[0];return l?(l.focus(),n.value&&(l.value=n.value,!1!==n.selectValueOnOpen&&l.select()),n.onInput&&s.on(l,"input",function(e){n.onInput(e,l.value,u)}),n.onKeyUp&&s.on(l,"keyup",function(e){n.onKeyUp(e,l.value,u)}),s.on(l,"keydown",function(e){n&&n.onKeyDown&&n.onKeyDown(e,l.value,u)||((27==e.keyCode||!1!==n.closeOnEnter&&13==e.keyCode)&&(l.blur(),s.e_stop(e),u()),13==e.keyCode&&o(l.value,e))}),!1!==n.closeOnBlur&&s.on(t,"focusout",function(e){null!==e.relatedTarget&&u()})):(e=t.getElementsByTagName("button")[0])&&(s.on(e,"click",function(){u(),r.focus()}),!1!==n.closeOnBlur&&s.on(e,"blur",u),e.focus()),u}),s.defineExtension("openConfirm",function(e,o,n){p(this,null);var t=f(this,e,n&&n.bottom),i=t.getElementsByTagName("button"),r=!1,u=this,l=1;function a(){r||(r=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),u.focus())}i[0].focus();for(var c=0;c<i.length;++c){var d=i[c];!function(o){s.on(d,"click",function(e){s.e_preventDefault(e),a(),o&&o(u)})}(o[c]),s.on(d,"blur",function(){--l,setTimeout(function(){l<=0&&a()},200)}),s.on(d,"focus",function(){++l})}}),s.defineExtension("openNotification",function(e,o){p(this,r);var n,t=f(this,e,o&&o.bottom),i=!1,e=o&&void 0!==o.duration?o.duration:5e3;function r(){i||(i=!0,clearTimeout(n),s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t))}return s.on(t,"click",function(e){s.e_preventDefault(e),r()}),e&&(n=setTimeout(r,e)),r})});

View File

@ -1 +0,0 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol()&&(e.inTag+=" ")),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});

View File

@ -1 +0,0 @@
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(a){"use strict";a.registerHelper("lint","javascript",function(e,r){if(!window.JSHINT)return window.console&&window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run."),[];if(r.indent||(r.indent=1),JSHINT(e,r,r.globals),e=JSHINT.data().errors,r=[],e)for(var n=e,o=r,i=0;i<n.length;i++){var t,d,s,c=n[i];c&&(c.line<=0?window.console&&window.console.warn("Cannot display JSHint error (invalid line "+c.line+")",c):(t=c.character-1,d=1+t,c.evidence&&-1<(s=c.evidence.substring(t).search(/.\b/))&&(d+=s),s={message:c.reason,severity:c.code&&c.code.startsWith("W")?"warning":"error",from:a.Pos(c.line-1,t),to:a.Pos(c.line-1,d)},o.push(s)))}return r})});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror"),require("../dialog/dialog")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../dialog/dialog"],e):e(CodeMirror)}(function(e){"use strict";function l(e,o){var r=Number(o);return/^[-+]/.test(o)?e.getCursor().line+r:r-1}e.defineOption("search",{bottom:!1}),e.commands.jumpToLine=function(t){var e,o,r,s,i,n=t.getCursor();i=(i=e=t).phrase("Jump to line:")+' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">'+i.phrase("(Use line:column or scroll% syntax)")+"</span>",o=t.phrase("Jump to line:"),r=n.line+1+":"+n.ch,s=function(e){var o,r;e&&((o=/^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(e))?t.setCursor(l(t,o[1]),Number(o[2])):(o=/^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(e))?(r=Math.round(t.lineCount()*Number(o[1])/100),/^[-+]/.test(o[1])&&(r=n.line+r+1),t.setCursor(r-1,n.ch)):(o=/^\s*\:?\s*([\+\-]?\d+)\s*/.exec(e))&&t.setCursor(l(t,o[1]),n.ch))},e.openDialog?e.openDialog(i,s,{value:r,selectValueOnOpen:!0,bottom:e.options.search.bottom}):s(prompt(o,r))},e.keyMap.default["Alt-G"]="jumpToLine"});

View File

@ -1,80 +0,0 @@
/* The lint marker gutter */
.CodeMirror-lint-markers {
width: 16px;
}
.CodeMirror-lint-tooltip {
background-color: #ffd;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: black;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
padding: 2px 5px;
position: fixed;
white-space: pre;
white-space: pre-wrap;
z-index: 100;
max-width: 600px;
opacity: 0;
transition: opacity .4s;
-moz-transition: opacity .4s;
-webkit-transition: opacity .4s;
-o-transition: opacity .4s;
-ms-transition: opacity .4s;
}
.CodeMirror-lint-mark {
background-position: left bottom;
background-repeat: repeat-x;
}
.CodeMirror-lint-mark-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
}
.CodeMirror-lint-mark-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
}
.CodeMirror-lint-marker {
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
display: inline-block;
height: 16px;
width: 16px;
vertical-align: middle;
position: relative;
}
.CodeMirror-lint-message {
padding-left: 18px;
background-position: top left;
background-repeat: no-repeat;
}
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-multiple {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right bottom;
width: 100%; height: 100%;
}
.CodeMirror-lint-line-error {
background-color: rgba(183, 76, 81, 0.08);
}
.CodeMirror-lint-line-warning {
background-color: rgba(255, 211, 0, 0.1);
}

View File

@ -1,292 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var GUTTER_ID = "CodeMirror-lint-markers";
var LINT_LINE_ID = "CodeMirror-lint-line-";
function showTooltip(cm, e, content) {
var tt = document.createElement("div");
tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme;
tt.appendChild(content.cloneNode(true));
if (cm.state.lint.options.selfContain)
cm.getWrapperElement().appendChild(tt);
else
document.body.appendChild(tt);
function position(e) {
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
tt.style.left = (e.clientX + 5) + "px";
}
CodeMirror.on(document, "mousemove", position);
position(e);
if (tt.style.opacity != null) tt.style.opacity = 1;
return tt;
}
function rm(elt) {
if (elt.parentNode) elt.parentNode.removeChild(elt);
}
function hideTooltip(tt) {
if (!tt.parentNode) return;
if (tt.style.opacity == null) rm(tt);
tt.style.opacity = 0;
setTimeout(function() { rm(tt); }, 600);
}
function showTooltipFor(cm, e, content, node) {
var tooltip = showTooltip(cm, e, content);
function hide() {
CodeMirror.off(node, "mouseout", hide);
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
}
var poll = setInterval(function() {
if (tooltip) for (var n = node;; n = n.parentNode) {
if (n && n.nodeType == 11) n = n.host;
if (n == document.body) return;
if (!n) { hide(); break; }
}
if (!tooltip) return clearInterval(poll);
}, 400);
CodeMirror.on(node, "mouseout", hide);
}
function LintState(cm, conf, hasGutter) {
this.marked = [];
if (conf instanceof Function) conf = {getAnnotations: conf};
if (!conf || conf === true) conf = {};
this.options = {};
this.linterOptions = conf.options || {};
for (var prop in defaults) this.options[prop] = defaults[prop];
for (var prop in conf) {
if (defaults.hasOwnProperty(prop)) {
if (conf[prop] != null) this.options[prop] = conf[prop];
} else if (!conf.options) {
this.linterOptions[prop] = conf[prop];
}
}
this.timeout = null;
this.hasGutter = hasGutter;
this.onMouseOver = function(e) { onMouseOver(cm, e); };
this.waitingFor = 0
}
var defaults = {
highlightLines: false,
tooltips: true,
delay: 500,
lintOnChange: true,
getAnnotations: null,
async: false,
selfContain: null,
formatAnnotation: null,
onUpdateLinting: null
}
function clearMarks(cm) {
var state = cm.state.lint;
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
if (state.options.highlightLines) clearErrorLines(cm);
for (var i = 0; i < state.marked.length; ++i)
state.marked[i].clear();
state.marked.length = 0;
}
function clearErrorLines(cm) {
cm.eachLine(function(line) {
var has = line.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(line.wrapClass);
if (has) cm.removeLineClass(line, "wrap", has[0]);
})
}
function makeMarker(cm, labels, severity, multiple, tooltips) {
var marker = document.createElement("div"), inner = marker;
marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity;
if (multiple) {
inner = marker.appendChild(document.createElement("div"));
inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple";
}
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
showTooltipFor(cm, e, labels, inner);
});
return marker;
}
function getMaxSeverity(a, b) {
if (a == "error") return a;
else return b;
}
function groupByLine(annotations) {
var lines = [];
for (var i = 0; i < annotations.length; ++i) {
var ann = annotations[i], line = ann.from.line;
(lines[line] || (lines[line] = [])).push(ann);
}
return lines;
}
function annotationTooltip(ann) {
var severity = ann.severity;
if (!severity) severity = "error";
var tip = document.createElement("div");
tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity;
if (typeof ann.messageHTML != 'undefined') {
tip.innerHTML = ann.messageHTML;
} else {
tip.appendChild(document.createTextNode(ann.message));
}
return tip;
}
function lintAsync(cm, getAnnotations) {
var state = cm.state.lint
var id = ++state.waitingFor
function abort() {
id = -1
cm.off("change", abort)
}
cm.on("change", abort)
getAnnotations(cm.getValue(), function(annotations, arg2) {
cm.off("change", abort)
if (state.waitingFor != id) return
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
cm.operation(function() {updateLinting(cm, annotations)})
}, state.linterOptions, cm);
}
function startLinting(cm) {
var state = cm.state.lint;
if (!state) return;
var options = state.options;
/*
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
*/
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
if (!getAnnotations) return;
if (options.async || getAnnotations.async) {
lintAsync(cm, getAnnotations)
} else {
var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);
if (!annotations) return;
if (annotations.then) annotations.then(function(issues) {
cm.operation(function() {updateLinting(cm, issues)})
});
else cm.operation(function() {updateLinting(cm, annotations)})
}
}
function updateLinting(cm, annotationsNotSorted) {
var state = cm.state.lint;
if (!state) return;
var options = state.options;
clearMarks(cm);
var annotations = groupByLine(annotationsNotSorted);
for (var line = 0; line < annotations.length; ++line) {
var anns = annotations[line];
if (!anns) continue;
// filter out duplicate messages
var message = [];
anns = anns.filter(function(item) { return message.indexOf(item.message) > -1 ? false : message.push(item.message) });
var maxSeverity = null;
var tipLabel = state.hasGutter && document.createDocumentFragment();
for (var i = 0; i < anns.length; ++i) {
var ann = anns[i];
var severity = ann.severity;
if (!severity) severity = "error";
maxSeverity = getMaxSeverity(maxSeverity, severity);
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity,
__annotation: ann
}));
}
// use original annotations[line] to show multiple messages
if (state.hasGutter)
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, annotations[line].length > 1,
options.tooltips));
if (options.highlightLines)
cm.addLineClass(line, "wrap", LINT_LINE_ID + maxSeverity);
}
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
}
function onChange(cm) {
var state = cm.state.lint;
if (!state) return;
clearTimeout(state.timeout);
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);
}
function popupTooltips(cm, annotations, e) {
var target = e.target || e.srcElement;
var tooltip = document.createDocumentFragment();
for (var i = 0; i < annotations.length; i++) {
var ann = annotations[i];
tooltip.appendChild(annotationTooltip(ann));
}
showTooltipFor(cm, e, tooltip, target);
}
function onMouseOver(cm, e) {
var target = e.target || e.srcElement;
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
var annotations = [];
for (var i = 0; i < spans.length; ++i) {
var ann = spans[i].__annotation;
if (ann) annotations.push(ann);
}
if (annotations.length) popupTooltips(cm, annotations, e);
}
CodeMirror.defineOption("lint", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
clearMarks(cm);
if (cm.state.lint.options.lintOnChange !== false)
cm.off("change", onChange);
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
clearTimeout(cm.state.lint.timeout);
delete cm.state.lint;
}
if (val) {
var gutters = cm.getOption("gutters"), hasLintGutter = false;
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
if (state.options.lintOnChange)
cm.on("change", onChange);
if (state.options.tooltips != false && state.options.tooltips != "gutter")
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
startLinting(cm);
}
});
CodeMirror.defineExtension("performLint", function() {
startLinting(this);
});
});

View File

@ -1 +0,0 @@
.CodeMirror-search-match{background:gold;border-top:1px solid orange;border-bottom:1px solid orange;-moz-box-sizing:border-box;box-sizing:border-box;opacity:.5}

View File

@ -1 +0,0 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("./searchcursor"),require("../scroll/annotatescrollbar")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./searchcursor","../scroll/annotatescrollbar"],t):t(CodeMirror)}(function(h){"use strict";function o(t,e,i,o){this.cm=t;var n,s={listenForChanges:!1};for(n in this.options=o)s[n]=o[n];s.className||(s.className="CodeMirror-search-match"),this.annotation=t.annotateScrollbar(s),this.query=e,this.caseFold=i,this.gap={from:t.firstLine(),to:t.lastLine()+1},this.matches=[],this.update=null,this.findMatches(),this.annotation.update(this.matches);var a=this;t.on("change",this.changeHandler=function(t,e){a.onChange(e)})}h.defineExtension("showMatchesOnScrollbar",function(t,e,i){return new o(this,t,e,i=(i="string"==typeof i?{className:i}:i)||{})});function c(t,e,i){return t<=e?t:Math.max(e,t+i)}o.prototype.findMatches=function(){if(this.gap){for(var t=0;t<this.matches.length;t++){if((e=this.matches[t]).from.line>=this.gap.to)break;e.to.line>=this.gap.from&&this.matches.splice(t--,1)}for(var e,i=this.cm.getSearchCursor(this.query,h.Pos(this.gap.from,0),{caseFold:this.caseFold,multiline:this.options.multiline}),o=this.options&&this.options.maxMatches||1e3;i.findNext();){if((e={from:i.from(),to:i.to()}).from.line>=this.gap.to)break;if(this.matches.splice(t++,0,e),this.matches.length>o)break}this.gap=null}},o.prototype.onChange=function(t){var e=t.from.line,i=h.changeEnd(t).line,o=i-t.to.line;if(this.gap?(this.gap.from=Math.min(c(this.gap.from,e,o),t.from.line),this.gap.to=Math.max(c(this.gap.to,e,o),t.from.line)):this.gap={from:t.from.line,to:i+1},o)for(var n=0;n<this.matches.length;n++){var s=this.matches[n],a=c(s.from.line,e,o),a=(a!=s.from.line&&(s.from=h.Pos(a,s.from.ch)),c(s.to.line,e,o));a!=s.to.line&&(s.to=h.Pos(a,s.to.ch))}clearTimeout(this.update);var r=this;this.update=setTimeout(function(){r.updateAfterChange()},250)},o.prototype.updateAfterChange=function(){this.findMatches(),this.annotation.update(this.matches)},o.prototype.clear=function(){this.cm.off("change",this.changeHandler),this.annotation.clear()}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(o){o.defineOption("showTrailingSpace",!1,function(e,i,n){(n=n==o.Init?!1:n)&&!i?e.removeOverlay("trailingspace"):!n&&i&&e.addOverlay({token:function(e){for(var i=e.string.length,n=i;n&&/\s/.test(e.string.charAt(n-1));--n);return n>e.pos?(e.pos=n,null):(e.pos=i,"trailingspace")},name:"trailingspace"})})});

View File

@ -1,22 +0,0 @@
LINKS="
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/dialog/dialog.min.css
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/dialog/dialog.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/trailingspace.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/javascript-lint.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/scroll/annotatescrollbar.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/search/matchesonscrollbar.min.css
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/search/matchesonscrollbar.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/search/search.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/search/searchcursor.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/css/css.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/htmlmixed/htmlmixed.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/xml/xml.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/base16-dark.min.css
"
for link in $LINKS; do
wget $link -O `basename $link`
done

File diff suppressed because one or more lines are too long

70
deps/codemirror_src/editor.mjs vendored Normal file
View File

@ -0,0 +1,70 @@
import {EditorState} from "@codemirror/state"
import {EditorView} from '@codemirror/view';
import {javascript} from "@codemirror/lang-javascript"
import {html} from "@codemirror/lang-html"
import {css} from "@codemirror/lang-css"
import {search} from "@codemirror/search"
import {oneDark} from "./theme-tf-dark.js"
import {lineNumbers, highlightActiveLineGutter, highlightSpecialChars, highlightTrailingWhitespace, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, highlightWhitespace} from '@codemirror/view';
import {foldGutter, indentUnit, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap} from '@codemirror/language';
import {history, defaultKeymap, historyKeymap, indentWithTab} from '@codemirror/commands';
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
import {autocompletion, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
import {lintKeymap} from '@codemirror/lint';
let updateListenerExtension = EditorView.updateListener.of((update) => {
if (update.docChanged && update.view.onDocChange) {
update.view.onDocChange();
}
});
const extensions = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
highlightWhitespace(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
indentUnit.of('\t'),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
highlightTrailingWhitespace(),
keymap.of([
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap,
indentWithTab,
]),
javascript(),
html(),
css(),
search(),
oneDark,
updateListenerExtension,
];
function TildeFriendsEditorView(parent) {
return new EditorView({
extensions: extensions,
parent: parent,
});
};
export {
TildeFriendsEditorView,
EditorState,
EditorView,
extensions,
};

15
deps/codemirror_src/package.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"dependencies": {
"@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@rollup/plugin-node-resolve": "^15.2.3",
"codemirror": "^6.0.1",
"rollup": "^4.9.6"
},
"devDependencies": {
"@rollup/plugin-terser": "^0.4.4"
}
}

12
deps/codemirror_src/rollup.config.mjs vendored Normal file
View File

@ -0,0 +1,12 @@
import {nodeResolve} from "@rollup/plugin-node-resolve"
import terser from '@rollup/plugin-terser';
export default {
input: "./editor.mjs",
output: {
file: "../codemirror/cm6.js",
format: "es",
plugins: [terser()],
},
plugins: [nodeResolve()]
}

134
deps/codemirror_src/theme-tf-dark.js vendored Normal file
View File

@ -0,0 +1,134 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = "#e5c07b", coral = "#e06c75", cyan = "#56b6c2", invalid = "#ffffff", ivory = "#abb2bf", stone = "#7d8799", // Brightened compared to original to increase contrast
malibu = "#61afef", sage = "#98c379", whiskey = "#d19a66", violet = "#c678dd", darkBackground = "#21252b", highlightBackground = "#2c313a", background = "#14161a", tooltipBackground = "#353a42", selection = "#3E4451", cursor = "#528bff";
/**
The colors used in the theme, as CSS color strings.
*/
const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor
};
/**
The editor theme styles for One Dark.
*/
const oneDarkTheme = /*@__PURE__*/EditorView.theme({
"&": {
color: ivory,
backgroundColor: background
},
".cm-content": {
caretColor: cursor
},
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: selection },
".cm-panels": { backgroundColor: darkBackground, color: ivory },
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
".cm-searchMatch": {
backgroundColor: "#72a1ff59",
outline: "1px solid #457dff"
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: "#6199ff2f"
},
".cm-activeLine": { backgroundColor: "#6699ff0b" },
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
backgroundColor: "#bad0f847"
},
".cm-gutters": {
backgroundColor: background,
color: stone,
border: "none"
},
".cm-activeLineGutter": {
backgroundColor: highlightBackground
},
".cm-foldPlaceholder": {
backgroundColor: "transparent",
border: "none",
color: "#ddd"
},
".cm-tooltip": {
border: "none",
backgroundColor: tooltipBackground
},
".cm-tooltip .cm-tooltip-arrow:before": {
borderTopColor: "transparent",
borderBottomColor: "transparent"
},
".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground
},
".cm-tooltip-autocomplete": {
"& > ul > li[aria-selected]": {
backgroundColor: highlightBackground,
color: ivory
}
}
}, { dark: true });
/**
The highlighting style for code in the One Dark theme.
*/
const oneDarkHighlightStyle = /*@__PURE__*/HighlightStyle.define([
{ tag: tags.keyword,
color: violet },
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
color: coral },
{ tag: [/*@__PURE__*/tags.function(tags.variableName), tags.labelName],
color: malibu },
{ tag: [tags.color, /*@__PURE__*/tags.constant(tags.name), /*@__PURE__*/tags.standard(tags.name)],
color: whiskey },
{ tag: [/*@__PURE__*/tags.definition(tags.name), tags.separator],
color: ivory },
{ tag: [tags.typeName, tags.className, tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace],
color: chalky },
{ tag: [tags.operator, tags.operatorKeyword, tags.url, tags.escape, tags.regexp, tags.link, /*@__PURE__*/tags.special(tags.string)],
color: cyan },
{ tag: [tags.meta, tags.comment],
color: stone },
{ tag: tags.strong,
fontWeight: "bold" },
{ tag: tags.emphasis,
fontStyle: "italic" },
{ tag: tags.strikethrough,
textDecoration: "line-through" },
{ tag: tags.link,
color: stone,
textDecoration: "underline" },
{ tag: tags.heading,
fontWeight: "bold",
color: coral },
{ tag: [tags.atom, tags.bool, /*@__PURE__*/tags.special(tags.variableName)],
color: whiskey },
{ tag: [tags.processingInstruction, tags.string, tags.inserted],
color: sage },
{ tag: tags.invalid,
color: invalid },
]);
/**
Extension to enable the One Dark theme (both the editor theme and
the highlight style).
*/
const oneDark = [oneDarkTheme, /*@__PURE__*/syntaxHighlighting(oneDarkHighlightStyle)];
export { color, oneDark, oneDarkHighlightStyle, oneDarkTheme };

View File

@ -40,17 +40,16 @@ jobs:
- name: Update packages list
run: sudo apt-get update
- name: Install dependencies
run: |
curl -sL -o - https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz | tar xJ -f - -C /opt/
sudo mv /opt/zig-* /opt/zig
- name: Install Zig
uses: goto-bus-stop/setup-zig@d866436887ad1b24590684f9d00480376663dd36
with:
version: 0.11.0
- name: Autogen
run: ./autogen.sh -s
- name: Compilation with zig
run: |
export PATH=/opt/zig:$PATH
zig build
zig build -Dtarget=x86_64-linux
zig build -Dtarget=aarch64-linux
@ -177,5 +176,4 @@ jobs:
- name: Android compilation
run: |
env ANDROID_NDK_HOME=/tmp/android/ndk ./dist-build/android-x86.sh
env ANDROID_NDK_HOME=/tmp/android/ndk ./dist-build/android-armv8-a.sh
env ANDROID_NDK_HOME=/tmp/android/ndk ./dist-build/android-aar.sh

View File

@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Zig
uses: goto-bus-stop/setup-zig@6fede2f0550d71291c0accf2834b216e69a2d67a
uses: goto-bus-stop/setup-zig@d866436887ad1b24590684f9d00480376663dd36
with:
version: 0.11.0
- uses: actions/checkout@v3
@ -46,7 +46,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Zig
uses: goto-bus-stop/setup-zig@6fede2f0550d71291c0accf2834b216e69a2d67a
uses: goto-bus-stop/setup-zig@d866436887ad1b24590684f9d00480376663dd36
with:
version: 0.11.0
- name: Set up emulation environment
@ -74,7 +74,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Zig
uses: goto-bus-stop/setup-zig@6fede2f0550d71291c0accf2834b216e69a2d67a
uses: goto-bus-stop/setup-zig@d866436887ad1b24590684f9d00480376663dd36
with:
version: 0.11.0
- name: Set up emulation environment
@ -108,7 +108,7 @@ jobs:
apk update
apk add alpine-sdk ca-certificates xz
- name: Install Zig
uses: goto-bus-stop/setup-zig@6fede2f0550d71291c0accf2834b216e69a2d67a
uses: goto-bus-stop/setup-zig@d866436887ad1b24590684f9d00480376663dd36
with:
version: 0.11.0
- uses: actions/checkout@v3
@ -127,7 +127,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Zig
uses: goto-bus-stop/setup-zig@6fede2f0550d71291c0accf2834b216e69a2d67a
uses: goto-bus-stop/setup-zig@d866436887ad1b24590684f9d00480376663dd36
with:
version: 0.11.0
- uses: actions/checkout@v3
@ -143,7 +143,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Zig
uses: goto-bus-stop/setup-zig@6fede2f0550d71291c0accf2834b216e69a2d67a
uses: goto-bus-stop/setup-zig@d866436887ad1b24590684f9d00480376663dd36
with:
version: 0.11.0
- uses: actions/checkout@v3

View File

@ -1,10 +1,13 @@
* Version 1.0.19-stable
- Building with `zig build` now works both with Zig 0.11 and Zig 0.12.
- Building with `zig build` now requires Zig 0.12.
- When using the traditional build system, -O3 is used instead of -Ofast.
- Improved detection of the compiler flags required on aarch64.
- Improved compatibility with custom build systems on aarch64.
- apple-xcframework: VisionOS packages are not built if Xcode doesn't
include that SDK.
- `crypto_kdf_hkdf_sha512_statebytes()` was added.
- When using Visual Studio, runtime CPU feature detection is now enabled
on Windows/aarch64.
* Version 1.0.19
This release includes all the changes from 1.0.18-stable, as well as two

View File

@ -1,7 +1,7 @@
/*
* ISC License
*
* Copyright (c) 2013-2023
* Copyright (c) 2013-2024
* Frank Denis <j at pureftpd dot org>
*
* Permission to use, copy, modify, and/or distribute this software for any

View File

@ -14,8 +14,8 @@
m4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])])
m4_ifndef([AC_AUTOCONF_VERSION],
[m4_copy([m4_PACKAGE_VERSION], [AC_AUTOCONF_VERSION])])dnl
m4_if(m4_defn([AC_AUTOCONF_VERSION]), [2.71],,
[m4_warning([this file was generated for autoconf 2.71.
m4_if(m4_defn([AC_AUTOCONF_VERSION]), [2.72],,
[m4_warning([this file was generated for autoconf 2.72.
You have another version of autoconf. It may work, but is not guaranteed to.
If you have problems, you may need to regenerate the build system entirely.
To do so, use the procedure documented by the package, typically 'autoreconf'.])])

View File

@ -48,7 +48,7 @@ jobs:
vmImage: "windows-2019"
steps:
- powershell: |
(New-Object Net.WebClient).DownloadFile("https://github.com/msys2/msys2-installer/releases/download/2023-07-18/msys2-base-x86_64-20230718.sfx.exe", "sfx.exe")
(New-Object Net.WebClient).DownloadFile("https://github.com/msys2/msys2-installer/releases/download/2024-01-13/msys2-base-x86_64-20230718.sfx.exe", "sfx.exe")
.\sfx.exe -y -o\
del sfx.exe
displayName: Install MSYS2
@ -87,7 +87,7 @@ jobs:
vmImage: "windows-2019"
steps:
- powershell: |
(New-Object Net.WebClient).DownloadFile("https://github.com/msys2/msys2-installer/releases/download/2023-07-18/msys2-base-x86_64-20230718.sfx.exe", "sfx.exe")
(New-Object Net.WebClient).DownloadFile("https://github.com/msys2/msys2-installer/releases/download/2024-01-13/msys2-base-x86_64-20240113.sfx.exe", "sfx.exe")
.\sfx.exe -y -o\
del sfx.exe
displayName: Install MSYS2

View File

@ -249,7 +249,7 @@ pub fn build(b: *std.build.Builder) !void {
else => {},
}
var allocator = heap.page_allocator;
const allocator = heap.page_allocator;
var walker = try src_dir.walk(allocator);
while (try walker.next()) |entry| {
const name = entry.basename;
@ -280,7 +280,7 @@ pub fn build(b: *std.build.Builder) !void {
fs.Dir.makePath(cwd, out_bin_path) catch {};
const out_bin_dir = try fs.Dir.openDir(cwd, out_bin_path, .{});
try test_dir.dir.copyFile("run.sh", out_bin_dir, "run.sh", .{});
var allocator = heap.page_allocator;
const allocator = heap.page_allocator;
var walker = try test_dir.walk(allocator);
if (build_tests) {
while (try walker.next()) |entry| {

View File

@ -49,7 +49,7 @@ BEGIN
VALUE "FileDescription", "The Sodium crypto library (libsodium)"
VALUE "FileVersion", LIBSODIUM_VERSION_STRING
VALUE "InternalName", "libsodium"
VALUE "LegalCopyright", "Copyright (c) 2013-2023 The libsodium authors."
VALUE "LegalCopyright", "Copyright (c) 2013-2024 The libsodium authors."
VALUE "OriginalFilename", "libsodium.dll"
VALUE "ProductName", LIBSODIUM_PRODUCT_NAME
VALUE "ProductVersion", LIBSODIUM_VERSION_STRING

4096
deps/libsodium/configure vendored

File diff suppressed because it is too large Load Diff

View File

@ -62,7 +62,7 @@ fi
--with-sysroot="${TOOLCHAIN_DIR}/sysroot" || exit 1
if [ "$NDK_PLATFORM" != "$NDK_PLATFORM_COMPAT" ]; then
egrep '^#define ' config.log | sort -u >config-def-compat.log
grep -E '^#define ' config.log | sort -u >config-def-compat.log
echo
echo "Configuring again for platform [${NDK_PLATFORM}]"
echo

File diff suppressed because one or more lines are too long

View File

@ -116,3 +116,8 @@ crypto_kdf_hkdf_sha512_bytes_max(void)
{
return crypto_kdf_hkdf_sha512_BYTES_MAX;
}
size_t crypto_kdf_hkdf_sha512_statebytes(void)
{
return sizeof(crypto_kdf_hkdf_sha512_state);
}

View File

@ -56,6 +56,7 @@ crypto_sign_ed25519_pk_to_curve25519(unsigned char *curve25519_pk,
return -1;
}
fe25519_1(one_minus_y);
/* assumes A.Z=1 */
fe25519_sub(one_minus_y, one_minus_y, A.Y);
fe25519_1(x);
fe25519_add(x, x, A.Y);

Some files were not shown because too many files have changed in this diff Show More