diff --git a/index.html b/index.html
index c60e235..ce27b9c 100644
--- a/index.html
+++ b/index.html
@@ -1,148 +1,379 @@
-
+
-
-
+
+
-
+
- eframe template
+ Waffle - Template
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
- Loading…
-
-
+
Loading…
+
-
+
+
+
+
+
+
+
+
diff --git a/src/app.rs b/src/app.rs
index 08cd25c..d84b367 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -3,15 +3,11 @@ use crate::utility::show_loading_spinner_custom;
use egui::Id;
use crate::db::github::{GithubDb, Repository};
use crate::db::idb::LANGUAGES;
+use crate::erust::uiux::search::SearchWidget;
+use crate::erust::state::{AppState, WaffleState};
+use crate::erust::uiux::auth::AuthWidget;
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq)]
-pub enum AppState {
- Init,
- Normal,
- Empty,
-}
-
-#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub enum LoadingState {
Idle,
Loading {
@@ -58,9 +54,15 @@ pub struct TemplateApp {
#[serde(skip)]
pending_app_state: Option,
#[serde(skip)]
+ waffle_state: WaffleState,
+ #[serde(skip)]
filtered_repos: Option>,
#[serde(skip)]
filter_loading: bool,
+ #[serde(skip)]
+ search_widget: Option,
+ #[serde(skip)]
+ auth_widget: AuthWidget,
}
impl Default for TemplateApp {
@@ -77,8 +79,11 @@ impl Default for TemplateApp {
toast_timer: 0.0,
app_state: AppState::Init,
pending_app_state: None,
+ waffle_state: WaffleState::new(),
filtered_repos: None,
filter_loading: false,
+ search_widget: Some(SearchWidget::new()),
+ auth_widget: AuthWidget::new(false),
}
}
}
@@ -141,17 +146,10 @@ impl TemplateApp {
pub fn filter_repos_async(&mut self, query: &str, ctx: &egui::Context) {
self.filter_loading = true;
self.filtered_repos = None;
- let query = query.to_string();
- let language = self.db.get_language();
- let ctx = ctx.clone(); // keep this, as it is used in the async block
- wasm_bindgen_futures::spawn_local(async move {
- let result = match crate::db::idb::open_waffle_db().await {
- Ok(db_conn) => crate::db::idb::filter_repos_in_idb::(&db_conn, &language, &query).await.unwrap_or_default(),
- Err(_) => vec![],
- };
- ctx.data_mut(|d| d.insert_temp(Id::new("waffle_filtered_repos"), result));
- ctx.request_repaint();
- });
+ if let Some(widget) = &mut self.search_widget {
+ widget.query = query.to_string();
+ widget.search(&self.db.get_language(), ctx);
+ }
}
pub fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
@@ -300,6 +298,14 @@ impl TemplateApp {
let filtered = self.filtered_repos.as_ref().cloned().unwrap_or_default();
ui.separator();
ui.label(format!("Results: {}", filtered.len()));
+ // --- Show app state at the bottom ---
+ ui.separator();
+ ui.label(format!("App State: {:?}", self.waffle_state.app_state));
+ if !self.waffle_state.log.is_empty() {
+ ui.separator();
+ ui.label("App Log:");
+ ui.label(&self.waffle_state.log);
+ }
});
egui::CentralPanel::default().show(ctx, |ui| {
// --- Logo image loading and display using egui_extras loader system ---
@@ -337,16 +343,26 @@ impl TemplateApp {
}
});
+ // --- Authentication Widget (Login/Register) ---
+ egui::Window::new("Authentication")
+ .collapsible(false)
+ .resizable(false)
+ .anchor(egui::Align2::CENTER_TOP, egui::Vec2::new(0.0, 40.0))
+ .show(ctx, |ui| {
+ self.auth_widget.show(ctx, ui);
+ });
+
// Update filtered_repos from egui context temp data if available
- if let Some(repos) = ctx.data(|d| d.get_temp::>(Id::new("waffle_filtered_repos"))) {
- let is_empty = repos.is_empty();
- self.filtered_repos = Some(repos.clone());
- // Set app_state based on whether there is data
- if is_empty {
- self.app_state = AppState::Empty;
+ if let Some(widget) = &mut self.search_widget {
+ widget.update_results_from_ctx(ctx);
+ // Use WaffleState to manage app state
+ if !widget.results.is_empty() {
+ self.waffle_state.set_ready(widget.results.clone());
} else {
- self.app_state = AppState::Normal;
+ self.waffle_state.set_empty();
}
+ self.filtered_repos = Some(self.waffle_state.filtered_repos.clone());
+ self.app_state = self.waffle_state.app_state.clone();
}
// Show welcome dialog if DB is empty
diff --git a/src/core/mod.rs b/src/core/mod.rs
index 037eb02..e69de29 100644
--- a/src/core/mod.rs
+++ b/src/core/mod.rs
@@ -1 +0,0 @@
-pub mod uiux;
\ No newline at end of file
diff --git a/src/core/uiux/mod.rs b/src/core/uiux/mod.rs
index e092c24..e69de29 100644
--- a/src/core/uiux/mod.rs
+++ b/src/core/uiux/mod.rs
@@ -1 +0,0 @@
-pub mod search;
\ No newline at end of file
diff --git a/src/core/uiux/search.rs b/src/core/uiux/search.rs
index b8e494d..e69de29 100644
--- a/src/core/uiux/search.rs
+++ b/src/core/uiux/search.rs
@@ -1,41 +0,0 @@
-use crate::db::github::Repository;
-use crate::db::idb;
-use egui::{Context, Id};
-
-pub struct SearchWidget {
- pub query: String,
- pub results: Vec,
- pub loading: bool,
-}
-
-impl SearchWidget {
- pub fn new() -> Self {
- Self {
- query: String::new(),
- results: Vec::new(),
- loading: false,
- }
- }
-
- pub fn search(&mut self, language: &str, ctx: &Context) {
- let query = self.query.clone();
- let language = language.to_string();
- let ctx = ctx.clone();
- self.loading = true;
- wasm_bindgen_futures::spawn_local(async move {
- let result = match idb::open_waffle_db().await {
- Ok(db_conn) => idb::filter_repos_in_idb::(&db_conn, &language, &query).await.unwrap_or_default(),
- Err(_) => vec![],
- };
- ctx.data_mut(|d| d.insert_temp(Id::new("waffle_search_results"), result));
- ctx.request_repaint();
- });
- }
-
- pub fn update_results_from_ctx(&mut self, ctx: &Context) {
- if let Some(results) = ctx.data(|d| d.get_temp::>(Id::new("waffle_search_results"))) {
- self.results = results.clone();
- self.loading = false;
- }
- }
-}
diff --git a/src/erust/mod.rs b/src/erust/mod.rs
new file mode 100644
index 0000000..43dc6d8
--- /dev/null
+++ b/src/erust/mod.rs
@@ -0,0 +1,2 @@
+pub mod uiux;
+pub mod state;
\ No newline at end of file
diff --git a/src/state.rs b/src/erust/state.rs
similarity index 60%
rename from src/state.rs
rename to src/erust/state.rs
index afc8bec..ab82a5c 100644
--- a/src/state.rs
+++ b/src/erust/state.rs
@@ -3,8 +3,10 @@ use crate::db::github::Repository;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppState {
Init,
- Normal,
Empty,
+ Syncing,
+ Normal,
+ Ready,
Error(String),
}
@@ -12,6 +14,7 @@ pub enum AppState {
pub struct WaffleState {
pub app_state: AppState,
pub filtered_repos: Vec,
+ pub log: String,
}
impl WaffleState {
@@ -19,6 +22,7 @@ impl WaffleState {
Self {
app_state: AppState::Init,
filtered_repos: Vec::new(),
+ log: String::new(),
}
}
@@ -27,21 +31,31 @@ impl WaffleState {
self.filtered_repos.clear();
}
- pub fn set_normal(&mut self, repos: Vec) {
+ pub fn set_syncing(&mut self) {
+ self.app_state = AppState::Syncing;
+ }
+
+ pub fn set_ready(&mut self, repos: Vec) {
if repos.is_empty() {
self.set_empty();
} else {
- self.app_state = AppState::Normal;
+ self.app_state = AppState::Ready;
self.filtered_repos = repos;
}
}
pub fn set_error(&mut self, msg: String) {
- self.app_state = AppState::Error(msg);
+ self.app_state = AppState::Error(msg.clone());
+ self.log.push_str(&format!("Error: {}\n", msg));
self.filtered_repos.clear();
}
+ pub fn log(&mut self, msg: &str) {
+ self.log.push_str(msg);
+ self.log.push('\n');
+ }
+
pub fn is_ready(&self) -> bool {
- matches!(self.app_state, AppState::Normal)
+ matches!(self.app_state, AppState::Ready)
}
}
diff --git a/src/erust/uiux/auth.rs b/src/erust/uiux/auth.rs
new file mode 100644
index 0000000..36ee717
--- /dev/null
+++ b/src/erust/uiux/auth.rs
@@ -0,0 +1,197 @@
+// erust/uiux/auth.rs
+use egui::{Context, Ui};
+use crate::erust::uiux::hcaptcha;
+use crate::erust::uiux::javascript_interop;
+
+pub enum AuthView {
+ Login,
+ Register,
+ Help,
+}
+
+pub struct AuthWidget {
+ pub email: String,
+ pub password: String,
+ pub confirm_password: String, // Only used for Register
+ pub captcha_token: Option,
+ pub error: Option,
+ pub view: AuthView,
+ pub is_registered: bool,
+}
+
+impl AuthWidget {
+ pub fn new(is_registered: bool) -> Self {
+ Self {
+ email: String::new(),
+ password: String::new(),
+ confirm_password: String::new(),
+ captcha_token: None,
+ error: None,
+ view: if is_registered { AuthView::Login } else { AuthView::Register },
+ is_registered,
+ }
+ }
+
+ pub fn show(&mut self, ctx: &Context, ui: &mut Ui) {
+ match self.view {
+ AuthView::Login => self.show_login(ctx, ui),
+ AuthView::Register => self.show_register(ctx, ui),
+ AuthView::Help => self.show_help(ctx, ui),
+ }
+ }
+
+ fn show_login(&mut self, _ctx: &Context, ui: &mut Ui) {
+ ui.heading("Login");
+ ui.separator();
+ ui.label("Email:");
+ ui.text_edit_singleline(&mut self.email);
+ ui.label("Password:");
+ ui.add(egui::TextEdit::singleline(&mut self.password).password(true));
+ ui.separator();
+
+ if ui.button("Solve Captcha").clicked() {
+ hcaptcha::open_captcha();
+ }
+ // Always show the captcha status message, and force a repaint if token is set
+ // Only show the solved message, not the token
+ if hcaptcha::get_captcha_token().is_some() {
+ ui.colored_label(egui::Color32::GREEN, "Captcha Solved: Token Set");
+ ui.ctx().request_repaint();
+ } else {
+ ui.label("Captcha required");
+ }
+ if ui.button("Login").clicked() {
+ if self.email.is_empty() || self.password.is_empty() || hcaptcha::get_captcha_token().is_none() {
+ self.error = Some("All fields and captcha are required".to_string());
+ } else {
+ let _token = hcaptcha::take_captcha_token();
+ // self.error = Some("(Stub) Would call Supabase login here".to_string());
+ login_with_js(self.email.as_str(), self.password.as_str(), _token.as_deref().unwrap_or(""));
+ }
+ }
+ if let Some(err) = &self.error {
+ ui.colored_label(egui::Color32::RED, err);
+ }
+ ui.horizontal(|ui| {
+ if ui.button("Register").clicked() {
+ self.view = AuthView::Register;
+ self.error = None;
+ }
+ if ui.button("Help").clicked() {
+ self.view = AuthView::Help;
+ self.error = None;
+ }
+ });
+ }
+
+ fn show_register(&mut self, _ctx: &Context, ui: &mut Ui) {
+ ui.heading("Register");
+ ui.label("Email:");
+ ui.text_edit_singleline(&mut self.email);
+ ui.label("Password:");
+ ui.add(egui::TextEdit::singleline(&mut self.password).password(true));
+ ui.label("Confirm Password:");
+ ui.add(egui::TextEdit::singleline(&mut self.confirm_password).password(true));
+ if ui.button("Solve Captcha").clicked() {
+ hcaptcha::open_captcha();
+ }
+ // Always show the captcha status message, and force a repaint if token is set
+ // Only show the solved message, not the token
+ if hcaptcha::get_captcha_token().is_some() {
+ ui.colored_label(egui::Color32::GREEN, "Captcha Solved: Token Set");
+ ui.ctx().request_repaint();
+ } else {
+ ui.label("Captcha required");
+ }
+ if ui.button("Register").clicked() {
+ if self.email.is_empty() || self.password.is_empty() || self.confirm_password.is_empty() || hcaptcha::get_captcha_token().is_none() {
+ self.error = Some("All fields and captcha are required".to_string());
+ } else if self.password != self.confirm_password {
+ self.error = Some("Passwords do not match".to_string());
+ } else {
+ let token = hcaptcha::take_captcha_token();
+ if let Some(token) = token {
+ let email = self.email.clone();
+ let password = self.password.clone();
+ self.error = Some("Processing registration...".to_string());
+ crate::erust::uiux::auth::register_with_js(
+ email.as_str(),
+ password.as_str(),
+ token.as_str(),
+ );
+ } else {
+ self.error = Some("Captcha token missing".to_string());
+ }
+ }
+ }
+ // Show spinner if processing
+ if let Some(err) = &self.error {
+ if err == "Processing registration..." {
+ ui.horizontal(|ui| {
+ ui.label("Processing registration...");
+ ui.add(egui::Spinner::default());
+ });
+ } else {
+ ui.colored_label(egui::Color32::RED, err);
+ }
+ }
+ ui.horizontal(|ui| {
+ if ui.button("Back to Login").clicked() {
+ self.view = AuthView::Login;
+ self.error = None;
+ }
+ if ui.button("Help").clicked() {
+ self.view = AuthView::Help;
+ self.error = None;
+ }
+ });
+ }
+
+ fn show_help(&mut self, _ctx: &Context, ui: &mut Ui) {
+ ui.heading("Forgot your password?");
+ ui.label("Enter your email to receive a reset link:");
+ ui.text_edit_singleline(&mut self.email);
+ if ui.button("Send Reset Link").clicked() {
+ if self.email.is_empty() {
+ self.error = Some("Email is required".to_string());
+ } else {
+ self.error = Some("(Stub) Would call Supabase password reset here".to_string());
+ }
+ }
+ if let Some(err) = &self.error {
+ ui.colored_label(egui::Color32::RED, err);
+ }
+ ui.horizontal(|ui| {
+ if ui.button("Back to Login").clicked() {
+ self.view = AuthView::Login;
+ self.error = None;
+ }
+ if ui.button("Register").clicked() {
+ self.view = AuthView::Register;
+ self.error = None;
+ }
+ });
+ }
+}
+
+/// Call this to trigger a register action via JS handler
+pub fn register_with_js(email: &str, password: &str, captcha_token: &str) {
+ javascript_interop::send_action_message(
+ "register",
+ email,
+ password,
+ captcha_token,
+ );
+}
+
+/// Call this to trigger a login action via JS handler
+pub fn login_with_js(email: &str, password: &str, captcha_token: &str) {
+ javascript_interop::send_action_message(
+ "login",
+ email,
+ password,
+ captcha_token,
+ );
+}
+
+// Make AuthWidget public for use in other modules
diff --git a/src/erust/uiux/hcaptcha.rs b/src/erust/uiux/hcaptcha.rs
new file mode 100644
index 0000000..e5e29cb
--- /dev/null
+++ b/src/erust/uiux/hcaptcha.rs
@@ -0,0 +1,47 @@
+// hcaptcha.rs - hCaptcha integration for WASM/egui
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsValue;
+use web_sys::js_sys;
+use std::cell::RefCell;
+
+thread_local! {
+ static LAST_TOKEN: RefCell