Compare commits
567 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3508d1e315 | |||
| e3177b8054 | |||
| 03e3760152 | |||
| 4740610923 | |||
| e28a0cde55 | |||
| 5b855fd835 | |||
| a2f5704581 | |||
| f7aa9424db | |||
| aa8b47a3dd | |||
| 11911c1898 | |||
| 4814c1971d | |||
| be9569f3fb | |||
| 900e72f95a | |||
| d2827f188b | |||
| cf9903b759 | |||
| 23f96461f4 | |||
| 9f2fd26e98 | |||
| 78d837c080 | |||
| 241b9312b7 | |||
| ed70ad7378 | |||
| 00213176d8 | |||
| 406650a45a | |||
| 56750ccf3c | |||
| dfc286b393 | |||
| 49a66f72fc | |||
| 3f237689da | |||
| cf1fb483b3 | |||
| b10f5e3f67 | |||
| c4fc24c513 | |||
| 3ac9c2d95e | |||
| e5ab4dafc0 | |||
| 10ae1911c3 | |||
| 73ebcdf0d6 | |||
| 5347523921 | |||
| 7ef70b953b | |||
| ccaca524fe | |||
| dd51f91cab | |||
| 537d98b41b | |||
| 9c4cadfc04 | |||
| 2001370441 | |||
| cc87b22757 | |||
| c0a65b30ad | |||
| c07e66c086 | |||
| a0cfc1be2b | |||
| 1505454793 | |||
| e1dff66283 | |||
| 5be801a086 | |||
| 94d4b05c29 | |||
| cebb889f7e | |||
| c4ed6ed034 | |||
| ec960bfefa | |||
| 79f689dde1 | |||
| 3b3654df56 | |||
| c66f008f07 | |||
| 37d9498d90 | |||
| 1ff67093db | |||
| daed37ccb8 | |||
| d41d807b4f | |||
| d6fa5c8a55 | |||
| 2dd608dfed | |||
| a98546f605 | |||
| 3567559d4e | |||
| 216476ee45 | |||
| 3fc28c07fb | |||
| 85f6ef063d | |||
| 1e71e2d68f | |||
| b24a29895f | |||
| 0167a2ae59 | |||
| 2c867103ca | |||
| 8c289df336 | |||
| 4489920cbf | |||
| 029a85081b | |||
| 1bc739d07f | |||
| c229e218f6 | |||
| a66f4ad4bd | |||
| 1dd687dab7 | |||
| 50ff6e2745 | |||
| 811dec713b | |||
| 617d6f4bd7 | |||
| 57cd2d6252 | |||
| ec64f8d048 | |||
| ed288a9dba | |||
| 27da0a4102 | |||
| 3c01ba1a76 | |||
| 252c8833ae | |||
| f45fb6efe6 | |||
| 8cc1f8d691 | |||
| bff22b5182 | |||
| d31d47eb32 | |||
| 5fe984c39d | |||
| 7f07b0daa7 | |||
| 5de9757d46 | |||
| f89276d7b8 | |||
| 30ba034206 | |||
| fa1e5aaa7f | |||
| 870c70180f | |||
| 6d35c26b3f | |||
| be4e693a27 | |||
| 5810276156 | |||
| d10ac3f87b | |||
| 9810bc09e9 | |||
| a0a13eb2a8 | |||
| 6e996797b8 | |||
| 663092b501 | |||
| 8ea13544de | |||
| e73daa6214 | |||
| d83a833b4d | |||
| ec3a2f29f0 | |||
| cf92c60a01 | |||
| b7f51b03bc | |||
| 903e69ff77 | |||
| c4167ddaad | |||
| 50bfdb0d68 | |||
| a6cb09ff1c | |||
| e4c9f23476 | |||
| 44e5415d43 | |||
| 1c653693ed | |||
| 39c470ad7a | |||
| 1103e538a5 | |||
| c0cd4cba6f | |||
| b91120e8d4 | |||
| 005774a4c2 | |||
| 16bbfebfba | |||
| 15505cd82d | |||
| 016d80e002 | |||
| 0f3c267a48 | |||
| 589bb02411 | |||
| c0f4ece17b | |||
| c3ae3cb768 | |||
| c9e0f9d985 | |||
| e3431c2fa3 | |||
| 5979b9771e | |||
| aa61832fb2 | |||
| 2ac6e982b1 | |||
| 3204ddcf07 | |||
| c87b1c133c | |||
| 9b275ecdae | |||
| d6fd7de361 | |||
| 49d66a133e | |||
| c559f26d0e | |||
| bbe9f1bad2 | |||
| 7e1fb6472d | |||
| 0ff8d20573 | |||
| 9c1f9448dc | |||
| 43a6081dd6 | |||
| 985e961876 | |||
| 098f6de047 | |||
| 1b0f90fd68 | |||
| 12252f407b | |||
| 3b6e3f47ab | |||
| 6a9ac9b025 | |||
| ae6aa4088b | |||
| c08f431180 | |||
| 123c1f56e9 | |||
| 35ac65a864 | |||
| e9f362cc87 | |||
| 65685c23e1 | |||
| 2f74748cea | |||
| f477bd66f3 | |||
| d7d77ae8f0 | |||
| 31110a740d | |||
| b64d8b1d7f | |||
| c46006aacc | |||
| 92f81b1493 | |||
| 70213cfc8f | |||
| 8a82bf5c50 | |||
| 37405384a2 | |||
| 54ea6cc53b | |||
| 339c00d815 | |||
| ea6b4dcde2 | |||
| 2b84623d1e | |||
| c8b3afa56b | |||
| 1348f3c24c | |||
| 62208ce3e1 | |||
| 813b2481de | |||
| 27b924ba61 | |||
| b40170b8ce | |||
| 8bfa9d2734 | |||
| c7cf76d4a8 | |||
| dfd2969b3e | |||
| 0e1866fe1d | |||
| b9ae46b913 | |||
| 06e7284055 | |||
| 93289e8fca | |||
| 130d5057d4 | |||
| be492d5084 | |||
| e0bf1d736f | |||
| 5a6b71cbeb | |||
| e6934cd5e2 | |||
| b5aada0792 | |||
| 165ea83ac3 | |||
| 440a82dee4 | |||
| 9c2d3e5e26 | |||
| 6fb6abcbe5 | |||
| dc449dafd1 | |||
| ecdaeebbfb | |||
| fa958b59bd | |||
| fb3d8521cd | |||
| 608c401cf3 | |||
| 1c3da90a24 | |||
| 34567f3375 | |||
| 51bcbeb48f | |||
| cc0f9c42df | |||
| a11bf5523b | |||
| 1921c3d901 | |||
| d568469e8b | |||
| 20d5c7e8d5 | |||
| 9f289ed9de | |||
| 93ee5e480b | |||
| 98a312701f | |||
| cbcf603b63 | |||
| f976f672cf | |||
| cfc3081e8a | |||
| 99818924ee | |||
| 9bbf3a027f | |||
| 93e01902e3 | |||
| 34919aba05 | |||
| a8ee4cf57f | |||
| b39548b4c6 | |||
| 4217c22ff6 | |||
| 4ab10670c9 | |||
| 2883f88de6 | |||
| e002a61a19 | |||
| 5893376279 | |||
| 411c5e4c4d | |||
| c2a77072d2 | |||
| c8a25934a6 | |||
| 7b38355cd4 | |||
| ddc54e0b98 | |||
| 8a7003782b | |||
| 8e6464dacb | |||
| 92b1dc0afb | |||
| 7562f51e07 | |||
| 09bba99e68 | |||
| 9d674cd49b | |||
| 88a0c9ea03 | |||
| 5014e0ce3e | |||
| b7a1ee9ebc | |||
| 292ceddd66 | |||
| 4b52b80000 | |||
| 9f20664c6e | |||
| 851a6aac0e | |||
| 1f1e73c47a | |||
| 112f61ca18 | |||
| 96eeacbe2b | |||
| 3f62da879c | |||
| aa30feb875 | |||
| 9ba3c2b7c5 | |||
| 320c708e10 | |||
| efa7294f59 | |||
| ae0e092935 | |||
| c77aecbfce | |||
| 700352ec45 | |||
| 664b125ce2 | |||
| 5f4b1c9e32 | |||
| d11d3f19bd | |||
| f34f4f2738 | |||
| 15db7c2310 | |||
| f9257ed04d | |||
| 15e6ef8488 | |||
| 9ae0a57f22 | |||
| 1e38c21f8e | |||
| bdc3c19163 | |||
| d55478da54 | |||
| 82bcc55645 | |||
| 07618ebe43 | |||
| 1492834d1e | |||
| 5ab6197356 | |||
| 0a789fe551 | |||
| caa8ff23ed | |||
| ee30d1d36d | |||
| 0d9415db9d | |||
| 8020e1126f | |||
| 3439422057 | |||
| 68d2bf736f | |||
| d78c39fd8c | |||
| b1dcad86b4 | |||
| 9b6124074d | |||
| 02cbaa1e80 | |||
| a12f1321c7 | |||
| 8b67f592ac | |||
| 319d17b337 | |||
| 476eaa85da | |||
| d26099855c | |||
| e47456da17 | |||
| a464d5d0b6 | |||
| 1cfb7b5615 | |||
| ac7c2f3d03 | |||
| 638d9e6e01 | |||
| 8b9df2a396 | |||
| d7fe911bde | |||
| 0acc3d511b | |||
| 4cf465f419 | |||
| b686d317a9 | |||
| dcef541852 | |||
| abdd733f11 | |||
| 942431e882 | |||
| 1c75ea046c | |||
| f32b6daa51 | |||
| 3736d6ba5e | |||
| 9788b01f35 | |||
| 9aec991da6 | |||
| 910701ce04 | |||
| 34b462d511 | |||
| 139e93b2f0 | |||
| 0dd7e9359e | |||
| 41cf0225e3 | |||
| 962254e511 | |||
| a7f2b24bac | |||
| 1323d988af | |||
| 7c49e5c749 | |||
| cd69ec4fa3 | |||
| 4c7e9fbee2 | |||
| 1639df5616 | |||
| 810cdbd790 | |||
| 0d4f4aec4e | |||
| 6b1863d3b4 | |||
| 27f5a3b16b | |||
| 876cd8291b | |||
| d0c46e4ef3 | |||
| feb8898ebf | |||
| 4fef8c5cfd | |||
| 7d56d8e35b | |||
| 5f1a3a9c8f | |||
| 0767b3156d | |||
| 9f16379b41 | |||
| be632aaf37 | |||
| 118c87faf7 | |||
| ec1e53d566 | |||
| 6a17ee414a | |||
| 6700686e4b | |||
| e8c34dd59b | |||
| 4c2da31bb3 | |||
| c0144b99bf | |||
| a0c32fc146 | |||
| a07b641adb | |||
| 0bb869fb33 | |||
| 72389e0129 | |||
| f49529fa70 | |||
| afcc34b5cc | |||
| 655b99cac8 | |||
| cc5091e28c | |||
| 1c72362c6b | |||
| 50ad5f681b | |||
| 50bf670931 | |||
| 7a8896864f | |||
| 51fbf148d9 | |||
| a9929438cd | |||
| 5a94b6b56c | |||
| 52cfbf60d4 | |||
| 29c10f8854 | |||
| ad761e388d | |||
| 07493ab0a6 | |||
| 7441011ae7 | |||
| d0818f456d | |||
| 06ea07a021 | |||
| 36d97ad5ca | |||
| a995eb2929 | |||
| c459a3033d | |||
| b4fbcf6bee | |||
| b9e679a514 | |||
| 64d73b93e4 | |||
| db70b05088 | |||
| 9428beeae5 | |||
| f9f7172702 | |||
| f1851b304c | |||
| d2ca6f1d46 | |||
| b27297cdc6 | |||
| 0d0edd7917 | |||
| fc6f12fb22 | |||
| d24096374f | |||
| 4f1d04009a | |||
| 79e9fde937 | |||
| 0ebaf6a171 | |||
| be2012f28d | |||
| ceefc8ffc6 | |||
| 0453b6903a | |||
| 691952249b | |||
| 0ceae2852e | |||
| 6d7ff38cf2 | |||
| 1b93ccf608 | |||
| 5b1ca3711a | |||
| 877f9299e1 | |||
| 677aca7a03 | |||
| 66b31a62d0 | |||
| 34923638c5 | |||
| bb61b3dc22 | |||
| 01ecae8979 | |||
| 53175c9ed7 | |||
| bc7a76755b | |||
| 92758f3e4e | |||
| b09767c526 | |||
| 2f93fd7c36 | |||
| 8acbcc548c | |||
| 19cf34f9d4 | |||
| 8c3f519016 | |||
| e63b42278c | |||
| 66ecd2fcf8 | |||
| f0d86f2392 | |||
| 5e39510f21 | |||
| 2cb4d65f3d | |||
| 15f2e05192 | |||
| a122333aaa | |||
| 06b2186bf9 | |||
| ed10dccfe2 | |||
| a1006dddb5 | |||
| 443a32dc81 | |||
| b034b4fe2f | |||
| 27b270148b | |||
| 269c64e4ed | |||
| eaf76e27f5 | |||
| 385b881068 | |||
| cf26696d12 | |||
| cb7ea40e7c | |||
| 5aaa55197e | |||
| d86d614520 | |||
| 138ca80c10 | |||
| d11a1622f8 | |||
| 42c996e16e | |||
| 1e37d75e49 | |||
| ad34d9d402 | |||
| 8c610f8a83 | |||
| f7f3e3cc03 | |||
| d68c6f9f2e | |||
| 90a5c4fbf8 | |||
| 042be6e229 | |||
| 4923c2e204 | |||
| b94d94e116 | |||
| d629ae8fbb | |||
| 1296a2e9ec | |||
| 009d02fa68 | |||
| 4cc57e9c91 | |||
| d373c6398e | |||
| 82746a0669 | |||
| 1212c3627b | |||
| 813f16ccee | |||
| 1c3cb91ecd | |||
| 5b1735db2b | |||
| bf31ee5fd6 | |||
| 1380b42c1d | |||
| dea853d840 | |||
| d72bf0739a | |||
| 481f5c0a97 | |||
| 2b017ac6b5 | |||
| 8a733ee337 | |||
| 9dd87a48a6 | |||
| 8fabbde13b | |||
| e0a378cb81 | |||
| 0b3329ca35 | |||
| 50c77b51db | |||
| c883ed19d6 | |||
| 795791219e | |||
| f6f4660cd2 | |||
| 9576f6e91e | |||
| f5e5bbefb2 | |||
| d4323fb5e0 | |||
| 8af1ddd10d | |||
| 62f6d9a413 | |||
| 783aed5826 | |||
| 141cd819a1 | |||
| 44fa96eb49 | |||
| 778b514b65 | |||
| afd58d69e4 | |||
| 4af9849b2b | |||
| 4dac44e720 | |||
| 71871901ef | |||
| d39e7584c0 | |||
| 4e9c5612ca | |||
| c8510dd45b | |||
| c234c17352 | |||
| cfae483d9d | |||
| d01ea13de4 | |||
| 9a73ee6952 | |||
| 28eb9e8c17 | |||
| 749c91f662 | |||
| 97ac17a12a | |||
| 32fd4fa8ed | |||
| 12fe4c6ba5 | |||
| b1e9fd95ca | |||
| d83043d8f2 | |||
| 2abeca6220 | |||
| 781810ed9c | |||
| 2142847de3 | |||
| ca42c266ef | |||
| f258ef1011 | |||
| 38cb763fd3 | |||
| 3fa78241ef | |||
| 3c7bc13be9 | |||
| 2441ca35b3 | |||
| 216a3977be | |||
| 647a51af15 | |||
| 530d4ce717 | |||
| e5d81b4d5c | |||
| 6eaeca1f3d | |||
| 4220034eab | |||
| 76a8ddd354 | |||
| 0bad38a815 | |||
| 48a8aad20e | |||
| 36ad0b3014 | |||
| 95fc103eaf | |||
| f5754780a8 | |||
| 7114c3bdf9 | |||
| 5b9d599e83 | |||
| ffa4871035 | |||
| 01832ac139 | |||
| cb7ff2bb37 | |||
| 35dd194b28 | |||
| 7dac857135 | |||
| 608ccafc70 | |||
| 4cdc9ef9b3 | |||
| db60d4e453 | |||
| f5d427a04f | |||
| e4893e446c | |||
| 79ffbf3d1d | |||
| 068bb07d6e | |||
| 1c9d118ba2 | |||
| 5308796bac | |||
| 669205aa4d | |||
| 9d2c2f7945 | |||
| e3b44b0adb | |||
| 1a303a9c38 | |||
| 2befa58fce | |||
| c8ded4ddb3 | |||
| 7d211f74d1 | |||
| 0f95d41785 | |||
| 6389b530d9 | |||
| 412769ff03 | |||
| d2349741f7 | |||
| 821bf8d63a | |||
| 7b296e4863 | |||
| 1acfcf088c | |||
| e9680afdff | |||
| 9695f12322 | |||
| 4060b7457b | |||
| a68344959d | |||
| 41638d10bf | |||
| 9b4e166608 | |||
| 52a65fcad1 | |||
| da7c114d41 | |||
| 62edee0860 | |||
| 22ba5e7c94 | |||
| 48e9a9c7dd | |||
| 9dbffb0c93 | |||
| f95eb0f1c9 | |||
| f3197c1af7 | |||
| 59f04c96c5 | |||
| bf2034b80c | |||
| deffaef2b5 | |||
| 157ec003b7 | |||
| ba4021ad73 | |||
| 5edb8111a2 | |||
| e206b40468 | |||
| 6ebd9320db | |||
| 597a750fff | |||
| 1273da6e71 | |||
| eb9b57eef4 | |||
| 5aaa05d579 | |||
| 07abb0840b | |||
| b8064510e3 | |||
| a88d84e6e6 | |||
| c3c8f16793 | |||
| ce1fed8c16 | |||
| 9a8f5edd58 | |||
| 992d45c8af | |||
| c646d6dc60 | |||
| 9067c28d24 | |||
| afacea3fbb |
@@ -0,0 +1,296 @@
|
||||
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
# Note that the GitHub Release will be created with a generated
|
||||
# title/body based on your changelogs.
|
||||
|
||||
name: Release
|
||||
permissions:
|
||||
"contents": "write"
|
||||
|
||||
# This task will run whenever you push a git tag that looks like a version
|
||||
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
# If you push multiple tags at once, separate instances of this workflow will
|
||||
# spin up, creating an independent announcement for each one. However, GitHub
|
||||
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||
# mistake.
|
||||
#
|
||||
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||
# will be marked as a prerelease.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||
publishing: ${{ !github.event.pull_request }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.29.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
|
||||
# Build and packages all the platform-specific things
|
||||
build-local-artifacts:
|
||||
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||
# Let the initial task tell us to not run (currently very blunt)
|
||||
needs:
|
||||
- plan
|
||||
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Target platforms/runners are computed by dist in create-release.
|
||||
# Each member of the matrix has the following arguments:
|
||||
#
|
||||
# - runner: the github runner
|
||||
# - dist-args: cli flags to pass to dist
|
||||
# - install-dist: expression to run to install dist on the runner
|
||||
#
|
||||
# Typically there will be:
|
||||
# - 1 "global" task that builds universal installers
|
||||
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container: ${{ matrix.container && matrix.container.image || null }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||
steps:
|
||||
- name: enable windows longpaths
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install Rust non-interactively if not already installed
|
||||
if: ${{ matrix.container }}
|
||||
run: |
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
fi
|
||||
- name: Install dist
|
||||
run: ${{ matrix.install_dist.run }}
|
||||
# Get the dist-manifest
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
${{ matrix.packages_install }}
|
||||
- name: Build artifacts
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
- id: cargo-dist
|
||||
name: Post-build
|
||||
# We force bash here just because github makes it really hard to get values up
|
||||
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||
# inconsistent syntax between shell and powershell.
|
||||
shell: bash
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
|
||||
# Build and package all the platform-agnostic(ish) things
|
||||
build-global-artifacts:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
# Determines if we should publish/announce
|
||||
host:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
- build-global-artifacts
|
||||
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||
RELEASE_COMMIT: "${{ github.sha }}"
|
||||
run: |
|
||||
# Write and read notes from a file to avoid quoting breaking things
|
||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -1,13 +1,31 @@
|
||||
# Mac OS
|
||||
.DS_Store
|
||||
|
||||
/app-next
|
||||
/app-html
|
||||
/datasets
|
||||
/datasets2
|
||||
/datasets_*
|
||||
# Builds
|
||||
target
|
||||
dist
|
||||
vecid-to-indexes.js
|
||||
|
||||
TODO.md
|
||||
# Copies
|
||||
*\ copy*
|
||||
|
||||
.stfolder
|
||||
/charts
|
||||
/price
|
||||
# Ignored
|
||||
_*
|
||||
|
||||
# Editors
|
||||
.vscode
|
||||
.zed
|
||||
|
||||
# Logs
|
||||
.log
|
||||
|
||||
# Environment variables/configs
|
||||
.env
|
||||
|
||||
# Profiling
|
||||
profile.json.gz
|
||||
flamegraph.svg
|
||||
*.trace
|
||||
|
||||
# AI
|
||||
CLAUDE.md
|
||||
|
||||
@@ -1,10 +1,123 @@
|
||||
# Changelog
|
||||
<!--
|
||||
# v0.X.Y | WIP
|
||||

|
||||
-->
|
||||
|
||||
## v. 0.3.0 | [853930](https://mempool.space/block/00000000000000000002eb5e9a7950ca2d5d98bd1ed28fc9098aa630d417985d) - 2024/07/26
|
||||
# v0.X.0 | WIP | A new beginning
|
||||
|
||||

|
||||

|
||||
|
||||
### Parser
|
||||
Full rewrite
|
||||
|
||||
# [kibo-v0.5.0](https://github.com/bitcoinresearchkit/brk/tree/eea56d394bf92c62c81da8b78b8c47ea730683f5) | [873199](https://mempool.space/block/0000000000000000000270925aa6a565be92e13164565a3f7994ca1966e48050) - 2024/12/04
|
||||
|
||||

|
||||
|
||||
## Datasets
|
||||
|
||||
- Added `Sell Side Risk Ratio` to all entities
|
||||
- Added `Open`, `High` and `Low` datasets
|
||||
- Added `Satoshis Per Dollar`
|
||||
- Added `All Time High`
|
||||
- Added `All Time High Date`
|
||||
- Added `Days Since All Time High`
|
||||
- Added `Max Days Between All Time Highs`
|
||||
- Added `Max Years Between All Time Highs`
|
||||
- Added `Drawdown`
|
||||
- Added `Adjusted Value Created`, `Adjusted Value Destroyed` and `Adjusted Spent Output Profit Ratio` to all entities
|
||||
- Added `Realized Profit To Loss Ratio` to all entities
|
||||
- Added `Hash Price Min`
|
||||
- Added `Hash Price Rebound`
|
||||
- Removed all year datasets (25) in favor for epoch datasets (5), the former was too granular to be really useful
|
||||
- Removed datasets split by liquidity for all datasets **already split by any address kind**, while fun to have, they took time to compute, ram, and space to store and no one was actually checking them
|
||||
- Fixed a lot of values in split by liquidity datasets
|
||||
|
||||
## Website
|
||||
|
||||
- Updated the design yet again which made the website for something more minimal and easier on the eyes
|
||||
- Added a *Save In Bitcoin* (DCA) simulation page
|
||||
- ~Added a dashboard~ Added the latest values to the tree next to each option instead, while less values are visible at a time, it's much more readable and organised
|
||||
- Added a library of PDFs
|
||||
- Fixed service worker not passing 304 (not modified) response and instead serving cached responses
|
||||
- Fixed history not being properly registered
|
||||
- Fixed window being moveable on iOS when in standalone mode when it shouldn't be
|
||||
- Added `Compare` section to all groups, to compare all datasets within a group
|
||||
- Updated `Solid Signals` library, which had an important breaking change on the `createEffect` function which might bring some bugs
|
||||
- Fixed some datasets paths
|
||||
- A lot of code reorg and file splits
|
||||
- Adopted a framework like approach to load pages while still being pure JS without a build step
|
||||
- Probably more that was forgotten
|
||||
|
||||
## Parser
|
||||
|
||||
- Added a `/datasets/last` json file with all the latest values
|
||||
- Added `--rpcconnect` parameter to the config
|
||||
- Added handling of SIGINT and SIGTERM terminal signals which menas you can now safely CTRL+C or kill the parser while it's exporting
|
||||
- Added config print at the start of the program
|
||||
- Compressed `empty_address_data` struct to save space (should shave of between up to 50% of the `address_index_to_empty_address_data` database)
|
||||
- Doubled the number of `txid_to_tx_data` databases from 4096 to 8192
|
||||
- ~Added `--recompute_computed true` argument, to allow recomputation of computed datasets in case of a bug~ Buggy for now
|
||||
- Fixed not saved arguments, not being processed properly
|
||||
- Fixed bug in `generic_map.multi_insert_simple_average`
|
||||
- Added defragmentation option `--first-defragment true` of databases to save space (which can save up to 50%)
|
||||
- Fixed bug in the computation of averages in `GenericMap`
|
||||
- Added support and paramer for cookie files with `--rpccookiefile`, and auto find if the path is `--datadir/.cookie`
|
||||
- Increased number of retries and time between them when fetching price from exchanges APIs
|
||||
|
||||
## Server
|
||||
|
||||
- Fixed links in several places missing the `/api` part and thus not working
|
||||
- Fixed broken last values routes
|
||||
- Added support for the `/datasets/last` file via the `/api/last` route
|
||||
- Added support for `.json` (won't change anything) and `.csv` (will download a csv file) extension at the end of datasets routes
|
||||
- Added `all=true` query parameter to dataset routes to get to full history
|
||||
|
||||
## Biter
|
||||
|
||||
- Moved back to this repo
|
||||
|
||||
# [kibo-v0.4.0](https://github.com/bitcoinresearchkit/brk/tree/a64c544815d9ef785e2fc1323582f774f16b9200) | [861950](https://mempool.space/block/00000000000000000000530d0e30ccf7deeace122dcc99f2668a06c6dad83629) - 2024/09/19
|
||||
|
||||

|
||||
|
||||
## Brand
|
||||
|
||||
- **Satonomics** is now **kibo** 🎉
|
||||
|
||||
## Website
|
||||
|
||||
- Complete redesign of the website
|
||||
- Rewrote the whole application and removed `node`/`npm`/`pnpm` dependencies in favor for pure `HTML`/`CSS`/`Javascript`
|
||||
- Website is now served by the server
|
||||
- Added Trading View attribution link to the settings frame and file in the lightweight charts folder
|
||||
- Many other changes
|
||||
|
||||
## Parser
|
||||
|
||||
- Changed the block iterator from a custom version of [bitcoin-explorer](https://crates.io/crates/bitcoin-explorer) to the homemade [biter](https://crates.io/crates/biter) which allows the parser to run alongside `bitcoind`
|
||||
- Added datasets compression thanks to [zstd](https://crates.io/crates/zstd) to reduce disk usage
|
||||
- Use the Bitcoin RPC server for various calls instead of running cli commands and then parsing the JSON from the output
|
||||
- **Important database changes that will need a full rescan**:
|
||||
- Changed databases page size from 1MB to 4KB for improved disk usage
|
||||
- Split txid_to_tx_data database in 4096 chunks (from 256) for improved disk usage
|
||||
- Split address_index_to_X databases to chunks of 25_000 instead of 50_000
|
||||
- Removed local Multisig database
|
||||
- Updated the config, run with `-h` to see possible args
|
||||
- Moved outputs from `/target/outputs` to `/out` to allow to run commands like `cargo clean` without side effects
|
||||
- Various first run fixes
|
||||
- Added to `-h` which arguments are saved, which is all of them at the time of writing
|
||||
|
||||
## Server
|
||||
|
||||
- Updated the code to support compressed binaries
|
||||
- Added serving of the website
|
||||
- Improved `Cache-Control` behavior
|
||||
|
||||
# [satonomics-v0.3.0](https://github.com/bitcoinresearchkit/brk/tree/b68b016091c45b071218fba01bac5b76e8eaf18c) | [853930](https://mempool.space/block/00000000000000000002eb5e9a7950ca2d5d98bd1ed28fc9098aa630d417985d) - 2024/07/26
|
||||
|
||||

|
||||
|
||||
## Parser
|
||||
|
||||
- Global
|
||||
- Improved self-hosting by:
|
||||
@@ -47,7 +160,7 @@
|
||||
- Price
|
||||
- Improved error message when price cannot be found
|
||||
|
||||
### App
|
||||
## App
|
||||
|
||||
- General
|
||||
- Added chart scroll button for nice animations à la Wicked
|
||||
@@ -73,17 +186,17 @@
|
||||
- Settings
|
||||
- Removed the horizontal scroll bar which was unintended
|
||||
|
||||
### Server
|
||||
## Server
|
||||
|
||||
- Run file
|
||||
- Only run with a watcher if `cargo watch` is available
|
||||
- Removed id_to_path file in favor for only `paths.d.ts` in `app/src/types`
|
||||
|
||||
## v. 0.2.0 | [851286](https://mempool.space/block/0000000000000000000281ca7f1bf8c50702bfca168c7af1bdc67c977c1ac8ed) - 2024/07/08
|
||||
# [satonomics-v0.2.0](https://github.com/bitcoinresearchkit/brk/tree/248187889283597c5dbb806292297453c25e97b8) | [851286](https://mempool.space/block/0000000000000000000281ca7f1bf8c50702bfca168c7af1bdc67c977c1ac8ed) - 2024/07/08
|
||||
|
||||

|
||||

|
||||
|
||||
### App
|
||||
## App
|
||||
|
||||
- General
|
||||
- Added the height version of all datasets and many optimizations to make them usable but only available on desktop and tablets for now
|
||||
@@ -111,24 +224,24 @@
|
||||
- Hopefully made scrollbars a little more subtle on WIndows and Linux, can't test
|
||||
- Generale style updates
|
||||
|
||||
### Parser
|
||||
## Parser
|
||||
|
||||
- Fixed ulimit only being run in Mac OS instead of whenever the program is detected
|
||||
|
||||
## v. 0.1.1 | [849240](https://mempool.space/block/000000000000000000002b8653988655071c07bb5f7181c038f9326bc86db741) - 2024/06/24
|
||||
# [satonomics-v0.1.1](https://github.com/bitcoinresearchkit/brk/tree/e55b5195a9de9aea306903c94ed63cb1720fda5f) | [849240](https://mempool.space/block/000000000000000000002b8653988655071c07bb5f7181c038f9326bc86db741) - 2024/06/24
|
||||
|
||||

|
||||

|
||||
|
||||
### Parser
|
||||
## Parser
|
||||
|
||||
- Fixed overflow in `Price` struct which caused many Realized Caps and Realized Prices to have completely bogus data
|
||||
- Fixed Realized Cap computation which was using rounded prices instead normal ones
|
||||
|
||||
### Server
|
||||
## Server
|
||||
|
||||
- Added the chunk, date and time of the request to the terminal logs
|
||||
|
||||
### App
|
||||
## App
|
||||
|
||||
- Chart
|
||||
- Added double click option on a legend to toggle the visibility of all other series
|
||||
@@ -161,14 +274,14 @@
|
||||
- Misc
|
||||
- Removed tracker even though it was a very privacy friendly as it appeared to not be working properly
|
||||
|
||||
### Price
|
||||
## Price
|
||||
|
||||
- Deleted old price datasets and their backups
|
||||
|
||||
## v. 0.1.0 | [848642](https://mempool.space/block/000000000000000000020be5761d70751252219a9557f55e91ecdfb86c4e026a) - 2024/06/19
|
||||
# [satonomics-v0.1.0](https://github.com/bitcoinresearchkit/brk/tree/a1a576d088c8f83ed32d48753a7611f70a964574) | [848642](https://mempool.space/block/000000000000000000020be5761d70751252219a9557f55e91ecdfb86c4e026a) - 2024/06/19
|
||||
|
||||

|
||||

|
||||
|
||||
## v. 0.0.X | [835444](https://mempool.space/block/000000000000000000009f93907a0dd83c080d5585cc7ec82c076d45f6d7c872) - 2024/03/20
|
||||
# satonomics-v0.0.1 | [835444](https://mempool.space/block/000000000000000000009f93907a0dd83c080d5585cc7ec82c076d45f6d7c872) - 2024/03/20
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/*"]
|
||||
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
|
||||
package.license = "MIT"
|
||||
package.edition = "2024"
|
||||
package.version = "0.0.84"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
package.readme = "README.md"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
[workspace.dependencies]
|
||||
axum = "0.8.4"
|
||||
bitcoin = { version = "0.32.7", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_bundler = { version = "0.0.84", path = "crates/brk_bundler" }
|
||||
brk_cli = { version = "0.0.84", path = "crates/brk_cli" }
|
||||
brk_computer = { version = "0.0.84", path = "crates/brk_computer" }
|
||||
brk_structs = { version = "0.0.84", path = "crates/brk_structs" }
|
||||
brk_error = { version = "0.0.84", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.0.84", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.0.84", path = "crates/brk_indexer" }
|
||||
brk_interface = { version = "0.0.84", path = "crates/brk_interface" }
|
||||
brk_logger = { version = "0.0.84", path = "crates/brk_logger" }
|
||||
brk_mcp = { version = "0.0.84", path = "crates/brk_mcp" }
|
||||
brk_parser = { version = "0.0.84", path = "crates/brk_parser" }
|
||||
brk_server = { version = "0.0.84", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.0.84", path = "crates/brk_store" }
|
||||
brk_vecs = { version = "0.0.84", path = "crates/brk_vecs" }
|
||||
brk_vecs_macros = { version = "0.0.84", path = "crates/brk_vecs_macros" }
|
||||
byteview = "=0.6.1"
|
||||
derive_deref = "1.1.1"
|
||||
fjall = "2.11.2"
|
||||
jiff = "0.2.15"
|
||||
log = "0.4.27"
|
||||
minreq = { version = "2.14.0", features = ["https", "serde_json"] }
|
||||
parking_lot = "0.12.4"
|
||||
rayon = "1.10.0"
|
||||
serde = "1.0.219"
|
||||
serde_bytes = "0.11.17"
|
||||
serde_derive = "1.0.219"
|
||||
serde_json = { version = "1.0.142", features = ["float_roundtrip"] }
|
||||
tokio = { version = "1.47.1", features = ["rt-multi-thread"] }
|
||||
zerocopy = "0.8.26"
|
||||
zerocopy-derive = "0.8.26"
|
||||
|
||||
[workspace.metadata.release]
|
||||
shared-version = true
|
||||
tag-name = "v{{version}}"
|
||||
pre-release-commit-message = "release: v{{version}}"
|
||||
tag-message = "release: v{{version}}"
|
||||
|
||||
[workspace.metadata.dist]
|
||||
cargo-dist-version = "0.29.0"
|
||||
ci = "github"
|
||||
installers = []
|
||||
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Satonomics
|
||||
Copyright (c) 2025 bitcoinresearchkit, kibo.money, satonomics
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,103 +1,101 @@
|
||||
# SATONOMICS
|
||||
# Bitcoin Research Kit
|
||||
|
||||

|
||||
<p align="left">
|
||||
<a href="https://github.com/bitcoinresearchkit/brk">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/bitcoinresearchkit/brk?style=social">
|
||||
</a>
|
||||
<a href="https://github.com/bitcoinresearchkit/brk/blob/main/LICENSE.md">
|
||||
<img src="https://img.shields.io/crates/l/brk" alt="License" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/brk">
|
||||
<img src="https://img.shields.io/crates/v/brk" alt="Version" />
|
||||
</a>
|
||||
<a href="https://docs.rs/brk">
|
||||
<img src="https://img.shields.io/docsrs/brk" alt="Documentation" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/crates/size/brk" alt="Size" />
|
||||
<a href="https://deps.rs/crate/brk">
|
||||
<img src="https://deps.rs/crate/brk/latest/status.svg" alt="Dependency status">
|
||||
</a>
|
||||
<a href="https://discord.gg/HaR3wpH3nr">
|
||||
<img src="https://img.shields.io/discord/1350431684562124850?label=discord" alt="Discord" />
|
||||
</a>
|
||||
<a href="https://primal.net/p/nprofile1qqsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03sqxu2e6">
|
||||
<img src="https://img.shields.io/badge/nostr-purple?link=https%3A%2F%2Fprimal.net%2Fp%2Fnprofile1qqsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03sqxu2e6" alt="Nostr" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Description
|
||||
The Bitcoin Research Kit is a high-performance toolchain designed to parse, index, compute, serve and visualize data from a Bitcoin node, enabling users to gain deeper insights into the Bitcoin network.
|
||||
|
||||
Satonomics is a better, FOSS, Bitcoin-only, self-hostable Glassnode.
|
||||
In other words it's an alternative to [Glassnode](https://glassnode.com), [mempool.space](https://mempool.space/) (soon) and [electrs](https://github.com/romanz/electrs) (soon) all in one package with a particular focus on simplicity and ease of use.
|
||||
|
||||
While [mempool.space](https://mempool.space) gives a very micro view of the network where you can follow the journey of any address, this tool is the exact opposite and very complimentary by giving you a much more global/macro view of the flow and various dynamics of the network via thousands of charts.
|
||||
The toolkit can be used in various ways to accommodate as many needs as possible:
|
||||
|
||||
To promote even more transparency and trust in the network, this project is committed to making on-chain data accessible and verifiable by all, no matter your intentions or financial situation. That is why, the whole project is completely free, from code to services, including a real-time API with thousands and thousands of routes which can be used at will.
|
||||
- **[Website](https://bitcoinresearchkit.org)** \
|
||||
Everyone is welcome to visit the official instance and showcase of the suite's capabilities. \
|
||||
It has a wide range of functionalities including charts, tables and simulations which you can visit for free and without the need for an account. \
|
||||
Also available at: [brekit.org](https://brekit.org) // [kibo.money](https://kibo.money) // [satonomics.xyz](https://satonomics.xyz)
|
||||
- **[API](https://github.com/bitcoinresearchkit/brk/tree/main/crates/brk_server#brk-server)** \
|
||||
Researchers and developers are free to use BRK's public API with  dataset variants at their disposal. \
|
||||
Just like the website, it's entirely free, with no authentication or rate-limiting.
|
||||
- **[AI](https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_mcp/README.md#brk-mcp)** \
|
||||
LLMs have to possibility to connect to BRK's backend through a [MCP](https://modelcontextprotocol.io/introduction). \
|
||||
It will give them access to the same tools as the API, with no restrictions, and allow you to have your very own data analysts.
|
||||
- **[CLI](https://crates.io/crates/brk_cli)** \
|
||||
Node runners are strongly encouraged to try out and self-host their own instance using BRK's command line interface. \
|
||||
The CLI has multiple cogs available for users to tweak to adapt to all situations with even the possibility for web developers to create their own custom website which could later on be added as an alternative front-end.
|
||||
- **[Crates](https://crates.io/crates/brk)** \
|
||||
Rust developers have access to a wide range crates, each built upon one another with its own specific purpose, enabling independent use and offering great flexibility.
|
||||
PRs are welcome, especially if their goal is to introduce additional datasets.
|
||||
|
||||
**Having anyone be able to easily do a health-check of the network is incredibly important and should be wanted by every single bitcoiner.**
|
||||
The primary goal of this project is to be fully-featured and accessible for everyone, regardless of their background or financial situation - whether that person is an enthusiast, researcher, miner, analyst, or simply curious.
|
||||
|
||||
## Warning
|
||||
In contrast, existing alternatives tend to be either [very costly](https://studio.glassnode.com/pricing) or missing essential features, with the vast majority being closed-source and unverifiable, which fundamentally undermines the principles of Bitcoin.
|
||||
|
||||
This project is in a very early stage. The web app will have bugs, the API might break and the data can definitely to be false or slightly false.
|
||||
## Crates
|
||||
|
||||
## Instances
|
||||
- [`brk`](https://crates.io/crates/brk): A wrapper around all other `brk-*` crates
|
||||
- [`brk_bundler`](https://crates.io/crates/brk_bundler): A thin wrapper around [`rolldown`](https://rolldown.rs/)
|
||||
- [`brk_cli`](https://crates.io/crates/brk_cli): A command line interface to run a BRK instance
|
||||
- [`brk_computer`](https://crates.io/crates/brk_computer): A Bitcoin dataset computer built on top of [`brk_indexer`](https://crates.io/crates/brk_indexer)
|
||||
- [`brk_error`](https://crates.io/crates/brk_error): Errors used throughout BRK
|
||||
- [`brk_fetcher`](https://crates.io/crates/brk_fetcher): A Bitcoin price fetcher
|
||||
- [`brk_indexer`](https://crates.io/crates/brk_indexer): A Bitcoin indexer built on top of [`brk_parser`](https://crates.io/crates/brk_parser)
|
||||
- [`brk_interface`](https://crates.io/crates/brk_interface): An interface to find and format data from BRK
|
||||
- [`brk_logger`](https://crates.io/crates/brk_logger): A thin wrapper around [`env_logger`](https://crates.io/crates/env_logger)
|
||||
- [`brk_mcp`](https://crates.io/crates/brk_mcp): A bridge for LLMs to access BRK
|
||||
- [`brk_parser`](https://crates.io/crates/brk_parser): A very fast Bitcoin block parser and iterator built on top of [`bitcoin-rust`](https://crates.io/crates/bitcoin)
|
||||
- [`brk_server`](https://crates.io/crates/brk_server): A server with an API for anything from BRK
|
||||
- [`brk_store`](https://crates.io/crates/brk_store): A thin wrapper around [`fjall`](https://crates.io/crates/fjall)
|
||||
- [`brk_structs`](https://crates.io/crates/brk_structs): Structs used throughout BRK
|
||||
- [`brk_vecs`](https://crates.io/crates/brk_vecs): A KISS index/value store
|
||||
- [`brk_vecs_macros`](https://crates.io/crates/brk_vecs_macros): Macros for [`brk_vecs`](https://crates.io/crates/brk_vecs)
|
||||
|
||||
Web App:
|
||||
## Hosting as a service
|
||||
|
||||
- [app.satonomics.xyz](https://app.satonomics.xyz)
|
||||
If you'd like to have your own instance hosted for you please contact [hosting@bitcoinresearchkit.org](mailto:hosting@bitcoinresearchkit.org).
|
||||
|
||||
API:
|
||||
- 2 separate dedicated servers (1 GB/s each) with different ISPs and Cloudflare integration for enhanced performance and optimal availability
|
||||
- 99.99% SLA
|
||||
- Configured for speed
|
||||
- Updates delivered at your convenience
|
||||
- Direct communication for feature requests and support
|
||||
- Bitcoin Core or Knots with desired version
|
||||
- Optional subdomains: `*.bitcoinresearchkit.org`, `*.kibo.money` and `*.satonomics.xyz`
|
||||
- Logo featured in the Readme if desired
|
||||
|
||||
- [api.satonomics.xyz](https://api.satonomics.xyz)
|
||||
- [api-bkp.satonomics.xyz](https://api-bkp.satonomics.xyz)
|
||||
Pricing: `0.01 BTC / month` *or* `0.1 BTC / year`
|
||||
|
||||
## Structure
|
||||
## Acknowledgments
|
||||
|
||||
- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain.
|
||||
- `server`: A small server which automatically creates routes to access through an API all created datasets.
|
||||
- `app`: A web app which displays the generated datasets in various charts and dashboards.
|
||||
Deepest gratitude to the [Open Sats](https://opensats.org/) public charity. Their grant — from December 2024 to the present — has been critical in sustaining this project.
|
||||
|
||||
## Goals / Philosophy
|
||||
Heartfelt thanks go out to every donor on [Nostr](https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44) and [Geyser.fund](https://geyser.fund/project/brk) whose support has ensured the availability of the [bitcoinresearchkit.org](https://bitcoinresearchkit.org) public instance.
|
||||
|
||||
Adjectives that describe what this project is or strives to be, in no particular order:
|
||||
## Donate
|
||||
|
||||
- **Best**: Replace Glassnode as the go to
|
||||
- **Diverse**: Have as many charts/datasets as possible and something for everyone
|
||||
- **Free**: Is and always will be completely free
|
||||
- **Open**: With a very permissive license
|
||||
- **Trustless**: You can verify and see exactly how each dataset is computed
|
||||
- **Independent**: Only one, easily swappable, dependency (Price API)
|
||||
- **Educational**: By providing many datasets that can be used to describe how Bitcoin works and why
|
||||
- **Timeless**: Be relevant and usable 10 years from now by being independent and not do address tagging
|
||||
- **Sovereign**: Be self-hostable on accessible hardware
|
||||
- **Versatile**: You can view the data in charts, you can download the data, you can fetch the data via an API
|
||||
- **Accessible**: Free Website and API with all the datasets for everyone
|
||||
[`bc1q09 8zsm89 m7kgyz e338vf ejhpdt 92ua9p 3peuve`](bitcoin:bc1q098zsm89m7kgyze338vfejhpdt92ua9p3peuve)
|
||||
|
||||
## Milestones
|
||||
[`lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkxmmww3jkuar8d35kgetj8yuq363hv4`](lightning:lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkxmmww3jkuar8d35kgetj8yuq363hv4)
|
||||
|
||||
Big features that are planned, in no particular order:
|
||||
|
||||
- **Homepage**: A landing page to explains the project and what it does
|
||||
- **More Datasets/Charts**: If a dataset can be computed, it should exist and have its related charts
|
||||
- **Dashboards**: For a quick and real-time view of the latest data of all the datasets
|
||||
- **NOSTR integration**: First to save preferences, later to add some social functionnality
|
||||
- **Datasets by block timestamp**: In addition to having datasets by block date and block height
|
||||
- **Descriptions**: Add text to describe all charts and what they mean
|
||||
- **Start9 Add-on**: By making the whole suite much easier to self-host, it's quite rough right now
|
||||
- **API Documentation**: Highly needed to explain what's what
|
||||
|
||||
_Maybe_:
|
||||
|
||||
- A Desktop app
|
||||
- A mobile app
|
||||
|
||||
## Brand
|
||||
|
||||
- **Name**: Willing to change if someone thinks of something better !
|
||||
- **Logo**: Most likely a placeholder
|
||||
|
||||
## Collaboration
|
||||
|
||||
- Repositories:
|
||||
- [Github](https://github.com/satonomics-org/satonomics)
|
||||
- [Codeberg](https://codeberg.org/satonomics/satonomics)
|
||||
- Issues:
|
||||
- [Github](https://github.com/satonomics-org/satonomics/issues)
|
||||
- [NOSTR](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/issues)
|
||||
- Proposals:
|
||||
- [Github](https://github.com/satonomics-org/satonomics/pulls)
|
||||
- [NOSTR](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/proposals)
|
||||
|
||||
## Proof of Work
|
||||
|
||||
Aka: Previous iterations
|
||||
|
||||
The initial idea was totally different yet morphed over time into what it is today: a fully FOSS self-hostable on-chain data generator
|
||||
|
||||
- https://github.com/drgarlic/satonomics
|
||||
- https://github.com/drgarlic/satonomics-parser
|
||||
- https://github.com/drgarlic/satonomics-explorer
|
||||
- https://github.com/drgarlic/satonomics-server
|
||||
- https://github.com/drgarlic/satonomics-app
|
||||
- https://github.com/drgarlic/bitalisys
|
||||
- https://github.com/drgarlic/bitesque-app
|
||||
- https://github.com/drgarlic/bitesque-back
|
||||
- https://github.com/drgarlic/bitesque-front
|
||||
- https://github.com/drgarlic/bitesque-assets
|
||||
- https://github.com/drgarlic/syf
|
||||
[Geyser Fund](https://geyser.fund/project/brk)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# TODO
|
||||
|
||||
- __crates__
|
||||
- _cli_
|
||||
- check disk space on first launch
|
||||
- add custom path support for config.toml
|
||||
- maybe add bitcoind download and launch support
|
||||
- via: https://github.com/rust-bitcoin/corepc/blob/master/node
|
||||
- test read/write speed, add warning if too low (<2gb/s)
|
||||
- pull latest version and notify is out of date
|
||||
- _computer_
|
||||
- **add rollback of states (in stateful)**
|
||||
- remove configurable format (raw/compressed) and chose sane ones instead
|
||||
- linear reads: compressed (height/date/... + txindex_to_height + txindex_to_version + ...)
|
||||
- random reads: raw (outputindex_to_value + ...)
|
||||
- add prices paid by percentile (percentile cost basis) back
|
||||
- add support for per index computation
|
||||
- fix min feerate which is always ZERO due to coinbase transaction
|
||||
- before computing multiple sources check their length, panic if not equal
|
||||
- add oracle price dataset (https://utxo.live/oracle/UTXOracle.py)
|
||||
- add address counts relative to all datasets
|
||||
- make decade, quarter, year datasets `computed` instead of `eager`
|
||||
- add 6 months (semester) interval datasets to builder
|
||||
- some datasets in `indexes` can probably be removed
|
||||
- add revived/sent supply datasets
|
||||
- add `in-sats` version of all price datasets (average and co)
|
||||
- add `p2pk` group (sum of `p2pk33` and `p2pk65`)
|
||||
- add chopiness datasets
|
||||
- add utxo count, address count, supply data for by reused addresses in groups by address type
|
||||
- add more date ranges (3-6 months and more)
|
||||
- add puell multiple dataset
|
||||
- add pi cycle dataset
|
||||
- add emas of price
|
||||
- add 7d and 30d ema to sell side risk ratio and sopr
|
||||
- don't compute everything for all cohorts as some datasets combinations are irrelevant
|
||||
- addresses/utxos by amount don't need mvrvz for example
|
||||
- add all possible charts from:
|
||||
- https://mainnet.observer
|
||||
- https://glassnode.com
|
||||
- https://checkonchain.com
|
||||
- https://researchbitcoin.net/exciting-update-coming-to-the-bitcoin-lab/
|
||||
- https://mempool.space/research
|
||||
- _indexer_
|
||||
- parse only the needed block number
|
||||
- maybe using https://developer.bitcoin.org/reference/rpc/getblockhash.html
|
||||
- _interface_
|
||||
- create pagination enum
|
||||
- from to
|
||||
- from option<count>
|
||||
- to option<count>
|
||||
- page + option<per page> default 1000 max 1000
|
||||
- from/to/count params don’t cap all combinations
|
||||
- example: from -10,000 count 10, won’t work if underlying vec isn’t 10k or more long
|
||||
- _parser_
|
||||
- save `vec` file instead of `json`
|
||||
- support lock file, process in read only if already opened in write mode
|
||||
- if less than X (10 maybe ?) get block using rpc instead of parsing the block files
|
||||
- _server_
|
||||
- api
|
||||
- add extensions support (.json .csv …)
|
||||
- if format instead of extension then don't download file
|
||||
- add support for https (rustls)
|
||||
- lru cache
|
||||
- _vec_
|
||||
- add native lock file support (once it's available in stable rust)
|
||||
- improve compressed mode (slow reads)
|
||||
- add ema support
|
||||
- __docs__
|
||||
- _README_
|
||||
- add a comparison table with alternatives
|
||||
- add contribution section where help is needed
|
||||
- documentation/mcp/datasets/different front ends
|
||||
- add faq
|
||||
- __websites__
|
||||
- _default_
|
||||
- explorer
|
||||
- blocks
|
||||
- transactions
|
||||
- addresses
|
||||
- miners
|
||||
- maybe xpubs
|
||||
- charts
|
||||
- improve some names and colors
|
||||
- remove `sum` series when it's a duplicate of the `base` (in subsidy for example)
|
||||
- selected unit sometimes changes when going back end forth
|
||||
- add support for custom charts
|
||||
- separate z-score charts from "realized price" (with their own prices), have 4y, 2y and 1y
|
||||
- price scale format depends on unit, hide digits for sats for example (if/when possible)
|
||||
- table
|
||||
- pagination
|
||||
- exports (.json, .csv,…)
|
||||
- search
|
||||
- datasets add legend, and keywords ?
|
||||
- height/address/txid
|
||||
- api
|
||||
- add api page with interactivity
|
||||
- global
|
||||
- **fix navigation/history**
|
||||
- move share button to footer ?
|
||||
- Use `ichart.createPane()` in wrapper
|
||||
- improve behavior when local storage is unavailable
|
||||
- by having a global state
|
||||
- __global__
|
||||
- check `TODO`s in codebase
|
||||
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
charts
|
||||
dist
|
||||
dev-dist
|
||||
.DS_Store
|
||||
visualizer
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
.wrangler
|
||||
paths.d.ts
|
||||
@@ -1,17 +0,0 @@
|
||||
# Satonomics - App
|
||||
|
||||
## Description
|
||||
|
||||
A web app to view the generated datasets in various charts.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Install `node`
|
||||
- Install `pnpm`
|
||||
- If using `cloudflare`, add cache rule to bypass the cache for `/sw.js`
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
pnpm deploy-prod
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
/* /index.html
|
||||
@@ -1,375 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Satonomics</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A better, FOSS, Bitcoin-only, self-hostable Glassnode"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="theme-color" content="#0c0a09" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="196x196"
|
||||
href="/assets/favicon-196.png"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/assets/apple-icon-180.png" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2048-2732.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2732-2048.jpg"
|
||||
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1668-2388.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2388-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1536-2048.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2048-1536.jpg"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1488-2266.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2266-1488.jpg"
|
||||
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1640-2360.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2360-1640.jpg"
|
||||
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1668-2224.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2224-1668.jpg"
|
||||
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1620-2160.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2160-1620.jpg"
|
||||
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1290-2796.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2796-1290.jpg"
|
||||
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1179-2556.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2556-1179.jpg"
|
||||
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1284-2778.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2778-1284.jpg"
|
||||
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1170-2532.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2532-1170.jpg"
|
||||
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1125-2436.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2436-1125.jpg"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1242-2688.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2688-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-828-1792.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1792-828.jpg"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1242-2208.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-2208-1242.jpg"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-750-1334.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1334-750.jpg"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-640-1136.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-1136-640.jpg"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2048-2732.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2732-2048.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1668-2388.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2388-1668.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1536-2048.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2048-1536.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1488-2266.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2266-1488.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1640-2360.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2360-1640.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1668-2224.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2224-1668.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1620-2160.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2160-1620.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1290-2796.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2796-1290.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1179-2556.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2556-1179.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1284-2778.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2778-1284.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1170-2532.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2532-1170.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1125-2436.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2436-1125.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1242-2688.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2688-1242.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-828-1792.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1792-828.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1242-2208.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-2208-1242.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-750-1334.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1334-750.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-640-1136.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="/assets/apple-splash-dark-1136-640.jpg"
|
||||
media="(prefers-color-scheme: dark) and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
class="text-high-contrast overflow-hidden bg-white dark:bg-black"
|
||||
style="font-size: 15px; line-height: 22px"
|
||||
>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "satonomics",
|
||||
"description": "A better, FOSS, Bitcoin-only, self-hostable Glassnode",
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "($npm_execpath outdated || read -p \"Press enter to ignore...\") && vite --host",
|
||||
"build": "vite build",
|
||||
"check": "tsc --noEmit --skipLibCheck --pretty",
|
||||
"check-watch": "$npm_execpath check --watch",
|
||||
"format": "prettier --write './src'",
|
||||
"prod": "$npm_execpath run build && vite preview --host",
|
||||
"pages-prod": "pnpm build && pnpm wrangler pages deploy ./dist",
|
||||
"pages-dev": "pnpm build && pnpm wrangler pages deploy --branch dev ./dist",
|
||||
"assets": "pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --manifest ./public/manifest.webmanifest --icon-only --favicon --background \"linear-gradient(to right bottom, rgb(249, 115, 22), rgb(154, 52, 18))\" --padding \"min(15vh, 15vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --splash-only --background \"linear-gradient(to right bottom, rgb(249, 115, 22), rgb(154, 52, 18))\" --padding \"min(33vh, 33vw)\" --path-override \"/assets\" && pnpm pwa-asset-generator ./public/logo/white.svg ./public/assets --index ./index.html --splash-only --dark-mode --background \"#0c0a09\" --padding \"min(33vh, 33vw)\" --path-override \"/assets\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@leeoniya/ufuzzy": "^1.0.14",
|
||||
"@solid-primitives/event-listener": "^2.3.3",
|
||||
"@solid-primitives/intersection-observer": "^2.1.6",
|
||||
"@solid-primitives/resize-observer": "^2.0.26",
|
||||
"lean-qr": "^2.3.4",
|
||||
"lightweight-charts": "^4.1.7",
|
||||
"solid-js": "^1.8.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@iconify-json/tabler": "^1.1.118",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"pwa-asset-generator": "^6.3.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
"unplugin-icons": "^0.19.0",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-solid": "^2.10.2",
|
||||
"workbox-window": "^7.1.0",
|
||||
"wrangler": "^3.66.0"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/** @type {import("prettier").Options} */
|
||||
export default {
|
||||
plugins: [
|
||||
'@ianvs/prettier-plugin-sort-imports',
|
||||
'prettier-plugin-tailwindcss', // MUST come last
|
||||
],
|
||||
|
||||
tailwindFunctions: ['classList'],
|
||||
|
||||
importOrder: ['<THIRD_PARTY_MODULES>', '', '^/?(~|src)/', '', '^[./]'],
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" fill="black">
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1004 B |
@@ -1,17 +0,0 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" fill="white">
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,5.12904)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,0.129039)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(1.14102,0,0,2.63158,-0.849652,-4.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,-9.87096)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
<g transform="matrix(0.285256,0,0,2.63158,8.78759,10.129)">
|
||||
<rect x="4.25" y="3.751" width="14.023" height="1.52"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1004 B |
@@ -1,194 +0,0 @@
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
const texts = [
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
"satonomics",
|
||||
|
||||
"stay humble, stack sats",
|
||||
"21 million",
|
||||
"cold storage",
|
||||
"utxo",
|
||||
"satoshi nakamoto",
|
||||
"hodl",
|
||||
`don't trust, verify`,
|
||||
"zap",
|
||||
"₿itcoin",
|
||||
"lightning",
|
||||
"nostr",
|
||||
"freedom tech",
|
||||
"2008/10/31",
|
||||
"2009/01/03",
|
||||
"2010/05/22",
|
||||
"hodl!",
|
||||
"Hal Finney",
|
||||
"Vote for better money",
|
||||
"gradually then suddenly",
|
||||
"timechain",
|
||||
"self custody",
|
||||
"be your own bank",
|
||||
"resistance money",
|
||||
"foss",
|
||||
"permissionless",
|
||||
"great reset",
|
||||
"orange pill",
|
||||
"borderless",
|
||||
"anonymous",
|
||||
"nyknyc",
|
||||
"low time preference",
|
||||
"absolute scarcity",
|
||||
"time is scarce",
|
||||
"ride or die",
|
||||
"cyberpunk",
|
||||
];
|
||||
|
||||
export function Background({
|
||||
mode,
|
||||
opacity,
|
||||
focused,
|
||||
}: {
|
||||
mode: SL<"Scroll" | "Static">;
|
||||
opacity: SL<{ text: string; value: number }>;
|
||||
focused: Accessor<boolean>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
class="absolute h-full w-full overflow-hidden will-change-auto"
|
||||
style={{
|
||||
opacity: opacity.selected().value,
|
||||
}}
|
||||
>
|
||||
<div class="-m-[2rem] -space-y-1 overflow-hidden md:-m-[1rem]">
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
<Line mode={mode} focused={focused} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute h-full w-full opacity-10 mix-blend-multiply">
|
||||
<Noise />
|
||||
</div>
|
||||
<div class="absolute h-full w-full opacity-10 mix-blend-hard-light">
|
||||
<Noise />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Line({
|
||||
mode,
|
||||
focused,
|
||||
}: {
|
||||
mode: SL<"Scroll" | "Static">;
|
||||
focused: Accessor<boolean>;
|
||||
}) {
|
||||
const shuffled = shuffle(texts).slice(0, 10);
|
||||
const joined = shuffled.join(". ");
|
||||
|
||||
return (
|
||||
<div class="select-none whitespace-nowrap">
|
||||
<TextWrapper mode={mode} focused={focused} joined={joined} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextWrapper({
|
||||
joined,
|
||||
mode,
|
||||
focused,
|
||||
}: {
|
||||
mode: SL<"Scroll" | "Static">;
|
||||
focused: Accessor<boolean>;
|
||||
joined: string;
|
||||
}) {
|
||||
const p = createRWS(undefined as HTMLParagraphElement | undefined);
|
||||
|
||||
const seconds = createRWS(joined.length * 2);
|
||||
|
||||
const wasOnceOn = createRWS(false);
|
||||
|
||||
createEffect(() => {
|
||||
if (!wasOnceOn() && mode.selected() === "Scroll") {
|
||||
wasOnceOn.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
seconds.set(Math.round(p()!.clientWidth / 20));
|
||||
});
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={p.set}
|
||||
class="inline-block px-2 text-[5dvh] font-black uppercase leading-none"
|
||||
style={{
|
||||
...(wasOnceOn()
|
||||
? {
|
||||
animation: `marquee ${seconds()}s linear infinite`,
|
||||
"animation-play-state":
|
||||
focused() && mode.selected() === "Scroll"
|
||||
? "running"
|
||||
: "paused",
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{joined} {wasOnceOn() ? joined : undefined}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function shuffle<T>([...arr]: T[]): T[] {
|
||||
let m = arr.length;
|
||||
|
||||
while (m) {
|
||||
const i = Math.floor(Math.random() * m--);
|
||||
[arr[m], arr[i]] = [arr[i], arr[m]];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
function Noise() {
|
||||
return (
|
||||
<svg
|
||||
class="size-full"
|
||||
viewBox="0 0 200 200"
|
||||
preserveAspectRatio="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<filter id="noiseFilter">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="3"
|
||||
numOctaves="3"
|
||||
stitchTiles="stitch"
|
||||
/>
|
||||
</filter>
|
||||
|
||||
<rect width="100%" height="100%" filter="url(#noiseFilter)" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
|
||||
export function Box({
|
||||
flex = true,
|
||||
absolute,
|
||||
padded = true,
|
||||
spaced = true,
|
||||
children,
|
||||
dark,
|
||||
classes,
|
||||
}: {
|
||||
flex?: boolean;
|
||||
absolute?: "top" | "bottom";
|
||||
padded?: boolean;
|
||||
spaced?: boolean;
|
||||
dark?: boolean;
|
||||
classes?: string;
|
||||
} & ParentProps) {
|
||||
return (
|
||||
<div
|
||||
class={classPropToString([
|
||||
"p-2",
|
||||
absolute
|
||||
? [
|
||||
"absolute inset-x-0",
|
||||
absolute === "top"
|
||||
? "top-0"
|
||||
: "pointer-events-none bottom-0 bg-gradient-to-b from-transparent to-orange-100 dark:to-black",
|
||||
]
|
||||
: "relative",
|
||||
classes,
|
||||
])}
|
||||
>
|
||||
<div
|
||||
class={classPropToString([
|
||||
"border-lighter pointer-events-auto relative overflow-hidden rounded-xl border shadow-md",
|
||||
dark
|
||||
? "bg-white/40 backdrop-blur-sm dark:bg-orange-100/5"
|
||||
: "bg-white/60 backdrop-blur-md dark:bg-orange-200/10",
|
||||
])}
|
||||
>
|
||||
<div
|
||||
class={classPropToString([
|
||||
flex && "flex w-full",
|
||||
spaced && "space-x-2",
|
||||
padded && "p-1.5",
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { random } from "/src/scripts/utils/math/random";
|
||||
|
||||
export function Button({
|
||||
onClick,
|
||||
children,
|
||||
}: { onClick: VoidFunction } & ParentProps) {
|
||||
return (
|
||||
<button
|
||||
class="group flex w-full flex-1 items-center justify-center rounded-lg px-2 py-1.5 hover:bg-orange-200/20 active:scale-95"
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ButtonRandomChart({ presets }: { presets: Presets }) {
|
||||
return (
|
||||
<button
|
||||
class="inline-flex rounded-md bg-orange-700 bg-opacity-80 px-1.5 py-0.5 font-medium hover:bg-opacity-100 active:scale-95"
|
||||
onClick={() => {
|
||||
const randomPreset = random(presets.list);
|
||||
if (randomPreset) {
|
||||
presets.select(randomPreset);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Open a random chart
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Button } from "./button";
|
||||
|
||||
export function Actions({
|
||||
presets,
|
||||
fullscreen,
|
||||
qrcode,
|
||||
}: {
|
||||
presets: Presets;
|
||||
qrcode: RWS<string>;
|
||||
fullscreen?: RWS<boolean>;
|
||||
}) {
|
||||
const ButtonShare = lazy(() =>
|
||||
import("./buttonShare").then((d) => ({ default: d.ButtonShare })),
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="flex space-x-1 p-1.5">
|
||||
<Show when={fullscreen}>
|
||||
{(fullscreen) => (
|
||||
<Button
|
||||
title="Toggle fullscreen"
|
||||
icon={() =>
|
||||
fullscreen()()
|
||||
? IconTablerLayoutSidebarLeftExpand
|
||||
: IconTablerLayoutSidebarRightExpand
|
||||
}
|
||||
onClick={() => {
|
||||
fullscreen().set((b) => !b);
|
||||
}}
|
||||
classes="hidden md:block"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<ButtonShare qrcode={qrcode} />
|
||||
|
||||
<Button
|
||||
title="Favorite"
|
||||
colors={() =>
|
||||
presets.selected().isFavorite()
|
||||
? "text-amber-500 bg-amber-500/15 hover:bg-amber-500/30"
|
||||
: ""
|
||||
}
|
||||
icon={() =>
|
||||
presets.selected().isFavorite()
|
||||
? IconTablerStarFilled
|
||||
: IconTablerStar
|
||||
}
|
||||
onClick={() => presets.selected().isFavorite.set((b) => !b)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
|
||||
export function Button({
|
||||
title,
|
||||
icon,
|
||||
colors,
|
||||
onClick,
|
||||
disabled,
|
||||
classes,
|
||||
}: {
|
||||
title: string;
|
||||
icon: () => ValidComponent;
|
||||
colors?: () => string;
|
||||
onClick: VoidFunction;
|
||||
disabled?: () => boolean;
|
||||
classes?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
title={title}
|
||||
disabled={disabled?.()}
|
||||
class={classPropToString([
|
||||
colors?.() || (disabled?.() ? "" : "hover:bg-orange-200/15"),
|
||||
!disabled?.() && "group",
|
||||
classes,
|
||||
"flex-none rounded-lg p-2 disabled:opacity-50",
|
||||
])}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Dynamic
|
||||
component={icon()}
|
||||
class="size-[1.125rem] group-active:scale-90"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Button } from "./button";
|
||||
|
||||
export function ButtonShare({ qrcode }: { qrcode: RWS<string> }) {
|
||||
return (
|
||||
<Button
|
||||
title="Share"
|
||||
icon={() => IconTablerShare}
|
||||
onClick={() =>
|
||||
import("lean-qr").then(({ generate }) =>
|
||||
qrcode.set(() =>
|
||||
generate(document.location.href).toDataURL({
|
||||
on: [0xff, 0xff, 0xff, 0xff],
|
||||
off: [0x00, 0x00, 0x00, 0x00],
|
||||
padX: 0,
|
||||
padY: 0,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
import { requestIdleCallbackPossible } from "/src/env";
|
||||
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
|
||||
import { createChart } from "/src/scripts/lightweightCharts/create";
|
||||
import { createSeriesGroup } from "/src/scripts/lightweightCharts/group";
|
||||
import { setMinMaxMarkers } from "/src/scripts/lightweightCharts/markers";
|
||||
import {
|
||||
debouncedUpdateVisiblePriceSeriesType,
|
||||
updateVisiblePriceSeriesType,
|
||||
} from "/src/scripts/lightweightCharts/price";
|
||||
import {
|
||||
initTimeScale,
|
||||
setInitialTimeRange,
|
||||
} from "/src/scripts/lightweightCharts/time";
|
||||
import { setWhitespace } from "/src/scripts/lightweightCharts/whitespace";
|
||||
import { SeriesType } from "/src/scripts/presets/enums";
|
||||
import { colors } from "/src/scripts/utils/colors";
|
||||
import { debounce } from "/src/scripts/utils/debounce";
|
||||
import { createSL } from "/src/scripts/utils/selectableList/static";
|
||||
import { webSockets } from "/src/scripts/ws";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { RadioGroup } from "../../settings";
|
||||
|
||||
export function Chart({
|
||||
activeDatasets,
|
||||
activeIds,
|
||||
charts,
|
||||
chartsDrawn,
|
||||
dark,
|
||||
datasets,
|
||||
exactRange,
|
||||
firstChartSetter,
|
||||
index,
|
||||
lastActiveIndex,
|
||||
lastChartIndex,
|
||||
legendSetter,
|
||||
preset: presetAccessor,
|
||||
priceSeriesType,
|
||||
seriesConfigs,
|
||||
seriesCount,
|
||||
}: {
|
||||
activeDatasets: ReadWriteSignal<ResourceDataset<any, any>[]>;
|
||||
activeIds: RWS<number[]>;
|
||||
charts: ReadWriteSignal<
|
||||
{
|
||||
chart: RWS<IChartApi | undefined>;
|
||||
whitespace: RWS<ISeriesApiAny | undefined>;
|
||||
}[]
|
||||
>;
|
||||
chartsDrawn: Accessor<ReadWriteSignal<boolean>[]>;
|
||||
dark: Accessor<boolean>;
|
||||
datasets: Datasets;
|
||||
exactRange: ReadWriteSignal<TimeRange>;
|
||||
firstChartSetter: Setter<IChartApi | undefined>;
|
||||
index: Accessor<number>;
|
||||
lastActiveIndex: Accessor<number | undefined>;
|
||||
lastChartIndex: Accessor<number>;
|
||||
legendSetter: Setter<SeriesLegend[]>;
|
||||
preset: Accessor<Preset>;
|
||||
priceSeriesType: ReadWriteSignal<PriceSeriesType>;
|
||||
seriesConfigs: SeriesConfig[];
|
||||
seriesCount: Accessor<number>;
|
||||
}) {
|
||||
const div = createRWS<HTMLDivElement | undefined>(undefined);
|
||||
const chartIndex = index();
|
||||
|
||||
const isDrawn = chartsDrawn()[chartIndex];
|
||||
const isLastDrawn = createMemo(
|
||||
() => chartsDrawn().findLastIndex((drawn) => drawn()) === chartIndex,
|
||||
);
|
||||
|
||||
const chartPriceModeKey = `chart-price-mode-${chartIndex}` as const;
|
||||
const chartPriceMode = createSL(["Linear", "Log"] as const, {
|
||||
saveable: {
|
||||
key: chartPriceModeKey,
|
||||
mode: "localStorage",
|
||||
},
|
||||
defaultValue: chartIndex === 0 ? "Log" : "Linear",
|
||||
});
|
||||
|
||||
createEffect(
|
||||
on([div, () => charts()[chartIndex]], ([div, chartConfig]) => {
|
||||
if (!div || !chartConfig) return;
|
||||
|
||||
const preset = presetAccessor();
|
||||
const scale = preset.scale;
|
||||
|
||||
const chart = createChart({
|
||||
scale,
|
||||
element: div,
|
||||
dark,
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
console.log("chart: undefined");
|
||||
return;
|
||||
}
|
||||
|
||||
const whitespace = setWhitespace(chart, scale);
|
||||
|
||||
batch(() => {
|
||||
chartConfig.chart.set(chart);
|
||||
chartConfig.whitespace.set(whitespace);
|
||||
|
||||
if (chartIndex === 0) {
|
||||
firstChartSetter(chart);
|
||||
}
|
||||
});
|
||||
|
||||
const range = exactRange();
|
||||
|
||||
setInitialTimeRange({ chart, range });
|
||||
|
||||
if (chartIndex === 0) {
|
||||
initTimeScale({
|
||||
scale,
|
||||
chart,
|
||||
activeIds,
|
||||
exactRange,
|
||||
});
|
||||
|
||||
if (range) {
|
||||
updateVisiblePriceSeriesType({
|
||||
scale,
|
||||
chart,
|
||||
priceSeriesType,
|
||||
timeRange: range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const chartLegend: SeriesLegend[] = [];
|
||||
|
||||
onCleanup(() => {
|
||||
chartLegend.length = 0;
|
||||
});
|
||||
|
||||
const markerCallback = () =>
|
||||
setMinMaxMarkers({
|
||||
scale,
|
||||
visibleRange: exactRange(),
|
||||
legendList: chartLegend,
|
||||
dark,
|
||||
activeIds: activeIds,
|
||||
});
|
||||
|
||||
const debouncedSetMinMaxMarkers = requestIdleCallbackPossible
|
||||
? () => requestIdleCallback(markerCallback)
|
||||
: debounce(
|
||||
markerCallback,
|
||||
seriesCount() * 10 + scale === "date" ? 50 : 100,
|
||||
);
|
||||
|
||||
createEffect(on([exactRange, dark], debouncedSetMinMaxMarkers));
|
||||
|
||||
if (chartIndex === 0) {
|
||||
const datasetPath: AnyDatasetPath = `/${scale}-to-price`;
|
||||
|
||||
const dataset = datasets.getOrImport(scale, datasetPath);
|
||||
|
||||
// Don't trigger reactivity by design
|
||||
activeDatasets().push(dataset);
|
||||
|
||||
const title = "Price";
|
||||
|
||||
function createPriceSeries(seriesType: PriceSeriesType) {
|
||||
let seriesConfig: SeriesConfig;
|
||||
|
||||
if (seriesType === "Candlestick") {
|
||||
seriesConfig = {
|
||||
datasetPath,
|
||||
title,
|
||||
seriesType: SeriesType.Candlestick,
|
||||
};
|
||||
} else {
|
||||
seriesConfig = {
|
||||
datasetPath,
|
||||
title,
|
||||
color: colors.white,
|
||||
};
|
||||
}
|
||||
|
||||
const priceSeries = createSeriesGroup({
|
||||
scale,
|
||||
datasets,
|
||||
index: -1,
|
||||
activeIds,
|
||||
seriesConfig,
|
||||
chart,
|
||||
chartLegend,
|
||||
lastActiveIndex,
|
||||
preset,
|
||||
disabled: () => priceSeriesType() !== seriesType,
|
||||
debouncedSetMinMaxMarkers,
|
||||
dark,
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const latest = webSockets.liveKrakenCandle.latest();
|
||||
|
||||
if (!latest) return;
|
||||
|
||||
const index = chunkIdToIndex(scale, latest.year);
|
||||
|
||||
const series = priceSeries.seriesList.at(index)?.();
|
||||
|
||||
series?.update(latest);
|
||||
});
|
||||
|
||||
return priceSeries;
|
||||
}
|
||||
|
||||
const priceCandlestickLegend = createPriceSeries("Candlestick");
|
||||
const priceLineLegend = createPriceSeries("Line");
|
||||
|
||||
createEffect(() => {
|
||||
priceCandlestickLegend.visible.set(priceLineLegend.visible());
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
priceLineLegend.visible.set(priceCandlestickLegend.visible());
|
||||
});
|
||||
}
|
||||
|
||||
[...seriesConfigs].reverse().forEach((seriesConfig, index) => {
|
||||
const dataset = datasets.getOrImport(scale, seriesConfig.datasetPath);
|
||||
|
||||
// Don't trigger reactivity by design
|
||||
activeDatasets().push(dataset);
|
||||
|
||||
createSeriesGroup({
|
||||
scale,
|
||||
datasets,
|
||||
activeIds,
|
||||
index,
|
||||
seriesConfig,
|
||||
chartLegend,
|
||||
chart,
|
||||
preset,
|
||||
lastActiveIndex,
|
||||
debouncedSetMinMaxMarkers,
|
||||
dark,
|
||||
});
|
||||
});
|
||||
|
||||
chartLegend.forEach((legend) => {
|
||||
createEffect(on(legend.visible, debouncedSetMinMaxMarkers));
|
||||
});
|
||||
|
||||
legendSetter((l) => {
|
||||
for (let i = 0; i < chartLegend.length; i++) {
|
||||
l.splice(0, 0, chartLegend[i]);
|
||||
}
|
||||
return l;
|
||||
});
|
||||
|
||||
createEffect(() =>
|
||||
isDrawn.set(() => chartLegend.some((legend) => legend.drawn())),
|
||||
);
|
||||
|
||||
createEffect(() =>
|
||||
chart.timeScale().applyOptions({
|
||||
visible: isLastDrawn(),
|
||||
}),
|
||||
);
|
||||
|
||||
createEffect(() =>
|
||||
chart.priceScale("right").applyOptions({
|
||||
mode: chartPriceMode.selected() === "Linear" ? 0 : 1,
|
||||
}),
|
||||
);
|
||||
|
||||
chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
|
||||
if (!logicalRange) return;
|
||||
|
||||
// Must be the chart with the visible timeScale
|
||||
if (chartIndex === lastChartIndex()) {
|
||||
debouncedUpdateVisiblePriceSeriesType({
|
||||
scale,
|
||||
chart,
|
||||
logicalRange,
|
||||
priceSeriesType,
|
||||
});
|
||||
}
|
||||
|
||||
for (
|
||||
let otherChartIndex = 0;
|
||||
otherChartIndex <= lastChartIndex();
|
||||
otherChartIndex++
|
||||
) {
|
||||
if (chartIndex !== otherChartIndex) {
|
||||
const chart = charts()[otherChartIndex].chart();
|
||||
|
||||
chart?.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.subscribeCrosshairMove(({ time, sourceEvent }) => {
|
||||
// Don't override crosshair position from scroll event
|
||||
if (time && !sourceEvent) return;
|
||||
|
||||
for (
|
||||
let otherChartIndex = 0;
|
||||
otherChartIndex <= lastChartIndex();
|
||||
otherChartIndex++
|
||||
) {
|
||||
const { whitespace: _whitespace, chart: _otherChart } =
|
||||
charts()[otherChartIndex];
|
||||
|
||||
const otherChart = _otherChart();
|
||||
const whitespace = _whitespace();
|
||||
|
||||
if (otherChart && whitespace && chartIndex !== otherChartIndex) {
|
||||
if (time) {
|
||||
otherChart.setCrosshairPosition(NaN, time, whitespace);
|
||||
} else {
|
||||
// No time when mouse goes outside the chart
|
||||
otherChart.clearCrosshairPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger reactivity now
|
||||
activeDatasets.set((l) => l);
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: isLastDrawn() ? "100%" : "calc(100% - 62px)",
|
||||
"margin-bottom": isLastDrawn() ? "" : "-2px",
|
||||
}}
|
||||
class={classPropToString([
|
||||
isDrawn()
|
||||
? ["max-h-full", !isLastDrawn() ? "border-b" : "mb-[-2px]"]
|
||||
: "max-h-0",
|
||||
"border-lighter relative h-full min-h-0 w-full cursor-crosshair",
|
||||
])}
|
||||
>
|
||||
<div ref={div.set} class="size-full" />
|
||||
|
||||
<Show when={isDrawn()}>
|
||||
<div class="text-low-contrast absolute left-0 top-0 px-2 py-1.5 text-xs">
|
||||
{chartIndex === 0
|
||||
? ("US Dollars" satisfies Unit)
|
||||
: presetAccessor().unit}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
bottom: `${isLastDrawn() ? 32 : 0}px`,
|
||||
right: `77px`,
|
||||
}}
|
||||
class="text-low-contrast absolute z-10 px-3 py-0.5"
|
||||
>
|
||||
<RadioGroup size="xs" title={chartPriceModeKey} sl={chartPriceMode} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
|
||||
import {
|
||||
getInitialTimeRange,
|
||||
setActiveIds,
|
||||
} from "/src/scripts/lightweightCharts/time";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Chart } from "./chart";
|
||||
|
||||
export function Charts({
|
||||
firstChartSetter,
|
||||
preset,
|
||||
datasets,
|
||||
legendSetter,
|
||||
dark,
|
||||
activeIds,
|
||||
}: {
|
||||
firstChartSetter: Setter<IChartApi | undefined>;
|
||||
preset: Accessor<Preset>;
|
||||
datasets: Datasets;
|
||||
legendSetter: Setter<SeriesLegend[]>;
|
||||
dark: Accessor<boolean>;
|
||||
activeIds: RWS<number[]>;
|
||||
}) {
|
||||
const scale = createMemo(() => preset().scale);
|
||||
const exactRange = createRWS(getInitialTimeRange(scale()));
|
||||
const priceSeriesType = createRWS<PriceSeriesType>("Candlestick");
|
||||
const activeDatasets = createRWS([] as ResourceDataset<any, any>[], {
|
||||
equals: false,
|
||||
});
|
||||
const chartSeriesConfigs = createRWS([] as SeriesConfig[][], {
|
||||
equals: false,
|
||||
});
|
||||
const charts = createRWS(
|
||||
[] as {
|
||||
chart: RWS<IChartApi | undefined>;
|
||||
whitespace: RWS<ISeriesApiAny | undefined>;
|
||||
}[],
|
||||
{
|
||||
equals: false,
|
||||
},
|
||||
);
|
||||
const lastChartIndex = createMemo(() => chartSeriesConfigs().length - 1);
|
||||
const seriesCount = createMemo(() =>
|
||||
chartSeriesConfigs().reduce(
|
||||
(acc, l) => (acc += l.length),
|
||||
1, // Because of price series
|
||||
),
|
||||
);
|
||||
const lastActiveIndex = createMemo(() => {
|
||||
const last = activeIds().at(-1);
|
||||
return last !== undefined
|
||||
? chunkIdToIndex(preset().scale, last)
|
||||
: undefined;
|
||||
});
|
||||
const chartsDrawn = createMemo(() =>
|
||||
chartSeriesConfigs().map((_) => createRWS(true)),
|
||||
);
|
||||
|
||||
createEffect(
|
||||
on([activeIds, activeDatasets], ([ids, activeDatasets]) => {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
for (let j = 0; j < activeDatasets.length; j++) {
|
||||
activeDatasets[j].fetch(id);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
createEffect(
|
||||
on(preset, (preset) => {
|
||||
const scale = preset.scale;
|
||||
|
||||
exactRange.set(getInitialTimeRange(scale));
|
||||
|
||||
chartSeriesConfigs.set(
|
||||
[preset.top || [], preset.bottom].flatMap((list) =>
|
||||
list ? [list] : [],
|
||||
),
|
||||
);
|
||||
|
||||
charts.set(() =>
|
||||
new Array(chartSeriesConfigs().length).fill(undefined).map(() => ({
|
||||
chart: createRWS(undefined as IChartApi | undefined),
|
||||
whitespace: createRWS(undefined as ISeriesApiAny | undefined),
|
||||
})),
|
||||
);
|
||||
|
||||
setActiveIds({
|
||||
exactRange: exactRange(),
|
||||
activeIds,
|
||||
});
|
||||
|
||||
legendSetter(() => []);
|
||||
}),
|
||||
);
|
||||
|
||||
onCleanup(() => {
|
||||
firstChartSetter(undefined);
|
||||
|
||||
charts().map(({ chart, whitespace }) => {
|
||||
chart()?.remove();
|
||||
chart.set(undefined);
|
||||
whitespace.set(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<For
|
||||
each={chartSeriesConfigs().filter(
|
||||
(configs, index) => index === 0 || configs.length !== 0,
|
||||
)}
|
||||
>
|
||||
{(seriesConfigs, index) => (
|
||||
<Chart
|
||||
activeDatasets={activeDatasets}
|
||||
activeIds={activeIds}
|
||||
charts={charts}
|
||||
chartsDrawn={chartsDrawn}
|
||||
dark={dark}
|
||||
datasets={datasets}
|
||||
exactRange={exactRange}
|
||||
firstChartSetter={firstChartSetter}
|
||||
index={index}
|
||||
lastActiveIndex={lastActiveIndex}
|
||||
lastChartIndex={lastChartIndex}
|
||||
legendSetter={legendSetter}
|
||||
preset={preset}
|
||||
priceSeriesType={priceSeriesType}
|
||||
seriesConfigs={seriesConfigs}
|
||||
seriesCount={seriesCount}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
);
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { chunkIdToIndex } from "/src/scripts/datasets/resource";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Scrollable } from "../../scrollable";
|
||||
|
||||
const transparency = "44";
|
||||
|
||||
export function Legend({
|
||||
scale,
|
||||
legend: legendList,
|
||||
dark,
|
||||
activeIds,
|
||||
}: {
|
||||
scale: Accessor<ResourceScale>;
|
||||
legend: Accessor<SeriesLegend[]>;
|
||||
dark: Accessor<boolean>;
|
||||
activeIds: Accessor<number[]>;
|
||||
}) {
|
||||
const hovered = createRWS<SeriesLegend | undefined>(undefined);
|
||||
|
||||
let toggle = false;
|
||||
|
||||
return (
|
||||
<Scrollable classes="items-center gap-1 p-1.5">
|
||||
<For each={legendList()}>
|
||||
{(legend) => {
|
||||
createEffect(() => {
|
||||
const range = activeIds();
|
||||
|
||||
for (let i = 0; i < range.length; i++) {
|
||||
const id = range[i];
|
||||
|
||||
const initialColors = {} as any;
|
||||
const darkenColors = {} as any;
|
||||
|
||||
const chunkIndex = chunkIdToIndex(scale(), id);
|
||||
|
||||
const series = legend.seriesList.at(chunkIndex)?.();
|
||||
|
||||
if (!series) return;
|
||||
|
||||
const seriesOptions = series.options();
|
||||
|
||||
if (!seriesOptions) continue;
|
||||
|
||||
Object.entries(seriesOptions).forEach(([k, v]) => {
|
||||
if (k.toLowerCase().includes("color") && v) {
|
||||
if (typeof v === "string" && !v.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
|
||||
v = (v as string).substring(0, 7);
|
||||
initialColors[k] = v;
|
||||
darkenColors[k] = `${v}${transparency}`;
|
||||
} else if (k === "lastValueVisible" && v) {
|
||||
initialColors[k] = true;
|
||||
darkenColors[k] = false;
|
||||
}
|
||||
});
|
||||
|
||||
createEffect((wasHovering: boolean) => {
|
||||
const hoveredLegend = hovered();
|
||||
const hovering = !!hovered();
|
||||
|
||||
if (wasHovering === hovering) {
|
||||
return hovering;
|
||||
}
|
||||
|
||||
if (hoveredLegend) {
|
||||
if (hoveredLegend.title !== legend.title) {
|
||||
series.applyOptions(darkenColors);
|
||||
}
|
||||
} else {
|
||||
series.applyOptions(initialColors);
|
||||
}
|
||||
|
||||
return hovering;
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
|
||||
let previousClickTime: number = 0;
|
||||
|
||||
return (
|
||||
<Show when={!legend.disabled()}>
|
||||
<button
|
||||
onMouseEnter={() => legend.visible() && hovered.set(legend)}
|
||||
onMouseLeave={() => hovered.set(undefined)}
|
||||
onTouchStart={() => legend.visible() && hovered.set(legend)}
|
||||
onTouchEnd={() => hovered.set(undefined)}
|
||||
onClick={() => {
|
||||
const currentClickTime = new Date().getTime();
|
||||
|
||||
if (currentClickTime - previousClickTime > 300) {
|
||||
legend.visible.set((visible) => !visible);
|
||||
} else {
|
||||
legendList().forEach((_legend) => {
|
||||
if (_legend.title != legend.title) {
|
||||
_legend.visible.set(toggle);
|
||||
}
|
||||
});
|
||||
|
||||
legend.visible.set(true);
|
||||
|
||||
toggle = !toggle;
|
||||
}
|
||||
|
||||
previousClickTime = currentClickTime;
|
||||
|
||||
if (legend.visible()) {
|
||||
hovered.set(legend);
|
||||
} else {
|
||||
hovered.set(undefined);
|
||||
}
|
||||
}}
|
||||
class="flex flex-none items-center space-x-1.5 rounded-full py-1.5 pl-2 pr-2.5 hover:bg-orange-800/20 active:scale-[0.975] dark:hover:bg-orange-200/20"
|
||||
>
|
||||
<span
|
||||
class="flex size-4 flex-col overflow-hidden rounded-full"
|
||||
style={{
|
||||
opacity: legend.visible() ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<For
|
||||
each={
|
||||
Array.isArray(legend.color)
|
||||
? legend.color.map((c) => c(dark))
|
||||
: [legend.color(dark)]
|
||||
}
|
||||
>
|
||||
{(color) => (
|
||||
<span
|
||||
class="w-full flex-1"
|
||||
style={{
|
||||
"background-color": color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</span>
|
||||
<span
|
||||
class="text-high-contrast decoration-high-contrast decoration-wavy decoration-[1.5px]"
|
||||
style={{
|
||||
"text-decoration-line": !legend.visible()
|
||||
? "line-through"
|
||||
: undefined,
|
||||
"--tw-text-opacity": legend.visible() ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
{legend.title}
|
||||
</span>
|
||||
<Show when={legend.dataset.url}>
|
||||
{(url) => (
|
||||
<a
|
||||
title="Dataset"
|
||||
class="border-superlight -my-0.5 !-mr-1 inline-flex size-6 flex-col overflow-hidden rounded-full border bg-white bg-opacity-5 p-1 pl-0.5 hover:bg-opacity-50 dark:bg-orange-200 dark:bg-opacity-5 dark:hover:bg-opacity-25"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
href={url()}
|
||||
target={
|
||||
url()?.startsWith("/") || url()?.startsWith("http")
|
||||
? "_blank"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<IconTablerDownload />
|
||||
</a>
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Scrollable>
|
||||
);
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
import { ONE_DAY_IN_MS } from "/src/scripts/utils/time";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { GENESIS_DAY } from "../../../../../scripts/lightweightCharts/whitespace";
|
||||
import { Box } from "../../box";
|
||||
import { Scrollable } from "../../scrollable";
|
||||
|
||||
const DELAY = 1;
|
||||
const MULTIPLIER = DELAY / 1000;
|
||||
const LEFT = -1;
|
||||
const RIGHT = 1;
|
||||
|
||||
export function TimeScale({
|
||||
scale,
|
||||
firstChart,
|
||||
}: {
|
||||
scale: Accessor<ResourceScale>;
|
||||
firstChart: RWS<IChartApi | undefined>;
|
||||
}) {
|
||||
const today = new Date();
|
||||
|
||||
const disabled = createMemo(() => !firstChart());
|
||||
|
||||
const scrollDirection = createRWS(0);
|
||||
|
||||
const timeScale = createMemo(() => {
|
||||
const chart = firstChart();
|
||||
if (!chart) return undefined;
|
||||
return chart.timeScale();
|
||||
});
|
||||
|
||||
let interval: number | undefined;
|
||||
|
||||
function createScrollLoop() {
|
||||
clearInterval(interval);
|
||||
const direction = scrollDirection();
|
||||
if (!direction) return;
|
||||
|
||||
// @ts-ignore
|
||||
interval = setInterval(() => {
|
||||
const time = timeScale();
|
||||
|
||||
if (!time) return;
|
||||
|
||||
const range = time.getVisibleLogicalRange();
|
||||
|
||||
if (!range) return;
|
||||
|
||||
const speed = (range.to - range.from) * MULTIPLIER * direction;
|
||||
|
||||
// @ts-ignore
|
||||
range.from += speed;
|
||||
// @ts-ignore
|
||||
range.to += speed;
|
||||
|
||||
time.setVisibleLogicalRange(range);
|
||||
}, DELAY);
|
||||
}
|
||||
|
||||
onCleanup(() => clearInterval(interval));
|
||||
|
||||
return (
|
||||
<Box dark padded={false} spaced={false} classes="short:hidden">
|
||||
<div class="flex items-center p-1.5">
|
||||
<Button
|
||||
square
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
scrollDirection.set((v) => (v === LEFT ? 0 : LEFT));
|
||||
createScrollLoop();
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={scrollDirection() === LEFT}
|
||||
fallback={<IconTablerPlayerTrackPrevFilled />}
|
||||
>
|
||||
<IconTablerPlayerPauseFilled />
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="border-lighter border-l" />
|
||||
<Scrollable classes="p-1.5 space-x-2">
|
||||
<Switch>
|
||||
<Match when={scale() === "date"}>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() => setTimeScale({ scale: scale(), timeScale })}
|
||||
>
|
||||
All Time
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 7 })
|
||||
}
|
||||
>
|
||||
1 Week
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 30 })
|
||||
}
|
||||
>
|
||||
1 Month
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 3 * 30 })
|
||||
}
|
||||
>
|
||||
3 Months
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 6 * 30 })
|
||||
}
|
||||
>
|
||||
6 Months
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({
|
||||
scale: scale(),
|
||||
timeScale,
|
||||
days: Math.ceil(
|
||||
(today.getTime() -
|
||||
new Date(`${today.getUTCFullYear()}-01-01`).getTime()) /
|
||||
ONE_DAY_IN_MS,
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Year To Date
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 365 })
|
||||
}
|
||||
>
|
||||
1 Year
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 2 * 365 })
|
||||
}
|
||||
>
|
||||
2 Years
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 4 * 365 })
|
||||
}
|
||||
>
|
||||
4 Years
|
||||
</Button>
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, days: 8 * 365 })
|
||||
}
|
||||
>
|
||||
8 Years
|
||||
</Button>
|
||||
<For
|
||||
each={new Array(
|
||||
new Date().getFullYear() - new Date("2009-01-01").getFullYear(),
|
||||
)
|
||||
.fill(0)
|
||||
.map((_, index) => index + 2009)
|
||||
.reverse()}
|
||||
>
|
||||
{(year) => (
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({ scale: scale(), timeScale, year })
|
||||
}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={scale() === "height"}>
|
||||
<Button minWidth disabled={() => true} onClick={() => {}}>
|
||||
24h
|
||||
</Button>
|
||||
<Button minWidth disabled={() => true} onClick={() => {}}>
|
||||
48h
|
||||
</Button>
|
||||
<For
|
||||
each={new Array(9)
|
||||
.fill(0)
|
||||
.flatMap((_, i) => [i, i + 0.5])
|
||||
.reverse()}
|
||||
>
|
||||
{(i) => (
|
||||
<Button
|
||||
minWidth
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setTimeScale({
|
||||
scale: scale(),
|
||||
timeScale,
|
||||
range: {
|
||||
from: i * 100_000,
|
||||
to: (i + 0.5) * 100_000,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{`${100 * (i + 0.5)}k`}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Scrollable>
|
||||
<div class="border-lighter border-l" />
|
||||
<div class="flex items-center p-1.5">
|
||||
<Button
|
||||
square
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
scrollDirection.set((v) => (v === RIGHT ? 0 : RIGHT));
|
||||
createScrollLoop();
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={scrollDirection() === RIGHT}
|
||||
fallback={<IconTablerPlayerTrackNextFilled />}
|
||||
>
|
||||
<IconTablerPlayerPauseFilled />
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({
|
||||
onClick,
|
||||
disabled,
|
||||
children,
|
||||
minWidth,
|
||||
square,
|
||||
}: ParentProps & {
|
||||
onClick: VoidFunction;
|
||||
disabled?: Accessor<boolean>;
|
||||
minWidth?: boolean;
|
||||
square?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
class={classPropToString([
|
||||
minWidth && "min-w-20",
|
||||
square ? "p-2" : "px-2 py-1.5",
|
||||
disabled?.()
|
||||
? "text-low-contrast"
|
||||
: "hover:bg-orange-50/20 active:scale-95",
|
||||
"flex-shrink-0 flex-grow whitespace-nowrap rounded-lg",
|
||||
])}
|
||||
onClick={onClick}
|
||||
disabled={disabled?.()}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function setTimeScale({
|
||||
timeScale,
|
||||
scale,
|
||||
days,
|
||||
year,
|
||||
range,
|
||||
}: {
|
||||
timeScale: Accessor<ITimeScaleApi<Time> | undefined>;
|
||||
scale: ResourceScale;
|
||||
days?: number;
|
||||
year?: number;
|
||||
range?: { from: number; to: number };
|
||||
}) {
|
||||
if (scale === "date") {
|
||||
let from = new Date();
|
||||
let to = new Date();
|
||||
|
||||
if (year) {
|
||||
from = new Date(`${year}-01-01`);
|
||||
to = new Date(`${year}-12-31`);
|
||||
} else if (days) {
|
||||
from.setDate(from.getUTCDate() - days);
|
||||
} else {
|
||||
from = new Date(GENESIS_DAY);
|
||||
}
|
||||
|
||||
timeScale()?.setVisibleRange({
|
||||
from: (from.getTime() / 1000) as Time,
|
||||
to: (to.getTime() / 1000) as Time,
|
||||
});
|
||||
} else if (scale === "height") {
|
||||
if (range) {
|
||||
timeScale()?.setVisibleRange({
|
||||
from: range.from as Time,
|
||||
to: range.to as Time,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export function Title({ presets }: { presets: Presets }) {
|
||||
return (
|
||||
<div
|
||||
class="flex flex-1 items-center overflow-y-auto p-1.5"
|
||||
style={{
|
||||
"scrollbar-width": "thin",
|
||||
}}
|
||||
>
|
||||
<div class="flex-1 -space-y-1 whitespace-nowrap px-1 md:mt-0.5 md:-space-y-1.5">
|
||||
<h3 class="text-xs opacity-50">{`/ ${[...presets.selected().path.map(({ name }) => name), presets.selected().name].join(" / ")}`}</h3>
|
||||
<h1 class="text-lg font-bold md:text-xl">{presets.selected().title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createWasIdleAccessor } from "/src/solid/idle";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Box } from "../box";
|
||||
import { Actions } from "./components/actions";
|
||||
import { Legend } from "./components/legend";
|
||||
import { TimeScale } from "./components/timeScale";
|
||||
import { Title } from "./components/title";
|
||||
|
||||
export function ChartFrame({
|
||||
presets,
|
||||
datasets,
|
||||
hide,
|
||||
qrcode,
|
||||
standalone,
|
||||
fullscreen,
|
||||
dark,
|
||||
}: {
|
||||
presets: Presets;
|
||||
hide?: Accessor<boolean>;
|
||||
qrcode: RWS<string>;
|
||||
datasets: Datasets;
|
||||
fullscreen?: RWS<boolean>;
|
||||
dark: Accessor<boolean>;
|
||||
standalone: boolean;
|
||||
}) {
|
||||
const legend = createRWS<SeriesLegend[]>([], { equals: false });
|
||||
|
||||
const firstChart = createRWS<IChartApi | undefined>(undefined);
|
||||
|
||||
const scale = createMemo(() => presets.selected().scale);
|
||||
|
||||
const activeIds = createRWS([] as number[], { equals: false });
|
||||
|
||||
const wasIdle = createWasIdleAccessor();
|
||||
|
||||
const Charts = lazy(() =>
|
||||
import("./components/charts").then((d) => ({ default: d.Charts })),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={classPropToString([
|
||||
standalone &&
|
||||
"border-lighter rounded-2xl border bg-gradient-to-b from-white/15 to-white/30 to-80% shadow-md dark:from-orange-100/5 dark:to-black/10",
|
||||
"flex size-full min-h-0 flex-1 flex-col overflow-hidden",
|
||||
])}
|
||||
style={{
|
||||
display: (hide ? hide() : false) ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
flex={false}
|
||||
dark
|
||||
padded={false}
|
||||
spaced={false}
|
||||
classes="short:hidden"
|
||||
>
|
||||
<Title presets={presets} />
|
||||
|
||||
<div class="border-lighter border-t" />
|
||||
|
||||
<div class="flex">
|
||||
<Legend
|
||||
legend={legend}
|
||||
scale={scale}
|
||||
activeIds={activeIds}
|
||||
dark={dark}
|
||||
/>
|
||||
|
||||
<div class="border-lighter border-l" />
|
||||
|
||||
<Actions presets={presets} qrcode={qrcode} fullscreen={fullscreen} />
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<div class="-mr-2 -mt-2 flex min-h-0 flex-1 flex-col">
|
||||
<Show when={wasIdle()}>
|
||||
<Charts
|
||||
firstChartSetter={firstChart.set}
|
||||
datasets={datasets}
|
||||
legendSetter={legend.set}
|
||||
preset={presets.selected}
|
||||
dark={dark}
|
||||
activeIds={activeIds}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<TimeScale firstChart={firstChart} scale={scale} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export function Counter({
|
||||
count,
|
||||
name,
|
||||
setRef,
|
||||
}: {
|
||||
count: () => number;
|
||||
name: string;
|
||||
setRef?: Setter<HTMLDivElement | undefined>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
ref={setRef}
|
||||
class="text-orange-100/75"
|
||||
style={{
|
||||
"border-style": count() ? "dashed" : "none",
|
||||
}}
|
||||
>
|
||||
Counted{" "}
|
||||
<span class="font-medium text-orange-400/75">
|
||||
{count().toLocaleString("en-us")}
|
||||
</span>{" "}
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Box } from "./box";
|
||||
import { Button, ButtonRandomChart } from "./button";
|
||||
import { Header } from "./header";
|
||||
import { Line } from "./line";
|
||||
import { Number } from "./number";
|
||||
|
||||
export function FavoritesFrame({
|
||||
presets,
|
||||
selectedFrame,
|
||||
}: {
|
||||
presets: Presets;
|
||||
selectedFrame: Accessor<FrameName>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="relative flex-1 overflow-y-auto overflow-x-hidden"
|
||||
style={{
|
||||
display: selectedFrame() !== "Favorites" ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4">
|
||||
<Header title="Favorites">
|
||||
<Number number={() => presets.favorites().length} /> presets marked as
|
||||
favorites.
|
||||
</Header>
|
||||
|
||||
<div class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<div
|
||||
class="space-y-0.5 py-1"
|
||||
// style={{
|
||||
// display: !presets.favorites().length ? "none" : undefined,
|
||||
// }}
|
||||
>
|
||||
<Show
|
||||
when={presets.favorites().length}
|
||||
fallback={
|
||||
<p>
|
||||
It seems like you couldn't find any interesting chart for your
|
||||
favorites ! You might want to try to{" "}
|
||||
<ButtonRandomChart presets={presets} />
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<For each={presets.favorites()}>
|
||||
{(preset) => (
|
||||
<Line
|
||||
id={`favorite-${preset.id}`}
|
||||
name={preset.title}
|
||||
onClick={() => presets.select(preset)}
|
||||
active={() => presets.selected() === preset}
|
||||
header={`/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="h-[25dvh] flex-none" />
|
||||
</div>
|
||||
|
||||
<Box absolute="bottom">
|
||||
<Button onClick={() => presets.selected().isFavorite.set((b) => !b)}>
|
||||
<span>
|
||||
{presets.selected().isFavorite()
|
||||
? "Remove from favorites"
|
||||
: "Add to favorites"}
|
||||
</span>
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Line } from "../../line";
|
||||
|
||||
export function File({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
active,
|
||||
depth,
|
||||
onClick,
|
||||
favorite,
|
||||
visited,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: JSXElement;
|
||||
active: Accessor<boolean>;
|
||||
depth: number;
|
||||
onClick: VoidFunction;
|
||||
favorite: Accessor<boolean>;
|
||||
visited: Accessor<boolean>;
|
||||
}) {
|
||||
const tail = createMemo(() =>
|
||||
favorite() ? (
|
||||
<span class="rounded-full bg-yellow-950 p-1">
|
||||
<IconTablerStarFilled class="size-3 text-amber-500" />
|
||||
</span>
|
||||
) : !visited() ? (
|
||||
<span class="mx-1.5 rounded-full bg-orange-500/50 p-1 text-transparent" />
|
||||
) : undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<Line
|
||||
id={id}
|
||||
depth={depth}
|
||||
active={active}
|
||||
name={name}
|
||||
icon={() => icon}
|
||||
onClick={onClick}
|
||||
tail={tail}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function randomDegree(min = 0, max = 360) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Line } from "../../line";
|
||||
|
||||
export function Folder({
|
||||
id,
|
||||
name,
|
||||
depth,
|
||||
open,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
depth: number;
|
||||
open: Accessor<boolean>;
|
||||
onClick: VoidFunction;
|
||||
children: number;
|
||||
}) {
|
||||
const icon = createMemo(() =>
|
||||
open() ? <IconTablerFolderOpen /> : <IconTablerFolder />,
|
||||
);
|
||||
|
||||
return (
|
||||
<Line
|
||||
id={id}
|
||||
depth={depth}
|
||||
name={name}
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
classes={() => (open() ? "opacity-60" : "")}
|
||||
tail={() => (
|
||||
<Show when={!open()}>
|
||||
<span class="rounded-full bg-orange-50/10 px-2 py-0.5 text-xs text-neutral-400">
|
||||
{children}
|
||||
</span>
|
||||
</Show>
|
||||
)}
|
||||
></Line>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { File } from "./file";
|
||||
import { Folder } from "./folder";
|
||||
|
||||
export function Tree({
|
||||
tree,
|
||||
selected,
|
||||
openedFolders,
|
||||
depth = 0,
|
||||
visible,
|
||||
selectPreset,
|
||||
path = [],
|
||||
favorites,
|
||||
}: {
|
||||
tree: PresetTree;
|
||||
selected: Accessor<Preset>;
|
||||
selectPreset(preset: Preset): void;
|
||||
openedFolders: RWS<Set<string>>;
|
||||
depth?: number;
|
||||
visible?: Accessor<boolean>;
|
||||
path?: FilePath;
|
||||
favorites: Accessor<Preset[]>;
|
||||
}) {
|
||||
return (
|
||||
<Show when={visible?.() || !visible}>
|
||||
<div>
|
||||
<For each={tree}>
|
||||
{(thing) => {
|
||||
const active = createMemo(() => thing.id === selected().id);
|
||||
const favorite = createMemo(() =>
|
||||
favorites().includes(thing as Preset),
|
||||
);
|
||||
const visited = (thing as Preset).visited;
|
||||
|
||||
if (!("tree" in thing)) {
|
||||
return (
|
||||
<File
|
||||
id={thing.id}
|
||||
name={thing.name}
|
||||
active={active}
|
||||
depth={depth}
|
||||
icon={thing.icon || IconTablerFile}
|
||||
favorite={favorite}
|
||||
visited={visited}
|
||||
onClick={() => {
|
||||
const selectedId = selected().id;
|
||||
|
||||
if (selectedId === thing.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Has been filled in createPresets
|
||||
selectPreset(thing as Preset);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const childrenVisible = createMemo(() =>
|
||||
openedFolders().has(thing.id),
|
||||
);
|
||||
|
||||
const childCount = countChildren(thing);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Folder
|
||||
id={thing.id}
|
||||
name={thing.name}
|
||||
depth={depth}
|
||||
open={childrenVisible}
|
||||
children={childCount}
|
||||
onClick={() => {
|
||||
openedFolders.set((s) => {
|
||||
if (childrenVisible()) {
|
||||
s.delete(thing.id);
|
||||
} else {
|
||||
s.add(thing.id);
|
||||
}
|
||||
|
||||
return s;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Tree
|
||||
tree={thing.tree}
|
||||
selected={selected}
|
||||
depth={depth + 1}
|
||||
openedFolders={openedFolders}
|
||||
visible={childrenVisible}
|
||||
path={[...path, { name: thing.name, id: thing.id }]}
|
||||
selectPreset={selectPreset}
|
||||
favorites={favorites}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
function countChildren(folder: PresetFolder) {
|
||||
let count = 0;
|
||||
|
||||
function _countChildren(tree: PartialPresetTree) {
|
||||
tree.forEach((anyPreset) => {
|
||||
if ("tree" in anyPreset) {
|
||||
_countChildren(anyPreset.tree);
|
||||
} else {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_countChildren(folder.tree);
|
||||
|
||||
return count;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { scrollIntoView } from "/src/scripts/utils/scroll";
|
||||
import { sleep } from "/src/scripts/utils/sleep";
|
||||
import { tick } from "/src/scripts/utils/tick";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { Box } from "../box";
|
||||
import { Button } from "../button";
|
||||
import { Header } from "../header";
|
||||
import { Number } from "../number";
|
||||
import { Tree } from "./components/tree";
|
||||
|
||||
export function FoldersFrame({
|
||||
presets,
|
||||
selectedFrame,
|
||||
}: {
|
||||
presets: Presets;
|
||||
selectedFrame: Accessor<FrameName>;
|
||||
}) {
|
||||
const div = createRWS<HTMLDivElement | undefined>(undefined);
|
||||
|
||||
onMount(() => {
|
||||
goToSelected(presets);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
class="relative flex size-full flex-1 flex-col"
|
||||
style={{
|
||||
display: selectedFrame() !== "Folders" ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<div class="flex max-h-full min-h-0 flex-1 flex-col gap-4 p-4">
|
||||
<Header title="Folders">
|
||||
<Number number={() => presets.list.length} /> charts organized in a
|
||||
tree like structure.
|
||||
</Header>
|
||||
|
||||
<div class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<Tree
|
||||
tree={presets.tree}
|
||||
openedFolders={presets.openedFolders}
|
||||
selected={presets.selected}
|
||||
selectPreset={presets.select}
|
||||
favorites={presets.favorites}
|
||||
/>
|
||||
|
||||
<div class="h-[50dvh] flex-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Box absolute="bottom">
|
||||
<Button
|
||||
onClick={() => {
|
||||
presets.openedFolders.set((s) => {
|
||||
s.clear();
|
||||
return s;
|
||||
});
|
||||
|
||||
sleep(10);
|
||||
|
||||
scrollIntoView(div());
|
||||
}}
|
||||
>
|
||||
Close all folders
|
||||
</Button>
|
||||
<Button onClick={() => goToSelected(presets)}>Go to selected</Button>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function goToSelected(presets: Presets) {
|
||||
batch(() =>
|
||||
presets.selected().path.forEach(({ id }) => {
|
||||
presets.openedFolders.set((s) => {
|
||||
s.add(id);
|
||||
return s;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await tick();
|
||||
|
||||
scrollIntoView(document.getElementById(presets.selected().id), "center");
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export function Header({ title, children }: { title: string } & ParentProps) {
|
||||
return (
|
||||
<div>
|
||||
<h3 class="text-lg font-bold md:text-xl">{title}</h3>
|
||||
<p class="text-orange-950/60 dark:text-orange-100/75">{children}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { run } from "/src/scripts/utils/run";
|
||||
|
||||
import { Box } from "./box";
|
||||
import { Button, ButtonRandomChart } from "./button";
|
||||
import { Header } from "./header";
|
||||
import { Line } from "./line";
|
||||
|
||||
export function HistoryFrame({
|
||||
presets,
|
||||
selectedFrame,
|
||||
}: {
|
||||
presets: Presets;
|
||||
selectedFrame: Accessor<FrameName>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="flex-1 overflow-y-auto overflow-x-hidden"
|
||||
style={{
|
||||
display: selectedFrame() !== "History" ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div class="flex max-h-full min-h-0 flex-1 flex-col p-4">
|
||||
<Header title="History">List of previously visited charts.</Header>
|
||||
|
||||
<div class="space-y-0.5 pt-4">
|
||||
<Show
|
||||
when={presets.history().length}
|
||||
fallback={
|
||||
<>
|
||||
<div class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<p>
|
||||
You somehow haven't visited by yourself a single chart.
|
||||
Impressive ! You might want to try to{" "}
|
||||
<ButtonRandomChart presets={presets} />
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<For each={presets.history()}>
|
||||
{({ preset, date }, index) => (
|
||||
<>
|
||||
<Show
|
||||
when={
|
||||
index() === 0 ||
|
||||
presets.history()[index()].date.toJSON().split("T")[0] !==
|
||||
presets
|
||||
.history()
|
||||
[index() - 1].date.toJSON()
|
||||
.split("T")[0]
|
||||
}
|
||||
>
|
||||
<div class="sticky top-[calc(-0.5rem-1px)] z-10 -mx-4 py-2">
|
||||
<div class="border-lighter border-y bg-[#F4EAE3] p-2 dark:bg-[rgb(25,15,15)]">
|
||||
<p class="ml-2">
|
||||
<Switch fallback={date.toLocaleDateString()}>
|
||||
<Match
|
||||
when={
|
||||
new Date().toJSON().split("T")[0] ===
|
||||
date.toJSON().split("T")[0]
|
||||
}
|
||||
>
|
||||
Today
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
run(() => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
return d;
|
||||
})
|
||||
.toJSON()
|
||||
.split("T")[0] === date.toJSON().split("T")[0]
|
||||
}
|
||||
>
|
||||
Yesterday
|
||||
</Match>
|
||||
</Switch>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Line
|
||||
id={`history-${preset.id}`}
|
||||
name={preset.title}
|
||||
onClick={() => presets.select(preset)}
|
||||
active={() => presets.selected() === preset}
|
||||
header={date.toLocaleTimeString()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="h-[25dvh] flex-none" />
|
||||
</div>
|
||||
|
||||
{/* <Box absolute="bottom">
|
||||
<Button
|
||||
onClick={() => {
|
||||
// search.set("");
|
||||
// inputRef()?.focus();
|
||||
}}
|
||||
>
|
||||
Previous day
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// search.set("");
|
||||
// inputRef()?.focus();
|
||||
}}
|
||||
>
|
||||
Next day
|
||||
</Button>
|
||||
</Box> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { scrollIntoView } from "/src/scripts/utils/scroll";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
export function Line({
|
||||
id,
|
||||
name: _name,
|
||||
icon,
|
||||
active,
|
||||
depth = 0,
|
||||
onClick,
|
||||
header,
|
||||
tail,
|
||||
classes: classes,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
onClick: VoidFunction;
|
||||
active?: Accessor<boolean>;
|
||||
depth?: number;
|
||||
header?: string;
|
||||
icon?: () => JSXElement;
|
||||
tail?: () => JSXElement;
|
||||
classes?: () => string;
|
||||
} & ParentProps) {
|
||||
const ref = createRWS<HTMLButtonElement | undefined>(undefined);
|
||||
|
||||
const [name, ...nameRest] = _name.split(" - ");
|
||||
|
||||
return (
|
||||
<button
|
||||
id={id}
|
||||
class={classPropToString([
|
||||
active?.()
|
||||
? "bg-orange-500/30 backdrop-blur-sm hover:bg-orange-500/50"
|
||||
: "hover:bg-orange-500/15",
|
||||
"relative -mx-2 flex w-[calc(100%+1rem)] items-center whitespace-nowrap rounded-lg px-2 hover:backdrop-blur-sm",
|
||||
classes?.(),
|
||||
])}
|
||||
ref={ref.set}
|
||||
onClick={() => {
|
||||
onClick();
|
||||
scrollIntoView(ref(), "nearest", "instant");
|
||||
}}
|
||||
title={name}
|
||||
>
|
||||
<For each={new Array(depth)}>
|
||||
{() => <span class="border-lighter ml-1 h-8 w-3 flex-none border-l" />}
|
||||
</For>
|
||||
<Show when={icon}>
|
||||
{(icon) => (
|
||||
<span
|
||||
class="-my-0.5 mr-1"
|
||||
// style={{
|
||||
// "margin-left": `${depth}rem`,
|
||||
// }}
|
||||
>
|
||||
{icon()()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<span
|
||||
class={classPropToString([
|
||||
!icon && "px-1",
|
||||
"inline-flex w-full flex-col -space-y-1 truncate py-1 text-left",
|
||||
])}
|
||||
>
|
||||
<Show when={header}>
|
||||
<span class="truncate text-xs opacity-50" innerHTML={header} />
|
||||
</Show>
|
||||
<span class="space-x-1 truncate">
|
||||
<span innerHTML={name} />
|
||||
<Show when={nameRest.length}>
|
||||
<span innerHTML={" - " + nameRest.join(" - ")} class="opacity-50" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<Show when={tail}>
|
||||
{(absolute) => (
|
||||
<span class="ml-0.5 flex items-center">{absolute()()}</span>
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export function Number({ number }: { number: () => number }) {
|
||||
return (
|
||||
<span class="font-medium text-orange-400/75">
|
||||
{number().toLocaleString("en-us")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer";
|
||||
|
||||
import { touchScreen } from "/src/env";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
export function Scrollable({
|
||||
children,
|
||||
classes,
|
||||
}: {
|
||||
classes?: string;
|
||||
} & ParentProps) {
|
||||
const maybeScrollable = createRWS<HTMLDivElement | undefined>(undefined);
|
||||
const scrollable = createRWS(false);
|
||||
const showLeftArrow = createRWS(false);
|
||||
const showRightArrow = createRWS(false);
|
||||
|
||||
onMount(() => {
|
||||
createResizeObserver(maybeScrollable, (_, el) => {
|
||||
if (el !== maybeScrollable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkScrollable();
|
||||
});
|
||||
});
|
||||
|
||||
function checkScrollable() {
|
||||
const div = maybeScrollable();
|
||||
|
||||
if (div) {
|
||||
scrollable.set(() => div.scrollWidth > div.clientWidth);
|
||||
}
|
||||
|
||||
checkArrows();
|
||||
}
|
||||
|
||||
function checkArrows() {
|
||||
const target = maybeScrollable()!;
|
||||
|
||||
const left = target.scrollLeft;
|
||||
const right =
|
||||
target.scrollWidth - Math.ceil(target.scrollLeft + target.clientWidth);
|
||||
|
||||
showLeftArrow.set(() => left > 0);
|
||||
showRightArrow.set(() => right > 0);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
createEffect(on(children, checkScrollable));
|
||||
|
||||
return (
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<For
|
||||
each={[
|
||||
{
|
||||
showArrow: showLeftArrow,
|
||||
side: "left-0",
|
||||
order: "",
|
||||
buttonPadding: "pl-2",
|
||||
iconPadding: "pr-0.5",
|
||||
scrollMultiplier: -1,
|
||||
chevronIcon: IconTablerChevronLeft,
|
||||
gradientDirection: "bg-gradient-to-r",
|
||||
},
|
||||
{
|
||||
showArrow: showRightArrow,
|
||||
side: "right-0",
|
||||
order: "order-2",
|
||||
buttonPadding: "pr-2",
|
||||
iconPadding: "pl-0.5",
|
||||
scrollMultiplier: 1,
|
||||
chevronIcon: IconTablerChevronRight,
|
||||
gradientDirection: "bg-gradient-to-l",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(obj) => (
|
||||
<Show when={scrollable() && obj.showArrow()}>
|
||||
<div
|
||||
class={[
|
||||
obj.side,
|
||||
"pointer-events-none absolute bottom-0 top-0 flex transition-opacity duration-200 ease-in-out",
|
||||
].join(" ")}
|
||||
>
|
||||
<Show when={!touchScreen}>
|
||||
<div
|
||||
class={[
|
||||
obj.order,
|
||||
obj.buttonPadding,
|
||||
"pointer-events-auto flex h-full items-center bg-stone-100/75 dark:bg-stone-900/75",
|
||||
].join(" ")}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
maybeScrollable()?.scrollBy({
|
||||
left: Math.floor(
|
||||
maybeScrollable()!.clientWidth *
|
||||
obj.scrollMultiplier *
|
||||
0.75,
|
||||
),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
class="border-light rounded-full border bg-stone-100 p-0.5 shadow transition hover:scale-110 active:scale-100 dark:bg-stone-900"
|
||||
>
|
||||
<Dynamic
|
||||
component={obj.chevronIcon}
|
||||
class={[`size-5 ${obj.iconPadding}`]}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class={[
|
||||
obj.gradientDirection,
|
||||
"h-full w-8 from-stone-100/75 to-transparent dark:from-stone-900/75",
|
||||
].join(" ")}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<div
|
||||
ref={maybeScrollable.set}
|
||||
onScroll={checkArrows}
|
||||
class={classPropToString([
|
||||
"no-scrollbar flex w-full overflow-x-auto",
|
||||
classes,
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
import uFuzzy from "@leeoniya/ufuzzy";
|
||||
import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
|
||||
|
||||
import { scrollIntoView } from "/src/scripts/utils/scroll";
|
||||
import { createRWS } from "/src/solid/rws";
|
||||
|
||||
import { INPUT_PRESET_SEARCH_ID } from "../..";
|
||||
import { Box } from "./box";
|
||||
import { Button, ButtonRandomChart } from "./button";
|
||||
import { Line } from "./line";
|
||||
|
||||
const PER_PAGE = 100;
|
||||
|
||||
export function SearchFrame({
|
||||
presets,
|
||||
selectedFrame,
|
||||
}: {
|
||||
presets: Presets;
|
||||
selectedFrame: Accessor<FrameName>;
|
||||
}) {
|
||||
const counterRef = createRWS<HTMLDivElement | undefined>(undefined);
|
||||
|
||||
const search = createRWS("", {
|
||||
equals: false,
|
||||
});
|
||||
|
||||
const inputRef = createRWS<HTMLInputElement | undefined>(undefined);
|
||||
|
||||
const config: uFuzzy.Options = {
|
||||
intraIns: Infinity,
|
||||
intraChars: `[a-z\d' ]`,
|
||||
};
|
||||
|
||||
const fuzzyMultiInsert = new uFuzzy({
|
||||
intraIns: 1,
|
||||
});
|
||||
const fuzzyMultiInsertFuzzier = new uFuzzy(config);
|
||||
const fuzzySingleError = new uFuzzy({
|
||||
intraMode: 1,
|
||||
...config,
|
||||
});
|
||||
const fuzzySingleErrorFuzzier = new uFuzzy({
|
||||
intraMode: 1,
|
||||
...config,
|
||||
});
|
||||
|
||||
let haystack = [] as string[];
|
||||
|
||||
function initHaystackIfNeeded() {
|
||||
if (haystack.length) return;
|
||||
|
||||
haystack = presets.list.map(
|
||||
(preset) =>
|
||||
`${preset.title}\t/ ${[...preset.path.map(({ name }) => name), preset.name].join(" / ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const searchResult = createMemo(() => {
|
||||
scrollIntoView(counterRef());
|
||||
|
||||
const needle = search();
|
||||
|
||||
if (!needle) return null;
|
||||
|
||||
const outOfOrder = 5;
|
||||
const infoThresh = 5_000;
|
||||
|
||||
let result = fuzzyMultiInsert.search(
|
||||
haystack,
|
||||
needle,
|
||||
undefined,
|
||||
infoThresh,
|
||||
);
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzyMultiInsert.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzySingleError.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzySingleErrorFuzzier.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzyMultiInsertFuzzier.search(
|
||||
haystack,
|
||||
needle,
|
||||
undefined,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.[0]?.length || !result?.[1]) {
|
||||
result = fuzzyMultiInsertFuzzier.search(
|
||||
haystack,
|
||||
needle,
|
||||
outOfOrder,
|
||||
infoThresh,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const resultCount = createMemo(() => searchResult()?.[0]?.length || 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="relative flex size-full flex-1 flex-col"
|
||||
style={{
|
||||
display: selectedFrame() !== "Search" ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div class="flex-1 space-y-1 overflow-y-auto p-4 pt-16">
|
||||
<p class="py-2 text-orange-100/75">
|
||||
<Show
|
||||
when={search()}
|
||||
fallback={
|
||||
<p>
|
||||
If you can't think of anything, you might want to try to{" "}
|
||||
<ButtonRandomChart presets={presets} />
|
||||
</p>
|
||||
}
|
||||
>
|
||||
Found{" "}
|
||||
<span class="font-medium text-orange-400/75">
|
||||
{resultCount().toLocaleString("en-us")}
|
||||
</span>{" "}
|
||||
presets.
|
||||
</Show>
|
||||
</p>
|
||||
|
||||
<Show when={search()}>
|
||||
<div class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<div
|
||||
class="py-1"
|
||||
style={{
|
||||
display: !resultCount() ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const r = searchResult();
|
||||
|
||||
if (r) {
|
||||
return (
|
||||
<ListSection
|
||||
haystack={haystack}
|
||||
presets={presets}
|
||||
searchResult={() => r}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Box absolute="top" padded={false}>
|
||||
<div
|
||||
class="relative flex w-full cursor-text items-center space-x-0.5 px-3 py-2 hover:bg-orange-200/5"
|
||||
onClick={() => inputRef()?.focus()}
|
||||
>
|
||||
<IconTablerSearch />
|
||||
<input
|
||||
id={INPUT_PRESET_SEARCH_ID}
|
||||
ref={inputRef.set}
|
||||
class="w-full bg-transparent p-1 caret-orange-500 placeholder:text-orange-200/50 focus:outline-none"
|
||||
placeholder="Search by name or path"
|
||||
value={search()}
|
||||
onFocus={initHaystackIfNeeded}
|
||||
onInput={(event) => search.set(event.target.value)}
|
||||
/>
|
||||
<span class="-mx-1 flex size-5 flex-none items-center justify-center rounded-md border border-current text-xs font-bold">
|
||||
<IconTablerSlash />
|
||||
</span>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box absolute="bottom">
|
||||
<Button
|
||||
onClick={() => {
|
||||
search.set("");
|
||||
inputRef()?.focus();
|
||||
}}
|
||||
>
|
||||
Reset search
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ListSection({
|
||||
searchResult,
|
||||
pageIndex = 0,
|
||||
haystack,
|
||||
presets,
|
||||
}: {
|
||||
searchResult: Accessor<uFuzzy.SearchResult>;
|
||||
pageIndex?: number;
|
||||
haystack: string[];
|
||||
presets: Presets;
|
||||
}) {
|
||||
const div = createRWS<HTMLDivElement | undefined>(undefined);
|
||||
|
||||
const useVisibilityObserver = createVisibilityObserver();
|
||||
|
||||
const visible = useVisibilityObserver(div);
|
||||
|
||||
const showNextPage = createMemo<boolean>(
|
||||
(previous) => previous || visible(),
|
||||
false,
|
||||
);
|
||||
|
||||
const list = createMemo(() =>
|
||||
computeList({
|
||||
searchResult: searchResult(),
|
||||
pageIndex,
|
||||
haystack,
|
||||
presets,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="pb-16">
|
||||
<For each={list()}>
|
||||
{({ preset, path, title }) => (
|
||||
<Line
|
||||
id={`search-${preset.id}`}
|
||||
name={title}
|
||||
onClick={() => presets.select(preset)}
|
||||
active={() => presets.selected() === preset}
|
||||
header={path}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={list().length === PER_PAGE}>
|
||||
<div ref={div.set}>
|
||||
<Show when={showNextPage()}>
|
||||
<ListSection
|
||||
searchResult={searchResult}
|
||||
haystack={haystack}
|
||||
presets={presets}
|
||||
pageIndex={pageIndex + 1}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function computeList({
|
||||
searchResult,
|
||||
pageIndex,
|
||||
haystack,
|
||||
presets,
|
||||
}: {
|
||||
searchResult: uFuzzy.SearchResult;
|
||||
pageIndex: number;
|
||||
haystack: string[];
|
||||
presets: Presets;
|
||||
}) {
|
||||
let list: {
|
||||
preset: Preset;
|
||||
path: string;
|
||||
title: string;
|
||||
}[] = [];
|
||||
|
||||
let [indexes, info, order] = searchResult || [null, null, null];
|
||||
|
||||
const minIndex = pageIndex * PER_PAGE;
|
||||
|
||||
if (indexes?.length) {
|
||||
const maxIndex = Math.min(
|
||||
(order || indexes).length - 1,
|
||||
minIndex + PER_PAGE - 1,
|
||||
);
|
||||
|
||||
list = Array(maxIndex - minIndex + 1);
|
||||
|
||||
if (info && order) {
|
||||
for (let i = minIndex; i <= maxIndex; i++) {
|
||||
let infoIdx = order[i];
|
||||
|
||||
const [title, path] = uFuzzy
|
||||
.highlight(haystack[info.idx[infoIdx]], info.ranges[infoIdx])
|
||||
.split("\t");
|
||||
|
||||
list[i % 100] = {
|
||||
preset: presets.list[info.idx[infoIdx]],
|
||||
path,
|
||||
title,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
for (let i = minIndex; i <= maxIndex; i++) {
|
||||
let index = indexes[i];
|
||||
|
||||
const [title, path] = haystack[index].split("\t");
|
||||
|
||||
list[i % 100] = {
|
||||
preset: presets.list[index],
|
||||
path,
|
||||
title,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import { version } from "/src/../package.json";
|
||||
import { ipad, iphone, macOS, safariOnly, standalone } from "/src/env";
|
||||
import { classPropToString } from "/src/solid/classes";
|
||||
|
||||
import { AnchorAPI } from "../strip/components/anchorAPI";
|
||||
import { AnchorGeyser } from "../strip/components/anchorGeyser";
|
||||
import { AnchorGit } from "../strip/components/anchorGit";
|
||||
import { AnchorNostr } from "../strip/components/anchorNostr";
|
||||
import { Header } from "./header";
|
||||
|
||||
export function SettingsFrame({
|
||||
selectedFrame,
|
||||
appTheme,
|
||||
backgroundMode,
|
||||
backgroundOpacity,
|
||||
}: {
|
||||
selectedFrame: Accessor<FrameName>;
|
||||
appTheme: SL<"System" | "Dark" | "Light">;
|
||||
backgroundMode: SL<"Scroll" | "Static">;
|
||||
backgroundOpacity: SL<{ text: string; value: number }>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="flex-1 overflow-y-auto overflow-x-hidden"
|
||||
style={{
|
||||
display: selectedFrame() !== "Settings" ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div class="space-y-4 p-4">
|
||||
<Header title="Settings">And other stuff</Header>
|
||||
|
||||
<div class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<div class="space-y-4">
|
||||
<Title>General</Title>
|
||||
|
||||
<FieldRadioGroup
|
||||
title="Theme"
|
||||
ariaTitle="App's theme"
|
||||
description="Options for the app's theme"
|
||||
sl={appTheme}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<div class="space-y-4">
|
||||
<Title>Background</Title>
|
||||
|
||||
<FieldRadioGroup
|
||||
title="Mode"
|
||||
ariaTitle="Background mode"
|
||||
description="Options for how the background in displayed"
|
||||
sl={backgroundMode}
|
||||
/>
|
||||
|
||||
<FieldRadioGroup
|
||||
title="Opacity"
|
||||
ariaTitle="Background mode"
|
||||
description="Options for the opacity of the text in the background"
|
||||
sl={backgroundOpacity}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<div class="space-y-4">
|
||||
<Title>Donations</Title>
|
||||
|
||||
<p>
|
||||
A <strong>massive thank you</strong> to everybody who sent their
|
||||
hard earned sats. This project, by being completely free, is very
|
||||
dependent and only founded by the goodwill of fellow ₿itcoiners.
|
||||
</p>
|
||||
<p>Top 10 Leaderboard:</p>
|
||||
<ol class="list-inside list-decimal">
|
||||
<For
|
||||
each={[
|
||||
{
|
||||
name: "_Checkɱate",
|
||||
url: "https://primal.net/p/npub1qh5sal68c8swet6ut0w5evjmj6vnw29x3k967h7atn45unzjyeyq6ceh9r",
|
||||
amount: 500_000,
|
||||
},
|
||||
{
|
||||
name: "avvi |",
|
||||
url: "https://primal.net/p/npub1md2q6fexrtmd5hx9gw2p5640vg662sjlpxyz3tdmu4j4g8hhkm6scn6hx3",
|
||||
amount: 5_000,
|
||||
},
|
||||
{
|
||||
name: "mutatrum",
|
||||
url: "https://primal.net/p/npub1hklphk7fkfdgmzwclkhshcdqmnvr0wkfdy04j7yjjqa9lhvxuflsa23u2k",
|
||||
amount: 5_000,
|
||||
},
|
||||
{
|
||||
name: "Gunnar",
|
||||
url: "https://primal.net/p/npub1rx9wg2d5lhah45xst3580sajcld44m0ll9u5dqhu2t74p6xwufaqwghtd4",
|
||||
amount: 1_000,
|
||||
},
|
||||
{
|
||||
name: "Blokchain Boog",
|
||||
url: "https://x.com/BlokchainB",
|
||||
amount: 1_500 + 1590,
|
||||
},
|
||||
{
|
||||
name: "Josh",
|
||||
url: "https://primal.net/p/npub1pc57ls4rad5kvsp733suhzl2d4u9y7h4upt952a2pucnalc59teq33dmza",
|
||||
amount: 1_000,
|
||||
},
|
||||
{
|
||||
name: "Alp",
|
||||
url: "https://primal.net/p/npub175nul9cvufswwsnpy99lvyhg7ad9nkccxhkhusznxfkr7e0zxthql9g6w0",
|
||||
amount: 1_000,
|
||||
},
|
||||
{
|
||||
name: "Ulysses",
|
||||
url: "https://primal.net/p/npub1n7n3dssm90hfsfjtamwh2grpzwjlvd2yffae9pqgg99583lxdypsnn9gtv",
|
||||
amount: 1_000,
|
||||
},
|
||||
{
|
||||
name: "btcschellingpt",
|
||||
url: "https://primal.net/p/npub1nvfgglea9zlcs58tcqlc6j26rt50ngkgdk7699wfq4txrx37aqcsz4e7zd",
|
||||
amount: 1_000 + 1_000,
|
||||
},
|
||||
{
|
||||
name: "Coinatra",
|
||||
url: "https://primal.net/p/npub1eut9kcejweegwp9waq3a4g03pvprdzkzvjjvl8fvj2a2wlx030eswzfna8",
|
||||
amount: 1_000,
|
||||
},
|
||||
{
|
||||
name: "Printer Go Brrrr",
|
||||
url: "https://primal.net/p/npub1l5pxvjzhw77h86tu0sml2gxg8jpwxch7fsj6d05n7vuqpq75v34syk4q0n",
|
||||
amount: 1_000,
|
||||
},
|
||||
{
|
||||
name: "b81776c32d7b",
|
||||
url: "https://primal.net/p/npub1hqthdsed0wpg57sqsc5mtyqxxgrh3s7493ja5h49v23v2nhhds4qk4w0kz",
|
||||
amount: 17_509,
|
||||
},
|
||||
{
|
||||
name: "DerGigi",
|
||||
url: "https://primal.net/p/npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc",
|
||||
amount: 6001,
|
||||
},
|
||||
{
|
||||
name: "Adarnit",
|
||||
url: "https://primal.net/p/npub17armdveqy42uhuuuwjc5m2dgjkz7t7epgvwpuccqw8jusm8m0g4sn86n3s",
|
||||
amount: 17_726,
|
||||
},
|
||||
{
|
||||
name: "Auburn Citadel",
|
||||
url: "https://primal.net/p/npub1730y5k2s9u82w9snx3hl37r8gpsrmqetc2y3xyx9h65yfpf28rtq0y635y",
|
||||
amount: 17_471,
|
||||
},
|
||||
{
|
||||
name: "Anon",
|
||||
amount: 210_000,
|
||||
},
|
||||
{
|
||||
name: "Daniel ∞/21M",
|
||||
url: "https://twitter.com/DanielAngelovBG",
|
||||
amount: 21_000,
|
||||
},
|
||||
{
|
||||
name: "Ivo",
|
||||
url: "https://primal.net/p/npub1mnwjn40hr042rsmzu64rsnwsw07uegg4tjkv620c94p6e797wkvq3qeujc",
|
||||
amount: 5_000,
|
||||
},
|
||||
]
|
||||
.sort((a, b) =>
|
||||
b.amount !== a.amount
|
||||
? b.amount - a.amount
|
||||
: a.name.localeCompare(b.name),
|
||||
)
|
||||
.slice(0, 10)}
|
||||
>
|
||||
{({ name, url, amount }) => (
|
||||
<li>
|
||||
<a href={url} target="_blank">
|
||||
{name}
|
||||
</a>{" "}
|
||||
- {amount.toLocaleString("en-us")} sats
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<Show when={!standalone && safariOnly && (macOS || ipad || iphone)}>
|
||||
<hr class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<div class="space-y-4">
|
||||
<Title>Install</Title>
|
||||
<p>
|
||||
<Show when={macOS}>
|
||||
This app can be installed by clicking on the "File" tab on the
|
||||
menu bar and then on "Add to dock".
|
||||
</Show>
|
||||
<Show when={iphone || ipad}>
|
||||
This app can be installed by tapping on the "Share" button tab
|
||||
of Safari and then on "Add to Home Screen".
|
||||
</Show>
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<hr class="border-lighter -mx-4 border-t" />
|
||||
|
||||
<div class="pt-4 md:hidden">
|
||||
<div class="flex items-center justify-center gap-8 py-1">
|
||||
<AnchorAPI />
|
||||
<AnchorGit />
|
||||
<AnchorNostr />
|
||||
<AnchorGeyser />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="pb-[10vh] pt-4 text-center">
|
||||
<span class="opacity-50">Version:</span>{" "}
|
||||
<a
|
||||
href="https://github.com/satonomics-org/satonomics/blob/main/CHANGELOG.md"
|
||||
target="_blank"
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Title({ children }: ParentProps) {
|
||||
return <p class="text-base font-medium">{children}</p>;
|
||||
}
|
||||
|
||||
export function FieldRadioGroup<
|
||||
T extends
|
||||
| string
|
||||
| {
|
||||
text: string;
|
||||
value: number;
|
||||
},
|
||||
>({
|
||||
title,
|
||||
sl,
|
||||
ariaTitle,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
ariaTitle: string;
|
||||
description: string;
|
||||
sl: SL<T>;
|
||||
}) {
|
||||
return (
|
||||
<fieldset aria-label={`Choose an option for: ${ariaTitle}`}>
|
||||
<p class="pb-0.5">{title}</p>
|
||||
|
||||
<p class="pb-1 text-sm opacity-50">{description}</p>
|
||||
|
||||
<RadioGroup sl={sl} title={title} />
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export function RadioGroup<
|
||||
T extends
|
||||
| string
|
||||
| {
|
||||
text: string;
|
||||
value: number;
|
||||
},
|
||||
>({ title, sl, size }: { title: string; sl: SL<T>; size?: Size }) {
|
||||
return (
|
||||
<div
|
||||
class={classPropToString([
|
||||
size === "xs" && "gap-0.5 rounded-md border p-0.5 text-xs",
|
||||
size === "sm" && "gap-1 rounded-md border p-1 text-sm",
|
||||
(!size || size === "base") && "gap-1.5 rounded-lg border p-1.5",
|
||||
"border-superlight -mx-2 mt-2 flex bg-stone-400/30 backdrop-blur-[2px] dark:bg-stone-950/75",
|
||||
])}
|
||||
>
|
||||
<For each={sl.list()}>
|
||||
{(value) => (
|
||||
<label
|
||||
class={classPropToString([
|
||||
size === "xs" && "rounded px-1.5 py-0",
|
||||
size === "sm" && "rounded px-2 py-1",
|
||||
(!size || size === "base") && "rounded-md px-3 py-1.5",
|
||||
value === sl.selected()
|
||||
? "border-lighter bg-orange-50/75 shadow dark:bg-orange-200/10"
|
||||
: "border-transparent",
|
||||
"flex flex-1 cursor-pointer select-none items-center justify-center border font-medium hover:bg-orange-50 focus:outline-none active:scale-95 active:bg-orange-50 dark:hover:bg-orange-200/20 dark:active:bg-orange-200/10",
|
||||
])}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`${title}-option`}
|
||||
value={typeof value === "object" ? value.value : value}
|
||||
class="sr-only"
|
||||
onClick={() => {
|
||||
sl.select(value);
|
||||
}}
|
||||
/>
|
||||
<span>{typeof value === "object" ? value.text : value}</span>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { touchScreen } from "/src/env";
|
||||
|
||||
export function Qrcode({ qrcode }: { qrcode: RWS<string> }) {
|
||||
return (
|
||||
<div
|
||||
class="absolute inset-0 z-50 flex size-full items-center justify-center bg-black/50 backdrop-blur-md"
|
||||
onClick={() => {
|
||||
qrcode.set("");
|
||||
}}
|
||||
>
|
||||
<div class="flex size-full max-h-[80dvh] max-w-md flex-col justify-center space-y-8 px-8 pb-8 text-base">
|
||||
<p class="pb-4 text-center text-3xl font-bold">Share</p>
|
||||
|
||||
<p>
|
||||
To share this page, you can either send the following QR Code with a
|
||||
phone:
|
||||
</p>
|
||||
<div class="flex min-h-0 w-full flex-1 flex-col items-center justify-center">
|
||||
<img
|
||||
class="aspect-square min-h-0 flex-1 grow object-contain"
|
||||
onClick={(event) => {
|
||||
event?.stopPropagation();
|
||||
}}
|
||||
src={qrcode()}
|
||||
style={{ "image-rendering": "pixelated" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Or if you prefer you can share this link instead:</p>
|
||||
<a
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
href={location.href}
|
||||
>
|
||||
{location.href}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{touchScreen ? "Touch" : "Click"} anywhere but on the QR Code to exit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||