Many fixes related to error message passing
This commit is contained in:
parent
e2952cb614
commit
4bc1ef4ba2
@ -1,4 +1,3 @@
|
||||
use hex;
|
||||
use serde::Deserialize;
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
@ -87,9 +86,9 @@ pub fn merge_colors(from_color: Rgb, to_color: Rgb, factor: f32) -> Rgb {
|
||||
let (to_r, to_g, to_b) = to_color.to_float_tuple();
|
||||
|
||||
// TODO: Do not use as u8
|
||||
let r = ((to_r - from_r) * factor + from_r) as u8;
|
||||
let g = ((to_g - from_g) * factor + from_g) as u8;
|
||||
let b = ((to_b - from_b) * factor + from_b) as u8;
|
||||
let r = (to_r - from_r).mul_add(factor, from_r) as u8;
|
||||
let g = (to_g - from_g).mul_add(factor, from_g) as u8;
|
||||
let b = (to_b - from_b).mul_add(factor, from_b) as u8;
|
||||
|
||||
Rgb(r, g, b)
|
||||
}
|
||||
|
@ -1,11 +1,23 @@
|
||||
use core::any::Any;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
pub type ProgramResult<T> = Result<T, ProgramError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Error(ProgramError),
|
||||
Terminated,
|
||||
String(String),
|
||||
InputPrompt(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProgramError {
|
||||
General(String),
|
||||
UiError(String),
|
||||
Boxed(Box<dyn Any + Send>),
|
||||
IoError(io::Error),
|
||||
}
|
||||
|
||||
impl From<String> for ProgramError {
|
||||
@ -32,6 +44,7 @@ impl fmt::Display for ProgramError {
|
||||
Self::General(s) => write!(f, "{}", s),
|
||||
Self::UiError(s) => write!(f, "Critial UI error: {}", s),
|
||||
Self::Boxed(s) => write!(f, "{:?}", s),
|
||||
Self::IoError(e) => write!(f, "{:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
src/main.rs
72
src/main.rs
@ -32,16 +32,22 @@ mod pattern;
|
||||
mod strip;
|
||||
mod ui;
|
||||
mod webui;
|
||||
use errors::ProgramError;
|
||||
use errors::{ProgramError, ProgramResult};
|
||||
use std::io;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::io::Write;
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::thread;
|
||||
use strip::{LedStrip, Message};
|
||||
use strip::LedStrip;
|
||||
use ui::console_ui_loop;
|
||||
|
||||
fn main() -> Result<(), ProgramError> {
|
||||
let (tx, rx) = channel::<Message>();
|
||||
let strip_handle = thread::spawn(move || -> Result<(), ProgramError> {
|
||||
fn main() -> ProgramResult<()> {
|
||||
// Strip control transmitter and receiver
|
||||
let (strip_tx, strip_rx) = channel::<strip::Message>();
|
||||
let (console_strip_tx, webui_strip_tx) = (strip_tx.clone(), strip_tx);
|
||||
|
||||
let (message_tx, message_rx) = channel::<errors::Message>();
|
||||
|
||||
make_child(message_tx.clone(), move |message_tx| -> ProgramResult<()> {
|
||||
let mut strip = LedStrip::new(strip::Config {
|
||||
// I have 89 right now, but start off with 20
|
||||
num_lights: 89,
|
||||
@ -51,25 +57,47 @@ fn main() -> Result<(), ProgramError> {
|
||||
global_brightness_max: 255,
|
||||
tick_time_ms: strip::DEFAULT_TICK_TIME_MS,
|
||||
})?;
|
||||
strip.strip_loop(&rx)
|
||||
strip.strip_loop(message_tx, &strip_rx)
|
||||
});
|
||||
|
||||
// I do not care if the console ui crashes
|
||||
let (g, h) = (tx.clone(), tx.clone());
|
||||
let _console_ui_handle = thread::spawn(move || -> Result<(), ProgramError> {
|
||||
let ret = console_ui_loop(&g);
|
||||
println!("Console ui dead: {:?}", ret);
|
||||
ret
|
||||
make_child(message_tx.clone(), move |message_tx| -> ProgramResult<()> {
|
||||
console_ui_loop(message_tx, &console_strip_tx)
|
||||
});
|
||||
|
||||
// I do not care if the web ui crashes
|
||||
let _web_ui_handle = thread::spawn(move || -> Result<(), io::Error> {
|
||||
let ret = webui::start(h);
|
||||
println!("Webui dead: {:?}", ret);
|
||||
ret
|
||||
// TODO: Do not join -- this is just because we are running on a computer with no spi env
|
||||
})
|
||||
.join()?;
|
||||
make_child(message_tx, move |message_tx| -> ProgramResult<()> {
|
||||
webui::start(message_tx.clone(), webui_strip_tx).map_err(ProgramError::IoError)
|
||||
});
|
||||
|
||||
strip_handle.join()?
|
||||
let mut input_prompt: Option<String> = None;
|
||||
loop {
|
||||
match message_rx.recv() {
|
||||
Ok(errors::Message::String(s)) => println!("\r{}", s),
|
||||
Ok(errors::Message::Error(e)) => println!("\rError!! {:?}", e),
|
||||
Ok(errors::Message::Terminated) => {
|
||||
panic!("A thread terminated")
|
||||
}
|
||||
Ok(errors::Message::InputPrompt(i)) => input_prompt = Some(i),
|
||||
Err(e) => {
|
||||
return Err(ProgramError::General(format!(
|
||||
"All transmitters hung up! {:?}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
}
|
||||
if let Some(ref s) = input_prompt {
|
||||
print!("{}: ", s);
|
||||
// We do not care if we can't flush
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_child<F: 'static>(message_tx: Sender<errors::Message>, f: F)
|
||||
where
|
||||
F: FnOnce(&Sender<errors::Message>) -> ProgramResult<()> + std::marker::Send,
|
||||
{
|
||||
thread::spawn(move || match f(&message_tx) {
|
||||
Ok(()) => message_tx.send(errors::Message::Terminated),
|
||||
Err(e) => message_tx.send(errors::Message::Error(e)),
|
||||
});
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ impl Pattern for MovingRainbow {
|
||||
Ok(true)
|
||||
}
|
||||
fn init(&mut self, num_lights: u16) -> Result<(), ()> {
|
||||
if num_lights < 1 || num_lights > 255 {
|
||||
if !(1..=255).contains(&num_lights) {
|
||||
return Err(());
|
||||
}
|
||||
if self.width < 1 {
|
||||
@ -39,14 +39,15 @@ impl Pattern for MovingRainbow {
|
||||
|
||||
// The length of the buffer
|
||||
// Always a factor of RAINBOW.len() * width
|
||||
let length_factor = u16::try_from(RAINBOW.len()).or(Err(()))? * u16::from(self.width);
|
||||
let length_factor = u16::try_from(RAINBOW.len()).or(Err(()))?.saturating_mul(u16::from(self.width));
|
||||
let buf_length = num_lights
|
||||
.checked_sub(1)
|
||||
.ok_or(())?
|
||||
.div_euclid(length_factor)
|
||||
.checked_add(1)
|
||||
.ok_or(())?
|
||||
* length_factor;
|
||||
.saturating_mul(length_factor)
|
||||
;
|
||||
|
||||
self.lights_buf = RAINBOW
|
||||
.iter()
|
||||
|
@ -32,7 +32,7 @@ impl Orb {
|
||||
color,
|
||||
center_width,
|
||||
backoff_width,
|
||||
total_width: center_width + backoff_width * 2,
|
||||
total_width: center_width.saturating_add(backoff_width.saturating_mul(2)),
|
||||
bounces: false,
|
||||
step: 0,
|
||||
step_max: 0,
|
||||
@ -75,7 +75,7 @@ impl Pattern for Orb {
|
||||
self.lights_buf = iter::empty::<Rgb>()
|
||||
.chain(ramp.clone().into_iter())
|
||||
.chain(iter::repeat(self.color).take(self.center_width.into()))
|
||||
.chain(ramp.clone().into_iter().rev())
|
||||
.chain(ramp.into_iter().rev())
|
||||
.chain(
|
||||
iter::repeat(other_color)
|
||||
.take(num_lights.saturating_sub(self.total_width.into()).into()),
|
||||
@ -86,7 +86,7 @@ impl Pattern for Orb {
|
||||
.lights_buf
|
||||
.len()
|
||||
.checked_sub(self.total_width.into())
|
||||
.unwrap_or(self.lights_buf.len());
|
||||
.unwrap_or_else(|| self.lights_buf.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
30
src/strip.rs
30
src/strip.rs
@ -1,10 +1,11 @@
|
||||
use crate::color;
|
||||
use crate::errors;
|
||||
use crate::errors::ProgramError;
|
||||
use crate::pattern::{self, Pattern};
|
||||
use std::cmp;
|
||||
use std::ops::Add;
|
||||
use std::process;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::time::{Duration, Instant};
|
||||
use ws2818_rgb_led_spi_driver::adapter_gen::Ws28xxAdapter;
|
||||
use ws2818_rgb_led_spi_driver::adapter_spi::Ws28xxSpiAdapter;
|
||||
@ -99,7 +100,11 @@ impl LedStrip {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn strip_loop(&mut self, rx: &Receiver<Message>) -> Result<(), ProgramError> {
|
||||
pub fn strip_loop(
|
||||
&mut self,
|
||||
message_tx: &Sender<errors::Message>,
|
||||
rx: &Receiver<Message>,
|
||||
) -> Result<(), ProgramError> {
|
||||
let mut exit = false;
|
||||
loop {
|
||||
let target_time = Instant::now().add(Duration::from_millis(self.config.tick_time_ms));
|
||||
@ -111,7 +116,10 @@ impl LedStrip {
|
||||
if pat.init(self.config.num_lights).is_ok() {
|
||||
self.pattern = pat;
|
||||
} else {
|
||||
println!("Clearing light strip: {:?}", pat);
|
||||
let _ = message_tx.send(errors::Message::String(format!(
|
||||
"Clearing light strip: {:?}",
|
||||
pat
|
||||
)));
|
||||
}
|
||||
}
|
||||
Message::ChangePattern(pat) => {
|
||||
@ -119,7 +127,10 @@ impl LedStrip {
|
||||
if pat.init(self.config.num_lights).is_ok() {
|
||||
self.pattern = pat;
|
||||
} else {
|
||||
println!("Error with pattern: {:?}", pat);
|
||||
let _ = message_tx.send(errors::Message::String(format!(
|
||||
"Error with pattern: {:?}",
|
||||
pat
|
||||
)));
|
||||
}
|
||||
}
|
||||
Message::SetNumLights(num_lights) => {
|
||||
@ -127,7 +138,10 @@ impl LedStrip {
|
||||
}
|
||||
Message::SetTickTime(tick_time_ms) => {
|
||||
if tick_time_ms < MIN_TICK_TIME {
|
||||
println!("Error with tick time: {}", tick_time_ms);
|
||||
let _ = message_tx.send(errors::Message::String(format!(
|
||||
"Error with tick time: {}",
|
||||
tick_time_ms
|
||||
)));
|
||||
}
|
||||
self.config.tick_time_ms = tick_time_ms;
|
||||
}
|
||||
@ -137,7 +151,9 @@ impl LedStrip {
|
||||
if pat.init(self.config.num_lights).is_ok() {
|
||||
self.pattern = Box::new(pat);
|
||||
} else {
|
||||
println!("Could not construct clear pattern");
|
||||
let _ = message_tx.send(errors::Message::String(
|
||||
String::from("Could not construct clear pattern")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -152,7 +168,7 @@ impl LedStrip {
|
||||
}
|
||||
|
||||
if exit {
|
||||
println!("Exiting as requested");
|
||||
let _ = message_tx.send(errors::Message::String(String::from("Exiting as requested")));
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
|
57
src/ui.rs
57
src/ui.rs
@ -1,21 +1,24 @@
|
||||
use crate::color::Rgb;
|
||||
use crate::errors::ProgramError;
|
||||
use crate::errors::{self, ProgramError, ProgramResult};
|
||||
use crate::pattern::{self, Pattern};
|
||||
use crate::strip;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub fn console_ui_loop(tx: &Sender<strip::Message>) -> Result<(), ProgramError> {
|
||||
pub fn console_ui_loop(
|
||||
message_tx: &Sender<errors::Message>,
|
||||
strip_tx: &Sender<strip::Message>,
|
||||
) -> ProgramResult<()> {
|
||||
loop {
|
||||
let line = get_line("Command (cfqs)")?;
|
||||
if let Err(msg) = parse_cmd(tx, &line) {
|
||||
let line = get_line(message_tx, "Command (cfqs)")?;
|
||||
if let Err(msg) = parse_cmd(strip_tx, &line) {
|
||||
println!("Command error: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cmd(tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
|
||||
fn parse_cmd(strip_tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
|
||||
match s
|
||||
.split(char::is_whitespace)
|
||||
.collect::<Vec<&str>>()
|
||||
@ -30,14 +33,14 @@ fn parse_cmd(tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
|
||||
b.parse::<u8>()
|
||||
.map_err(|_| String::from("Blue could not be parsed"))?,
|
||||
);
|
||||
change_pattern(tx, Box::new(pattern::Fade::new(color)))
|
||||
change_pattern(strip_tx, Box::new(pattern::Fade::new(color)))
|
||||
}
|
||||
["f", c] => {
|
||||
let color_value = c
|
||||
.parse::<u8>()
|
||||
.map_err(|_| String::from("Could not parse color"))?;
|
||||
let color = Rgb(color_value, color_value, color_value);
|
||||
change_pattern(tx, Box::new(pattern::Fade::new(color)))
|
||||
change_pattern(strip_tx, Box::new(pattern::Fade::new(color)))
|
||||
}
|
||||
["m", r, g, b] => {
|
||||
let color = Rgb(
|
||||
@ -48,49 +51,53 @@ fn parse_cmd(tx: &Sender<strip::Message>, s: &str) -> Result<(), String> {
|
||||
b.parse::<u8>()
|
||||
.map_err(|_| String::from("Blue could not be parsed"))?,
|
||||
);
|
||||
change_pattern(tx, Box::new(pattern::MovingPixel::new(color)))
|
||||
change_pattern(strip_tx, Box::new(pattern::MovingPixel::new(color)))
|
||||
}
|
||||
["m", c] => {
|
||||
let color_value = c
|
||||
.parse::<u8>()
|
||||
.map_err(|_| String::from("Could not parse color"))?;
|
||||
let color = Rgb(color_value, color_value, color_value);
|
||||
change_pattern(tx, Box::new(pattern::MovingPixel::new(color)))
|
||||
change_pattern(strip_tx, Box::new(pattern::MovingPixel::new(color)))
|
||||
},
|
||||
["c", r, g, b] => {
|
||||
let color = parse_color(r, g, b)?;
|
||||
change_pattern(tx, Box::new(pattern::Solid::new(color)))
|
||||
change_pattern(strip_tx, Box::new(pattern::Solid::new(color)))
|
||||
}
|
||||
["c", c] => {
|
||||
let color = parse_color(c, c, c)?;
|
||||
change_pattern(tx, Box::new(pattern::Solid::new(color)))
|
||||
change_pattern(strip_tx, Box::new(pattern::Solid::new(color)))
|
||||
},
|
||||
["r"] =>change_pattern(tx, Box::new(pattern::MovingRainbow::new(4, true))),
|
||||
["r"] =>change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(4, true))),
|
||||
["r", w] => {
|
||||
let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?;
|
||||
change_pattern(tx, Box::new(pattern::MovingRainbow::new(width, true)))
|
||||
change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(width, true)))
|
||||
},
|
||||
["r", w, f] => {
|
||||
let width = w.parse::<u8>().map_err(|_| String::from("Width could not be parsed"))?;
|
||||
change_pattern(tx, Box::new(pattern::MovingRainbow::new(width, ["t", "T"].contains(f))))
|
||||
change_pattern(strip_tx, Box::new(pattern::MovingRainbow::new(width, ["t", "T"].contains(f))))
|
||||
},
|
||||
["b", r1, g1, b1, r2, g2, b2, r3, g3, b3] => {
|
||||
let left = parse_color(r1, g1, b1)?;
|
||||
let right = parse_color(r2, g2, b2)?;
|
||||
let combined = parse_color(r3, g3, b3)?;
|
||||
change_pattern(tx, Box::new(pattern::Collide::new(left, right, combined)))
|
||||
change_pattern(strip_tx, Box::new(pattern::Collide::new(left, right, combined)))
|
||||
}
|
||||
["x"] => tx
|
||||
["x"] => strip_tx
|
||||
.send(strip::Message::ClearLights)
|
||||
.map_err(|e| e.to_string()),
|
||||
["q"] => tx.send(strip::Message::Quit).map_err(|e| e.to_string()),
|
||||
["s", n] => tx
|
||||
["q"] => {
|
||||
strip_tx.send(strip::Message::Quit).map_err(|e| e.to_string())?;
|
||||
// TODO
|
||||
panic!("i");
|
||||
},
|
||||
["s", n] => strip_tx
|
||||
.send(strip::Message::SetNumLights(
|
||||
n.parse::<u16>()
|
||||
.map_err(|_| String::from("Could not parse light count"))?,
|
||||
))
|
||||
.map_err(|e| e.to_string()),
|
||||
["t", n] => tx.send(strip::Message::SetTickTime(
|
||||
["t", n] => strip_tx.send(strip::Message::SetTickTime(
|
||||
n.parse::<u64>()
|
||||
.map_err(|_| String::from("Could not parse light count"))?,
|
||||
))
|
||||
@ -110,13 +117,17 @@ fn parse_color(r: &str, g: &str, b: &str) -> Result<Rgb, String> {
|
||||
))
|
||||
}
|
||||
|
||||
fn change_pattern(tx: &Sender<strip::Message>, pat: Box<dyn Pattern + Send>) -> Result<(), String> {
|
||||
tx.send(strip::Message::ChangePattern(pat))
|
||||
fn change_pattern(
|
||||
strip_tx: &Sender<strip::Message>,
|
||||
pat: Box<dyn Pattern + Send>,
|
||||
) -> Result<(), String> {
|
||||
strip_tx
|
||||
.send(strip::Message::ChangePattern(pat))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn get_line(prompt: &str) -> Result<String, ProgramError> {
|
||||
print!("{}: ", prompt);
|
||||
fn get_line(message_tx: &Sender<errors::Message>, prompt: &str) -> ProgramResult<String> {
|
||||
let _ = message_tx.send(errors::Message::InputPrompt(String::from(prompt)));
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.map_err(|_| ProgramError::UiError(String::from("Could not flush stdout")))?;
|
||||
|
23
src/webui.rs
23
src/webui.rs
@ -1,9 +1,10 @@
|
||||
use crate::errors;
|
||||
use crate::pattern;
|
||||
use crate::strip;
|
||||
use actix_web::error::JsonPayloadError;
|
||||
use actix_web::web::JsonConfig;
|
||||
use actix_web::{post, web, App, HttpServer, Responder, Result};
|
||||
use actix_web_static_files;
|
||||
use actix_web_static_files::ResourceFiles;
|
||||
use std::io;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@ -23,38 +24,42 @@ async fn set_color(
|
||||
println!("Got params: {:?}", params);
|
||||
data.strip_tx
|
||||
.lock()
|
||||
.or(Err(io::Error::new(
|
||||
.map_err(|_| io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed to get a lock",
|
||||
)))?
|
||||
))?
|
||||
.send(strip::Message::ChangePattern(params.0.to_pattern()))
|
||||
.or(Err(io::Error::new(
|
||||
.map_err(|_| io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed to send to channel",
|
||||
)))?;
|
||||
))?;
|
||||
Ok(format!("{:?}", params))
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
pub async fn start(tx: Sender<strip::Message>) -> std::io::Result<()> {
|
||||
println!("Starting webui");
|
||||
pub async fn start(
|
||||
message_tx: Sender<errors::Message>,
|
||||
strip_tx: Sender<strip::Message>,
|
||||
) -> std::io::Result<()> {
|
||||
let _ = message_tx.send(errors::Message::String(String::from("Starting webui")));
|
||||
HttpServer::new(move || {
|
||||
let generated = generate();
|
||||
App::new()
|
||||
.data(AppState {
|
||||
strip_tx: Arc::new(Mutex::new(tx.clone())),
|
||||
strip_tx: Arc::new(Mutex::new(strip_tx.clone())),
|
||||
})
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.app_data(
|
||||
JsonConfig::default().error_handler(|err: JsonPayloadError, _req| {
|
||||
// let _ = message_tx.send(errors::Message::String(format!("JSON error: {:?}", err)));
|
||||
println!("JSON error: {:?}", err);
|
||||
err.into()
|
||||
}),
|
||||
)
|
||||
.service(set_color),
|
||||
)
|
||||
.service(actix_web_static_files::ResourceFiles::new("/", generated))
|
||||
.service(ResourceFiles::new("/", generated))
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
.run()
|
||||
|
@ -62,17 +62,17 @@
|
||||
const response = fetch(url, {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
// mode: 'no-cors', // no-cors, *cors, same-origin
|
||||
//cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
//credentials: 'same-origin', // include, *same-origin, omit
|
||||
// cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
// credentials: 'same-origin', // include, *same-origin, omit
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
//redirect: 'follow', // manual, *follow, error
|
||||
//referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
|
||||
// redirect: 'follow', // manual, *follow, error
|
||||
// referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
|
||||
body: JSON.stringify(getFormData()) // body data type must match "Content-Type" header
|
||||
}).then(r => console.log(response));
|
||||
//return response.json(); // parses JSON response into native JavaScript objects
|
||||
// return response.json(); // parses JSON response into native JavaScript objects
|
||||
}
|
||||
</script>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user