mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-28 03:08:10 -07:00
global: snapshot
This commit is contained in:
@@ -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
|
## 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.
|
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.
|
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.**
|
**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
|
## 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
|
## Instances
|
||||||
|
|
||||||
Web App:
|
- [kibo.money](https://kibo.money)
|
||||||
|
- [backup.kibo.money](https://backup.kibo.money)
|
||||||
- [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
|
## Structure
|
||||||
|
|
||||||
- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain.
|
- `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.
|
- `website`: A web app which displays the generated datasets in various charts and dashboards.
|
||||||
- `app`: 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
|
### 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
|
```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
|
```bash
|
||||||
# Install rustup
|
|
||||||
# Update ./run.sh if needed
|
|
||||||
./run.sh
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Then the easiest to let others access your server is with `cloudflared` which will also cache requests.
|
Then the easiest to let others access your server is with `cloudflared` which will also cache requests.
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
## 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:
|
|
||||||
|
|
||||||
- **Best**: Replace Glassnode as the go to
|
- **Best**: Replace Glassnode as the go to
|
||||||
- **Diverse**: Have as many charts/datasets as possible and something for everyone
|
- **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
|
- **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
|
- **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
|
- **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
|
- **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
|
- **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
|
- **Datasets by block timestamp**: In addition to having datasets by date and height
|
||||||
- **Descriptions**: Add text to describe all charts and what they mean
|
- **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
|
- **API Documentation**: Highly needed to explain what's what
|
||||||
|
|
||||||
_Maybe_:
|
## Iterations
|
||||||
|
|
||||||
- A Desktop app
|
A list of all the previous versions and ideas:
|
||||||
- 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
|
||||||
- https://github.com/drgarlic/satonomics-parser
|
- 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
|
|
||||||
Generated
+1
-1
@@ -2482,6 +2482,7 @@ version = "0.4.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"derive_deref",
|
"derive_deref",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -2492,7 +2493,6 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"swc",
|
"swc",
|
||||||
"swc_common",
|
"swc_common",
|
||||||
"swc_ecma_minifier",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
]
|
]
|
||||||
|
|||||||
+8
-8
@@ -5,17 +5,17 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7.5"
|
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" }
|
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"] }
|
reqwest = { version = "0.12.7", features = ["json"] }
|
||||||
serde = { version = "1.0.210", features = ["derive"] }
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.128" }
|
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 = "0.286.0"
|
||||||
swc_common = "0.37.5"
|
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
|
if cargo watch --help &> /dev/null; then
|
||||||
TRIGGER="./in/datasets_len.txt"
|
TRIGGER="./in/datasets_len.txt"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use color_eyre::{eyre::eyre, owo_colors::OwoColorize};
|
use color_eyre::{eyre::eyre, owo_colors::OwoColorize};
|
||||||
use reqwest::{header::HOST, StatusCode};
|
use reqwest::StatusCode;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use parser::{log, Date, DateMap, Height, HeightMap, MapChunkId, HEIGHT_MAP_CHUNK_SIZE, OHLC};
|
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::{
|
use crate::{
|
||||||
api::structs::{Chunk, Kind, Route},
|
api::structs::{Chunk, Kind, Route},
|
||||||
header_map::HeaderMapUtils,
|
header_map::HeaderMapUtils,
|
||||||
response::typed_value_to_response,
|
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::response::typed_value_to_response;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
chunk: Option<usize>,
|
chunk: Option<usize>,
|
||||||
@@ -168,13 +169,8 @@ where
|
|||||||
|
|
||||||
let offsetted_to_url = |offseted| {
|
let offsetted_to_url = |offseted| {
|
||||||
datasets.get(&ChunkId::from_usize(offseted)).map(|_| {
|
datasets.get(&ChunkId::from_usize(offseted)).map(|_| {
|
||||||
let host = headers[HOST].to_str().unwrap();
|
let scheme = headers.get_scheme();
|
||||||
let scheme = if host.contains("0.0.0.0") || host.contains("localhost") {
|
let host = headers.get_host();
|
||||||
"http"
|
|
||||||
} else {
|
|
||||||
"https"
|
|
||||||
};
|
|
||||||
|
|
||||||
format!("{scheme}://{host}{}?chunk={offseted}", route.url_path)
|
format!("{scheme}://{host}{}?chunk={offseted}", route.url_path)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use axum::{extract::State, http::HeaderMap, response::Response};
|
use axum::{extract::State, http::HeaderMap, response::Response};
|
||||||
use reqwest::header::HOST;
|
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 {
|
pub async fn fallback(headers: HeaderMap, State(app_state): State<AppState>) -> Response {
|
||||||
generic_to_reponse(
|
generic_to_reponse(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod dataset;
|
mod dataset;
|
||||||
mod fallback;
|
mod fallback;
|
||||||
|
mod response;
|
||||||
|
|
||||||
pub use dataset::*;
|
pub use dataset::*;
|
||||||
pub use fallback::*;
|
pub use fallback::*;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ impl Routes {
|
|||||||
.map(|route| format!("\"{}\"", route.url_path))
|
.map(|route| format!("\"{}\"", route.url_path))
|
||||||
.join(" | ");
|
.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);
|
let date_type = map_to_type("Date", &self.date);
|
||||||
@@ -112,7 +112,7 @@ impl Routes {
|
|||||||
|
|
||||||
fs::write(
|
fs::write(
|
||||||
format!("{WEBSITE_TYPES_PATH}/paths.d.ts"),
|
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();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-14
@@ -1,14 +1,23 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use axum::http::{header, HeaderMap};
|
use axum::http::{header, HeaderMap};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use parser::log;
|
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 {
|
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_cors(&mut self);
|
||||||
|
|
||||||
fn insert_cache_control(&mut self, max_age: u64, stale_while_revalidate: u64);
|
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(&mut self, path: &Path);
|
||||||
fn insert_content_type_image_icon(&mut self);
|
fn insert_content_type_image_icon(&mut self);
|
||||||
@@ -24,6 +33,30 @@ pub trait HeaderMapUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HeaderMapUtils for HeaderMap {
|
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) {
|
fn insert_cors(&mut self) {
|
||||||
self.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
|
self.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
|
||||||
self.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "*".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
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||||
fn insert_content_type(&mut self, path: &Path) {
|
fn insert_content_type(&mut self, path: &Path) {
|
||||||
match path.extension().unwrap().to_str().unwrap() {
|
match path.extension().unwrap().to_str().unwrap() {
|
||||||
@@ -60,24 +99,15 @@ impl HeaderMapUtils for HeaderMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn insert_content_type_image_icon(&mut self) {
|
fn insert_content_type_image_icon(&mut self) {
|
||||||
self.insert(
|
self.insert(header::CONTENT_TYPE, "image/x-icon".parse().unwrap());
|
||||||
header::CONTENT_TYPE,
|
|
||||||
"image/x-icon".parse().unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_content_type_image_jpeg(&mut self) {
|
fn insert_content_type_image_jpeg(&mut self) {
|
||||||
self.insert(
|
self.insert(header::CONTENT_TYPE, "image/jpeg".parse().unwrap());
|
||||||
header::CONTENT_TYPE,
|
|
||||||
"image/jpeg".parse().unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_content_type_image_png(&mut self) {
|
fn insert_content_type_image_png(&mut self) {
|
||||||
self.insert(
|
self.insert(header::CONTENT_TYPE, "image/png".parse().unwrap());
|
||||||
header::CONTENT_TYPE,
|
|
||||||
"image/png".parse().unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_content_type_application_javascript(&mut self) {
|
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) {
|
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) {
|
fn insert_content_type_text_css(&mut self) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use website::WebsiteRoutes;
|
|||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod header_map;
|
mod header_map;
|
||||||
mod response;
|
|
||||||
mod website;
|
mod website;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use axum::{
|
|||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use parser::log;
|
use parser::log;
|
||||||
use reqwest::StatusCode;
|
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 {
|
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;
|
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);
|
let content = minify_js(path);
|
||||||
|
|
||||||
response = Response::new(content.into());
|
response = Response::new(content.into());
|
||||||
@@ -67,6 +73,12 @@ fn path_to_response(path: &Path) -> Response {
|
|||||||
headers.insert_cors();
|
headers.insert_cors();
|
||||||
headers.insert_content_type(path);
|
headers.insert_content_type(path);
|
||||||
|
|
||||||
|
if !is_localhost {
|
||||||
|
headers.insert_cache_control(10, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.insert_last_modified(date);
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
pwa-asset-generator "../assets/logo-dove-orange.svg" "./assets" \
|
pwa-asset-generator "../assets/logo-dove-orange.svg" "./assets" \
|
||||||
--index "./assets/index.html" \
|
--index "./assets/index.html" \
|
||||||
--manifest "./manifest.webmanifest" \
|
--manifest "./manifest.webmanifest" \
|
||||||
|
|||||||
+151
-369
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -11,22 +11,14 @@
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
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" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Tailwind base */
|
||||||
|
|
||||||
*,
|
*,
|
||||||
::after,
|
::after,
|
||||||
::before,
|
::before,
|
||||||
@@ -271,7 +263,6 @@
|
|||||||
--line-height-xl: 1.75rem; /* 28px */
|
--line-height-xl: 1.75rem; /* 28px */
|
||||||
|
|
||||||
--font-weight-base: 450;
|
--font-weight-base: 450;
|
||||||
--font-weight-medium: 575;
|
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
|
|
||||||
--transform-scale-active: scaleX(0.95) scaleY(0.9);
|
--transform-scale-active: scaleX(0.95) scaleY(0.9);
|
||||||
@@ -365,11 +356,14 @@
|
|||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
html[data-display="standalone"] & {
|
||||||
|
border-top: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
|
|
||||||
@@ -444,7 +438,6 @@
|
|||||||
h4 {
|
h4 {
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
line-height: var(--line-height-base);
|
line-height: var(--line-height-base);
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
@@ -517,7 +510,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--off-color);
|
color: var(--off-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@@ -539,9 +531,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> span.emoji {
|
> span.emoji {
|
||||||
line-height: 0.9;
|
|
||||||
filter: grayscale(100%) brightness(60%) contrast(150%);
|
filter: grayscale(100%) brightness(60%) contrast(150%);
|
||||||
font-size: 1.0625rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> svg.favorite {
|
> svg.favorite {
|
||||||
@@ -582,7 +572,7 @@
|
|||||||
color: var(--off-color);
|
color: var(--off-color);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
line-height: var(--line-height-2xs);
|
line-height: var(--line-height-2xs);
|
||||||
overflow: visible;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:not(input[type="radio"]):not(svg) {
|
> *:not(input[type="radio"]):not(svg) {
|
||||||
@@ -645,10 +635,14 @@
|
|||||||
order: 999;
|
order: 999;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
html[data-display="standalone"] & {
|
||||||
|
padding-bottom: 1.5rem /* 24px */;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
border-width: 0px 1px 0px 0px;
|
border-width: 0px 1px 0px 0px;
|
||||||
order: 0;
|
order: 0;
|
||||||
padding: 1rem 0.75rem 1.375rem 0.75rem;
|
padding: 1rem 0.75rem 1.375rem 0.75rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&,
|
&,
|
||||||
@@ -767,68 +761,101 @@
|
|||||||
.tree {
|
.tree {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
line-height: var(--line-height-sm);
|
line-height: var(--line-height-sm);
|
||||||
|
|
||||||
summary {
|
ul {
|
||||||
list-style: none;
|
overflow: hidden;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::marker,
|
li {
|
||||||
&::-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: "";
|
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: -12px;
|
padding-left: 12px;
|
||||||
left: -1px;
|
border-left: 1px;
|
||||||
width: 9px;
|
|
||||||
height: 1.75rem;
|
&:has(input:checked) {
|
||||||
border-color: var(--border-color);
|
&::before {
|
||||||
border-width: 0 0 1px 1px;
|
border-color: var(--orange) !important;
|
||||||
border-radius: 0px 0px 0px 4px;
|
}
|
||||||
|
|
||||||
|
> 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 {
|
.sats {
|
||||||
font-weight: 500;
|
|
||||||
color: var(--orange);
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -916,8 +942,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
margin: 0 -1.5rem;
|
margin: -0.75rem -1.5rem;
|
||||||
padding: 0 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
@@ -976,12 +1002,6 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
> .shadow-bottom {
|
|
||||||
bottom: 1.75rem;
|
|
||||||
width: 80px;
|
|
||||||
left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .chart-wrapper {
|
> .chart-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1103,8 +1123,14 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
#chart-list > & {
|
||||||
|
bottom: 1.75rem;
|
||||||
|
width: 80px;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.shadow-left:not(:has(~ #selected-frame[hidden])) {
|
.shadow-left {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to left,
|
to left,
|
||||||
@@ -1118,7 +1144,7 @@
|
|||||||
z-index: 30;
|
z-index: 30;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.shadow-right:not(:has(~ #selected-frame[hidden])) {
|
.shadow-right {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
@@ -1131,6 +1157,10 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
&:not(:has(~ #selected-frame:not([hidden]))) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -1144,42 +1174,38 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
// Keep in sync with js files
|
||||||
|
|
||||||
/** @typedef {'system' | 'dark' | 'light'} SettingsTheme */
|
/**
|
||||||
|
* @import { SettingsTheme } from "./types/self"
|
||||||
|
*/
|
||||||
const settingsThemeLocalStorageKey = "settings-theme";
|
const settingsThemeLocalStorageKey = "settings-theme";
|
||||||
|
const theme = /** @type {SettingsTheme} */ (
|
||||||
let theme = /** @type {SettingsTheme} */ (
|
localStorage.getItem(settingsThemeLocalStorageKey)
|
||||||
localStorage.getItem(settingsThemeLocalStorageKey) || "system"
|
|
||||||
);
|
);
|
||||||
if (theme !== "dark" && theme !== "light" && theme !== "system") {
|
|
||||||
theme = "system";
|
|
||||||
}
|
|
||||||
|
|
||||||
const preferredColorSchemeMatchMedia = window.matchMedia(
|
const preferredColorSchemeMatchMedia = window.matchMedia(
|
||||||
"(prefers-color-scheme: dark)",
|
"(prefers-color-scheme: dark)"
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
function updateDataTheme() {
|
theme === "dark" ||
|
||||||
localStorage.setItem(settingsThemeLocalStorageKey, theme);
|
(theme !== "light" && preferredColorSchemeMatchMedia.matches)
|
||||||
if (
|
) {
|
||||||
theme === "dark" ||
|
window.document.documentElement.dataset.theme = "dark";
|
||||||
((!theme || theme === "system") &&
|
} else {
|
||||||
preferredColorSchemeMatchMedia.matches)
|
delete window.document.documentElement.dataset.theme;
|
||||||
) {
|
|
||||||
window.document.documentElement.dataset.theme = "dark";
|
|
||||||
} else {
|
|
||||||
window.document.documentElement.dataset.theme = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ("standalone" in window.navigator && !!window.navigator.standalone) {
|
||||||
if (theme === "system") {
|
window.document.documentElement.dataset.display = "standalone";
|
||||||
updateDataTheme();
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Icons -->
|
<!-- Icons -->
|
||||||
@@ -1583,7 +1609,7 @@
|
|||||||
for="folders-filter-all"
|
for="folders-filter-all"
|
||||||
title="Chart"
|
title="Chart"
|
||||||
>
|
>
|
||||||
<span class="absolute" id="folders-filter-all-count">0</span>
|
<span class="absolute" id="folders-filter-all-count"></span>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="folders-filter"
|
name="folders-filter"
|
||||||
@@ -1597,9 +1623,10 @@
|
|||||||
for="folders-filter-favorites"
|
for="folders-filter-favorites"
|
||||||
title="Chart"
|
title="Chart"
|
||||||
>
|
>
|
||||||
<span class="absolute" id="folders-filter-favorites-count"
|
<span
|
||||||
>0</span
|
class="absolute"
|
||||||
>
|
id="folders-filter-favorites-count"
|
||||||
|
></span>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="folders-filter"
|
name="folders-filter"
|
||||||
@@ -1613,7 +1640,7 @@
|
|||||||
for="folders-filter-new"
|
for="folders-filter-new"
|
||||||
title="Chart"
|
title="Chart"
|
||||||
>
|
>
|
||||||
<span class="absolute" id="folders-filter-new-count">0</span>
|
<span class="absolute" id="folders-filter-new-count"></span>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="folders-filter"
|
name="folders-filter"
|
||||||
@@ -1682,7 +1709,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
<h4>General</h4>
|
<h4>General</h4>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="field">
|
<div class="field" id="settings-theme-field">
|
||||||
<legend>Theme</legend>
|
<legend>Theme</legend>
|
||||||
<hr />
|
<hr />
|
||||||
<div>
|
<div>
|
||||||
@@ -1695,7 +1722,6 @@
|
|||||||
name="settings-theme"
|
name="settings-theme"
|
||||||
id="settings-theme-system-input"
|
id="settings-theme-system-input"
|
||||||
value="system"
|
value="system"
|
||||||
onchange="theme = 'system'; updateDataTheme();"
|
|
||||||
/>
|
/>
|
||||||
System
|
System
|
||||||
</label>
|
</label>
|
||||||
@@ -1708,7 +1734,6 @@
|
|||||||
name="settings-theme"
|
name="settings-theme"
|
||||||
id="settings-theme-dark-input"
|
id="settings-theme-dark-input"
|
||||||
value="dark"
|
value="dark"
|
||||||
onchange="theme = 'dark'; updateDataTheme();"
|
|
||||||
/>
|
/>
|
||||||
Dark
|
Dark
|
||||||
</label>
|
</label>
|
||||||
@@ -1721,33 +1746,10 @@
|
|||||||
name="settings-theme"
|
name="settings-theme"
|
||||||
id="settings-theme-light-input"
|
id="settings-theme-light-input"
|
||||||
value="light"
|
value="light"
|
||||||
onchange="theme = 'light'; updateDataTheme();"
|
|
||||||
/>
|
/>
|
||||||
Light
|
Light
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<small>Options for the application's color scheme</small>
|
<small>Options for the application's color scheme</small>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -1765,194 +1767,7 @@
|
|||||||
<ol id="leaderboard"></ol>
|
<ol id="leaderboard"></ol>
|
||||||
<small>And everybody else !</small>
|
<small>And everybody else !</small>
|
||||||
|
|
||||||
<script>
|
<p id="settings-install-instructions" hidden></p>
|
||||||
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>
|
|
||||||
|
|
||||||
<hr class="md:hidden" />
|
<hr class="md:hidden" />
|
||||||
|
|
||||||
@@ -1965,43 +1780,10 @@
|
|||||||
"
|
"
|
||||||
class="md:hidden"
|
class="md:hidden"
|
||||||
></p>
|
></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 />
|
<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>
|
<span>Charts are displayed via </span>
|
||||||
<a
|
<a
|
||||||
href="https://www.tradingview.com"
|
href="https://www.tradingview.com"
|
||||||
@@ -2067,7 +1849,7 @@
|
|||||||
style="
|
style="
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 400;
|
font-weight: var(--font-weight-base);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
line-height: var(--line-height-2xs);
|
line-height: var(--line-height-2xs);
|
||||||
"
|
"
|
||||||
|
|||||||
+4839
-147
File diff suppressed because it is too large
Load Diff
Vendored
+3
-9
File diff suppressed because one or more lines are too long
Vendored
+7
-3
@@ -16,6 +16,9 @@ import {
|
|||||||
ISeriesApi,
|
ISeriesApi,
|
||||||
} from "../libraries/lightweight-charts/types";
|
} from "../libraries/lightweight-charts/types";
|
||||||
import { DatePath, HeightPath } from "./paths";
|
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> };
|
type Signal<T> = Accessor<T> & { set: Setter<T> };
|
||||||
|
|
||||||
@@ -52,6 +55,7 @@ type AnySpecificSeriesBlueprint =
|
|||||||
|
|
||||||
type SpecificSeriesBlueprintWithChart<A extends AnySpecificSeriesBlueprint> = {
|
type SpecificSeriesBlueprintWithChart<A extends AnySpecificSeriesBlueprint> = {
|
||||||
chart: IChartApi;
|
chart: IChartApi;
|
||||||
|
owner: Owner | null;
|
||||||
} & Omit<A, "type">;
|
} & Omit<A, "type">;
|
||||||
|
|
||||||
type SeriesBlueprint = {
|
type SeriesBlueprint = {
|
||||||
@@ -129,7 +133,7 @@ interface OHLC {
|
|||||||
|
|
||||||
interface ResourceDataset<
|
interface ResourceDataset<
|
||||||
S extends Scale,
|
S extends Scale,
|
||||||
Type extends OHLC | number = number,
|
Type extends OHLC | number = number
|
||||||
> {
|
> {
|
||||||
scale: S;
|
scale: S;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -147,7 +151,7 @@ interface FetchedResult<
|
|||||||
SingleValueData | ValuedCandlestickData
|
SingleValueData | ValuedCandlestickData
|
||||||
> = DatasetValue<
|
> = DatasetValue<
|
||||||
Type extends number ? SingleValueData : ValuedCandlestickData
|
Type extends number ? SingleValueData : ValuedCandlestickData
|
||||||
>,
|
>
|
||||||
> {
|
> {
|
||||||
at: Date | null;
|
at: Date | null;
|
||||||
json: Signal<FetchedJSON<S, Type> | null>;
|
json: Signal<FetchedJSON<S, Type> | null>;
|
||||||
@@ -177,7 +181,7 @@ interface FetchedChunk {
|
|||||||
|
|
||||||
type FetchedDataset<
|
type FetchedDataset<
|
||||||
S extends Scale,
|
S extends Scale,
|
||||||
Type extends number | OHLC,
|
Type extends number | OHLC
|
||||||
> = S extends "date" ? FetchedDateDataset<Type> : FetchedHeightDataset<Type>;
|
> = S extends "date" ? FetchedDateDataset<Type> : FetchedHeightDataset<Type>;
|
||||||
|
|
||||||
interface Versioned {
|
interface Versioned {
|
||||||
|
|||||||
Reference in New Issue
Block a user