Compare commits
917 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9adaff488a | |||
| 9f6168915f | |||
| 64b90dd678 | |||
| 93e02aed44 | |||
| 8302660d88 | |||
| 2c0e3d1119 | |||
| 7bbf03766e | |||
| 7a2ba17d20 | |||
| ac30f0e512 | |||
| 2e1037ff36 | |||
| 626c52044d | |||
| f7ee4e487a | |||
| 7b3e172948 | |||
| 6bb1a2a311 | |||
| 3b00a92fa4 | |||
| f39681bb2b | |||
| 967d2c7f35 | |||
| b0d933a7ab | |||
| 96e0df110e | |||
| 91a6129e8d | |||
| d9c829c3c6 | |||
| 467dfcc4b8 | |||
| 8a938c00f6 | |||
| 5661735f3e | |||
| 1c7434ff83 | |||
| d75c2a881b | |||
| ddb1db7a8e | |||
| 407a365055 | |||
| 335cbce09e | |||
| 922a0abb60 | |||
| 25a0ebe51e | |||
| 3a836ab0f4 | |||
| 524ab3de05 | |||
| e77993fb76 | |||
| 0c442b4a71 | |||
| 670aa95494 | |||
| 5ffb66c0dc | |||
| b675b70067 | |||
| 1484eae53c | |||
| b12a72ea1a | |||
| 1b9e18f98b | |||
| 8fe0af349d | |||
| 5826d78e35 | |||
| 325811fee7 | |||
| 69f6d32d4a | |||
| ea70c381de | |||
| 6f45ec13f3 | |||
| 3bc0615000 | |||
| 69729842a4 | |||
| 5f4fc646f5 | |||
| 85570c73cb | |||
| 3a3f6b8593 | |||
| 426d7797a3 | |||
| cb0abc324e | |||
| e832ffbe23 | |||
| abffdec497 | |||
| 70e7e24b4f | |||
| 13ab7d39d7 | |||
| 3cae817915 | |||
| c33444a92e | |||
| 3e9b1cc2b2 | |||
| ac6175688d | |||
| a6f8108165 | |||
| 8cff55a405 | |||
| bd376f86ea | |||
| d9f28e85af | |||
| ed18fd55e1 | |||
| 5b06098368 | |||
| e89a67b9a7 | |||
| 445959f5b9 | |||
| 647f177f31 | |||
| 705dbdbd7e | |||
| 31d2f8ef37 | |||
| 236b4097c5 | |||
| f5790d5c8a | |||
| f08ac7f916 | |||
| e77d338357 | |||
| 5d6325ae30 | |||
| 9ba77dac0f | |||
| f9856cf0aa | |||
| de93f08e93 | |||
| d538280f4b | |||
| bbb74b76c8 | |||
| eadf93b804 | |||
| f29443fc15 | |||
| 75a023bdd8 | |||
| d30344ee3c | |||
| 02d635d48b | |||
| 40ec356cc3 | |||
| 5a5d4da57d | |||
| efb247d104 | |||
| 457b0e24c5 | |||
| 6e0ac138d8 | |||
| 26c6c92bb8 | |||
| e1ad45f44b | |||
| aebca14d78 | |||
| 42b0d7a174 | |||
| a37c2474fe | |||
| 5f308e9da7 | |||
| 3aadced85d | |||
| 9375d5aded | |||
| 2c8205146c | |||
| 8d5a2b911d | |||
| 7d5de7bf24 | |||
| 4b1410855a | |||
| 78a4d1af65 | |||
| 5e3519aad4 | |||
| 4386ef47fe | |||
| 135a18d56f | |||
| 71f45479b9 | |||
| bcb8d5bed6 | |||
| 8f19bf7350 | |||
| 25860636f0 | |||
| 8c2402cacb | |||
| 4b910ceaa7 | |||
| 4a0ce6337f | |||
| e134ed11a9 | |||
| 03b83846ef | |||
| 7c86c803fa | |||
| a31d9dc15e | |||
| 57749da919 | |||
| 9ad3acbdf9 | |||
| 6fa53aca9f | |||
| bd53168c4e | |||
| 08d17b4a09 | |||
| c5657b9c31 | |||
| 549e2da05b | |||
| c5e912593a | |||
| a86085c2db | |||
| edbec6fd5c | |||
| a76139c0ea | |||
| 59f1296d56 | |||
| 14ae41c7ba | |||
| df09b3aa28 | |||
| f9fad2d775 | |||
| fa609c73ba | |||
| 9b2f334130 | |||
| a006cefd71 | |||
| 4b2ada14a0 | |||
| 1ad8d8a631 | |||
| 3ca83a2289 | |||
| 2ccf0ef856 | |||
| f7f065c6e0 | |||
| 593af69230 | |||
| 032f3cb66b | |||
| 692a1889ab | |||
| 825a4a77c0 | |||
| 882a3525af | |||
| b491b1f41f | |||
| db5d784ff7 | |||
| db57db4bd9 | |||
| c5e9b75261 | |||
| c59ac62e45 | |||
| 9c8b9b1a3b | |||
| 158b0254ed | |||
| 3526a177fc | |||
| e755f2856a | |||
| 2ec3ca8308 | |||
| 1cf75b48b5 | |||
| abde9ed162 | |||
| 998db1beed | |||
| 79e352d06e | |||
| b8f77433b9 | |||
| 96b967f6fb | |||
| 68c71e62d6 | |||
| 60a38b4108 | |||
| f4a1384dc4 | |||
| b88f0bab56 | |||
| f23907768f | |||
| f280b03cab | |||
| 554c0e565d | |||
| cfc5f7633b | |||
| 82050c7c01 | |||
| f4edb695de | |||
| dc2fa233ab | |||
| a1f31a14be | |||
| d27cc02e8c | |||
| fcc74ba212 | |||
| f48ad577d3 | |||
| 60c73f5635 | |||
| 24248215e9 | |||
| b6ec133368 | |||
| 35e567cfb6 | |||
| 25c697cca1 | |||
| 30dc695741 | |||
| 9e41d51702 | |||
| dc86514329 | |||
| c644781d18 | |||
| eedc0dd075 | |||
| c8c62b504b | |||
| 8467e218ae | |||
| e8f77ab2e5 | |||
| 1d2c927d94 | |||
| 81da73bc53 | |||
| 2dcbd8df99 | |||
| 37f5f50867 | |||
| f6a2a0540b | |||
| dc2e847f58 | |||
| e77fe0253e | |||
| 3d3787a8d9 | |||
| 11b323ef00 | |||
| df577ca7f5 | |||
| a2ba4d89f3 | |||
| 2ad55bf558 | |||
| cf08e470ef | |||
| 82e59d409e | |||
| 7d01e9e91e | |||
| 1e4acfe124 | |||
| 4f1653b086 | |||
| 6cd60a064b | |||
| 8072c4670c | |||
| 4ffa2e3993 | |||
| 9b230d23dd | |||
| baa7c9cc22 | |||
| 33a92cfad4 | |||
| e9f6295014 | |||
| 71078b5bdd | |||
| 6cce92af22 | |||
| d3b8520c41 | |||
| 5425085953 | |||
| db0298ac1b | |||
| 7bfca87caf | |||
| 5f87594ead | |||
| bb46481d7f | |||
| 1821d5d57b | |||
| 6ad15221de | |||
| 83d74da556 | |||
| 114228e8eb | |||
| a53f89c849 | |||
| 7ff79c3164 | |||
| db344749b6 | |||
| 1c6ece48a8 | |||
| b622285999 | |||
| 5fde0101bf | |||
| a6062d4c39 | |||
| 66f1e92cb6 | |||
| d9c4653f82 | |||
| cfdf8fdbca | |||
| 138b2bd357 | |||
| 16b14b1fe1 | |||
| c4ce718bb2 | |||
| 62d4b35c93 | |||
| 7407c032e5 | |||
| 9d03fdf31d | |||
| dfe5148f17 | |||
| 0d5b792c57 | |||
| 2279aa8f18 | |||
| d45686128e | |||
| 5b6ce5d8ee | |||
| aad34c4d52 | |||
| 470082cc65 | |||
| 6554f35710 | |||
| 335fe24a54 | |||
| 3831ef7b25 | |||
| 8127337a09 | |||
| 9a59c2e541 | |||
| 27adca5653 | |||
| 2c5b502da9 | |||
| 23f6397a97 | |||
| 43117825d7 | |||
| cc5701ea62 | |||
| 9524eafea1 | |||
| c28a0f96f7 | |||
| 301dee96dc | |||
| 185fc7b6ed | |||
| 6d194dbb71 | |||
| d34f4bdd12 | |||
| 17dc4bde5e | |||
| ce50b14591 | |||
| f7bd319954 | |||
| e9c0121a18 | |||
| 01aa425f81 | |||
| 38d5c7dff6 | |||
| e3b4b9b618 | |||
| a5951c58f3 | |||
| 504d6eaa9f | |||
| 6253fa30ef | |||
| 47f7cef4f4 | |||
| 72bba06e71 | |||
| 9b92c5ce38 | |||
| dfa077a1c9 | |||
| 18fb2e7d4d | |||
| a610fd53e2 | |||
| 16abce1f2d | |||
| f3b42f34a6 | |||
| 6483d324de | |||
| 5ab97050dd | |||
| 17eed70903 | |||
| 88067c03b7 | |||
| 7c1e5b913f | |||
| 0014235e91 | |||
| a39b7be1d1 | |||
| de98c5f706 | |||
| 10b496e845 | |||
| bbe7bf390d | |||
| 4777b3400a | |||
| acaa70e944 | |||
| 4049d694f7 | |||
| e155a3dacf | |||
| a224e4c4d8 | |||
| edaeda5424 | |||
| 09d974913d | |||
| f82edb290a | |||
| 3d8b33ae94 | |||
| 565ecbd436 | |||
| 3359dfcc29 | |||
| 1c2afd14dd | |||
| fe5343c1d6 | |||
| 08cfefc02a | |||
| f6d9332c48 | |||
| cc6913c854 | |||
| 8c75fbd0a4 | |||
| 0de6d62409 | |||
| 5ba7ce5b7c | |||
| e106d30852 | |||
| 30affc884b | |||
| 745717ea49 | |||
| 4efd98b758 | |||
| 36640e3710 | |||
| 311c4fd29d | |||
| f50374f983 | |||
| 82ceb7f021 | |||
| 0aba3bc1d8 | |||
| f6c984ff3c | |||
| 4091ab6b6c | |||
| fb9fd5b51a | |||
| 9389700a01 | |||
| 016c1b2233 | |||
| 38b8a08297 | |||
| c9ffd3ad99 | |||
| 61f960de28 | |||
| da1ff2cacc | |||
| 05036c682f | |||
| 7d47bc8042 | |||
| 98cfd160ef | |||
| b5e3262b67 | |||
| 009fb35c4c | |||
| 8648d3131a | |||
| 00c316c35d | |||
| 5f8de8e756 | |||
| ee5dc8fc41 | |||
| a61926988a | |||
| bd8c4dfb6b | |||
| ce9b4bc4dd | |||
| 8b12b00114 | |||
| 1775cc1d54 | |||
| e4bd09df24 | |||
| 5e8c7da4df | |||
| c85592eefe | |||
| 05861c9113 | |||
| 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,2 @@
|
||||
[build]
|
||||
rustflags = ["-C", "target-cpu=native"]
|
||||
@@ -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.30.2/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 plan, local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.result == 'success' && 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,46 @@
|
||||
# Mac OS
|
||||
.DS_Store
|
||||
|
||||
/app-next
|
||||
/app-html
|
||||
/datasets
|
||||
/datasets2
|
||||
/datasets_*
|
||||
# Builds
|
||||
target
|
||||
websites/dist
|
||||
bridge/
|
||||
/ids.txt
|
||||
rust_out
|
||||
|
||||
TODO.md
|
||||
# Copies
|
||||
*\ copy*
|
||||
|
||||
.stfolder
|
||||
/charts
|
||||
/price
|
||||
# Ignored
|
||||
_*
|
||||
!__*.py
|
||||
/*.md
|
||||
/*.py
|
||||
/*.json
|
||||
/*.html
|
||||
/research
|
||||
/filter_*
|
||||
|
||||
# Logs
|
||||
*.log*
|
||||
|
||||
# Environment variables/configs
|
||||
.env
|
||||
|
||||
# Profiling
|
||||
profile.json.gz
|
||||
flamegraph.svg
|
||||
*.trace
|
||||
|
||||
# AI
|
||||
.claude/settings*
|
||||
|
||||
# Expand
|
||||
expand.rs
|
||||
|
||||
# Benchmarks
|
||||
[0-9]/
|
||||
/benches
|
||||
|
||||
# AI
|
||||
.claude
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## v. 0.3.0 | [853930](https://mempool.space/block/00000000000000000002eb5e9a7950ca2d5d98bd1ed28fc9098aa630d417985d) - 2024/07/26
|
||||
|
||||

|
||||
|
||||
### Parser
|
||||
|
||||
- Global
|
||||
- Improved self-hosting by:
|
||||
- Fixing an incredibly annoying bug that made the program panic because of a wrong utxo/address durable state after a or many new datasets were added/changed after a first successful parse of the chain
|
||||
- Fixing a bug that would crash the program if launched for the first time ever
|
||||
- Auto fetch prices from the main Satonomics instance if missing instead of only trying Kraken's and Binance's API which are limited to the last 16 hours
|
||||
- Merged the core of `HeightMap` and `DateMap` structs into `GenericMap`
|
||||
- Added `Height` struct and many others
|
||||
- Reorganized outputs of both the parser and the server for ease of use and easier sync compatibility
|
||||
- CLI
|
||||
- Added an argument parser for improved UX with several options
|
||||
- Datasets
|
||||
- Added the following datasets for all entities:
|
||||
- Value destroyed
|
||||
- Value created
|
||||
- Spent Output Profit Ratio (SOPR)
|
||||
- Added the following ratio datasets and their variations to all prices {realized, moving average, any cointime, etc}:
|
||||
- Market Price to {X}
|
||||
- Market Price to {X} Ratio
|
||||
- Market Price to {X} Ratio 1 Week SMA
|
||||
- Market Price to {X} Ratio 1 Month SMA
|
||||
- Market Price to {X} Ratio 1 Year SMA
|
||||
- Market Price to {X} Ratio 1 Year SMA Momentum Oscillator
|
||||
- Market Price to {X} Ratio 99th Percentile
|
||||
- Market Price to {X} Ratio 99.5th Percentile
|
||||
- Market Price to {X} Ratio 99.9th Percentile
|
||||
- Market Price to {X} Ratio 1st Percentile
|
||||
- Market Price to {X} Ratio 0.5th Percentile
|
||||
- {X} 1% Top Probability
|
||||
- {X} 0.5% Top Probability
|
||||
- {X} 0.1% Top Probability
|
||||
- {X} 1% Bottom Probability
|
||||
- {X} 0.5% Bottom Probability
|
||||
- {X} 0.1% Bottom Probability
|
||||
- Added block metadatasets and their variants (raw/sum/average/min/max/percentiles):
|
||||
- Block size
|
||||
- Block weight
|
||||
- Block VBytes
|
||||
- Block interval
|
||||
- Price
|
||||
- Improved error message when price cannot be found
|
||||
|
||||
### App
|
||||
|
||||
- General
|
||||
- Added chart scroll button for nice animations à la Wicked
|
||||
- Added scale mode switch (Linear/Logarithmic) at the bottom right of all charts
|
||||
- Added unit at the top left of all charts
|
||||
- Added a backup API in case the main one fails or is offline
|
||||
- Complete redesign of the datasets object
|
||||
- Removed import of routes in JSON in favor for hardcoded typed routes in string format which resulted in:
|
||||
- \+ A much lighter app
|
||||
- \+ Better Lighthouse score
|
||||
- \- Slower Typescript server
|
||||
- Fixed datasets with null values crashing their fetch function
|
||||
- Added a 'Go to a random chart' button in several places
|
||||
- Chart
|
||||
- Fixed series color being set to default ones after hovering the legend
|
||||
- Fixed chart starting showing candlesticks and quickly switching to a line when it should've started directly with the line
|
||||
- Separated the QRCode generator library from the main chunk and made it imported on click
|
||||
- Fixed timescale changing on small screen after changing charts
|
||||
- Folders
|
||||
- Added the size in the "filename" of address cohorts grouped by size
|
||||
- Favorites
|
||||
- Added a 'favorite' and 'unfavorite' button at the bottom
|
||||
- Settings
|
||||
- Removed the horizontal scroll bar which was unintended
|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
- Added a light theme
|
||||
- Charts
|
||||
- Added split panes in order to have the vertical axis visible for all datasets
|
||||
- Added min and max values on the charts
|
||||
- Fixed legend hovering on mobile not resetting on touch end
|
||||
- Added "3 months" and yearly time scale setters (from year 2009 to today)
|
||||
- Hide scrollbar of timescale setters and instead added scroll buttons to the legend only visible on desktop
|
||||
- Improved Share/QR Code screen
|
||||
- Changed all Area series to Line series
|
||||
- Fixed horizontal scrollable legend not updating on preset change
|
||||
- Performance
|
||||
- Improved app's reactivity
|
||||
- Added some chunk splitting for a faster initial load
|
||||
- Global improvements that increased the Lighthouse's performance score
|
||||
- Settings
|
||||
- Finally made a proper component where you can chose the app's theme, between a moving or static background and its text opacity
|
||||
- Added donations section with a leaderboard
|
||||
- Added various links that are visible on the bottom side of the strip on desktop to mobile users
|
||||
- Added install instructions when not installed for Apple users
|
||||
- Misc
|
||||
- Support mini window size, could be useful for embedded views
|
||||
- Hopefully made scrollbars a little more subtle on WIndows and Linux, can't test
|
||||
- Generale style updates
|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
- Added the chunk, date and time of the request to the terminal logs
|
||||
|
||||
### App
|
||||
|
||||
- Chart
|
||||
- Added double click option on a legend to toggle the visibility of all other series
|
||||
- Added highlight effect to a legend by darkening the color of all the other series on the chart while hovering it with the mouse
|
||||
- Added an API link in the legend for each dataset where applicable (when isn't generated locally)
|
||||
- Save fullscreen preference in local storage and url
|
||||
- Improved resize bar on desktop
|
||||
- Changed resize button logo
|
||||
- Changed the share button to visible on small screen too
|
||||
- Improved share screen
|
||||
- Fixed time range shifting not being the one in url params or saved in local storage
|
||||
- Fixed time range shifting on series toggling via the legend
|
||||
- Fixed time range shifting on fullscreen
|
||||
- Fixed time range shifting on resize of the sidebar
|
||||
- Set default view at first load to last 6 months
|
||||
- Added some padding around the datasets (year 1970 to 2100)
|
||||
- History
|
||||
- Changed background for the sticky dates from blur to a solid color as it didn't appear properly in Firefox
|
||||
- Build
|
||||
- Tried to add lazy loads to have split chunks after build, to have much faster load times and they worked great ! But they completely broke Safari on iOS, we can't have nice things
|
||||
- Removed many libraries and did some things manually instead to improve build size
|
||||
- Strip
|
||||
- Temporarily removed the Home button on the strip bar on desktop as there is no landing page yet
|
||||
- Settings
|
||||
- Added version
|
||||
- PWA
|
||||
- Fixed background update
|
||||
- Changed update check frequency to 1 minute (~1kb to fetch every minute which is very reasonable)
|
||||
- Added a nice banner to ask the user to install the update
|
||||
- Misc
|
||||
- Removed tracker even though it was a very privacy friendly as it appeared to not be working properly
|
||||
|
||||
### Price
|
||||
|
||||
- Deleted old price datasets and their backups
|
||||
|
||||
## v. 0.1.0 | [848642](https://mempool.space/block/000000000000000000020be5761d70751252219a9557f55e91ecdfb86c4e026a) - 2024/06/19
|
||||
|
||||

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

|
||||
@@ -0,0 +1,95 @@
|
||||
[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.1.0-alpha.6"
|
||||
package.homepage = "https://bitcoinresearchkit.org"
|
||||
package.repository = "https://github.com/bitcoinresearchkit/brk"
|
||||
package.readme = "README.md"
|
||||
|
||||
[profile.dev]
|
||||
lto = "thin"
|
||||
codegen-units = 16
|
||||
opt-level = 2
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
overflow-checks = false
|
||||
|
||||
[profile.bloaty]
|
||||
debug = true
|
||||
lto = false
|
||||
strip = false
|
||||
inherits = "release"
|
||||
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
||||
[workspace.dependencies]
|
||||
aide = { version = "0.16.0-alpha.2", features = ["axum-json", "axum-query"] }
|
||||
axum = { version = "0.8.8", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
|
||||
bitcoin = { version = "0.32.8", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_alloc = { version = "0.1.0-alpha.6", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.1.0-alpha.6", path = "crates/brk_bencher" }
|
||||
brk_bindgen = { version = "0.1.0-alpha.6", path = "crates/brk_bindgen" }
|
||||
brk_cli = { version = "0.1.0-alpha.6", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.1.0-alpha.6", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.1.0-alpha.6", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.1.0-alpha.6", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.1.0-alpha.6", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.1.0-alpha.6", path = "crates/brk_fetcher" }
|
||||
brk_indexer = { version = "0.1.0-alpha.6", path = "crates/brk_indexer" }
|
||||
brk_iterator = { version = "0.1.0-alpha.6", path = "crates/brk_iterator" }
|
||||
brk_logger = { version = "0.1.0-alpha.6", path = "crates/brk_logger" }
|
||||
brk_mempool = { version = "0.1.0-alpha.6", path = "crates/brk_mempool" }
|
||||
brk_query = { version = "0.1.0-alpha.6", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_reader = { version = "0.1.0-alpha.6", path = "crates/brk_reader" }
|
||||
brk_rpc = { version = "0.1.0-alpha.6", path = "crates/brk_rpc" }
|
||||
brk_server = { version = "0.1.0-alpha.6", path = "crates/brk_server" }
|
||||
brk_store = { version = "0.1.0-alpha.6", path = "crates/brk_store" }
|
||||
brk_traversable = { version = "0.1.0-alpha.6", path = "crates/brk_traversable", features = ["pco", "derive"] }
|
||||
brk_traversable_derive = { version = "0.1.0-alpha.6", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.1.0-alpha.6", path = "crates/brk_types" }
|
||||
byteview = "0.10.0"
|
||||
color-eyre = "0.6.5"
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = "3.0.1"
|
||||
jiff = { version = "0.2.18", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
minreq = { version = "2.14.1", features = ["https", "json-using-serde"] }
|
||||
parking_lot = "0.12.5"
|
||||
rayon = "1.11.0"
|
||||
rustc-hash = "2.1.1"
|
||||
schemars = "1.2.0"
|
||||
serde = "1.0.228"
|
||||
serde_bytes = "0.11.19"
|
||||
serde_derive = "1.0.228"
|
||||
serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] }
|
||||
smallvec = "1.15.1"
|
||||
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
vecdb = { version = "0.5.11", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
|
||||
[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.30.2"
|
||||
ci = "github"
|
||||
allow-dirty = ["ci"]
|
||||
installers = []
|
||||
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]
|
||||
@@ -1,103 +0,0 @@
|
||||
# SATONOMICS
|
||||
|
||||

|
||||
|
||||
## Description
|
||||
|
||||
Satonomics is a better, FOSS, Bitcoin-only, self-hostable Glassnode.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**Having anyone be able to easily do a health-check of the network is incredibly important and should be wanted by every single bitcoiner.**
|
||||
|
||||
## Warning
|
||||
|
||||
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.
|
||||
|
||||
## Instances
|
||||
|
||||
Web App:
|
||||
|
||||
- [app.satonomics.xyz](https://app.satonomics.xyz)
|
||||
|
||||
API:
|
||||
|
||||
- [api.satonomics.xyz](https://api.satonomics.xyz)
|
||||
- [api-bkp.satonomics.xyz](https://api-bkp.satonomics.xyz)
|
||||
|
||||
## Structure
|
||||
|
||||
- `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.
|
||||
|
||||
## Goals / Philosophy
|
||||
|
||||
Adjectives that describe what this project is or strives to be, in no particular order:
|
||||
|
||||
- **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
|
||||
|
||||
## Milestones
|
||||
|
||||
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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Clickable } from "./clickable";
|
||||
|
||||
export function Anchor(args: {
|
||||
title: string;
|
||||
href: string;
|
||||
icon?: () => ValidComponent;
|
||||
}) {
|
||||
return <Clickable {...args} />;
|
||||
}
|
||||