Compare commits
10 Commits
bd8b0fecf1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d609118de | |||
| d11bf28625 | |||
| 9bbe1dd214 | |||
| e3ef695069 | |||
| 5bdfd6835d | |||
| 27716c5ec0 | |||
| dea8c45939 | |||
| 22367f45f2 | |||
| e5853268de | |||
| b37386b9e8 |
844
Cargo.lock
generated
844
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ publish = false
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7", optional = true }
|
||||
axum = { version = "0.8", optional = true }
|
||||
camino = { version = "1.1", optional = true }
|
||||
chrono = { version = "0.4.39", features = ["now", "serde"] }
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
@@ -23,18 +23,17 @@ derive_more = { version = "2", features = [
|
||||
] }
|
||||
http = "1"
|
||||
image = { version = "0.25", optional = true }
|
||||
leptos = { version = "0.7.7", features = ["tracing"] }
|
||||
leptos_axum = { version = "0.7", optional = true }
|
||||
leptos_meta = { version = "0.7" }
|
||||
leptos_router = { version = "0.7.0" }
|
||||
leptos = { version = "0.8", features = ["tracing"] }
|
||||
leptos_axum = { version = "0.8", optional = true }
|
||||
leptos_meta = { version = "0.8" }
|
||||
leptos_router = { version = "0.8" }
|
||||
moonboard-parser = { workspace = true, optional = true }
|
||||
rand = { version = "0.9", default-features = false, features = ["thread_rng"] }
|
||||
ron = { version = "0.8" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
server_fn = { version = "0.7.4", features = ["cbor"] }
|
||||
server_fn = { version = "0.8", features = ["cbor"] }
|
||||
smart-default = "0.7.1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tracing = { version = "0.1" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
@@ -46,7 +45,6 @@ xdg = { version = "2.5", optional = true }
|
||||
uuid = { version = "1.12", features = ["serde", "v4"] }
|
||||
redb = { version = "2.4", optional = true }
|
||||
bincode = { version = "1.3", optional = true }
|
||||
serde_json = { version = "1" }
|
||||
codee = { version = "0.3" }
|
||||
error_reporter = { version = "1" }
|
||||
getrandom = { version = "0.3.1" }
|
||||
@@ -54,9 +52,6 @@ getrandom = { version = "0.3.1" }
|
||||
[dev-dependencies]
|
||||
test-try = "0.1"
|
||||
|
||||
[dev-dependencies.serde_json]
|
||||
version = "1"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "getrandom/wasm_js", "uuid/js"]
|
||||
ssr = [
|
||||
@@ -65,7 +60,6 @@ ssr = [
|
||||
"dep:image",
|
||||
"dep:bincode",
|
||||
"dep:tokio",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:leptos_axum",
|
||||
"dep:confik",
|
||||
@@ -77,6 +71,11 @@ ssr = [
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# getrandom is depended on in order to turn on its wasm feature indirectly
|
||||
ignored = ["getrandom"]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ascend"
|
||||
|
||||
@@ -3,19 +3,22 @@ pub mod ron {
|
||||
|
||||
use codee::Decoder;
|
||||
use codee::Encoder;
|
||||
use leptos::prelude::FromServerFnError;
|
||||
use leptos::prelude::ServerFnErrorErr;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use server_fn::ServerFnError;
|
||||
use server_fn::ContentType;
|
||||
use server_fn::codec::Encoding;
|
||||
use server_fn::codec::FromReq;
|
||||
use server_fn::codec::FromRes;
|
||||
use server_fn::codec::IntoReq;
|
||||
use server_fn::codec::IntoRes;
|
||||
use server_fn::error::IntoAppError;
|
||||
use server_fn::request::ClientReq;
|
||||
use server_fn::request::Req;
|
||||
use server_fn::response::ClientRes;
|
||||
use server_fn::response::Res;
|
||||
use server_fn::response::TryRes;
|
||||
|
||||
pub struct Ron;
|
||||
|
||||
@@ -44,10 +47,13 @@ pub mod ron {
|
||||
}
|
||||
|
||||
impl Encoding for Ron {
|
||||
const CONTENT_TYPE: &'static str = "application/ron";
|
||||
const METHOD: http::Method = http::Method::POST;
|
||||
}
|
||||
|
||||
impl ContentType for Ron {
|
||||
const CONTENT_TYPE: &'static str = "application/ron";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RonEncoded<T>(pub T);
|
||||
|
||||
@@ -74,9 +80,10 @@ pub mod ron {
|
||||
where
|
||||
Request: ClientReq<Err>,
|
||||
T: Serialize,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<Err>> {
|
||||
let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
|
||||
let data = Ron::encode(&self.0).map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into_app_error())?;
|
||||
Request::try_new_post(path, Ron::CONTENT_TYPE, accepts, data)
|
||||
}
|
||||
}
|
||||
@@ -86,21 +93,25 @@ pub mod ron {
|
||||
where
|
||||
Request: Req<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
||||
async fn from_req(req: Request) -> Result<Self, Err> {
|
||||
let data = req.try_into_string().await?;
|
||||
Ron::decode(&data).map(RonEncoded).map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
Ron::decode(&data)
|
||||
.map(RonEncoded)
|
||||
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
|
||||
}
|
||||
}
|
||||
|
||||
// IntoRes
|
||||
impl<CustErr, T, Response> IntoRes<Ron, Response, CustErr> for RonEncoded<T>
|
||||
impl<Err, T, Response> IntoRes<Ron, Response, Err> for RonEncoded<T>
|
||||
where
|
||||
Response: Res<CustErr>,
|
||||
Response: TryRes<Err>,
|
||||
T: Serialize + Send,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
|
||||
let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
async fn into_res(self) -> Result<Response, Err> {
|
||||
let data = Ron::encode(&self.0).map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into_app_error())?;
|
||||
Response::try_from_string(Ron::CONTENT_TYPE, data)
|
||||
}
|
||||
}
|
||||
@@ -110,12 +121,13 @@ pub mod ron {
|
||||
where
|
||||
Response: ClientRes<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
||||
async fn from_res(res: Response) -> Result<Self, Err> {
|
||||
let data = res.try_into_string().await?;
|
||||
Ron::decode(&data)
|
||||
.map(RonEncoded)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
.map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use leptos::prelude::*;
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn Attempt(#[prop(into)] date: Signal<DateTime<Utc>>, #[prop(into)] attempt: Signal<Option<models::Attempt>>) -> impl IntoView {
|
||||
tracing::trace!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let s = time_ago(date.get());
|
||||
|
||||
@@ -19,9 +19,7 @@ pub fn Attempt(#[prop(into)] date: Signal<DateTime<Utc>>, #[prop(into)] attempt:
|
||||
};
|
||||
|
||||
let text_color = match attempt.get() {
|
||||
Some(models::Attempt::Flash) => "text-cyan-500",
|
||||
Some(models::Attempt::Send) => "text-teal-500",
|
||||
Some(models::Attempt::Attempt) => "text-pink-500",
|
||||
Some(attempt) => attempt.gradient().class_text(),
|
||||
None => "",
|
||||
};
|
||||
|
||||
|
||||
@@ -19,34 +19,40 @@ pub fn Button(
|
||||
) -> impl IntoView {
|
||||
let margin = "mx-2 my-1 sm:mx-5 sm:my-2.5";
|
||||
|
||||
let icon_view = icon.get().map(|i| {
|
||||
let icon_view = i.into_view();
|
||||
let mut classes = "self-center".to_string();
|
||||
classes.push(' ');
|
||||
classes.push_str(margin);
|
||||
classes.push(' ');
|
||||
classes.push_str(color.class_text());
|
||||
let icon_view = move || {
|
||||
icon.get().map(|i| {
|
||||
let icon_view = i.into_view();
|
||||
let mut classes = "self-center".to_string();
|
||||
classes.push(' ');
|
||||
classes.push_str(margin);
|
||||
classes.push(' ');
|
||||
classes.push_str(color.class_text());
|
||||
|
||||
view! { <div class=classes>{icon_view}</div> }
|
||||
});
|
||||
view! { <div class=classes>{icon_view}</div> }
|
||||
})
|
||||
};
|
||||
|
||||
let separator = (icon.read().is_some() && text.read().is_some()).then(|| {
|
||||
let mut classes = "w-0.5 bg-linear-to-br min-w-0.5".to_string();
|
||||
classes.push(' ');
|
||||
classes.push_str(color.class_from());
|
||||
classes.push(' ');
|
||||
classes.push_str(color.class_to());
|
||||
let separator = move || {
|
||||
(icon.read().is_some() && text.read().is_some()).then(|| {
|
||||
let mut classes = "w-0.5 bg-linear-to-br min-w-0.5".to_string();
|
||||
classes.push(' ');
|
||||
classes.push_str(color.class_from());
|
||||
classes.push(' ');
|
||||
classes.push_str(color.class_to());
|
||||
|
||||
view! { <div class=classes /> }
|
||||
});
|
||||
view! { <div class=classes /> }
|
||||
})
|
||||
};
|
||||
|
||||
let text_view = text.get().map(|text| {
|
||||
let mut classes = "self-center uppercase w-full text-sm sm:text-base md:text-lg font-thin".to_string();
|
||||
classes.push(' ');
|
||||
classes.push_str(margin);
|
||||
let text_view = move || {
|
||||
text.get().map(|text| {
|
||||
let mut classes = "self-center uppercase w-full text-sm sm:text-base md:text-lg font-thin".to_string();
|
||||
classes.push(' ');
|
||||
classes.push_str(margin);
|
||||
|
||||
view! { <div class=classes>{text}</div> }
|
||||
});
|
||||
view! { <div class=classes>{text}</div> }
|
||||
})
|
||||
};
|
||||
|
||||
let class = move || {
|
||||
let mut classes = vec![];
|
||||
|
||||
@@ -7,6 +7,7 @@ pub enum Icon {
|
||||
WrenchSolid,
|
||||
ForwardSolid,
|
||||
Check,
|
||||
Heart,
|
||||
HeartOutline,
|
||||
ArrowPath,
|
||||
PaperAirplaneSolid,
|
||||
@@ -26,6 +27,7 @@ impl Icon {
|
||||
Icon::WrenchSolid => view! { <WrenchSolid /> }.into_any(),
|
||||
Icon::ForwardSolid => view! { <ForwardSolid /> }.into_any(),
|
||||
Icon::Check => view! { <Check /> }.into_any(),
|
||||
Icon::Heart => view! { <Heart /> }.into_any(),
|
||||
Icon::HeartOutline => view! { <HeartOutline /> }.into_any(),
|
||||
Icon::ArrowPath => view! { <ArrowPath /> }.into_any(),
|
||||
Icon::PaperAirplaneSolid => view! { <PaperAirplaneSolid /> }.into_any(),
|
||||
@@ -123,6 +125,20 @@ pub fn Check() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Heart() -> impl IntoView {
|
||||
view! {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
|
||||
</svg>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn HeartOutline() -> impl IntoView {
|
||||
view! {
|
||||
|
||||
@@ -22,12 +22,14 @@ pub fn OutlinedBox(children: Children, color: Gradient, #[prop(optional)] highli
|
||||
let mut c = "py-1.5 rounded-md".to_string();
|
||||
if highlight() {
|
||||
let bg = match color {
|
||||
Gradient::PinkOrange => "bg-pink-900",
|
||||
Gradient::PinkOrange => "bg-rose-900",
|
||||
Gradient::CyanBlue => "bg-cyan-800",
|
||||
Gradient::TealLime => "bg-teal-700",
|
||||
Gradient::PurplePink => "bg-purple-900",
|
||||
Gradient::TealLime => "bg-emerald-700",
|
||||
Gradient::PurplePink => "bg-fuchsia-950",
|
||||
Gradient::PurpleBlue => "bg-purple-900",
|
||||
Gradient::Orange => "bg-orange-900",
|
||||
Gradient::Pink => "bg-pink-900",
|
||||
Gradient::PinkRed => "bg-fuchsia-950",
|
||||
};
|
||||
|
||||
c.push(' ');
|
||||
|
||||
@@ -7,9 +7,11 @@ pub enum Gradient {
|
||||
PurplePink,
|
||||
#[default]
|
||||
Orange,
|
||||
Pink,
|
||||
PinkRed,
|
||||
}
|
||||
impl Gradient {
|
||||
pub fn class_from(&self) -> &str {
|
||||
pub fn class_from(&self) -> &'static str {
|
||||
match self {
|
||||
Gradient::PinkOrange => "from-pink-500",
|
||||
Gradient::CyanBlue => "from-cyan-500",
|
||||
@@ -17,10 +19,12 @@ impl Gradient {
|
||||
Gradient::PurplePink => "from-purple-500",
|
||||
Gradient::PurpleBlue => "from-purple-600",
|
||||
Gradient::Orange => "from-orange-400",
|
||||
Gradient::Pink => "from-pink-400",
|
||||
Gradient::PinkRed => "from-pink-400",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn class_to(&self) -> &str {
|
||||
pub fn class_to(&self) -> &'static str {
|
||||
match self {
|
||||
Gradient::PinkOrange => "to-orange-400",
|
||||
Gradient::CyanBlue => "to-blue-500",
|
||||
@@ -28,17 +32,21 @@ impl Gradient {
|
||||
Gradient::PurplePink => "to-pink-500",
|
||||
Gradient::PurpleBlue => "to-blue-500",
|
||||
Gradient::Orange => "to-orange-500",
|
||||
Gradient::Pink => "to-pink-500",
|
||||
Gradient::PinkRed => "to-red-500",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn class_text(&self) -> &str {
|
||||
pub fn class_text(&self) -> &'static str {
|
||||
match self {
|
||||
Gradient::PinkOrange => "text-pink-500",
|
||||
Gradient::PinkOrange => "text-rose-400",
|
||||
Gradient::CyanBlue => "text-cyan-500",
|
||||
Gradient::TealLime => "text-teal-300",
|
||||
Gradient::PurplePink => "text-purple-500",
|
||||
Gradient::TealLime => "text-emerald-300",
|
||||
Gradient::PurplePink => "text-fuchsia-500",
|
||||
Gradient::PurpleBlue => "text-purple-600",
|
||||
Gradient::Orange => "text-orange-400",
|
||||
Gradient::Pink => "text-pink-400",
|
||||
Gradient::PinkRed => "text-pink-400",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +42,13 @@ pub mod v4 {
|
||||
pub problems: BTreeSet<Problem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Problem {
|
||||
pub pattern: Pattern,
|
||||
pub method: v2::Method,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Pattern {
|
||||
pub pattern: BTreeMap<v1::HoldPosition, v1::HoldRole>,
|
||||
}
|
||||
@@ -160,7 +160,7 @@ pub mod v2 {
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)]
|
||||
pub struct ProblemUid(pub uuid::Uuid);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display, Copy, Hash)]
|
||||
pub enum Method {
|
||||
#[display("Feet follow hands")]
|
||||
FeetFollowHands,
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
use super::*;
|
||||
use crate::gradient::Gradient;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
impl Problem {
|
||||
/// Returns all possible transformations for the pattern. Not for the method.
|
||||
#[must_use]
|
||||
pub fn transformations(&self, wall_dimensions: WallDimensions) -> HashSet<Self> {
|
||||
self.pattern
|
||||
.transformations(wall_dimensions)
|
||||
.into_iter()
|
||||
.map(|pattern| Self {
|
||||
pattern,
|
||||
method: self.method,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
#[must_use]
|
||||
pub fn canonicalize(&self) -> Self {
|
||||
let mut pattern = self.clone();
|
||||
let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
|
||||
let min_col = pattern.pattern.keys().map(|hold_position| hold_position.col).min().unwrap_or(0);
|
||||
pattern.pattern = pattern
|
||||
.pattern
|
||||
.iter()
|
||||
@@ -25,7 +42,7 @@ impl Pattern {
|
||||
#[must_use]
|
||||
pub fn shift_left(&self, shift: u64) -> Option<Self> {
|
||||
// Out of bounds check
|
||||
if let Some(min_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).min() {
|
||||
if let Some(min_col) = self.pattern.keys().map(|hold_position| hold_position.col).min() {
|
||||
if shift > min_col {
|
||||
return None;
|
||||
}
|
||||
@@ -35,7 +52,7 @@ impl Pattern {
|
||||
.pattern
|
||||
.iter()
|
||||
.map(|(hold_position, hold_role)| {
|
||||
let mut hold_position = hold_position.clone();
|
||||
let mut hold_position = *hold_position;
|
||||
hold_position.col -= shift;
|
||||
(hold_position, *hold_role)
|
||||
})
|
||||
@@ -47,7 +64,7 @@ impl Pattern {
|
||||
#[must_use]
|
||||
pub fn shift_right(&self, wall_dimensions: WallDimensions, shift: u64) -> Option<Self> {
|
||||
// Out of bounds check
|
||||
if let Some(max_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).max() {
|
||||
if let Some(max_col) = self.pattern.keys().map(|hold_position| hold_position.col).max() {
|
||||
if max_col + shift >= wall_dimensions.cols {
|
||||
return None;
|
||||
}
|
||||
@@ -57,7 +74,7 @@ impl Pattern {
|
||||
.pattern
|
||||
.iter()
|
||||
.map(|(hold_position, hold_role)| {
|
||||
let mut hold_position = hold_position.clone();
|
||||
let mut hold_position = *hold_position;
|
||||
hold_position.col += shift;
|
||||
(hold_position, *hold_role)
|
||||
})
|
||||
@@ -70,8 +87,8 @@ impl Pattern {
|
||||
pub fn mirror(&self) -> Self {
|
||||
let mut pattern = self.clone();
|
||||
|
||||
let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
|
||||
let max_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).max().unwrap_or(0);
|
||||
let min_col = pattern.pattern.keys().map(|hold_position| hold_position.col).min().unwrap_or(0);
|
||||
let max_col = pattern.pattern.keys().map(|hold_position| hold_position.col).max().unwrap_or(0);
|
||||
|
||||
pattern.pattern = pattern
|
||||
.pattern
|
||||
@@ -88,6 +105,23 @@ impl Pattern {
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Returns all possible transformations for the pattern
|
||||
#[must_use]
|
||||
pub fn transformations(&self, wall_dimensions: WallDimensions) -> HashSet<Self> {
|
||||
let mut transformations = HashSet::new();
|
||||
|
||||
let pattern = self.canonicalize();
|
||||
for mut pat in [pattern.mirror(), pattern] {
|
||||
transformations.insert(pat.clone());
|
||||
while let Some(p) = pat.shift_right(wall_dimensions, 1) {
|
||||
transformations.insert(p.clone());
|
||||
pat = p;
|
||||
}
|
||||
}
|
||||
|
||||
transformations
|
||||
}
|
||||
}
|
||||
|
||||
impl UserInteraction {
|
||||
@@ -184,6 +218,14 @@ impl Attempt {
|
||||
Attempt::Flash => Icon::BoltSolid,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn gradient(&self) -> Gradient {
|
||||
match self {
|
||||
Attempt::Attempt => Gradient::PinkOrange,
|
||||
Attempt::Send => Gradient::TealLime,
|
||||
Attempt::Flash => Gradient::CyanBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Problem {
|
||||
|
||||
@@ -29,20 +29,18 @@ pub fn Settings() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn Import(wall_uid: WallUid) -> impl IntoView {
|
||||
let import_from_mini_moonboard = ServerAction::<ImportFromMiniMoonboard>::new();
|
||||
|
||||
let onclick = move |_mouse_event| {
|
||||
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid });
|
||||
};
|
||||
|
||||
view! {
|
||||
<p>"Import problems from"</p>
|
||||
<button on:click=onclick>"Mini Moonboard"</button>
|
||||
}
|
||||
}
|
||||
// #[component]
|
||||
// #[tracing::instrument(skip_all)]
|
||||
// fn Import(wall_uid: WallUid) -> impl IntoView {
|
||||
// let import_from_mini_moonboard = ServerAction::<ImportFromMiniMoonboard>::new();
|
||||
// let onclick = move |_mouse_event| {
|
||||
// import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid });
|
||||
// };
|
||||
// view! {
|
||||
// <p>"Import problems from"</p>
|
||||
// <button on:click=onclick>"Mini Moonboard"</button>
|
||||
// }
|
||||
// }
|
||||
|
||||
#[server]
|
||||
#[tracing::instrument]
|
||||
|
||||
@@ -8,11 +8,13 @@ use crate::gradient::Gradient;
|
||||
use crate::models;
|
||||
use crate::models::HoldRole;
|
||||
use crate::server_functions;
|
||||
use crate::server_functions::SetIsFavorite;
|
||||
use leptos::Params;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::params::Params;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Params, PartialEq, Clone)]
|
||||
@@ -60,7 +62,7 @@ struct Context {
|
||||
wall: Signal<models::Wall>,
|
||||
user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>,
|
||||
problem: Signal<Option<models::Problem>>,
|
||||
filtered_problems: Signal<BTreeSet<models::Problem>>,
|
||||
filtered_problem_transformations: Signal<Vec<HashSet<models::Problem>>>,
|
||||
user_interaction: Signal<Option<models::UserInteraction>>,
|
||||
todays_attempt: Signal<Option<models::Attempt>>,
|
||||
latest_attempt: Signal<Option<models::DatedAttempt>>,
|
||||
@@ -70,6 +72,7 @@ struct Context {
|
||||
cb_next_problem: Callback<()>,
|
||||
cb_set_problem: Callback<models::Problem>,
|
||||
cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
|
||||
cb_set_is_favorite: Callback<bool>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -93,7 +96,9 @@ fn Controller(
|
||||
|
||||
// Derive signals
|
||||
let user_interaction = signals::user_interaction(user_interactions.into(), problem.into());
|
||||
let filtered_problems = signals::filtered_problems(wall, filter_holds.into());
|
||||
let problem_transformations = signals::problem_transformations(wall);
|
||||
|
||||
let filtered_problem_transformations = signals::filtered_problem_transformations(problem_transformations.into(), filter_holds.into());
|
||||
let todays_attempt = signals::todays_attempt(user_interaction);
|
||||
let latest_attempt = signals::latest_attempt(user_interaction);
|
||||
|
||||
@@ -103,6 +108,20 @@ fn Controller(
|
||||
upsert_todays_attempt.dispatch(RonEncoded(attempt));
|
||||
});
|
||||
|
||||
// Set favorite
|
||||
let set_is_favorite = ServerAction::<RonEncoded<server_functions::SetIsFavorite>>::new();
|
||||
let cb_set_is_favorite = Callback::new(move |is_favorite| {
|
||||
let wall_uid = wall.read().uid;
|
||||
let Some(problem) = problem.get() else {
|
||||
return;
|
||||
};
|
||||
set_is_favorite.dispatch(RonEncoded(SetIsFavorite {
|
||||
wall_uid,
|
||||
problem,
|
||||
is_favorite,
|
||||
}));
|
||||
});
|
||||
|
||||
// Callback: Set specific problem
|
||||
let cb_set_problem: Callback<models::Problem> = Callback::new(move |problem| {
|
||||
set_problem.set(Some(problem));
|
||||
@@ -111,14 +130,23 @@ fn Controller(
|
||||
// Callback: Set next problem to a random problem
|
||||
let cb_set_random_problem: Callback<()> = Callback::new(move |_| {
|
||||
// TODO: remove current problem from population
|
||||
let population = filtered_problems.read();
|
||||
let population = filtered_problem_transformations.read();
|
||||
let population = population.deref();
|
||||
|
||||
use rand::seq::IteratorRandom;
|
||||
let mut rng = rand::rng();
|
||||
let problem = population.iter().choose(&mut rng);
|
||||
|
||||
set_problem.set(problem.cloned());
|
||||
// Pick pattern
|
||||
let Some(problem_set) = population.iter().choose(&mut rng) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Pick problem out of pattern transformations
|
||||
let Some(problem) = problem_set.iter().choose(&mut rng) else {
|
||||
return;
|
||||
};
|
||||
|
||||
set_problem.set(Some(problem.clone()));
|
||||
});
|
||||
|
||||
// Callback: On click hold, Add/Remove hold position to problem filter
|
||||
@@ -148,6 +176,16 @@ fn Controller(
|
||||
}
|
||||
});
|
||||
|
||||
// Update user interactions after setting favorite
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(v)) = set_is_favorite.value().get() {
|
||||
let v = v.into_inner();
|
||||
user_interactions.update(|map| {
|
||||
map.insert(v.problem.clone(), v);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
provide_context(Context {
|
||||
wall,
|
||||
problem: problem.into(),
|
||||
@@ -158,9 +196,10 @@ fn Controller(
|
||||
cb_remove_hold_from_filter,
|
||||
cb_next_problem: cb_set_random_problem,
|
||||
cb_set_problem,
|
||||
cb_set_is_favorite,
|
||||
todays_attempt,
|
||||
filter_holds: filter_holds.into(),
|
||||
filtered_problems: filtered_problems.into(),
|
||||
filtered_problem_transformations: filtered_problem_transformations.into(),
|
||||
user_interactions: user_interactions.into(),
|
||||
});
|
||||
|
||||
@@ -186,7 +225,7 @@ fn View() -> impl IntoView {
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="flex flex-row justify-around">
|
||||
<div class="flex flex-row justify-between">
|
||||
<NextProblemButton />
|
||||
</div>
|
||||
</Section>
|
||||
@@ -195,12 +234,14 @@ fn View() -> impl IntoView {
|
||||
|
||||
<Section title="Current problem">
|
||||
{move || ctx.problem.get().map(|problem| view! { <ProblemInfo problem /> })}
|
||||
<Separator /> <Transformations /> <Separator /><AttemptRadioGroup />
|
||||
<Separator /> <History />
|
||||
<Separator /> <div class="flex flex-row gap-2 justify-between">
|
||||
<Transformations />
|
||||
<FavoriteButton />
|
||||
</div> <Separator /> <AttemptRadioGroup /> <Separator /> <History />
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row flex-auto justify-end items-start px-2 pt-3">
|
||||
<div class="flex flex-col px-2 pt-3 gap-4">
|
||||
<HoldsButton />
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,6 +302,16 @@ fn Transformations() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn LikedButton() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let _ctx = use_context::<Context>().unwrap();
|
||||
|
||||
view! { <Button text="Saved" icon=Icon::HeartOutline color=Gradient::PinkRed /> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn HoldsButton() -> impl IntoView {
|
||||
@@ -277,6 +328,25 @@ fn HoldsButton() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn FavoriteButton() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
let ui_toggle = Signal::derive(move || {
|
||||
let guard = ctx.user_interaction.read();
|
||||
guard.as_ref().map(|user_interaction| user_interaction.is_favorite).unwrap_or(false)
|
||||
});
|
||||
let on_click = Callback::new(move |_| {
|
||||
ctx.cb_set_is_favorite.run(!ui_toggle.get());
|
||||
});
|
||||
let icon = Signal::derive(move || if ui_toggle.get() { Icon::Heart } else { Icon::HeartOutline });
|
||||
let text = Signal::derive(move || if ui_toggle.get() { "Saved" } else { "Save" }.to_string());
|
||||
view! { <Button text icon on_click color=Gradient::PinkRed /> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn NextProblemButton() -> impl IntoView {
|
||||
@@ -315,9 +385,11 @@ fn Filter() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
let problems_count = ctx.filtered_problem_transformations.read().iter().map(|set| set.len()).sum::<usize>();
|
||||
|
||||
let problems_counter = {
|
||||
let name = view! { <p class="mr-4 font-light text-right text-orange-300">{"Problems:"}</p> };
|
||||
let value = view! { <p class="text-white">{ctx.filtered_problems.read().len()}</p> };
|
||||
let value = view! { <p class="text-white">{problems_count}</p> };
|
||||
view! {
|
||||
<div class="grid grid-rows-none gap-x-0.5 gap-y-1 grid-cols-[auto_1fr]">
|
||||
{name} {value}
|
||||
@@ -336,13 +408,15 @@ fn Filter() -> impl IntoView {
|
||||
let mut interaction_counters = InteractionCounters::default();
|
||||
let interaction_counters_view = {
|
||||
let user_ints = ctx.user_interactions.read();
|
||||
for problem in ctx.filtered_problems.read().iter() {
|
||||
if let Some(user_int) = user_ints.get(problem) {
|
||||
match user_int.best_attempt().map(|da| da.attempt) {
|
||||
Some(models::Attempt::Flash) => interaction_counters.flash += 1,
|
||||
Some(models::Attempt::Send) => interaction_counters.send += 1,
|
||||
Some(models::Attempt::Attempt) => interaction_counters.attempt += 1,
|
||||
None => {}
|
||||
for problem_set in ctx.filtered_problem_transformations.read().iter() {
|
||||
for problem in problem_set {
|
||||
if let Some(user_int) = user_ints.get(problem) {
|
||||
match user_int.best_attempt().map(|da| da.attempt) {
|
||||
Some(models::Attempt::Flash) => interaction_counters.flash += 1,
|
||||
Some(models::Attempt::Send) => interaction_counters.send += 1,
|
||||
Some(models::Attempt::Attempt) => interaction_counters.attempt += 1,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,6 +649,7 @@ mod signals {
|
||||
use leptos::prelude::*;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn latest_attempt(user_interaction: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::DatedAttempt>> {
|
||||
Signal::derive(move || user_interaction.read().as_ref().and_then(models::UserInteraction::latest_attempt))
|
||||
@@ -600,6 +675,18 @@ mod signals {
|
||||
})
|
||||
}
|
||||
|
||||
/// Maps each problem to a set of problems comprising all transformation of the problem pattern.
|
||||
pub(crate) fn problem_transformations(wall: Signal<models::Wall>) -> Memo<Vec<HashSet<models::Problem>>> {
|
||||
Memo::new(move |_prev_val| {
|
||||
let wall = wall.read();
|
||||
wall.problems
|
||||
.iter()
|
||||
.map(|problem| problem.transformations(wall.wall_dimensions))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
pub(crate) fn filtered_problems(
|
||||
wall: Signal<models::Wall>,
|
||||
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
||||
@@ -610,12 +697,34 @@ mod signals {
|
||||
wall.problems
|
||||
.iter()
|
||||
.filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
|
||||
.map(|problem| problem.clone())
|
||||
.cloned()
|
||||
.collect::<BTreeSet<models::Problem>>()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn filtered_problem_transformations(
|
||||
problem_transformations: Signal<Vec<HashSet<models::Problem>>>,
|
||||
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
||||
) -> Memo<Vec<HashSet<models::Problem>>> {
|
||||
Memo::new(move |_prev_val| {
|
||||
let filter_holds = filter_holds.read();
|
||||
let problem_transformations = problem_transformations.read();
|
||||
|
||||
problem_transformations
|
||||
.iter()
|
||||
.map(|problem_set| {
|
||||
problem_set
|
||||
.iter()
|
||||
.filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
|
||||
.cloned()
|
||||
.collect::<HashSet<models::Problem>>()
|
||||
})
|
||||
.filter(|set| !set.is_empty())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn hold_role(problem: Signal<Option<models::Problem>>, hold_position: models::HoldPosition) -> Signal<Option<models::HoldRole>> {
|
||||
Signal::derive(move || problem.get().and_then(|p| p.pattern.pattern.get(&hold_position).copied()))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Err
|
||||
if is_at_version(db, 3).await? {
|
||||
migrate_to_v4(db).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -71,13 +70,13 @@ pub async fn migrate_to_v4(db: &Database) -> Result<(), Box<dyn std::error::Erro
|
||||
.map(|problem_uid| {
|
||||
let old_prob = &problems_dump[&(wall_uid, problem_uid)];
|
||||
let method = old_prob.method;
|
||||
let problem = models::Problem {
|
||||
|
||||
models::Problem {
|
||||
pattern: models::Pattern {
|
||||
pattern: old_prob.holds.clone(),
|
||||
},
|
||||
method,
|
||||
};
|
||||
problem
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let wall = models::Wall {
|
||||
|
||||
@@ -43,10 +43,8 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database
|
||||
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
|
||||
};
|
||||
|
||||
let problem = models::Problem {
|
||||
pattern: models::Pattern { pattern },
|
||||
method,
|
||||
};
|
||||
let pattern = models::Pattern { pattern }.canonicalize();
|
||||
let problem = models::Problem { pattern, method };
|
||||
problems.push(problem);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError>
|
||||
use crate::server::db::Database;
|
||||
use leptos::prelude::expect_context;
|
||||
use redb::ReadableTable;
|
||||
tracing::trace!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let db = expect_context::<Database>();
|
||||
|
||||
@@ -45,7 +45,7 @@ pub(crate) async fn get_wall_by_uid(wall_uid: models::WallUid) -> Result<RonEnco
|
||||
use crate::server::db::Database;
|
||||
use crate::server::db::DatabaseOperationError;
|
||||
use leptos::prelude::expect_context;
|
||||
tracing::trace!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
||||
enum Error {
|
||||
@@ -84,7 +84,7 @@ pub(crate) async fn get_user_interaction(
|
||||
use crate::server::db::Database;
|
||||
use crate::server::db::DatabaseOperationError;
|
||||
use leptos::prelude::expect_context;
|
||||
tracing::trace!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||
enum Error {
|
||||
@@ -129,7 +129,7 @@ pub(crate) async fn get_user_interactions_for_wall(
|
||||
use crate::server::db::DatabaseOperationError;
|
||||
use leptos::prelude::expect_context;
|
||||
use redb::ReadableTable;
|
||||
tracing::trace!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||
enum Error {
|
||||
@@ -188,7 +188,7 @@ pub(crate) async fn upsert_todays_attempt(
|
||||
use crate::server::db::DatabaseOperationError;
|
||||
use leptos::prelude::expect_context;
|
||||
|
||||
tracing::trace!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
#[derive(Debug, Error, Display, From)]
|
||||
enum Error {
|
||||
@@ -244,3 +244,62 @@ pub(crate) async fn upsert_todays_attempt(
|
||||
.map_err(ServerFnError::new)
|
||||
.map(RonEncoded::new)
|
||||
}
|
||||
|
||||
/// Sets is_favorite field for a problem
|
||||
#[server(
|
||||
input = Ron,
|
||||
output = Ron,
|
||||
custom = RonEncoded
|
||||
)]
|
||||
#[tracing::instrument(err(Debug))]
|
||||
pub(crate) async fn set_is_favorite(
|
||||
wall_uid: models::WallUid,
|
||||
problem: models::Problem,
|
||||
is_favorite: bool,
|
||||
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
|
||||
use crate::server::db::Database;
|
||||
use crate::server::db::DatabaseOperationError;
|
||||
use leptos::prelude::expect_context;
|
||||
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
#[derive(Debug, Error, Display, From)]
|
||||
enum Error {
|
||||
#[display("Wall not found: {_0:?}")]
|
||||
WallNotFound(#[error(not(source))] models::WallUid),
|
||||
|
||||
DatabaseOperation(DatabaseOperationError),
|
||||
}
|
||||
|
||||
async fn inner(wall_uid: models::WallUid, problem: models::Problem, is_favorite: bool) -> Result<UserInteraction, Error> {
|
||||
let db = expect_context::<Database>();
|
||||
|
||||
let user_interaction = db
|
||||
.write(|txn| {
|
||||
let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
||||
|
||||
let key = (wall_uid, problem.clone());
|
||||
|
||||
// Pop or default
|
||||
let mut user_interaction = user_table
|
||||
.remove(&key)?
|
||||
.map(|guard| guard.value())
|
||||
.unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem));
|
||||
|
||||
user_interaction.is_favorite = is_favorite;
|
||||
|
||||
user_table.insert(key, user_interaction.clone())?;
|
||||
|
||||
Ok(user_interaction)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(user_interaction)
|
||||
}
|
||||
|
||||
inner(wall_uid, problem, is_favorite)
|
||||
.await
|
||||
.map_err(error_reporter::Report::new)
|
||||
.map_err(ServerFnError::new)
|
||||
.map(RonEncoded::new)
|
||||
}
|
||||
|
||||
BIN
docs/ascend.jpg
BIN
docs/ascend.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 392 KiB |
18
flake.lock
generated
18
flake.lock
generated
@@ -10,11 +10,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1741078851,
|
||||
"narHash": "sha256-1Qu/Uu+yPUDhHM2XjTbwQqpSrYhhHu7TpHHrT7UO/0o=",
|
||||
"lastModified": 1746696290,
|
||||
"narHash": "sha256-YokYinNgGIu80OErVMuFoIELhetzb45aWKTiKYNXvWA=",
|
||||
"owner": "plul",
|
||||
"repo": "basecamp",
|
||||
"rev": "3e4579d8b4400506e5f53069448b3471608b5281",
|
||||
"rev": "108ef2874fd8f934602cda5bfdc0e58a541c6b4a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -25,11 +25,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1742456341,
|
||||
"narHash": "sha256-yvdnTnROddjHxoQqrakUQWDZSzVchczfsuuMOxg476c=",
|
||||
"lastModified": 1746576598,
|
||||
"narHash": "sha256-FshoQvr6Aor5SnORVvh/ZdJ1Sa2U4ZrIMwKBX5k2wu0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7344a3b78128f7b1765dba89060b015fb75431a7",
|
||||
"rev": "b3582c75c7f21ce0b429898980eddbbf05c68e55",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -53,11 +53,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1742524367,
|
||||
"narHash": "sha256-KzTwk/5ETJavJZYV1DEWdCx05M4duFCxCpRbQSKWpng=",
|
||||
"lastModified": 1746671794,
|
||||
"narHash": "sha256-V+mpk2frYIEm85iYf+KPDmCGG3zBRAEhbv0E3lHdG2U=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "70bf752d176b2ce07417e346d85486acea9040ef",
|
||||
"rev": "ceec434b8741c66bb8df5db70d7e629a9d9c598f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
Reference in New Issue
Block a user