diff --git a/src/app/browser/proxy.rs b/src/app/browser/proxy.rs index afd3753b..b80e7fa5 100644 --- a/src/app/browser/proxy.rs +++ b/src/app/browser/proxy.rs @@ -1,13 +1,15 @@ //! Proxy settings dialog -mod rules; +mod ignore; +mod rule; use super::Profile; use adw::{ PreferencesGroup, PreferencesPage, prelude::{AdwDialogExt, PreferencesDialogExt, PreferencesGroupExt, PreferencesPageExt}, }; -use rules::Rules; +use ignore::Ignore; +use rule::Rule; use std::rc::Rc; pub trait Proxy { @@ -17,7 +19,8 @@ pub trait Proxy { impl Proxy for adw::PreferencesDialog { fn proxy(profile: &Rc) -> Self { // Init components - let rules = Rules::build(profile); + let ignore = Ignore::build(profile); + let rule = Rule::build(profile); // Init widget let d = adw::PreferencesDialog::builder() @@ -32,23 +35,24 @@ impl Proxy for adw::PreferencesDialog { .build(); p.add(&{ let g = PreferencesGroup::new(); - g.add(&rules.widget); + g.add(&rule.widget); g }); - /* @TODO URL entry p.add(&{ - let g = PreferencesGroup::builder().title("Test").build(); - //g.add(&Box::rules(profile)); - g - });*/ p }); - d.add( - &PreferencesPage::builder() + d.add(&{ + let p = PreferencesPage::builder() .title("Exceptions") .icon_name("action-unavailable-symbolic") - .build(), - ); + .build(); + p.add(&{ + let g = PreferencesGroup::new(); + g.add(&ignore.widget); + g + }); + p + }); d.add( &PreferencesPage::builder() @@ -61,18 +65,27 @@ impl Proxy for adw::PreferencesDialog { let profile = profile.clone(); move |_| { profile.proxy.rule.clear(); - for rule in rules.take() { - if rule.validate() { + for r in rule.take() { + if r.validate() { profile.proxy.rule.add( - rule.id, - rule.is_enabled(), - rule.priority(), - rule.request().to_string(), - rule.url().to_string(), - rule.time, + r.id, + r.is_enabled(), + r.priority(), + r.request().to_string(), + r.url().to_string(), + r.time, ) } } + profile.proxy.ignore.clear(); + for i in ignore.take() { + if i.validate() { + profile + .proxy + .ignore + .add(i.id, i.is_enabled(), i.host().to_string(), i.time) + } + } } }); d diff --git a/src/app/browser/proxy/ignore.rs b/src/app/browser/proxy/ignore.rs new file mode 100644 index 00000000..058db65b --- /dev/null +++ b/src/app/browser/proxy/ignore.rs @@ -0,0 +1,91 @@ +mod row; + +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use super::Profile; +use gtk::{ + Box, + glib::{GString, uuid_string_random}, + prelude::BoxExt, +}; +use row::Row; + +pub struct Ignore { + pub widget: Box, + rows: Rc>>, +} + +impl Ignore { + pub fn build(profile: &Rc) -> Self { + let config = profile.proxy.ignore.all(); + + let rows: Rc>> = + Rc::new(RefCell::new(HashMap::with_capacity(config.len()))); + + let form = Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .build(); + + { + let mut r = rows.borrow_mut(); + + for proxy in config { + let key = uuid_string_random(); + let row = Row::build( + proxy.id, + Some(&proxy.time), + Some(&proxy.host), + proxy.is_enabled, + { + let form = form.clone(); + let key = key.clone(); + let rows = rows.clone(); + move || form.remove(&rows.borrow_mut().remove(&key).unwrap().widget) + }, + ); + row.validate(); + form.append(&row.widget); + assert!(r.insert(key, row).is_none()) + } + } + + let add = { + let b = Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .build(); + + b.append(&row::new({ + let form = form.clone(); + let rows = rows.clone(); + move || { + let key = uuid_string_random(); + let row = Row::build(None, None, None, false, { + let rows = rows.clone(); + let key = key.clone(); + let form = form.clone(); + move || form.remove(&rows.borrow_mut().remove(&key).unwrap().widget) + }); + form.append(&row.widget); + assert!(rows.borrow_mut().insert(key, row).is_none()) + } + })); + b + }; + + let widget = Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .build(); + + widget.append(&form); + widget.append(&add); + + Self { rows, widget } + } + + pub fn take(&self) -> Vec { + self.rows.take().into_values().collect() + } +} diff --git a/src/app/browser/proxy/ignore/row.rs b/src/app/browser/proxy/ignore/row.rs new file mode 100644 index 00000000..559f4f4e --- /dev/null +++ b/src/app/browser/proxy/ignore/row.rs @@ -0,0 +1,183 @@ +use gtk::{ + Align, Box, Button, Entry, Switch, + glib::{DateTime, GString}, + prelude::{BoxExt, ButtonExt, EditableExt, WidgetExt}, +}; + +pub struct Row { + pub id: Option, + host: Entry, + status: Switch, + pub time: DateTime, + pub widget: Box, +} + +impl Row { + // Constructors + + pub fn build( + id: Option, + time: Option<&DateTime>, + host: Option<&str>, + is_enabled: bool, + on_delete: impl Fn() + 'static, + ) -> Self { + // Init components + + let status = Switch::builder() + .active(is_enabled) + .valign(Align::Center) + .build(); + + let host = Entry::builder() + .hexpand(true) + .placeholder_text("Host") + .text(host.unwrap_or_default()) + .build(); + + let delete = Button::builder() + .css_classes(["error"]) + .icon_name("user-trash-symbolic") + .tooltip_text("Delete") + .build(); + + // Init widget + + let widget = Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + + widget.append(&status); + widget.append(&host); + widget.append(&delete); + + // Activate + + delete.connect_clicked({ + let c = std::rc::Rc::new(on_delete); + move |this| { + use adw::{ + AlertDialog, ResponseAppearance, + prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, + }; + + const RESPONSE_CONFIRM: (&str, &str) = ("confirm", "Confirm"); + const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel"); + + let dialog = AlertDialog::builder() + .heading("Delete this host?") + .close_response(RESPONSE_CANCEL.0) + .default_response(RESPONSE_CONFIRM.0) + .build(); + + dialog.add_responses(&[RESPONSE_CANCEL, RESPONSE_CONFIRM]); + dialog.set_response_appearance(RESPONSE_CONFIRM.0, ResponseAppearance::Destructive); + dialog.connect_response(None, { + let c = c.clone(); + move |dialog, response| { + dialog.set_response_enabled(response, false); // prevent double-click + if response == RESPONSE_CONFIRM.0 { + c() + } + } + }); + dialog.present(Some(this)) + } + }); + + host.connect_changed(move |this| { + validate(this); + }); + + status.connect_state_set({ + let host = host.clone(); + move |_, state| { + validate(&host); + + host.set_sensitive(state); + + gtk::glib::Propagation::Proceed + } + }); + + Self { + id, + status, + time: time.cloned().unwrap_or(DateTime::now_local().unwrap()), + host, + widget, + } + } + + // Actions + + pub fn validate(&self) -> bool { + validate(&self.host) + } + + // Getters + + pub fn host(&self) -> GString { + self.host.text() + } + + pub fn is_enabled(&self) -> bool { + self.status.is_active() + } +} + +pub fn new(on_add: impl Fn() + 'static) -> Box { + let b = Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + + b.append(&{ + let add = Button::builder() + .css_classes(["success"]) + .hexpand(true) + .icon_name("list-add-symbolic") + .tooltip_text("Add proxy") + .build(); + + add.connect_clicked(move |_| on_add()); + add + }); + b +} + +fn validate(host: &Entry) -> bool { + fn highlight(entry: &Entry, error: Result<(), String>) { + const E: &str = "error"; + match error { + Err(e) => { + entry.set_css_classes(&[E]); + entry.set_tooltip_text(Some(&e)) + } + Ok(()) => { + entry.remove_css_class(E); + entry.set_tooltip_text(Some("Value is valid")) + } + } + } + + fn validate_host(value: &str) -> Result<(), String> { + match gtk::gio::InetAddress::from_string(value) { + Some(address) => { + if address.to_string() != value { + Err("Host could not be parsed properly".to_string()) + } else { + Ok(()) + } + } + None => Err("Valid host is required".to_string()), + } + } + + let v = validate_host(&host.text()); + let is_valid_host = v.is_ok(); + highlight(host, v); + + is_valid_host +} diff --git a/src/app/browser/proxy/rules.rs b/src/app/browser/proxy/rule.rs similarity index 65% rename from src/app/browser/proxy/rules.rs rename to src/app/browser/proxy/rule.rs index d8cbb9ab..24116dd6 100644 --- a/src/app/browser/proxy/rules.rs +++ b/src/app/browser/proxy/rule.rs @@ -1,6 +1,4 @@ -mod rule; - -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +mod row; use super::Profile; use gtk::{ @@ -8,18 +6,19 @@ use gtk::{ glib::{GString, uuid_string_random}, prelude::BoxExt, }; -use rule::Rule; +use row::Row; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; -pub struct Rules { +pub struct Rule { pub widget: Box, - rules: Rc>>, + rows: Rc>>, } -impl Rules { +impl Rule { pub fn build(profile: &Rc) -> Self { let config = profile.proxy.rule.all(); - let rules: Rc>> = + let rows: Rc>> = Rc::new(RefCell::new(HashMap::with_capacity(config.len()))); let form = Box::builder() @@ -28,11 +27,11 @@ impl Rules { .build(); { - let mut r = rules.borrow_mut(); + let mut r = rows.borrow_mut(); for proxy in config { let key = uuid_string_random(); - let rule = Rule::build( + let rule = Row::build( proxy.id, Some(&proxy.time), Some(&proxy.request), @@ -40,10 +39,10 @@ impl Rules { Some(proxy.priority), proxy.is_enabled, { - let rules = rules.clone(); + let rows = rows.clone(); let key = key.clone(); let form = form.clone(); - move || form.remove(&rules.borrow_mut().remove(&key).unwrap().widget) + move || form.remove(&rows.borrow_mut().remove(&key).unwrap().widget) }, ); rule.validate(); @@ -58,19 +57,19 @@ impl Rules { .spacing(8) .build(); - b.append(&rule::new({ - let rules = rules.clone(); + b.append(&row::new({ + let rows = rows.clone(); let form = form.clone(); move || { let key = uuid_string_random(); - let rule = Rule::build(None, None, None, None, None, false, { - let rules = rules.clone(); + let row = Row::build(None, None, None, None, None, false, { + let rows = rows.clone(); let key = key.clone(); let form = form.clone(); - move || form.remove(&rules.borrow_mut().remove(&key).unwrap().widget) + move || form.remove(&rows.borrow_mut().remove(&key).unwrap().widget) }); - form.append(&rule.widget); - assert!(rules.borrow_mut().insert(key, rule).is_none()) + form.append(&row.widget); + assert!(rows.borrow_mut().insert(key, row).is_none()) } })); b @@ -84,10 +83,10 @@ impl Rules { widget.append(&form); widget.append(&add); - Self { rules, widget } + Self { rows, widget } } - pub fn take(&self) -> Vec { - self.rules.take().into_values().collect() + pub fn take(&self) -> Vec { + self.rows.take().into_values().collect() } } diff --git a/src/app/browser/proxy/rules/rule.rs b/src/app/browser/proxy/rule/row.rs similarity index 98% rename from src/app/browser/proxy/rules/rule.rs rename to src/app/browser/proxy/rule/row.rs index ffc14989..673ca9b4 100644 --- a/src/app/browser/proxy/rules/rule.rs +++ b/src/app/browser/proxy/rule/row.rs @@ -4,7 +4,7 @@ use gtk::{ prelude::{BoxExt, ButtonExt, EditableExt, WidgetExt}, }; -pub struct Rule { +pub struct Row { pub id: Option, priority: Entry, request: Entry, @@ -14,7 +14,7 @@ pub struct Rule { pub widget: Box, } -impl Rule { +impl Row { // Constructors pub fn build( @@ -220,7 +220,7 @@ fn validate(priority: &Entry, url: &Entry) -> bool { Ok(uri) => { if uri.scheme().is_empty() { Err("Scheme is empty".to_string()) - } else if uri.host().is_none() { + } else if uri.host().is_none_or(|h| h.is_empty()) { Err("Host is required".to_string()) } else if uri.port() == -1 { Err("Port is required".to_string()) diff --git a/src/profile/proxy.rs b/src/profile/proxy.rs index 5feb5e9b..a66e6ed6 100644 --- a/src/profile/proxy.rs +++ b/src/profile/proxy.rs @@ -27,7 +27,7 @@ impl Proxy { pub fn save(&self) -> Result<()> { self.rule.save()?; - //self.ignore.save()?; + self.ignore.save()?; Ok(()) } diff --git a/src/profile/proxy/ignore.rs b/src/profile/proxy/ignore.rs index 33d78b85..3b95ee03 100644 --- a/src/profile/proxy/ignore.rs +++ b/src/profile/proxy/ignore.rs @@ -3,6 +3,7 @@ mod memory; use anyhow::Result; use database::Database; +use gtk::glib::DateTime; use memory::Memory; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; @@ -25,10 +26,12 @@ impl Ignore { { // build in-memory index... let mut m = memory.borrow_mut(); - for i in rows { + for row in rows { m.push(Memory { - is_enabled: i.is_enabled, - host: i.host, + id: Some(row.id), + host: row.host, + is_enabled: row.is_enabled, + time: DateTime::from_unix_local(row.time)?, }); } } @@ -36,7 +39,35 @@ impl Ignore { Ok(Self { database, memory }) } - // Actions + // Setters + + pub fn add(&self, id: Option, is_enabled: bool, host: String, time: DateTime) { + self.memory.borrow_mut().push(Memory { + id, + host, + is_enabled, + time, + }) // @TODO validate? + } + + pub fn clear(&self) { + self.memory.borrow_mut().clear(); + } + + pub fn save(&self) -> Result<()> { + let rules = self.memory.take(); + let mut keep_id = Vec::with_capacity(rules.len()); + for rule in rules { + keep_id.push(self.database.persist( + rule.id, + rule.time.to_unix(), + rule.is_enabled, + rule.host, + )?); + } + self.database.clean(keep_id)?; + Ok(()) + } // Getters diff --git a/src/profile/proxy/ignore/database.rs b/src/profile/proxy/ignore/database.rs index 190d98e7..6bbbe181 100644 --- a/src/profile/proxy/ignore/database.rs +++ b/src/profile/proxy/ignore/database.rs @@ -26,6 +26,36 @@ impl Database { pub fn rows(&self) -> Result> { rows(&self.pool.get()?.unchecked_transaction()?, self.profile_id) } + + // Setters + + pub fn clean(&self, keep_id: Vec) -> Result<()> { + let mut c = self.pool.get()?; + let tx = c.transaction()?; + clean(&tx, keep_id)?; + tx.commit()?; + Ok(()) + } + + pub fn persist( + &self, + id: Option, + time: i64, + is_enabled: bool, + host: String, + ) -> Result { + let mut c = self.pool.get()?; + let tx = c.transaction()?; + let id = match id { + Some(id) => { + update(&tx, id, time, is_enabled, host)?; + id + } + None => insert(&tx, self.profile_id, time, is_enabled, host)?, + }; + tx.commit()?; + Ok(id) + } } // Low-level DB API @@ -46,6 +76,54 @@ pub fn init(tx: &Transaction) -> Result { )?) } +fn clean(tx: &Transaction, keep_id: Vec) -> Result { + if keep_id.is_empty() { + return Ok(0); + } + Ok(tx.execute( + &format!( + "DELETE FROM `profile_proxy_ignore` WHERE `id` NOT IN ({})", + keep_id + .into_iter() + .map(|id| id.to_string()) + .collect::>() + .join(",") + ), + [], + )?) +} + +fn insert( + tx: &Transaction, + profile_id: i64, + time: i64, + is_enabled: bool, + host: String, +) -> Result { + tx.execute( + "INSERT INTO `profile_proxy_ignore` ( + `profile_id`, + `time`, + `is_enabled`, + `host` + ) VALUES (?, ?, ?, ?)", + (profile_id, time, is_enabled, host), + )?; + Ok(tx.last_insert_rowid()) +} + +fn update(tx: &Transaction, id: i64, time: i64, is_enabled: bool, host: String) -> Result { + Ok(tx.execute( + "UPDATE `profile_proxy_ignore` + SET `time` = ?, + `is_enabled` = ?, + `host` = ? + + WHERE `id` = ?", + (time, is_enabled, host, id), + )?) +} + fn rows(tx: &Transaction, profile_id: i64) -> Result> { let mut stmt = tx.prepare( "SELECT `id`, @@ -60,9 +138,9 @@ fn rows(tx: &Transaction, profile_id: i64) -> Result> { let result = stmt.query_map([profile_id], |row| { Ok(Row { - //id: row.get(0)?, + id: row.get(0)?, //profile_id: row.get(1)?, - //time: DateTime::from_unix_local(row.get(2)?).unwrap(), + time: row.get(2)?, host: row.get(3)?, is_enabled: row.get(4)?, }) diff --git a/src/profile/proxy/ignore/database/row.rs b/src/profile/proxy/ignore/database/row.rs index f43ab22b..8755e5fe 100644 --- a/src/profile/proxy/ignore/database/row.rs +++ b/src/profile/proxy/ignore/database/row.rs @@ -1,4 +1,6 @@ pub struct Row { pub host: String, + pub id: i64, pub is_enabled: bool, + pub time: i64, } diff --git a/src/profile/proxy/ignore/memory.rs b/src/profile/proxy/ignore/memory.rs index c95ed3d0..fa6c3d84 100644 --- a/src/profile/proxy/ignore/memory.rs +++ b/src/profile/proxy/ignore/memory.rs @@ -1,5 +1,7 @@ #[derive(Clone)] pub struct Memory { pub host: String, + pub id: Option, pub is_enabled: bool, + pub time: gtk::glib::DateTime, } diff --git a/src/profile/proxy/rule.rs b/src/profile/proxy/rule.rs index 14a012b8..0a11ca1f 100644 --- a/src/profile/proxy/rule.rs +++ b/src/profile/proxy/rule.rs @@ -29,10 +29,10 @@ impl Rule { for row in rows { m.push(Memory { id: Some(row.id), - time: row.time, is_enabled: row.is_enabled, priority: row.priority, request: row.request, + time: DateTime::from_unix_local(row.time)?, url: row.url, }); } @@ -52,8 +52,7 @@ impl Rule { url: String, time: DateTime, ) { - let mut rules = self.memory.borrow_mut(); - rules.push(Memory { + self.memory.borrow_mut().push(Memory { id, time, is_enabled, @@ -73,7 +72,7 @@ impl Rule { for rule in rules { keep_id.push(self.database.persist( rule.id, - rule.time, + rule.time.to_unix(), rule.is_enabled, rule.priority, rule.request, diff --git a/src/profile/proxy/rule/database.rs b/src/profile/proxy/rule/database.rs index e84899fb..aef737e5 100644 --- a/src/profile/proxy/rule/database.rs +++ b/src/profile/proxy/rule/database.rs @@ -1,7 +1,6 @@ mod row; use anyhow::Result; -use gtk::glib::DateTime; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; use row::Row; @@ -41,7 +40,7 @@ impl Database { pub fn persist( &self, id: Option, - time: DateTime, + time: i64, is_enabled: bool, priority: i32, request: String, @@ -109,7 +108,7 @@ fn clean(tx: &Transaction, keep_id: Vec) -> Result { fn insert( tx: &Transaction, profile_id: i64, - time: DateTime, + time: i64, is_enabled: bool, priority: i32, request: String, @@ -124,14 +123,7 @@ fn insert( `request`, `url` ) VALUES (?, ?, ?, ?, ?, ?)", - ( - profile_id, - time.to_unix(), - is_enabled, - priority, - request, - url, - ), + (profile_id, time, is_enabled, priority, request, url), )?; Ok(tx.last_insert_rowid()) } @@ -139,7 +131,7 @@ fn insert( fn update( tx: &Transaction, id: i64, - time: DateTime, + time: i64, is_enabled: bool, priority: i32, request: String, @@ -154,7 +146,7 @@ fn update( `url` = ? WHERE `id` = ?", - (time.to_unix(), is_enabled, priority, request, url, id), + (time, is_enabled, priority, request, url, id), )?) } @@ -177,7 +169,7 @@ fn rows(tx: &Transaction, profile_id: i64) -> Result> { Ok(Row { id: row.get(0)?, //profile_id: row.get(1)?, - time: DateTime::from_unix_local(row.get(2)?).unwrap(), + time: row.get(2)?, is_enabled: row.get(3)?, priority: row.get(4)?, request: row.get(5)?, diff --git a/src/profile/proxy/rule/database/row.rs b/src/profile/proxy/rule/database/row.rs index 7b3ea34c..67c45da5 100644 --- a/src/profile/proxy/rule/database/row.rs +++ b/src/profile/proxy/rule/database/row.rs @@ -3,6 +3,6 @@ pub struct Row { pub is_enabled: bool, pub priority: i32, pub request: String, - pub time: gtk::glib::DateTime, + pub time: i64, pub url: String, }