leptos_qr_scanner/
lib.rs

1// Extracted from https://github.com/sectore/fm-faucet-leptos/blob/main/src/component/scan.rs by  Jens K./sectore, licensed under MIT License
2
3use js_sys::Object;
4use leptos::html::Video;
5use leptos::*;
6use std::sync::Arc;
7
8use wasm_bindgen::prelude::*;
9
10#[wasm_bindgen(module = "/public/qr-scanner-worker.min.js")]
11extern "C" {
12    #[derive(Clone, Debug)]
13    type QrWorker;
14
15    #[wasm_bindgen(method, js_name = createWorker)]
16    fn createWorker(this: &QrWorker);
17}
18
19#[wasm_bindgen(module = "/public/qr-scanner-wrapper.min.js")]
20extern "C" {
21    #[derive(Clone, Debug)]
22    type QrScanner;
23
24    #[wasm_bindgen(constructor, js_name = new)]
25    fn qr_new(
26        video_elem: &web_sys::HtmlVideoElement,
27        callback: &js_sys::Function,
28        options: &JsValue,
29    ) -> QrScanner;
30
31    #[wasm_bindgen(method, js_name = start)]
32    fn qr_start(this: &QrScanner);
33    #[wasm_bindgen(method, js_name = stop)]
34    fn qr_stop(this: &QrScanner);
35    #[wasm_bindgen(method, js_name = destroy)]
36    fn qr_destroy(this: &QrScanner);
37}
38
39#[wasm_bindgen]
40pub fn process_js_value_with_cast(js_value: JsValue) -> Result<String, JsValue> {
41    // Attempt to cast JsValue to an Object
42    if let Ok(obj) = js_value.dyn_into::<Object>() {
43        // Try to get the 'data' property from the object
44        if let Ok(data) = js_sys::Reflect::get(&obj, &JsValue::from_str("data")) {
45            // Convert the value to a string if possible
46            if let Some(data_string) = data.as_string() {
47                return Ok(data_string);
48            }
49        }
50    }
51
52    // Return an error if the extraction fails
53    Err(JsValue::from_str("Failed to extract the data properly"))
54}
55
56#[component]
57pub fn Scan<A, F>(
58    active: A,
59    on_scan: F,
60    class: &'static str,
61    video_class: &'static str,
62) -> impl IntoView
63where
64    A: SignalGet<Value = bool> + 'static,
65    F: Fn(String) + 'static,
66{
67    let video_ref = create_node_ref::<Video>();
68    let (error, set_error) = create_signal(None);
69
70    let o_scanner: StoredValue<Option<QrScanner>> = store_value(None);
71
72    let on_scan = Arc::new(on_scan);
73    let scan = move || {
74        if let Some(video) = video_ref.get() {
75            let on_scan_inner = on_scan.clone();
76            let callback = Closure::wrap(Box::new(move |result: JsValue| {
77                match process_js_value_with_cast(result) {
78                    Ok(data) => {
79                        on_scan_inner(data);
80                    }
81                    Err(e) => {
82                        let error_message = format!("Error: {:?}", e);
83                        set_error.set(Some(error_message));
84                    }
85                };
86            }) as Box<dyn Fn(JsValue)>);
87
88            // Options of `QrScanner`
89            // JS: {returnDetailedScanResult: true} - Enforce reporting detailed scan results
90            // https://github.com/nimiq/qr-scanner/#2-create-a-qrscanner-instance
91            let options = js_sys::Object::new();
92            js_sys::Reflect::set(
93                &options,
94                &JsValue::from_str("returnDetailedScanResult"),
95                &JsValue::from_bool(true),
96            )
97            .unwrap();
98
99            let scanner = QrScanner::qr_new(&video, callback.as_ref().unchecked_ref(), &options);
100            scanner.qr_start();
101            callback.forget();
102
103            o_scanner.set_value(Some(scanner));
104        }
105    };
106
107    let cancel = move || {
108        if let Some(scanner) = o_scanner.get_value() {
109            scanner.qr_stop();
110            scanner.qr_destroy();
111            o_scanner.set_value(None);
112        }
113    };
114
115    create_effect(move |_| {
116        if active.get() {
117            scan();
118        } else {
119            cancel();
120        }
121    });
122
123    view! {
124        <div class=class>
125            <video _ref=video_ref class=video_class style="object-fit: cover;"></video>
126            <Show
127                when=move || error.get().is_some()
128                fallback=|| {
129                    view! { "" }
130                }
131            >
132                <p>{error.get()}</p>
133            </Show>
134        </div>
135    }
136}