use super::launcher::{rel_to, unix_now};
use super::*;
use crate::vpinplay::{self, Event, PushTable};
impl App {
fn push_candidates(&self) -> Vec<(String, PushTable)> {
let dir = std::path::PathBuf::from(&self.tables_dir);
self.tables
.iter()
.filter_map(|t| {
let vps_id = t.vps_id.clone().filter(|_| t.vps_shareable)?;
if t.user.rating_tenths == 0 && t.user.start_count == 0 {
return None;
}
Some((
rel_to(&dir, &t.path),
PushTable {
vps_id,
vpx_path: t.path.clone(),
filename: t
.path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default(),
rating_tenths: t.user.rating_tenths,
last_run: t.user.last_run,
start_count: t.user.start_count,
run_time_secs: t.user.run_time,
},
))
})
.collect()
}
fn spawn_vpinplay(&mut self, job: impl FnOnce() -> Event + Send + 'static) {
let (tx, rx) = crossbeam_channel::unbounded();
self.vpinplay_rx = Some(rx);
std::thread::Builder::new()
.name("pinready-vpinplay".into())
.spawn(move || {
let _ = tx.send(job());
})
.ok();
}
pub(super) fn start_ratings_refresh_if_stale(&mut self) {
if !self.vpinplay.enabled || self.vpinplay_rx.is_some() {
return;
}
let fetched_at: i64 = self
.db
.get_config(vpinplay::KEY_RATINGS_AT)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if !self.community.is_empty() && unix_now() - fetched_at < vpinplay::RATINGS_MAX_AGE_SECS {
return;
}
let endpoint = self.vpinplay.endpoint.clone();
self.spawn_vpinplay(move || {
Event::Ratings(vpinplay::fetch_ratings(&endpoint).map_err(|e| format!("{e:#}")))
});
}
pub(super) fn push_after_session(&mut self, rel_path: &str) {
if !self.vpinplay.ready() {
return;
}
let Some((_, table)) = self
.push_candidates()
.into_iter()
.find(|(rel, _)| rel == rel_path)
else {
return;
};
let settings = self.vpinplay.clone();
self.spawn_vpinplay(move || {
Event::Pushed(
vpinplay::push(&settings, &[table], unix_now()).map_err(|e| format!("{e:#}")),
)
});
}
fn push_everything(&mut self) {
if !self.vpinplay.ready() {
return;
}
let tables: Vec<PushTable> = self.push_candidates().into_iter().map(|(_, t)| t).collect();
let settings = self.vpinplay.clone();
self.vpinplay_status = t!("vpinplay_sending", count = tables.len()).to_string();
self.spawn_vpinplay(move || {
Event::Pushed(
vpinplay::push(&settings, &tables, unix_now()).map_err(|e| format!("{e:#}")),
)
});
}
fn check_user_name(&mut self) {
let endpoint = self.vpinplay.endpoint.clone();
let name = self.vpinplay.user_id.clone();
self.vpinplay_status = t!("vpinplay_checking").to_string();
self.spawn_vpinplay(move || {
Event::NameChecked(
vpinplay::user_available(&endpoint, &name).map_err(|e| format!("{e:#}")),
)
});
}
pub(super) fn process_vpinplay(&mut self) {
let Some(rx) = &self.vpinplay_rx else {
return;
};
let event = match rx.try_recv() {
Ok(event) => event,
Err(crossbeam_channel::TryRecvError::Empty) => return,
Err(crossbeam_channel::TryRecvError::Disconnected) => {
self.vpinplay_rx = None;
return;
}
};
self.vpinplay_rx = None;
match event {
Event::Ratings(Ok(ratings)) => {
log::info!(
"VPinPlay: community ratings loaded for {} tables",
ratings.len()
);
if let Err(e) = self.db.replace_community_ratings(&ratings, unix_now()) {
log::error!("could not keep the community ratings: {e}");
}
self.vpinplay_status = t!("vpinplay_ratings_ok", count = ratings.len()).to_string();
self.community = ratings;
}
Event::Ratings(Err(e)) => {
log::warn!("VPinPlay ratings: {e}");
self.vpinplay_status = t!("vpinplay_error", error = e).to_string();
}
Event::Pushed(Ok(count)) => {
self.vpinplay_status = t!("vpinplay_sent", count = count).to_string();
}
Event::Pushed(Err(e)) => {
log::warn!("VPinPlay sync: {e}");
self.vpinplay_status = t!("vpinplay_error", error = e).to_string();
}
Event::NameChecked(Ok(true)) => {
self.vpinplay_status = t!("vpinplay_name_free").to_string();
}
Event::NameChecked(Ok(false)) => {
self.vpinplay_status = t!("vpinplay_name_taken").to_string();
}
Event::NameChecked(Err(e)) => {
self.vpinplay_status = t!("vpinplay_error", error = e).to_string();
}
}
}
pub(super) fn render_vpinplay_section(&mut self, ui: &mut egui::Ui) {
self.process_vpinplay();
theme::section(
ui,
&t!("vpinplay_heading"),
Some(&t!("vpinplay_intro")),
|ui| {
ui.colored_label(theme::pal(ui.ctx()).accent_text, t!("vpinplay_recommend"));
ui.add_space(6.0);
let mut enabled = self.vpinplay.enabled;
ui.horizontal(|ui| {
ui.checkbox(&mut enabled, t!("vpinplay_toggle"));
help_marker(ui, &t!("vpinplay_privacy"));
});
if enabled != self.vpinplay.enabled {
if let Err(e) = self.vpinplay.set_enabled(&self.db, enabled) {
log::error!("could not store the VPinPlay choice: {e}");
}
if enabled {
self.start_ratings_refresh_if_stale();
}
}
ui.add_space(4.0);
ui.label(egui::RichText::new(t!("vpinplay_privacy")).weak());
if !self.vpinplay.enabled {
return;
}
ui.add_space(8.0);
let mut user = self.vpinplay.user_id.clone();
let mut initials = self.vpinplay.initials.clone();
ui.horizontal(|ui| {
ui.label(t!("vpinplay_user"));
ui.add(egui::TextEdit::singleline(&mut user).desired_width(220.0));
ui.label(t!("vpinplay_initials"));
ui.add(egui::TextEdit::singleline(&mut initials).desired_width(60.0));
});
if (user != self.vpinplay.user_id || initials != self.vpinplay.initials)
&& let Err(e) = self.vpinplay.set_identity(&self.db, &user, &initials)
{
log::error!("could not store the VPinPlay identity: {e}");
}
if !self.vpinplay.ready() {
ui.colored_label(theme::pal(ui.ctx()).warning, t!("vpinplay_need_identity"));
return;
}
ui.horizontal(|ui| {
if ui.button(t!("vpinplay_check_name")).clicked() {
self.check_user_name();
}
if ui.button(t!("vpinplay_sync_now")).clicked() {
self.push_everything();
}
});
if !self.vpinplay_status.is_empty() {
ui.label(&self.vpinplay_status);
}
if self.vpinplay_rx.is_some() {
ui.ctx().request_repaint();
}
},
);
}
}