This commit is contained in:
Asger Juul Brunshøj 2025-04-01 13:46:28 +02:00
parent ed6aa4b9c9
commit 91bea767d0
14 changed files with 225 additions and 463 deletions

View File

@ -43,8 +43,7 @@ pub fn App() -> impl IntoView {
<Routes fallback=|| "Not found">
<Route path=path!("/") view=Home />
<Route path=path!("/wall/:wall_uid") view=pages::wall::Page />
<Route path=path!("/wall/:wall_uid/edit") view=pages::edit_wall::EditWall />
<Route path=path!("/wall/:wall_uid/routes") view=pages::routes::Routes />
<Route path=path!("/wall/:wall_uid/holds") view=pages::holds::Page />
</Routes>
</Router>
}

View File

@ -6,15 +6,15 @@ use leptos::prelude::*;
pub fn ProblemInfo(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
tracing::trace!("Enter problem info");
let name = Signal::derive(move || problem.read().name.clone());
let set_by = Signal::derive(move || problem.read().set_by.clone());
let method = Signal::derive(move || problem.read().method.to_string());
// let name = Signal::derive(move || problem.read().name.clone());
// let set_by = Signal::derive(move || problem.read().set_by.clone());
view! {
<div class="grid grid-rows-none gap-y-1 gap-x-0.5 grid-cols-[auto_1fr]">
<NameValue name="Name:" value=name />
<NameValue name="Method:" value=method />
<NameValue name="Set By:" value=set_by />
// <NameValue name="Name:" value=name />
// <NameValue name="Set By:" value=set_by />
</div>
}
}

View File

