Created
July 9, 2024 10:18
-
-
Save frnsys/2e927e7049784c565fc39c67fb43ca86 to your computer and use it in GitHub Desktop.
scanner
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::{ | |
anim::{animation, Anim}, | |
state, | |
util::{detect_center_element, nodelist_to_elements, to_ws_el}, | |
views::cards::{CardFocusArea, Cards, DragRect, Draggable}, | |
write_state, | |
}; | |
use leptos::*; | |
use std::{rc::Rc, time::Duration}; | |
use wasm_bindgen::JsCast; | |
use leptos_use::{use_interval_fn}; | |
#[derive(Clone)] | |
pub struct ScannerControls { | |
pub reject_scan: Rc<dyn Fn() + 'static>, | |
pub progress_elem: HtmlElement<html::Div>, | |
} | |
fn scan_card_inner( | |
controls: ScannerControls, | |
scan_time: f32, | |
scan_time_multiplier: f32, | |
is_scanning: ReadSignal<bool>, | |
set_stop_anim: WriteSignal<Option<Anim>>, | |
on_finish_scan: Callback<ScannerControls, bool>, | |
stop_scanning_card: Callback<()>, | |
) { | |
let duration = scan_time * 1000. * scan_time_multiplier; | |
let (anim, vals) = animation( | |
[0.], | |
[100.], | |
duration, | |
move || { | |
if is_scanning.get() { | |
let scan_time_multiplier = (scan_time_multiplier * 4. / 5.).max(0.2); | |
let keep_scanning = on_finish_scan.call(controls.clone()); | |
if keep_scanning { | |
scan_card_inner( | |
controls.clone(), | |
scan_time, | |
scan_time_multiplier, | |
is_scanning, | |
set_stop_anim, | |
on_finish_scan, | |
stop_scanning_card, | |
); | |
} else { | |
stop_scanning_card.call(()); | |
} | |
} | |
}, | |
true, | |
); | |
// anim.start(); | |
set_stop_anim.set(Some(anim)); | |
} | |
#[component(transparent)] | |
pub fn Scanner( | |
children: Children, | |
scan_time: f32, | |
reveal_target: f32, | |
#[prop(into)] should_show: Signal<bool>, | |
#[prop(into)] scan_allowed: Signal<bool>, | |
#[prop(into)] drag_rect: Signal<Option<DragRect>>, | |
#[prop(into)] on_finish_scan: Callback<ScannerControls, bool>, | |
#[prop(into)] target_ref: NodeRef<html::Div>, | |
#[prop(into)] progress_ref: NodeRef<html::Div>, | |
#[prop(into)] set_y_bound: Callback<(f32, f32)>, | |
) -> impl IntoView { | |
let (scan_time_multiplier, set_scan_time_multiplier) = create_signal(1.); | |
let (is_scanning, set_is_scanning) = create_signal(false); | |
let (scan_anim, set_scan_anim) = create_signal::<Option<Anim>>(None); | |
let (top_y, set_top_y) = create_signal(0.); | |
let (bot_y, set_bot_y) = create_signal(0.); | |
let get_edges = move || { | |
if let Some(target) = target_ref.get() { | |
let rect = to_ws_el(target).get_bounding_client_rect(); | |
let top_y = rect.y() as f32 + reveal_target; | |
let bot_y = top_y + rect.height() as f32; | |
set_top_y.set(top_y); | |
set_bot_y.set(bot_y); | |
set_y_bound.call((bot_y, top_y)); | |
} | |
}; | |
// TODO | |
// window.addEventListener('resize', this.getEdges); | |
// beforeUnmount() { | |
// window.removeEventListener('resize', this.getEdges); | |
// }, | |
create_effect(move |_| { | |
get_edges(); | |
// Hacky...double-check position | |
// after animations have finished | |
set_timeout(move || get_edges(), Duration::from_millis(500)); | |
}); | |
let reject_scan = move || { | |
if let Some(target) = target_ref.get() { | |
if let Some(elem) = target.parent_element() { | |
elem.class_list().add_1("scan-fail"); | |
set_timeout( | |
move || { | |
elem.class_list().remove_1("scan-fail"); | |
}, | |
Duration::from_millis(500), | |
); | |
} | |
target.class_list().add_1("no-scan"); | |
} | |
if let Some(elem) = document().query_selector(".draggable.active").unwrap() { | |
elem.class_list().add_1("scan-reject"); | |
} | |
}; | |
let stop_scanning_card = move |_| { | |
set_is_scanning.set(false); | |
if let Some(target) = target_ref.get() { | |
target.class_list().remove_2("scanning", "no-scan").unwrap(); | |
if let Some(elem) = target.parent_element() { | |
elem.class_list().remove_1("scan-ok"); | |
} | |
} | |
if let Some(elem) = document().query_selector(".draggable.active").unwrap() { | |
elem.class_list().remove_1("scan-reject"); | |
} | |
if let Some(anim) = scan_anim.get() { | |
anim.stop(); | |
set_scan_anim.set(None); | |
// if let Some(progress) = progress_ref.get() { | |
// progress.style("width", "0"); | |
// } | |
} | |
}; | |
let scan_card = move || { | |
if let Some(progress) = progress_ref.get() { | |
let controls = ScannerControls { | |
reject_scan: Rc::new(reject_scan), | |
progress_elem: progress, | |
}; | |
scan_card_inner( | |
controls, | |
scan_time, | |
1., | |
is_scanning, | |
set_scan_anim, | |
on_finish_scan, | |
stop_scanning_card.into(), | |
); | |
// TODO | |
// create_effect(move |_| { | |
// if let Some(progress) = progress_ref.get() { | |
// let p = vals.get()[0]; | |
// let width = format!("{p}%"); | |
// progress.style("width", width); | |
// } | |
// }); | |
} | |
}; | |
let stop_drag = move || { | |
stop_scanning_card(()); | |
if let Some(target) = target_ref.get() { | |
target.style("transform", "translate(0, 0)"); | |
} | |
}; | |
// Movement handling | |
let check_drag = move |drag_rect: DragRect| { | |
if should_show.get() { | |
if let Some(target) = target_ref.get() { | |
let target = target | |
.style("visibility", "visible") | |
.style("transform", format!("translate(0, {reveal_target}px)")); | |
logging::log!("INTERSECT CHECK: {:?} [bot: {} top: {}]", drag_rect, bot_y.get(), top_y.get()); | |
let intersects = drag_rect.top_y < bot_y.get() && drag_rect.bot_y > top_y.get(); | |
if intersects { | |
let scan_ok = scan_allowed.get(); | |
if scan_ok { | |
set_is_scanning.set(true); | |
if let Some(elem) = target.parent_element() { | |
elem.class_list().add_1("scan-ok"); | |
} | |
target.class_list().add_1("scanning"); | |
logging::log!("SCAN TRIGGERED"); | |
scan_card(); | |
} else { | |
reject_scan(); | |
} | |
} else { | |
stop_scanning_card(()); | |
} | |
} | |
} | |
}; | |
create_effect(move |_| { | |
if let Some(rect) = drag_rect.get() { | |
check_drag(rect); | |
} else { | |
stop_drag(); | |
} | |
}); | |
view! { {children()} } | |
} | |
#[component] | |
pub fn AddScanner( | |
scan_time: f32, | |
#[prop(into)] should_show: Signal<bool>, | |
#[prop(into)] scan_allowed: Signal<bool>, | |
#[prop(into)] drag_rect: Signal<Option<DragRect>>, | |
#[prop(into)] on_finish_scan: Callback<ScannerControls, bool>, | |
#[prop(into)] set_y_bound: Callback<(f32, f32)>, | |
) -> impl IntoView { | |
let progress_ref = create_node_ref::<html::Div>(); | |
let target_ref = create_node_ref::<html::Div>(); | |
view! { | |
<Scanner | |
reveal_target=65. | |
scan_time | |
should_show | |
scan_allowed | |
drag_rect | |
on_finish_scan | |
set_y_bound | |
target_ref | |
progress_ref | |
> | |
<div class="scanbar-wrapper" ref=target_ref> | |
<div class="mini-scanbar"> | |
<div class="scanbar-base"> | |
<div class="scan-progress-bar test-fill-bar" ref=progress_ref></div> | |
</div> | |
<div class="scanbar-led scanbar-led-ok"></div> | |
<div class="scanbar-led scanbar-led-bad"></div> | |
<div class="card-scan-target"></div> | |
</div> | |
</div> | |
</Scanner> | |
} | |
} | |
#[component] | |
pub fn RemoveScanner( | |
scan_time: f32, | |
#[prop(into)] label: Signal<String>, | |
#[prop(into)] should_show: Signal<bool>, | |
#[prop(into)] scan_allowed: Signal<bool>, | |
#[prop(into)] drag_rect: Signal<Option<DragRect>>, | |
#[prop(into)] on_finish_scan: Callback<ScannerControls, bool>, | |
#[prop(into)] set_y_bound: Callback<(f32, f32)>, | |
) -> impl IntoView { | |
let progress_ref = create_node_ref::<html::Div>(); | |
let target_ref = create_node_ref::<html::Div>(); | |
view! { | |
<Scanner | |
reveal_target=-60. | |
scan_time | |
should_show | |
scan_allowed | |
drag_rect | |
on_finish_scan | |
set_y_bound | |
target_ref | |
progress_ref | |
> | |
<div class="card-withdraw-target" ref=target_ref> | |
{label} | |
<div class="withdraw-bar" ref=progress_ref></div> | |
</div> | |
</Scanner> | |
} | |
} | |
pub trait Scannable: Clone + 'static { | |
fn id(&self) -> String; | |
fn as_card(&self) -> View; | |
} | |
pub struct CardScanProps { | |
pub should_show: Signal<bool>, | |
pub scan_allowed: Signal<bool>, | |
pub on_finish_scan: Callback<ScannerControls, bool>, | |
pub scan_time: f32, | |
} | |
#[derive(Clone, PartialEq)] | |
enum Mode { | |
Any, | |
Scan, | |
Scroll, | |
} | |
impl Mode { | |
fn can_scroll(&self) -> bool { | |
match self { | |
Mode::Any | Mode::Scroll => true, | |
_ => false, | |
} | |
} | |
fn can_scan(&self) -> bool { | |
match self { | |
Mode::Any | Mode::Scan => true, | |
_ => false, | |
} | |
} | |
} | |
#[component] | |
pub fn ScannerCards<I: Scannable>( | |
#[prop(into)] items: Signal<Vec<I>>, | |
#[prop(into)] remove_label: Signal<String>, | |
add_props: CardScanProps, | |
remove_props: CardScanProps, | |
) -> impl IntoView { | |
let (mode, set_mode) = create_signal(Mode::Any); | |
let (focused_idx, set_focused_idx) = create_signal(None); | |
let (drag_rect, set_drag_rect) = create_signal(None); | |
let (card_height, set_card_height) = create_signal(0.); | |
let can_scroll = move || mode.get().can_scroll(); | |
let can_scan = move || mode.get().can_scan(); | |
let on_drag = move |rect: DragRect| { | |
// This triggers the scanner functionalities | |
set_drag_rect.set(Some(rect)); | |
set_mode.set(Mode::Scan); | |
}; | |
let update_focused = move || { | |
// Figure out what the focused card is | |
// TODO next_tick? | |
// TODO use refs? | |
if let Some(scroller) = document().query_selector(".cards").unwrap() { | |
let els = document().query_selector_all(".draggable").unwrap(); | |
if els.length() > 0 { | |
let els = nodelist_to_elements(els); | |
if let Some(idx) = detect_center_element( | |
scroller | |
.dyn_into::<web_sys::HtmlElement>() | |
.expect("Is an html element"), | |
&els, | |
) { | |
set_card_height.set(els[idx].get_bounding_client_rect().height()); | |
set_focused_idx.set(Some(idx)); | |
} | |
} | |
} | |
}; | |
use_interval_fn( | |
move || { | |
if focused_idx.get().is_none() { | |
update_focused(); | |
} | |
}, | |
60, | |
); | |
let on_scroll_start = move |_| { | |
set_mode.set(Mode::Scroll); | |
}; | |
let on_scroll_end = move |_| { | |
set_mode.set(Mode::Any); | |
update_focused(); | |
}; | |
let on_drag_stop = move |_| { | |
// This stops/cancels the scanner functionalities | |
set_mode.set(Mode::Any); | |
set_drag_rect.set(None); | |
}; | |
let focused = move || { | |
// TODO | |
// items(focused_idx.get()) | |
focused_idx.get().map(|idx| idx) | |
}; | |
let (top_y_bound, set_top_y_bound) = create_signal(0.); | |
let (bot_y_bound, set_bot_y_bound) = create_signal(0.); | |
let y_bounds = move || [top_y_bound.get(), bot_y_bound.get()]; | |
// TODO | |
// let y_bounds = move || { | |
// if (this.$refs.addScanner && this.$refs.removeScanner) { | |
// return [ | |
// this.$refs.addScanner.botY - 10, | |
// this.$refs.removeScanner.topY + 10 - this.cardHeight, | |
// ]; | |
// } else { | |
// return null; | |
// } | |
// }; | |
let on_focus = move |idx| { | |
write_state!(|state, ui| { | |
set_focused_idx.set(idx); | |
if let Some(idx) = idx { | |
let item: I = items.with(|items| items[idx].clone()); | |
let id = item.id(); | |
if ui.viewed.contains(&id) { | |
ui.viewed.push(id); | |
} | |
} | |
}); | |
}; | |
view! { | |
<Show when=move || focused().is_some()> | |
<AddScanner | |
scan_time=add_props.scan_time | |
drag_rect | |
should_show=add_props.should_show | |
scan_allowed=add_props.scan_allowed | |
on_finish_scan=add_props.on_finish_scan | |
set_y_bound=move |(top, bot)| { | |
set_top_y_bound.set(bot - 10.); | |
} | |
/> | |
<RemoveScanner | |
scan_time=remove_props.scan_time | |
drag_rect | |
label=remove_label | |
should_show=remove_props.should_show | |
scan_allowed=remove_props.scan_allowed | |
on_finish_scan=remove_props.on_finish_scan | |
set_y_bound=move |(top, bot)| { | |
set_bot_y_bound.set(top + 10. - card_height.get() as f32); | |
} | |
/> | |
</Show> | |
<Cards | |
enabled=can_scroll | |
on_focus | |
on_scroll_start | |
on_scroll_end | |
> | |
<For | |
each=move || items.get().into_iter().enumerate() | |
key=|(_, item)| item.id() | |
children=move |(i, item)| { | |
let draggable = move || { | |
can_scan() && focused_idx.get() == Some(i) | |
}; | |
let card = item.as_card(); | |
let id = move || item.id(); | |
view! { | |
<Draggable | |
on_drag | |
on_drag_stop | |
y_bounds | |
id=id.into_signal() | |
draggable=draggable.into_signal() | |
> | |
{card} | |
</Draggable> | |
} | |
} | |
/> | |
</Cards> | |
<CardFocusArea/> | |
} | |
} | |
// TODO | |
// <ProjectCard | |
// :project="projects[i]" | |
// @change="$emit('change')" /> | |
// onScrollStarted() { | |
// state.help[scrollTip] = true; | |
// this.onScrollStart(); | |
// }, | |
// onDragStarted(rect) { | |
// state.help[scanTip] = true; | |
// this.onDrag(rect); | |
// }, | |
// |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment