mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-24 08:44:46 -07:00
global: snapshot
This commit is contained in:
135
README.md
135
README.md
@@ -1,75 +1,95 @@
|
||||
# SATONOMICS
|
||||
<p align="center">
|
||||
<a href="https://kibo.money" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/satonomics-org/satonomics/main/assets/logo-full-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/satonomics-org/satonomics/main/assets/logo-full-light.svg">
|
||||
<img alt="kibō" src="https://raw.githubusercontent.com/satonomics-org/satonomics/main/assets/logo-full-light.svg" width="300" height="auto" style="max-width: 100%;">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||

|
||||
<p align="center">
|
||||
A better, FOSS, Bitcoin-only, self-hostable Glassnode.
|
||||
</p>
|
||||
|
||||
## Description
|
||||
|
||||
kibō (hope) 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.
|
||||
|
||||
## Donations
|
||||
|
||||
The project is a lot of work and being worked on full-time. It doesn't have any ads and solely relies on donations. If you find this project useful, any sat would really help make it even better and would be really appreciated.
|
||||
This project was started as an answer to the outrageous pricing from Glassnode (and their third tier starting at $833.33/month !).
|
||||
|
||||
You can donate on the project's [Geyser Fund](https://geyser.fund/project/satonomics/).
|
||||
But it is a lot of work and has been worked on _**full-time since November of 2023**_ and has also been operational since then without any ads.
|
||||
|
||||
_**At the time of writing (2024-09-12), this project has made around 2,200,000 sats, which is around $1300 or $120/month. It's unsustainable.**_
|
||||
|
||||
So if you find this project useful, [please send some sats](https://geyser.fund/project/satonomics/), it would be really appreciated.
|
||||
|
||||
[Geyser Fund](https://geyser.fund/project/satonomics/)
|
||||
|
||||
## Warning
|
||||
|
||||
This project is in a very early stage. Until more people look at the code and check the various computations, the datasets might be in the worst case completely 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)
|
||||
- [kibo.money](https://kibo.money)
|
||||
- [backup.kibo.money](https://backup.kibo.money)
|
||||
|
||||
## 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.
|
||||
- `website`: A web app which displays the generated datasets in various charts and dashboards.
|
||||
- `server`: A small server which will serve the
|
||||
|
||||
## How to run
|
||||
## Setup
|
||||
|
||||
### Requirements
|
||||
|
||||
- `rustup`
|
||||
- 1 TB of free space (will use 60-80% of that)
|
||||
- A running instance of bitcoin-core with txindex=1 and rpc credentials
|
||||
|
||||
### Parser
|
||||
### Docker
|
||||
|
||||
Coming soon
|
||||
|
||||
### Manual
|
||||
|
||||
#### Hardware
|
||||
|
||||
#### 1. Rust
|
||||
|
||||
```bash
|
||||
./run.sh --datadir=$HOME/Developer/bitcoin
|
||||
# https://www.rust-lang.org/tools/install
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
# https://github.com/watchexec/cargo-watch?tab=readme-ov-file#install
|
||||
cargo install cargo-watch --locked
|
||||
```
|
||||
|
||||
### Server
|
||||
#### 2. Parser
|
||||
|
||||
```bash
|
||||
# The first run needs several information about your bitcoin-core config
|
||||
./run.sh --datadir=$HOME/Developer/bitcoin --rpcuser=satoshi --rpcpassword=nakamoto
|
||||
|
||||
# Next time you can just do: ./run.sh
|
||||
# As everything is saved in
|
||||
```
|
||||
|
||||
#### Server
|
||||
|
||||
```bash
|
||||
# Install rustup
|
||||
# Update ./run.sh if needed
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Then the easiest to let others access your server is with `cloudflared` which will also cache requests.
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
- Needs to stop the node to parse the files (at least for now)
|
||||
- Needs a **LOT** a disk space for databases (~700 GB for data from 2009 to mid 2024)
|
||||
|
||||
## Goals / Philosophy
|
||||
|
||||
Adjectives that describe what this project is or strives to be, in no particular order:
|
||||
## Philosophy
|
||||
|
||||
- **Best**: Replace Glassnode as the go to
|
||||
- **Diverse**: Have as many charts/datasets as possible and something for everyone
|
||||
@@ -83,46 +103,25 @@ Adjectives that describe what this project is or strives to be, in no particular
|
||||
- **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
|
||||
## Logo
|
||||
|
||||
Big features that are planned, in no particular order:
|
||||
The dove (borrowed from [svgrepo](https://www.svgrepo.com/svg/351969/dove) for now) represents _**hope**_ (kibō in japanese).
|
||||
|
||||
The orange background represents Bitcoin and when in a circle, it also represents the sun, which means that while it's our hope for a better future, we still have to be careful with our collective goals and actions, to not end up like Icarus.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- **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
|
||||
- **Nostr integration**: First to save preferences, later to add some social functionnality
|
||||
- **Datasets by block timestamp**: In addition to having datasets by date and 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
|
||||
- **Start9 support**: 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_:
|
||||
## Iterations
|
||||
|
||||
- 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.
|
||||
A list of all the previous versions and ideas:
|
||||
|
||||
- https://github.com/drgarlic/satonomics
|
||||
- https://github.com/drgarlic/satonomics-parser
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# if [ "$(uname)" == "Darwin" ]; then
|
||||
# if [ mdutil -s / | grep "disabled" ]; then
|
||||
# sudo mdutil -a -i on
|
||||
# fi
|
||||
# fi
|
||||
|
||||
bitcoin-cli -datadir=/Users/k/Developer/bitcoin stop
|
||||
2
server/Cargo.lock
generated
2
server/Cargo.lock
generated
@@ -2482,6 +2482,7 @@ version = "0.4.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bincode",
|
||||
"chrono",
|
||||
"color-eyre",
|
||||
"derive_deref",
|
||||
"itertools",
|
||||
@@ -2492,7 +2493,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"swc",
|
||||
"swc_common",
|
||||
"swc_ecma_minifier",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
]
|
||||
|
||||
@@ -5,17 +5,17 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.5"
|
||||
color-eyre = "0.6.3"
|
||||
itertools = "0.13.0"
|
||||
regex = "1.10.6"
|
||||
bincode = { git = "https://github.com/bincode-org/bincode.git" }
|
||||
chrono = "0.4.38"
|
||||
color-eyre = "0.6.3"
|
||||
derive_deref = "1.1.1"
|
||||
itertools = "0.13.0"
|
||||
parser = { path = "../parser" }
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.7", features = ["json"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = { version = "1.0.128" }
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
tower-http = { version = "0.5.2", features = ["compression-full"] }
|
||||
parser = { path = "../parser" }
|
||||
derive_deref = "1.1.1"
|
||||
swc = "0.286.0"
|
||||
swc_common = "0.37.5"
|
||||
swc_ecma_minifier = "0.205.2"
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
tower-http = { version = "0.5.2", features = ["compression-full"] }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if cargo watch --help &> /dev/null; then
|
||||
TRIGGER="./in/datasets_len.txt"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use color_eyre::{eyre::eyre, owo_colors::OwoColorize};
|
||||
use reqwest::{header::HOST, StatusCode};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
|
||||
use parser::{log, Date, DateMap, Height, HeightMap, MapChunkId, HEIGHT_MAP_CHUNK_SIZE, OHLC};
|
||||
@@ -14,10 +14,11 @@ use parser::{log, Date, DateMap, Height, HeightMap, MapChunkId, HEIGHT_MAP_CHUNK
|
||||
use crate::{
|
||||
api::structs::{Chunk, Kind, Route},
|
||||
header_map::HeaderMapUtils,
|
||||
response::typed_value_to_response,
|
||||
AppState,
|
||||
};
|
||||
|
||||
use super::response::typed_value_to_response;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Params {
|
||||
chunk: Option<usize>,
|
||||
@@ -168,13 +169,8 @@ where
|
||||
|
||||
let offsetted_to_url = |offseted| {
|
||||
datasets.get(&ChunkId::from_usize(offseted)).map(|_| {
|
||||
let host = headers[HOST].to_str().unwrap();
|
||||
let scheme = if host.contains("0.0.0.0") || host.contains("localhost") {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
|
||||
let scheme = headers.get_scheme();
|
||||
let host = headers.get_host();
|
||||
format!("{scheme}://{host}{}?chunk={offseted}", route.url_path)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use axum::{extract::State, http::HeaderMap, response::Response};
|
||||
use reqwest::header::HOST;
|
||||
|
||||
use crate::{response::generic_to_reponse, AppState};
|
||||
use crate::AppState;
|
||||
|
||||
use super::response::generic_to_reponse;
|
||||
|
||||
pub async fn fallback(headers: HeaderMap, State(app_state): State<AppState>) -> Response {
|
||||
generic_to_reponse(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod dataset;
|
||||
mod fallback;
|
||||
mod response;
|
||||
|
||||
pub use dataset::*;
|
||||
pub use fallback::*;
|
||||
|
||||
@@ -101,7 +101,7 @@ impl Routes {
|
||||
.map(|route| format!("\"{}\"", route.url_path))
|
||||
.join(" | ");
|
||||
|
||||
format!("// This file is auto generated by the server\n// Manual changes are forbidden\n\ntype {}Path = {};\n", name, paths)
|
||||
format!("export type {}Path = {};\n", name, paths)
|
||||
};
|
||||
|
||||
let date_type = map_to_type("Date", &self.date);
|
||||
@@ -112,7 +112,7 @@ impl Routes {
|
||||
|
||||
fs::write(
|
||||
format!("{WEBSITE_TYPES_PATH}/paths.d.ts"),
|
||||
format!("{date_type}\n{height_type}\n{last_type}"),
|
||||
format!("// This file is auto generated by the server\n// Manual changes are forbidden\n\n{date_type}\n{height_type}\n{last_type}"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
use std::path::Path;
|
||||
|
||||
use axum::http::{header, HeaderMap};
|
||||
use chrono::{DateTime, Utc};
|
||||
use parser::log;
|
||||
use reqwest::header::HOST;
|
||||
|
||||
const STALE_IF_ERROR: u64 = 31_536_000; // 1 Year
|
||||
const STALE_IF_ERROR: u64 = 30_000_000; // 1 Year ish
|
||||
|
||||
pub trait HeaderMapUtils {
|
||||
fn get_scheme(&self) -> &str;
|
||||
fn get_host(&self) -> &str;
|
||||
fn check_if_host_is_any_local(&self) -> bool;
|
||||
fn check_if_host_is_0000(&self) -> bool;
|
||||
fn check_if_host_is_localhost(&self) -> bool;
|
||||
|
||||
fn insert_cors(&mut self);
|
||||
|
||||
fn insert_cache_control(&mut self, max_age: u64, stale_while_revalidate: u64);
|
||||
fn insert_last_modified(&mut self, date: DateTime<Utc>);
|
||||
|
||||
fn insert_content_type(&mut self, path: &Path);
|
||||
fn insert_content_type_image_icon(&mut self);
|
||||
@@ -24,6 +33,30 @@ pub trait HeaderMapUtils {
|
||||
}
|
||||
|
||||
impl HeaderMapUtils for HeaderMap {
|
||||
fn get_scheme(&self) -> &str {
|
||||
if self.check_if_host_is_any_local() {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
}
|
||||
}
|
||||
|
||||
fn get_host(&self) -> &str {
|
||||
self[HOST].to_str().unwrap()
|
||||
}
|
||||
|
||||
fn check_if_host_is_any_local(&self) -> bool {
|
||||
self.check_if_host_is_localhost() || self.check_if_host_is_0000()
|
||||
}
|
||||
|
||||
fn check_if_host_is_0000(&self) -> bool {
|
||||
self.get_host().contains("0.0.0.0")
|
||||
}
|
||||
|
||||
fn check_if_host_is_localhost(&self) -> bool {
|
||||
self.get_host().contains("localhost")
|
||||
}
|
||||
|
||||
fn insert_cors(&mut self) {
|
||||
self.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
|
||||
self.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "*".parse().unwrap());
|
||||
@@ -39,6 +72,12 @@ impl HeaderMapUtils for HeaderMap {
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_last_modified(&mut self, date: DateTime<Utc>) {
|
||||
let formatted = date.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
|
||||
|
||||
self.insert(header::LAST_MODIFIED, formatted.parse().unwrap());
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
fn insert_content_type(&mut self, path: &Path) {
|
||||
match path.extension().unwrap().to_str().unwrap() {
|
||||
@@ -60,24 +99,15 @@ impl HeaderMapUtils for HeaderMap {
|
||||
}
|
||||
|
||||
fn insert_content_type_image_icon(&mut self) {
|
||||
self.insert(
|
||||
header::CONTENT_TYPE,
|
||||
"image/x-icon".parse().unwrap(),
|
||||
);
|
||||
self.insert(header::CONTENT_TYPE, "image/x-icon".parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_content_type_image_jpeg(&mut self) {
|
||||
self.insert(
|
||||
header::CONTENT_TYPE,
|
||||
"image/jpeg".parse().unwrap(),
|
||||
);
|
||||
self.insert(header::CONTENT_TYPE, "image/jpeg".parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_content_type_image_png(&mut self) {
|
||||
self.insert(
|
||||
header::CONTENT_TYPE,
|
||||
"image/png".parse().unwrap(),
|
||||
);
|
||||
self.insert(header::CONTENT_TYPE, "image/png".parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_content_type_application_javascript(&mut self) {
|
||||
@@ -92,7 +122,10 @@ impl HeaderMapUtils for HeaderMap {
|
||||
}
|
||||
|
||||
fn insert_content_type_application_manifest_json(&mut self) {
|
||||
self.insert(header::CONTENT_TYPE, "application/manifest+json".parse().unwrap());
|
||||
self.insert(
|
||||
header::CONTENT_TYPE,
|
||||
"application/manifest+json".parse().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_content_type_text_css(&mut self) {
|
||||
|
||||
@@ -10,7 +10,6 @@ use website::WebsiteRoutes;
|
||||
|
||||
mod api;
|
||||
mod header_map;
|
||||
mod response;
|
||||
mod website;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
|
||||
@@ -9,6 +9,7 @@ use axum::{
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use parser::log;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
@@ -38,17 +39,22 @@ pub async fn file_handler(headers: HeaderMap, path: extract::Path<String>) -> Re
|
||||
}
|
||||
}
|
||||
|
||||
path_to_response(&path)
|
||||
path_to_response(headers, &path)
|
||||
}
|
||||
|
||||
pub async fn index_handler(headers: HeaderMap) -> Response {
|
||||
path_to_response(&str_to_path("index.html"))
|
||||
path_to_response(headers, &str_to_path("index.html"))
|
||||
}
|
||||
|
||||
fn path_to_response(path: &Path) -> Response {
|
||||
fn path_to_response(headers: HeaderMap, path: &Path) -> Response {
|
||||
let mut response;
|
||||
|
||||
if path.extension().unwrap() == "js" {
|
||||
let time = path.metadata().unwrap().modified().unwrap();
|
||||
let date: DateTime<Utc> = time.into();
|
||||
|
||||
let is_localhost = headers.check_if_host_is_localhost();
|
||||
|
||||
if !is_localhost && path.extension().unwrap() == "js" {
|
||||
let content = minify_js(path);
|
||||
|
||||
response = Response::new(content.into());
|
||||
@@ -67,6 +73,12 @@ fn path_to_response(path: &Path) -> Response {
|
||||
headers.insert_cors();
|
||||
headers.insert_content_type(path);
|
||||
|
||||
if !is_localhost {
|
||||
headers.insert_cache_control(10, 50);
|
||||
}
|
||||
|
||||
headers.insert_last_modified(date);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
pwa-asset-generator "../assets/logo-dove-orange.svg" "./assets" \
|
||||
--index "./assets/index.html" \
|
||||
--manifest "./manifest.webmanifest" \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@@ -11,22 +11,14 @@
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#fffaf6"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#110f0e"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Styles -->
|
||||
|
||||
<style>
|
||||
/* Tailwind base */
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
@@ -271,7 +263,6 @@
|
||||
--line-height-xl: 1.75rem; /* 28px */
|
||||
|
||||
--font-weight-base: 450;
|
||||
--font-weight-medium: 575;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--transform-scale-active: scaleX(0.95) scaleY(0.9);
|
||||
@@ -365,11 +356,14 @@
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
|
||||
html[data-display="standalone"] & {
|
||||
border-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
background-color: var(--background-color);
|
||||
|
||||
@@ -444,7 +438,6 @@
|
||||
h4 {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -517,7 +510,6 @@
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--off-color);
|
||||
position: relative;
|
||||
|
||||
@@ -539,9 +531,7 @@
|
||||
}
|
||||
|
||||
> span.emoji {
|
||||
line-height: 0.9;
|
||||
filter: grayscale(100%) brightness(60%) contrast(150%);
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
|
||||
> svg.favorite {
|
||||
@@ -582,7 +572,7 @@
|
||||
color: var(--off-color);
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: var(--line-height-2xs);
|
||||
overflow: visible;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
> *:not(input[type="radio"]):not(svg) {
|
||||
@@ -645,10 +635,14 @@
|
||||
order: 999;
|
||||
overflow-y: auto;
|
||||
|
||||
html[data-display="standalone"] & {
|
||||
padding-bottom: 1.5rem /* 24px */;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
border-width: 0px 1px 0px 0px;
|
||||
order: 0;
|
||||
padding: 1rem 0.75rem 1.375rem 0.75rem;
|
||||
padding: 1rem 0.75rem 1.375rem 0.75rem !important;
|
||||
}
|
||||
|
||||
&,
|
||||
@@ -767,68 +761,101 @@
|
||||
.tree {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
|
||||
summary {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
ul {
|
||||
overflow: hidden;
|
||||
|
||||
&::marker,
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-highlight] {
|
||||
* {
|
||||
color: var(--orange) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* &:hover * {
|
||||
color: var(--orange) !important;
|
||||
} */
|
||||
|
||||
> span.marker {
|
||||
color: var(--border-color);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
z-index: 10;
|
||||
margin-left: -5px;
|
||||
margin-bottom: 0.0625rem;
|
||||
}
|
||||
|
||||
> small {
|
||||
margin-top: 0.125rem;
|
||||
margin-right: 2.5px;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
border-left: 1px;
|
||||
|
||||
&:not(:has(~ li:not([hidden]))) {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
li {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -1px;
|
||||
width: 9px;
|
||||
height: 1.75rem;
|
||||
border-color: var(--border-color);
|
||||
border-width: 0 0 1px 1px;
|
||||
border-radius: 0px 0px 0px 4px;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
border-left: 1px;
|
||||
|
||||
&:has(input:checked) {
|
||||
&::before {
|
||||
border-color: var(--orange) !important;
|
||||
}
|
||||
|
||||
> details > summary > span.marker {
|
||||
color: var(--orange) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:has(~ li:not([hidden]))) {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -1px;
|
||||
width: 9px;
|
||||
height: 1.75rem;
|
||||
border-color: var(--border-color);
|
||||
border-width: 0 0 1px 1px;
|
||||
border-radius: 0px 0px 0px 4px;
|
||||
}
|
||||
|
||||
&:has(input:checked) {
|
||||
> details > summary::after {
|
||||
border-color: var(--orange) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(~ li input:checked) {
|
||||
border-color: var(--orange) !important;
|
||||
|
||||
&::before {
|
||||
z-index: -10;
|
||||
}
|
||||
}
|
||||
|
||||
> details > summary {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&::marker,
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
details[open] > & {
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 12px;
|
||||
border-color: var(--border-color);
|
||||
border-width: 0 0 0px 1px;
|
||||
}
|
||||
}
|
||||
|
||||
> span.marker {
|
||||
color: var(--border-color);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
z-index: 10;
|
||||
margin-left: -5px;
|
||||
margin-bottom: 0.0625rem;
|
||||
}
|
||||
|
||||
> small {
|
||||
margin-top: 0.125rem;
|
||||
margin-right: 2.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,7 +903,6 @@
|
||||
}
|
||||
|
||||
.sats {
|
||||
font-weight: 500;
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
@@ -916,8 +942,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin: 0 -1.5rem;
|
||||
padding: 0 1.5rem;
|
||||
margin: -0.75rem -1.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
overflow-x: auto;
|
||||
|
||||
> div {
|
||||
@@ -976,12 +1002,6 @@
|
||||
min-height: 0;
|
||||
z-index: 20;
|
||||
|
||||
> .shadow-bottom {
|
||||
bottom: 1.75rem;
|
||||
width: 80px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
> .chart-wrapper {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
@@ -1103,8 +1123,14 @@
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
#chart-list > & {
|
||||
bottom: 1.75rem;
|
||||
width: 80px;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
.shadow-left:not(:has(~ #selected-frame[hidden])) {
|
||||
.shadow-left {
|
||||
position: absolute;
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
@@ -1118,7 +1144,7 @@
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
}
|
||||
.shadow-right:not(:has(~ #selected-frame[hidden])) {
|
||||
.shadow-right {
|
||||
position: absolute;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
@@ -1131,6 +1157,10 @@
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
&:not(:has(~ #selected-frame:not([hidden]))) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -1144,42 +1174,38 @@
|
||||
|
||||
<script>
|
||||
// @ts-check
|
||||
// Keep in sync with js files
|
||||
|
||||
/** @typedef {'system' | 'dark' | 'light'} SettingsTheme */
|
||||
|
||||
/**
|
||||
* @import { SettingsTheme } from "./types/self"
|
||||
*/
|
||||
const settingsThemeLocalStorageKey = "settings-theme";
|
||||
|
||||
let theme = /** @type {SettingsTheme} */ (
|
||||
localStorage.getItem(settingsThemeLocalStorageKey) || "system"
|
||||
const theme = /** @type {SettingsTheme} */ (
|
||||
localStorage.getItem(settingsThemeLocalStorageKey)
|
||||
);
|
||||
if (theme !== "dark" && theme !== "light" && theme !== "system") {
|
||||
theme = "system";
|
||||
}
|
||||
|
||||
const preferredColorSchemeMatchMedia = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
"(prefers-color-scheme: dark)"
|
||||
);
|
||||
|
||||
function updateDataTheme() {
|
||||
localStorage.setItem(settingsThemeLocalStorageKey, theme);
|
||||
if (
|
||||
theme === "dark" ||
|
||||
((!theme || theme === "system") &&
|
||||
preferredColorSchemeMatchMedia.matches)
|
||||
) {
|
||||
window.document.documentElement.dataset.theme = "dark";
|
||||
} else {
|
||||
window.document.documentElement.dataset.theme = undefined;
|
||||
}
|
||||
if (
|
||||
theme === "dark" ||
|
||||
(theme !== "light" && preferredColorSchemeMatchMedia.matches)
|
||||
) {
|
||||
window.document.documentElement.dataset.theme = "dark";
|
||||
} else {
|
||||
delete window.document.documentElement.dataset.theme;
|
||||
}
|
||||
|
||||
updateDataTheme();
|
||||
const backgroundColor = getComputedStyle(
|
||||
window.document.documentElement
|
||||
).getPropertyValue("--background-color");
|
||||
const meta = window.document.createElement("meta");
|
||||
meta.name = "theme-color";
|
||||
meta.content = backgroundColor;
|
||||
window.document.getElementsByTagName("head")[0].appendChild(meta);
|
||||
|
||||
preferredColorSchemeMatchMedia.addEventListener("change", () => {
|
||||
if (theme === "system") {
|
||||
updateDataTheme();
|
||||
}
|
||||
});
|
||||
if ("standalone" in window.navigator && !!window.navigator.standalone) {
|
||||
window.document.documentElement.dataset.display = "standalone";
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Icons -->
|
||||
@@ -1583,7 +1609,7 @@
|
||||
for="folders-filter-all"
|
||||
title="Chart"
|
||||
>
|
||||
<span class="absolute" id="folders-filter-all-count">0</span>
|
||||
<span class="absolute" id="folders-filter-all-count"></span>
|
||||
<input
|
||||
type="radio"
|
||||
name="folders-filter"
|
||||
@@ -1597,9 +1623,10 @@
|
||||
for="folders-filter-favorites"
|
||||
title="Chart"
|
||||
>
|
||||
<span class="absolute" id="folders-filter-favorites-count"
|
||||
>0</span
|
||||
>
|
||||
<span
|
||||
class="absolute"
|
||||
id="folders-filter-favorites-count"
|
||||
></span>
|
||||
<input
|
||||
type="radio"
|
||||
name="folders-filter"
|
||||
@@ -1613,7 +1640,7 @@
|
||||
for="folders-filter-new"
|
||||
title="Chart"
|
||||
>
|
||||
<span class="absolute" id="folders-filter-new-count">0</span>
|
||||
<span class="absolute" id="folders-filter-new-count"></span>
|
||||
<input
|
||||
type="radio"
|
||||
name="folders-filter"
|
||||
@@ -1682,7 +1709,7 @@
|
||||
<hr />
|
||||
<h4>General</h4>
|
||||
<fieldset>
|
||||
<div class="field">
|
||||
<div class="field" id="settings-theme-field">
|
||||
<legend>Theme</legend>
|
||||
<hr />
|
||||
<div>
|
||||
@@ -1695,7 +1722,6 @@
|
||||
name="settings-theme"
|
||||
id="settings-theme-system-input"
|
||||
value="system"
|
||||
onchange="theme = 'system'; updateDataTheme();"
|
||||
/>
|
||||
System
|
||||
</label>
|
||||
@@ -1708,7 +1734,6 @@
|
||||
name="settings-theme"
|
||||
id="settings-theme-dark-input"
|
||||
value="dark"
|
||||
onchange="theme = 'dark'; updateDataTheme();"
|
||||
/>
|
||||
Dark
|
||||
</label>
|
||||
@@ -1721,33 +1746,10 @@
|
||||
name="settings-theme"
|
||||
id="settings-theme-light-input"
|
||||
value="light"
|
||||
onchange="theme = 'light'; updateDataTheme();"
|
||||
/>
|
||||
Light
|
||||
</label>
|
||||
</div>
|
||||
<script>
|
||||
switch (/** @type {SettingsTheme} */ (theme)) {
|
||||
case "light": {
|
||||
window.document
|
||||
.getElementById("settings-theme-light-input")
|
||||
?.click();
|
||||
break;
|
||||
}
|
||||
case "dark": {
|
||||
window.document
|
||||
.getElementById("settings-theme-dark-input")
|
||||
?.click();
|
||||
break;
|
||||
}
|
||||
case "system": {
|
||||
window.document
|
||||
.getElementById("settings-theme-system-input")
|
||||
?.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<small>Options for the application's color scheme</small>
|
||||
</fieldset>
|
||||
@@ -1765,194 +1767,7 @@
|
||||
<ol id="leaderboard"></ol>
|
||||
<small>And everybody else !</small>
|
||||
|
||||
<script>
|
||||
const leaderboard = window.document.getElementById("leaderboard");
|
||||
if (!leaderboard) throw "Leaderboard should exist by now";
|
||||
|
||||
const donations = [
|
||||
{
|
||||
name: "_Checkɱate",
|
||||
// url: "https://xcancel.com/_Checkmatey_",
|
||||
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://xcancel.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,
|
||||
},
|
||||
{
|
||||
name: "lassdas",
|
||||
url: "https://primal.net/p/npub1gmhctt2hmjqz8ay2x8h5f8fl3h4fpfcezwqneal3usu3u65qca4s8094ea",
|
||||
amount: 210_000,
|
||||
},
|
||||
{
|
||||
name: "anon",
|
||||
amount: 21_000,
|
||||
},
|
||||
{
|
||||
name: "xplbzx",
|
||||
url: "https://primal.net/p/npub1e0f808a350rxrhppu4zylzljt3arfpvrrpqdg6ft78xy6u49kq5slf0g92",
|
||||
amount: 12_110,
|
||||
},
|
||||
{
|
||||
name: "SoundMoney=Prosperity4ALL",
|
||||
url: "https://xcancel.com/SoundmoneyP",
|
||||
amount: 420_000,
|
||||
},
|
||||
{
|
||||
name: "Johan",
|
||||
url: "https://primal.net/p/npub1a4sd4cprrucfkvkfq9zs99ur4xe7lxw3uhhgvuzx6nqxhnpa2yyqlsa26u",
|
||||
amount: 500_000,
|
||||
},
|
||||
{
|
||||
name: "highperfocused",
|
||||
url: "https://primal.net/p/npub1fq8vrf63vsrqjrwqgtwlvauqauc0yme6se8g8dqhcpf6tfs3equqntmzut",
|
||||
amount: 4620,
|
||||
},
|
||||
{
|
||||
name: "ClearMined",
|
||||
url: "https://primal.net/p/npub1dj8zwktp3eyktfhs5mjlw8v0v2838xlquxr7ddsanayhcw98fcks8ddrq9",
|
||||
amount: 300_000,
|
||||
},
|
||||
];
|
||||
|
||||
donations.sort((a, b) =>
|
||||
b.amount !== a.amount
|
||||
? b.amount - a.amount
|
||||
: a.name.localeCompare(b.name),
|
||||
);
|
||||
|
||||
donations.slice(0, 21).forEach(({ name, url, amount }) => {
|
||||
const li = window.document.createElement("li");
|
||||
leaderboard.append(li);
|
||||
|
||||
const a = window.document.createElement("a");
|
||||
a.href = url || "";
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.innerHTML = name;
|
||||
li.append(a);
|
||||
|
||||
li.append(" — ");
|
||||
|
||||
const small = window.document.createElement("small");
|
||||
small.classList.add("sats");
|
||||
small.innerHTML = `${amount.toLocaleString("en-us")} sats`;
|
||||
li.append(small);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const standalone =
|
||||
"standalone" in window.navigator && !!window.navigator.standalone;
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const isChrome = userAgent.includes("chrome");
|
||||
const safari = userAgent.includes("safari");
|
||||
const safariOnly = safari && !isChrome;
|
||||
const macOS = userAgent.includes("mac os");
|
||||
const iphone = userAgent.includes("iphone");
|
||||
const ipad = userAgent.includes("ipad");
|
||||
|
||||
if (!standalone && safariOnly && (macOS || ipad || iphone)) {
|
||||
const frame = window.document.getElementById("settings-frame");
|
||||
if (!frame) throw "Settings frame should exist by now";
|
||||
|
||||
const hr = window.document.createElement("hr");
|
||||
frame.append(hr);
|
||||
|
||||
const heading = window.document.createElement("h4");
|
||||
heading.innerHTML = "Install";
|
||||
frame.append(heading);
|
||||
|
||||
const p = window.document.createElement("p");
|
||||
frame.append(p);
|
||||
if (macOS) {
|
||||
p.innerHTML = `This app can be installed by clicking on the <strong>File</strong> tab on the menu bar and then on <strong>Add to dock</strong>.`;
|
||||
} else {
|
||||
p.innerHTML = `This app can be installed by tapping on the <strong>Share</strong> button tab of Safari and then on <strong>Add to Home Screen</strong>.`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<p id="settings-install-instructions" hidden></p>
|
||||
|
||||
<hr class="md:hidden" />
|
||||
|
||||
@@ -1965,43 +1780,10 @@
|
||||
"
|
||||
class="md:hidden"
|
||||
></p>
|
||||
<script>
|
||||
const anchorApi = /** @type {HTMLAnchorElement | undefined} */ (
|
||||
window.document.getElementById("anchor-api")?.cloneNode(true)
|
||||
);
|
||||
|
||||
const anchorGit = /** @type {HTMLAnchorElement | undefined} */ (
|
||||
window.document.getElementById("anchor-git")?.cloneNode(true)
|
||||
);
|
||||
|
||||
const anchorNostr = /** @type {HTMLAnchorElement | undefined} */ (
|
||||
window.document.getElementById("anchor-nostr")?.cloneNode(true)
|
||||
);
|
||||
|
||||
const anchorGeyser = /** @type {HTMLAnchorElement | undefined} */ (
|
||||
window.document.getElementById("anchor-geyser")?.cloneNode(true)
|
||||
);
|
||||
|
||||
if (!anchorApi || !anchorGit || !anchorNostr || !anchorGeyser)
|
||||
throw "Anchors should exist by now";
|
||||
|
||||
anchorApi.id = "";
|
||||
anchorGit.id = "";
|
||||
anchorNostr.id = "";
|
||||
anchorGeyser.id = "";
|
||||
|
||||
const nav = window.document.getElementById("settings-nav");
|
||||
if (!nav) throw "Should exist by now";
|
||||
|
||||
nav.append(anchorApi);
|
||||
nav.append(anchorGit);
|
||||
nav.append(anchorNostr);
|
||||
nav.append(anchorGeyser);
|
||||
</script>
|
||||
|
||||
<hr />
|
||||
|
||||
<p style="color: var(--off-color); padding: 1rem; text-align: center">
|
||||
<p style="color: var(--off-color); text-align: center">
|
||||
<span>Charts are displayed via </span>
|
||||
<a
|
||||
href="https://www.tradingview.com"
|
||||
@@ -2067,7 +1849,7 @@
|
||||
style="
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-weight: var(--font-weight-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: var(--line-height-2xs);
|
||||
"
|
||||
|
||||
4986
website/script.js
4986
website/script.js
File diff suppressed because it is too large
Load Diff
12
website/types/paths.d.ts
vendored
12
website/types/paths.d.ts
vendored
File diff suppressed because one or more lines are too long
10
website/types/self.d.ts
vendored
10
website/types/self.d.ts
vendored
@@ -16,6 +16,9 @@ import {
|
||||
ISeriesApi,
|
||||
} from "../libraries/lightweight-charts/types";
|
||||
import { DatePath, HeightPath } from "./paths";
|
||||
import { Owner } from "../libraries/solid-signals/types/owner";
|
||||
|
||||
type SettingsTheme = "system" | "dark" | "light";
|
||||
|
||||
type Signal<T> = Accessor<T> & { set: Setter<T> };
|
||||
|
||||
@@ -52,6 +55,7 @@ type AnySpecificSeriesBlueprint =
|
||||
|
||||
type SpecificSeriesBlueprintWithChart<A extends AnySpecificSeriesBlueprint> = {
|
||||
chart: IChartApi;
|
||||
owner: Owner | null;
|
||||
} & Omit<A, "type">;
|
||||
|
||||
type SeriesBlueprint = {
|
||||
@@ -129,7 +133,7 @@ interface OHLC {
|
||||
|
||||
interface ResourceDataset<
|
||||
S extends Scale,
|
||||
Type extends OHLC | number = number,
|
||||
Type extends OHLC | number = number
|
||||
> {
|
||||
scale: S;
|
||||
url: string;
|
||||
@@ -147,7 +151,7 @@ interface FetchedResult<
|
||||
SingleValueData | ValuedCandlestickData
|
||||
> = DatasetValue<
|
||||
Type extends number ? SingleValueData : ValuedCandlestickData
|
||||
>,
|
||||
>
|
||||
> {
|
||||
at: Date | null;
|
||||
json: Signal<FetchedJSON<S, Type> | null>;
|
||||
@@ -177,7 +181,7 @@ interface FetchedChunk {
|
||||
|
||||
type FetchedDataset<
|
||||
S extends Scale,
|
||||
Type extends number | OHLC,
|
||||
Type extends number | OHLC
|
||||
> = S extends "date" ? FetchedDateDataset<Type> : FetchedHeightDataset<Type>;
|
||||
|
||||
interface Versioned {
|
||||
|
||||
Reference in New Issue
Block a user