@ -8,17 +8,16 @@ pub use v2::ImageFilename;
pub use v2::ImageResolution;
pub use v2::ImageUid;
pub use v2::Method;
pub use v2::ProblemUid;
pub use v2::Root;
pub use v2::Wall;
pub use v2::WallDimensions;
pub use v2::WallUid;
pub use v3::Attempt;
pub use v3::UserInteraction;
pub use v4::DatedAttempt;
pub use v4::Pattern;
pub use v4::Problem;
pub use v4::ProblemHolds;
pub use v4::Transformation;
pub use v4::UserInteraction;
pub use v4::Wall;
mod semantics;
@ -28,6 +27,8 @@ pub mod v4 {
use super::v3;
use chrono::DateTime;
use chrono::Utc;
use derive_more::Display;
use derive_more::FromStr;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
@ -38,24 +39,39 @@ pub mod v4 {
pub uid: v2::WallUid,
pub wall_dimensions: v2::WallDimensions,
pub holds: BTreeMap<v1::HoldPosition, v2::Hold>,
/// Canonicalized.
pub problems: BTreeSet<Problem>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Problem {
pub holds: ProblemHolds,
pub pattern: Pattern,
pub method: v2::Method,
pub transformation: Transformation,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Pattern {
pub pattern: BTreeMap<v1::HoldPosition, v1::HoldRole>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default)]
pub struct Transformation {
pub horizontal_shift: u64,
pub shift_right: u64,
pub mirror: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProblemHolds(pub BTreeMap<v1::HoldPosition, v1::HoldRole>);
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserInteraction {
pub wall_uid: v2::WallUid,
pub problem: Problem,
/// Dates on which this problem was attempted, and how it went
pub attempted_on: BTreeMap<chrono::DateTime<chrono::Utc>, v3::Attempt>,
/// Is among favorite problems
pub is_favorite: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct DatedAttempt {
@ -117,7 +133,6 @@ pub mod v2 {
pub struct Wall {
pub uid: WallUid,
// TODO: Replace by walldimensions
pub rows: u64,
pub cols: u64,
@ -152,11 +167,11 @@ pub mod v2 {
#[display("Feet follow hands")]
FeetFollowHands,
#[display("Footless")]
Footless,
#[display("Footless plus kickboard")]
FootlessPlusKickboard,
#[display("Footless")]
Footless,
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View File

@ -3,87 +3,73 @@ use chrono::DateTime;
use chrono::Utc;
use std::collections::BTreeMap;
impl Problem {
pub fn normalize(&self) -> Self {
let Self {
holds,
method,
transformation,
} = self;
let mut transformation = *transformation;
let min_col = holds.0.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
transformation.horizontal_shift += min_col;
let holds = {
let holds = holds
.0
.iter()
.map(|(hold_position, hold_role)| {
let hold_position = HoldPosition {
row: hold_position.row,
col: hold_position.col - min_col,
};
(hold_position, *hold_role)
})
.collect();
ProblemHolds(holds)
};
let mut new = Self {
holds,
method: *method,
transformation,
};
if new.transformation.mirror {
new = new.mirror();
}
new
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);
pattern.pattern = pattern
.pattern
.iter()
.map(|(hold_position, hold_role)| {
let hold_position = HoldPosition {
row: hold_position.row,
col: hold_position.col - min_col,
};
(hold_position, *hold_role)
})
.collect();
std::cmp::min(pattern.mirror(), pattern)
}
#[must_use]
pub fn apply_transformation(&self, transformation: Transformation) -> Self {
let mut holds = self.clone();
if transformation.shift_right > 0 {
holds = holds.shift_right(transformation.shift_right);
}
if transformation.mirror {
holds = holds.mirror();
}
holds
}
#[must_use]
pub fn shift_right(&self, shift: u64) -> Self {
let mut pattern = self.clone();
pattern.pattern = pattern
.pattern
.iter()
.map(|(hold_position, hold_role)| {
let mut hold_position = hold_position.clone();
hold_position.col += shift;
(hold_position, *hold_role)
})
.collect();
pattern
}
#[must_use]
pub fn mirror(&self) -> Self {
let Self {
holds,
method,
transformation,
} = self;
let mut pattern = self.clone();
let min_col = holds.0.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
let max_col = holds.0.iter().map(|(hold_position, _)| hold_position.col).max().unwrap_or(0);
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 holds = {
let holds = holds
.0
.iter()
.map(|(hold_position, hold_role)| {
let HoldPosition { row, col } = *hold_position;
let mut mirrored_col = col;
mirrored_col += 2 * (max_col - col);
mirrored_col -= max_col - min_col;
let hold_position = HoldPosition { row, col: mirrored_col };
(hold_position, *hold_role)
})
.collect();
ProblemHolds(holds)
};
pattern.pattern = pattern
.pattern
.iter()
.map(|(hold_position, hold_role)| {
let HoldPosition { row, col } = *hold_position;
let mut mirrored_col = col;
mirrored_col += 2 * (max_col - col);
mirrored_col -= max_col - min_col;
let hold_position = HoldPosition { row, col: mirrored_col };
(hold_position, *hold_role)
})
.collect();
let transformation = {
let mut transformation = *transformation;
transformation.mirror = !transformation.mirror;
transformation
};
Self {
holds,
method: *method,
transformation,
}
}
pub fn shift(&self, shift: i64) -> Self {
todo!()
pattern
}
}
@ -147,12 +133,6 @@ impl WallUid {
pub(crate) fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
}
impl ProblemUid {
pub(crate) fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
pub(crate) fn min() -> Self {
Self(uuid::Uuid::nil())
}
@ -188,66 +168,68 @@ impl Attempt {
}
}
impl std::str::FromStr for Problem {
type Err = ron::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let problem = ron::from_str(s)?;
Ok(problem)
}
}
impl std::fmt::Display for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = ron::to_string(self).unwrap();
write!(f, "{s}")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test_try::test_try]
fn normalize_empty_problem() {
let problem = Problem {
holds: ProblemHolds([].into_iter().collect()),
method: Method::FeetFollowHands,
transformation: Transformation::default(),
fn canonicalize_empty_pattern() {
let pattern = Pattern {
pattern: [].into_iter().collect(),
};
let normalized = problem.normalize();
assert_eq!(problem, normalized);
let mirrored = problem.mirror();
assert_eq!(problem, mirrored);
let canonicalized = pattern.canonicalize();
assert_eq!(pattern, canonicalized);
let mirrored = pattern.mirror();
assert_eq!(pattern, mirrored);
}
#[test_try::test_try]
fn normalize_problem() {
let problem = Problem {
holds: ProblemHolds(
[
(HoldPosition { row: 0, col: 1 }, HoldRole::End),
(HoldPosition { row: 7, col: 6 }, HoldRole::Start),
]
.into_iter()
.collect(),
),
method: Method::FeetFollowHands,
transformation: Transformation::default(),
fn canonicalize_pattern() {
let pattern = Pattern {
pattern: [
(HoldPosition { row: 0, col: 1 }, HoldRole::End),
(HoldPosition { row: 7, col: 6 }, HoldRole::Start),
]
.into_iter()
.collect(),
};
let normalized = problem.normalize();
let canonicalized = pattern.canonicalize();
assert_eq!(normalized.holds.0[&HoldPosition { row: 0, col: 0 }], HoldRole::End);
assert_eq!(normalized.holds.0[&HoldPosition { row: 7, col: 5 }], HoldRole::Start);
assert_eq!(normalized.transformation.horizontal_shift, 1);
assert_eq!(normalized.transformation.mirror, false);
assert_eq!(canonicalized.pattern[&HoldPosition { row: 0, col: 0 }], HoldRole::End);
assert_eq!(canonicalized.pattern[&HoldPosition { row: 7, col: 5 }], HoldRole::Start);
}
#[test_try::test_try]
fn mirror_problem() {
let problem = Problem {
holds: ProblemHolds(
[
(HoldPosition { row: 0, col: 1 }, HoldRole::End),
(HoldPosition { row: 7, col: 6 }, HoldRole::Start),
]
.into_iter()
.collect(),
),
method: Method::FeetFollowHands,
transformation: Transformation::default(),
fn mirror_pattern() {
let pattern = Pattern {
pattern: [
(HoldPosition { row: 0, col: 1 }, HoldRole::End),
(HoldPosition { row: 7, col: 6 }, HoldRole::Start),
]
.into_iter()
.collect(),
};
let normalized = problem.normalize();
let mirrored = pattern.mirror();
assert_eq!(normalized.holds.0[&HoldPosition { row: 0, col: 0 }], HoldRole::End);
assert_eq!(normalized.holds.0[&HoldPosition { row: 7, col: 5 }], HoldRole::Start);
assert_eq!(normalized.transformation.horizontal_shift, 1);
assert_eq!(normalized.transformation.mirror, false);
assert_eq!(mirrored.pattern[&HoldPosition { row: 0, col: 6 }], HoldRole::End);
assert_eq!(mirrored.pattern[&HoldPosition { row: 7, col: 1 }], HoldRole::Start);
}
}

View File

@ -1,4 +1,3 @@
pub mod edit_wall;
pub mod routes;
pub mod holds;
pub mod settings;
pub mod wall;

View File

@ -24,7 +24,7 @@ struct RouteParams {
}
#[component]
pub fn EditWall() -> impl IntoView {
pub fn Page() -> impl IntoView {
let params = leptos_router::hooks::use_params::<RouteParams>();
let wall_uid = Signal::derive(move || {
params
@ -85,8 +85,8 @@ fn Ready(wall: models::Wall) -> impl IntoView {
}
let style = {
let grid_rows = crate::css::grid_rows_n(wall.rows);
let grid_cols = crate::css::grid_cols_n(wall.cols);
let grid_rows = crate::css::grid_rows_n(wall.wall_dimensions.rows);
let grid_cols = crate::css::grid_cols_n(wall.wall_dimensions.cols);
[grid_rows, grid_cols].join(" ")
};

View File

@ -1,102 +0,0 @@
use crate::components;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
use crate::models;
use leptos::Params;
use leptos::prelude::*;
use leptos_router::params::Params;
#[derive(Params, PartialEq, Clone)]
struct RouteParams {
// Is never None
wall_uid: Option<models::WallUid>,
}
#[component]
#[tracing::instrument(skip_all)]
pub fn Routes() -> impl IntoView {
tracing::debug!("Enter");
let params = leptos_router::hooks::use_params::<RouteParams>();
let wall_uid = Signal::derive(move || {
params
.get()
.expect("gets wall_uid from URL")
.wall_uid
.expect("wall_uid param is never None")
});
let wall = crate::resources::wall_by_uid(wall_uid);
let problems = crate::resources::problems_for_wall(wall_uid);
let header_items = move || HeaderItems {
left: vec![HeaderItem {
text: "← Ascend".to_string(),
link: Some(format!("/wall/{}", wall_uid.get())),
}],
middle: vec![HeaderItem {
text: "Routes".to_string(),
link: None,
}],
right: vec![],
};
let suspend = move || {
Suspend::new(async move {
let wall = wall.await;
let problems = problems.await;
let v = move || -> Result<_, ServerFnError> {
let wall = wall.clone()?;
let problems = problems.clone()?;
let wall_dimensions = models::WallDimensions {
rows: wall.rows,
cols: wall.cols,
};
let problems_sample = move || problems.values().take(10).cloned().collect::<Vec<_>>();
Ok(view! {
<div>
<For
each=problems_sample
key=|problem| problem.uid
children=move |problem: models::Problem| {
view! {
<Problem dim=wall_dimensions problem />
<hr class="my-8 h-px bg-gray-700 border-0" />
}
}
/>
</div>
})
};
view! { <ErrorBoundary fallback=|_errors| "error">{v}</ErrorBoundary> }
})
};
view! {
<div class="min-h-screen min-w-screen bg-neutral-950">
<StyledHeader items=Signal::derive(header_items) />
<div class="container mx-auto mt-6">
<Suspense fallback=|| view! { <p>"loading"</p> }>{suspend}</Suspense>
</div>
</div>
}
}
#[component]
#[tracing::instrument(skip_all)]
fn Problem(#[prop(into)] dim: Signal<models::WallDimensions>, #[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
view! {
<div class="flex items-start">
<div class="flex-none">
<components::Problem dim problem />
</div>
<components::ProblemInfo problem=problem.get() />
</div>
}
}

View File

@ -35,7 +35,6 @@ pub fn Page() -> impl IntoView {
});
let wall = crate::resources::wall_by_uid(wall_uid);
let problems = crate::resources::problems_for_wall(wall_uid);
let user_interactions = crate::resources::user_interactions(wall_uid);
leptos::view! {
@ -46,10 +45,9 @@ pub fn Page() -> impl IntoView {
{move || Suspend::new(async move {
tracing::debug!("executing main suspend");
let wall = wall.await?;
let problems = problems.await?;
let user_interactions = user_interactions.await?;
let user_interactions = RwSignal::new(user_interactions);
Ok::<_, ServerFnError>(view! { <Controller wall problems user_interactions /> })
Ok::<_, ServerFnError>(view! { <Controller wall user_interactions /> })
})}
</Suspense>
</div>
@ -59,9 +57,9 @@ pub fn Page() -> impl IntoView {
#[derive(Debug, Clone, Copy)]
struct Context {
wall: Signal<models::Wall>,
user_interactions: Signal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>,
problem: Signal<Option<models::Problem>>,
filtered_problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
filtered_problems: Signal<BTreeSet<models::Problem>>,
user_interaction: Signal<Option<models::UserInteraction>>,
todays_attempt: Signal<Option<models::Attempt>>,
latest_attempt: Signal<Option<models::DatedAttempt>>,
@ -76,13 +74,12 @@ struct Context {
#[tracing::instrument(skip_all)]
fn Controller(
#[prop(into)] wall: Signal<models::Wall>,
#[prop(into)] problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
#[prop(into)] user_interactions: RwSignal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
#[prop(into)] user_interactions: RwSignal<BTreeMap<models::Problem, models::UserInteraction>>,
) -> impl IntoView {
crate::tracing::on_enter!();
// Extract data from URL
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
let (problem_url, set_problem_url) = leptos_router::hooks::query_signal::<models::Problem>("problem");
// Filter
let (filter_holds, set_filter_holds) = signal(BTreeSet::new());
@ -94,8 +91,8 @@ fn Controller(
// Derive signals
let wall_uid = signals::wall_uid(wall);
let problem = signals::problem(problems, problem_uid.into());
let user_interaction = signals::user_interaction(user_interactions.into(), problem_uid.into());
let problems = signals::problems(wall);
let user_interaction = signals::user_interaction(user_interactions.into(), problem_url.into());
let filtered_problems = signals::filtered_problems(problems, filter_holds.into());
let todays_attempt = signals::todays_attempt(user_interaction);
let latest_attempt = signals::latest_attempt(user_interaction);
@ -116,7 +113,7 @@ fn Controller(
let mut rng = rand::rng();
let problem_uid = population.choose(&mut rng);
set_problem_uid.set(problem_uid);
set_problem_url.set(problem_uid);
});
// Callback: On click hold, Add/Remove hold position to problem filter
@ -130,7 +127,7 @@ fn Controller(
// Set a problem when wall is set (loaded)
Effect::new(move |_prev_value| {
if problem_uid.get().is_none() {
if problem_url.get().is_none() {
tracing::debug!("Setting initial problem");
cb_set_random_problem.run(());
}
@ -370,7 +367,7 @@ fn AttemptRadioGroup() -> impl IntoView {
let ctx = use_context::<Context>().unwrap();
let problem_uid = Signal::derive(move || ctx.problem.read().as_ref().map(|p| p.uid));
let problem = ctx.problem;
let mut attempt_radio_buttons = vec![];
for variant in [models::Attempt::Flash, models::Attempt::Send, models::Attempt::Attempt] {
@ -379,10 +376,10 @@ fn AttemptRadioGroup() -> impl IntoView {
let onclick = move |_| {
let attempt = if ui_toggle.get() { None } else { Some(variant) };
if let Some(problem_uid) = problem_uid.get() {
if let Some(problem) = problem.get() {
ctx.cb_upsert_todays_attempt.run(server_functions::UpsertTodaysAttempt {
wall_uid: ctx.wall.read().uid,
problem_uid,
problem,
attempt,
});
}
@ -469,9 +466,9 @@ fn Wall() -> impl IntoView {
}
let style = {
let grid_rows = crate::css::grid_rows_n(wall.rows);
let grid_cols = crate::css::grid_cols_n(wall.cols);
let max_width = format!("{}vh", wall.cols as f64 / wall.rows as f64 * 100.);
let grid_rows = crate::css::grid_rows_n(wall.wall_dimensions.rows);
let grid_cols = crate::css::grid_cols_n(wall.wall_dimensions.cols);
let max_width = format!("{}vh", wall.wall_dimensions.cols as f64 / wall.wall_dimensions.rows as f64 * 100.);
format!("max-height: 100vh; max-width: {max_width}; {}", [grid_rows, grid_cols].join(" "))
};
@ -568,27 +565,16 @@ mod signals {
})
}
pub(crate) fn problem(
problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
problem_uid: Signal<Option<models::ProblemUid>>,
) -> Signal<Option<models::Problem>> {
Signal::derive(move || {
let problem_uid = problem_uid.get()?;
let problems = problems.read();
problems.get(&problem_uid).cloned()
})
}
pub(crate) fn filtered_problems(
problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
wall: Signal<models::Wall>,
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
) -> Memo<BTreeMap<models::ProblemUid, models::Problem>> {
) -> Memo<BTreeSet<models::Problem>> {
Memo::new(move |_prev_val| {
let filter_holds = filter_holds.read();
problems.with(|problems| {
problems
wall.with(|wall| {
wall.problems
.iter()
.filter(|(_, problem)| filter_holds.iter().all(|hold_pos| problem.holds.contains_key(hold_pos)))
.filter(|(_, problem)| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
.map(|(problem_uid, problem)| (*problem_uid, problem.clone()))
.collect::<BTreeMap<models::ProblemUid, models::Problem>>()
})
@ -596,6 +582,6 @@ mod signals {
}
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.holds.get(&hold_position).copied()))
Signal::derive(move || problem.get().and_then(|p| p.pattern.pattern.get(&hold_position).copied()))
}
}

View File

@ -17,47 +17,15 @@ pub fn wall_by_uid(wall_uid: Signal<models::WallUid>) -> RonResource<models::Wal
)
}
/// Version of [problem_by_uid] that short circuits if the input problem_uid signal is None.
pub fn problem_by_uid_optional(
wall_uid: Signal<models::WallUid>,
problem_uid: Signal<Option<models::ProblemUid>>,
) -> RonResource<Option<models::Problem>> {
Resource::new_with_options(
move || (wall_uid.get(), problem_uid.get()),
move |(wall_uid, problem_uid)| async move {
let Some(problem_uid) = problem_uid else {
return Ok(None);
};
crate::server_functions::get_problem_by_uid(wall_uid, problem_uid)
.await
.map(RonEncoded::into_inner)
.map(Some)
},
false,
)
}
/// Returns all problems for a wall
pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<BTreeMap<models::ProblemUid, models::Problem>> {
Resource::new_with_options(
move || wall_uid.get(),
move |wall_uid| async move { crate::server_functions::get_problems_for_wall(wall_uid).await.map(RonEncoded::into_inner) },
false,
)
}
/// Returns user interaction for a single problem
pub fn user_interaction(
wall_uid: Signal<models::WallUid>,
problem_uid: Signal<Option<models::ProblemUid>>,
) -> RonResource<Option<models::UserInteraction>> {
pub fn user_interaction(wall_uid: Signal<models::WallUid>, problem: Signal<Option<models::Problem>>) -> RonResource<Option<models::UserInteraction>> {
Resource::new_with_options(
move || (wall_uid.get(), problem_uid.get()),
move |(wall_uid, problem_uid)| async move {
let Some(problem_uid) = problem_uid else {
move || (wall_uid.get(), problem.get()),
move |(wall_uid, problem)| async move {
let Some(problem) = problem else {
return Ok(None);
};
crate::server_functions::get_user_interaction(wall_uid, problem_uid)
crate::server_functions::get_user_interaction(wall_uid, problem)
.await
.map(RonEncoded::into_inner)
},

View File

@ -121,13 +121,6 @@ pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperat
assert!(table.is_empty()?);
}
// Problems table
{
// Opening the table creates the table
let table = txn.open_table(current::TABLE_PROBLEMS)?;
assert!(table.is_empty()?);
}
// User table
{
// Opening the table creates the table
@ -147,11 +140,11 @@ use crate::models;
pub mod current {
use super::v2;
use super::v3;
pub use v2::TABLE_PROBLEMS;
use super::v4;
pub use v2::TABLE_ROOT;
pub use v2::TABLE_WALLS;
pub use v3::TABLE_USER;
pub use v3::VERSION;
pub use v4::TABLE_USER;
pub use v4::TABLE_WALLS;
}
pub mod v4 {
@ -161,7 +154,9 @@ pub mod v4 {
pub const VERSION: u64 = 4;
// TODO
pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallUid>, Bincode<models::v4::Wall>> = TableDefinition::new("walls");
pub const TABLE_USER: TableDefinition<Bincode<(models::v2::WallUid, models::v4::Problem)>, Bincode<models::v4::UserInteraction>> =
TableDefinition::new("user");
}
pub mod v3 {

View File

@ -9,6 +9,25 @@ pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Err
if is_at_version(db, 2).await? {
migrate_to_v3(db).await?;
}
if is_at_version(db, 3).await? {
migrate_to_v4(db).await?;
}
Ok(())
}
/// migrate: walls table
/// migrate: user table
/// remove: problems table
#[tracing::instrument(skip_all, err)]
pub async fn migrate_to_v4(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
use redb::ReadableTableMetadata;
tracing::warn!("MIGRATING TO VERSION 4");
todo!();
db.write(|txn| Ok(())).await?;
db.set_version(db::Version { version: db::v4::VERSION }).await?;
Ok(())
}

View File

@ -20,11 +20,9 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database
tracing::info!("Parsing mini moonboard problems from {file_path}");
let set_by = "mini-mb-2020-parser";
let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?;
for mini_mb_problem in mini_moonboard.problems {
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
let mut pattern = BTreeMap::<HoldPosition, HoldRole>::new();
for mv in mini_mb_problem.moves {
let row = mv.description.row();
let col = mv.description.column();
@ -36,43 +34,29 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database
(false, true) => HoldRole::End,
(false, false) => HoldRole::Normal,
};
holds.insert(hold_position, role);
pattern.insert(hold_position, role);
}
let name = mini_mb_problem.name;
let method = match mini_mb_problem.method {
mini_moonboard::Method::FeetFollowHands => models::Method::FeetFollowHands,
mini_moonboard::Method::Footless => models::Method::Footless,
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
};
let problem_id = models::ProblemUid::create();
let problem = models::Problem {
uid: problem_id,
name,
set_by: set_by.to_owned(),
holds,
pattern: models::Pattern { pattern },
method,
date_added: chrono::Utc::now(),
};
problems.push(problem);
}
db.write(|txn| {
let mut walls_table = txn.open_table(db::current::TABLE_WALLS)?;
let mut problems_table = txn.open_table(db::current::TABLE_PROBLEMS)?;
let mut wall = walls_table.get(wall_uid)?.unwrap().value();
wall.problems.extend(problems.iter().map(|p| p.uid));
wall.problems.extend(problems);
walls_table.insert(wall_uid, wall)?;
for problem in problems {
let key = (wall_uid, problem.uid);
problems_table.insert(key, problem)?;
}
Ok(())
})
.await?;

View File

@ -71,68 +71,6 @@ pub(crate) async fn get_wall_by_uid(wall_uid: models::WallUid) -> Result<RonEnco
Ok(RonEncoded::new(wall))
}
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_problems_for_wall(
wall_uid: models::WallUid,
) -> Result<RonEncoded<BTreeMap<models::ProblemUid, models::Problem>>, ServerFnError> {
use crate::server::db::Database;
use crate::server::db::DatabaseOperationError;
use leptos::prelude::expect_context;
tracing::trace!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
enum Error {
#[display("Wall not found: {_0:?}")]
WallNotFound(#[error(not(source))] models::WallUid),
DatabaseOperation(DatabaseOperationError),
}
async fn inner(wall_uid: models::WallUid) -> Result<BTreeMap<models::ProblemUid, models::Problem>, Error> {
let db = expect_context::<Database>();
let problems = db
.read(|txn| {
let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
tracing::debug!("getting wall");
let wall = walls_table
.get(wall_uid)?
.ok_or(Error::WallNotFound(wall_uid))
.map_err(DatabaseOperationError::custom)?
.value();
tracing::debug!("got wall");
drop(walls_table);
tracing::debug!("open problems table");
let problems_table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
tracing::debug!("opened problems table");
let problems = wall
.problems
.iter()
.map(|problem_uid| problems_table.get(&(wall_uid, *problem_uid)))
.filter_map(|res| res.transpose())
.map_res(|guard| guard.value())
.map_res(|problem| (problem.uid, problem))
.collect::<Result<BTreeMap<models::ProblemUid, models::Problem>, _>>()?;
Ok(problems)
})
.await?;
Ok(problems)
}
let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?;
Ok(RonEncoded::new(problems))
}
/// Returns user interaction for a single wall problem
#[server(
input = Ron,
@ -142,7 +80,7 @@ pub(crate) async fn get_problems_for_wall(
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_user_interaction(
wall_uid: models::WallUid,
problem_uid: models::ProblemUid,
problem: models::Problem,
) -> Result<RonEncoded<Option<models::UserInteraction>>, ServerFnError> {
use crate::server::db::Database;
use crate::server::db::DatabaseOperationError;
@ -157,13 +95,13 @@ pub(crate) async fn get_user_interaction(
DatabaseOperation(DatabaseOperationError),
}
async fn inner(wall_uid: models::WallUid, problem_uid: models::ProblemUid) -> Result<Option<UserInteraction>, Error> {
async fn inner(wall_uid: models::WallUid, problem: models::Problem) -> Result<Option<UserInteraction>, Error> {
let db = expect_context::<Database>();
let user_interaction = db
.read(|txn| {
let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
let user_interaction = user_table.get((wall_uid, problem_uid))?.map(|guard| guard.value());
let user_interaction = user_table.get(&(wall_uid, problem))?.map(|guard| guard.value());
Ok(user_interaction)
})
.await?;
@ -171,7 +109,7 @@ pub(crate) async fn get_user_interaction(
Ok(user_interaction)
}
let user_interaction = inner(wall_uid, problem_uid)
let user_interaction = inner(wall_uid, problem)
.await
.map_err(error_reporter::Report::new)
.map_err(ServerFnError::new)?;
@ -187,7 +125,7 @@ pub(crate) async fn get_user_interaction(
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_user_interactions(
wall_uid: models::WallUid,
) -> Result<RonEncoded<BTreeMap<models::ProblemUid, models::UserInteraction>>, ServerFnError> {
) -> Result<RonEncoded<BTreeMap<models::Problem, models::UserInteraction>>, ServerFnError> {
use crate::server::db::Database;
use crate::server::db::DatabaseOperationError;
use leptos::prelude::expect_context;
@ -198,7 +136,7 @@ pub(crate) async fn get_user_interactions(
DatabaseOperation(DatabaseOperationError),
}
async fn inner(wall_uid: models::WallUid) -> Result<BTreeMap<models::ProblemUid, models::UserInteraction>, Error> {
async fn inner(wall_uid: models::WallUid) -> Result<BTreeMap<models::Problem, models::UserInteraction>, Error> {
let db = expect_context::<Database>();
let user_interactions = db
@ -224,43 +162,6 @@ pub(crate) async fn get_user_interactions(
Ok(RonEncoded::new(user_interaction))
}
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err(Debug))]
pub(crate) async fn get_problem_by_uid(
wall_uid: models::WallUid,
problem_uid: models::ProblemUid,
) -> Result<RonEncoded<models::Problem>, ServerFnError> {
use crate::server::db::Database;
use crate::server::db::DatabaseOperationError;
use leptos::prelude::expect_context;
tracing::trace!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display)]
enum Error {
#[display("Problem not found: {_0:?}")]
NotFound(#[error(not(source))] models::ProblemUid),
}
let db = expect_context::<Database>();
let problem = db
.read(|txn| {
let table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
let problem = table
.get((wall_uid, problem_uid))?
.ok_or(Error::NotFound(problem_uid))
.map_err(DatabaseOperationError::custom)?
.value();
Ok(problem)
})
.await?;
Ok(RonEncoded::new(problem))
}
/// Inserts or updates today's attempt.
#[server(
input = Ron,
@ -270,7 +171,7 @@ pub(crate) async fn get_problem_by_uid(
#[tracing::instrument(err(Debug))]
pub(crate) async fn upsert_todays_attempt(
wall_uid: models::WallUid,
problem_uid: models::ProblemUid,
problem: models::Problem,
attempt: Option<models::Attempt>,
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
use crate::server::db::Database;
@ -287,20 +188,20 @@ pub(crate) async fn upsert_todays_attempt(
DatabaseOperation(DatabaseOperationError),
}
async fn inner(wall_uid: models::WallUid, problem_uid: models::ProblemUid, attempt: Option<models::Attempt>) -> Result<UserInteraction, Error> {
async fn inner(wall_uid: models::WallUid, problem: models::Problem, attempt: Option<models::Attempt>) -> 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_uid);
let key = (wall_uid, problem);
// Pop or default
let mut user_interaction = user_table
.remove(key)?
.map(|guard| guard.value())
.unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem_uid));
.unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem));
// If the last entry is from today, remove it
if let Some(entry) = user_interaction.attempted_on.last_entry() {
@ -327,7 +228,7 @@ pub(crate) async fn upsert_todays_attempt(
Ok(user_interaction)
}
inner(wall_uid, problem_uid, attempt)
inner(wall_uid, problem, attempt)
.await
.map_err(error_reporter::Report::new)
.map_err(ServerFnError::new)

16
notes.txt Normal file
View File

@ -0,0 +1,16 @@
# Random selection: Sample from filtered
[patterns with variations that satisfy filter]
[variations]
# Random selection no filter: Sample equally weighted patterns
[patterns]
[random variation within pattern]
Normalize: shift left, and use minimum of mirrored pattern pair
# Filter stats:
patterns: X
pattern variations: Y