Build 19: open-wallet shortcuts, Pay QR scan, configurable pairing, hide yellow
- Lower-left sidebar cards are now individual shortcuts: tapping the identity chip opens identity settings, tapping the node card jumps straight to the Node menu (was: both opened generic settings). - Pay screen gains a scan-to-pay QR puck top-right that opens the camera and prefills the recipient while keeping the typed amount (reuses the Home scan + SendFlow::request_scan/prefill_amount path). - Replace the USD-only "fiat preview" with a configurable "Pairing": Off / USD / EUR / GBP / JPY / CNY / Bitcoin / Sats (default USD). price.rs now fetches GRIN against any vs_currency (sats price vs btc, ×1e8) and caches per code; a Settings → Pairing sub-page picks it; the Pay, send, and balance previews all route through one pairing_preview() helper. - Hide the Yellow theme from the picker (cycle is Dark ↔ Light now); the ThemeKind::Yellow tokens stay defined — it's beta, not removed. Verified live on :2 (Default wallet): node card → Node menu; identity chip → identity settings; QR puck renders top-right of Pay; Pairing → Sats shows "≈ 1,912 sats" for 50ツ over a live BTC rate, resets to USD; theme cycles Dark↔Light, never Yellow. 34 lib tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+182
-57
@@ -92,6 +92,7 @@ enum SettingsPage {
|
||||
Node,
|
||||
Relays,
|
||||
Nips,
|
||||
Pairing,
|
||||
}
|
||||
|
||||
impl Default for GoblinWalletView {
|
||||
@@ -496,14 +497,25 @@ impl GoblinWalletView {
|
||||
}
|
||||
|
||||
// Node status + profile cards pinned to the bottom (node info lives
|
||||
// here so the surface needs no separate network column).
|
||||
// here so the surface needs no separate network column). Each card is
|
||||
// its own shortcut: the node card opens the Node menu, the identity
|
||||
// chip opens identity settings.
|
||||
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
||||
let width = ui.available_width();
|
||||
let bottom = ui.allocate_ui_with_layout(
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2::new(width, 196.0),
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
self.node_card_ui(ui, wallet);
|
||||
// Node status card → Settings → Node menu.
|
||||
let node = ui
|
||||
.scope(|ui| self.node_card_ui(ui, wallet))
|
||||
.response
|
||||
.interact(Sense::click())
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
if node.clicked() {
|
||||
self.tab = Tab::Me;
|
||||
self.settings_page = SettingsPage::Node;
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
let (handle, connected, npub_hex) = wallet
|
||||
.nostr_service()
|
||||
@@ -519,35 +531,42 @@ impl GoblinWalletView {
|
||||
.unwrap_or_else(|| ("Anonymous".to_string(), false, String::new()));
|
||||
let hue = data::hue_of(&npub_hex);
|
||||
let tex = self.handle_tex(ui.ctx(), wallet, &handle);
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
w::avatar_any(ui, &handle, 36.0, hue, tex.as_ref());
|
||||
ui.add_space(10.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(
|
||||
RichText::new(&handle)
|
||||
.font(FontId::new(14.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(if connected {
|
||||
"synced · Tor"
|
||||
} else {
|
||||
"connecting…"
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.surface_text_mute),
|
||||
);
|
||||
// Identity chip → identity settings.
|
||||
let id_resp = ui
|
||||
.scope(|ui| {
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
w::avatar_any(ui, &handle, 36.0, hue, tex.as_ref());
|
||||
ui.add_space(10.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(
|
||||
RichText::new(&handle)
|
||||
.font(FontId::new(14.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(if connected {
|
||||
"synced · Tor"
|
||||
} else {
|
||||
"connecting…"
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.surface_text_mute),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.response
|
||||
.interact(Sense::click())
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
if id_resp.clicked() {
|
||||
self.tab = Tab::Me;
|
||||
self.settings_page = SettingsPage::Main;
|
||||
}
|
||||
},
|
||||
);
|
||||
// Both bottom cards (node status + profile) open Settings.
|
||||
if bottom.response.interact(Sense::click()).clicked() {
|
||||
self.tab = Tab::Me;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -789,11 +808,37 @@ impl GoblinWalletView {
|
||||
fn pay_ui(&mut self, ui: &mut egui::Ui, _wallet: &Wallet) {
|
||||
let t = theme::tokens();
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Pay")
|
||||
.font(FontId::new(28.0, fonts::bold()))
|
||||
.color(t.text),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Pay")
|
||||
.font(FontId::new(28.0, fonts::bold()))
|
||||
.color(t.text),
|
||||
);
|
||||
// Scan-to-pay QR, top-right (mirrors the Home header scan puck):
|
||||
// open the scanner with the typed amount preserved.
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let (rect, resp) = ui.allocate_exact_size(Vec2::splat(36.0), Sense::click());
|
||||
ui.painter().circle_filled(rect.center(), 18.0, t.surface2);
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
QR_CODE,
|
||||
FontId::new(17.0, fonts::regular()),
|
||||
t.surface_text,
|
||||
);
|
||||
if resp
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.on_hover_text("Scan to pay")
|
||||
.clicked()
|
||||
{
|
||||
let mut f = SendFlow::default();
|
||||
f.prefill_amount(self.pay_amount.clone());
|
||||
f.request_scan();
|
||||
self.pay_amount.clear();
|
||||
self.send = Some(f);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Big centered amount.
|
||||
let display = if self.pay_amount.is_empty() {
|
||||
@@ -804,12 +849,12 @@ impl GoblinWalletView {
|
||||
let tall = ui.available_height() > 560.0;
|
||||
ui.add_space(if tall { 56.0 } else { 24.0 });
|
||||
w::amount_text_centered(ui, &display, 76.0);
|
||||
if let Some(rate) = crate::http::grin_usd_rate() {
|
||||
if let Ok(grin) = display.parse::<f64>() {
|
||||
if let Ok(grin) = display.parse::<f64>() {
|
||||
if let Some(preview) = pairing_preview(grin) {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(format!("≈ ${:.2}", grin * rate))
|
||||
RichText::new(preview)
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
@@ -1178,6 +1223,7 @@ impl GoblinWalletView {
|
||||
SettingsPage::Node => return self.node_settings_ui(ui, wallet),
|
||||
SettingsPage::Relays => return self.relays_ui(ui, wallet),
|
||||
SettingsPage::Nips => return self.nips_ui(ui),
|
||||
SettingsPage::Pairing => return self.pairing_settings_ui(ui),
|
||||
SettingsPage::Main => {}
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
@@ -1436,22 +1482,21 @@ impl GoblinWalletView {
|
||||
}
|
||||
|
||||
ui.add_space(16.0);
|
||||
let mut open_pairing = false;
|
||||
settings_group(ui, "Privacy", |ui| {
|
||||
settings_row(ui, "Tor routing", "All payments routed over Tor");
|
||||
// Tap to cycle the incoming-payment accept policy.
|
||||
if settings_row_btn(ui, "Auto-accept", accept_policy_label(wallet)) {
|
||||
cycle_accept_policy(wallet);
|
||||
}
|
||||
// Tap to toggle the fiat (USD) preview.
|
||||
let fiat = if crate::AppConfig::fiat_preview() {
|
||||
"On"
|
||||
} else {
|
||||
"Off"
|
||||
};
|
||||
if settings_row_btn(ui, "Fiat preview (USD)", fiat) {
|
||||
crate::AppConfig::toggle_fiat_preview();
|
||||
// Amount pairing: what the ≈ preview is shown against.
|
||||
if settings_row_nav(ui, "Pairing", crate::AppConfig::pairing().label()) {
|
||||
open_pairing = true;
|
||||
}
|
||||
});
|
||||
if open_pairing {
|
||||
self.settings_page = SettingsPage::Pairing;
|
||||
}
|
||||
|
||||
ui.add_space(16.0);
|
||||
w::kicker(ui, "Appearance");
|
||||
@@ -1547,6 +1592,52 @@ impl GoblinWalletView {
|
||||
}
|
||||
|
||||
/// Node connection editor: pick integrated/external, add or remove nodes.
|
||||
fn pairing_settings_ui(&mut self, ui: &mut egui::Ui) {
|
||||
let t = theme::tokens();
|
||||
if self.sub_header(ui, "Pairing") {
|
||||
self.settings_page = SettingsPage::Main;
|
||||
return;
|
||||
}
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
|
||||
.show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("What your balance and amounts are shown against.")
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(12.0);
|
||||
let current = crate::AppConfig::pairing();
|
||||
settings_group(ui, "Pair with", |ui| {
|
||||
for p in crate::settings::Pairing::ALL {
|
||||
let active = p == current;
|
||||
let row = ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new(p.label())
|
||||
.font(FontId::new(15.0, fonts::medium()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if active {
|
||||
ui.label(
|
||||
RichText::new(crate::gui::icons::CHECK)
|
||||
.font(FontId::new(16.0, fonts::regular()))
|
||||
.color(t.pos),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
if !active && row.response.interact(Sense::click()).clicked() {
|
||||
crate::AppConfig::set_pairing(p);
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(16.0);
|
||||
});
|
||||
}
|
||||
|
||||
fn node_settings_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
use crate::wallet::types::ConnectionMethod;
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection};
|
||||
@@ -2754,12 +2845,14 @@ fn accept_policy_label(wallet: &Wallet) -> &'static str {
|
||||
.unwrap_or("Anyone")
|
||||
}
|
||||
|
||||
/// Cycle the color theme Dark → Light → Yellow → Dark and re-apply visuals.
|
||||
/// Cycle the color theme Dark ↔ Light and re-apply visuals. Yellow is kept
|
||||
/// defined (gui/theme.rs) but out of the picker for now — it's still in beta;
|
||||
/// `Yellow => Dark` is an escape hatch for anyone whose config already has it.
|
||||
fn cycle_theme(ctx: &egui::Context) {
|
||||
use crate::gui::theme::ThemeKind;
|
||||
let next = match crate::AppConfig::theme() {
|
||||
ThemeKind::Dark => ThemeKind::Light,
|
||||
ThemeKind::Light => ThemeKind::Yellow,
|
||||
ThemeKind::Light => ThemeKind::Dark,
|
||||
ThemeKind::Yellow => ThemeKind::Dark,
|
||||
};
|
||||
crate::AppConfig::set_theme(next);
|
||||
@@ -2842,16 +2935,48 @@ fn fmt_thousands(n: u64) -> String {
|
||||
}
|
||||
|
||||
fn fiat_line(data: &Option<WalletData>) -> Option<String> {
|
||||
let _ = data;
|
||||
// Fiat rate provider is wired in P7; hide the line until a rate is available.
|
||||
crate::http::grin_usd_rate().map(|rate| {
|
||||
let spendable = data
|
||||
.as_ref()
|
||||
.map(|d| d.info.amount_currently_spendable)
|
||||
.unwrap_or(0);
|
||||
let grin = spendable as f64 / 1_000_000_000.0;
|
||||
format!("≈ ${:.2} · 1ツ = ${:.4}", grin * rate, rate)
|
||||
})
|
||||
let p = crate::AppConfig::pairing();
|
||||
let vs = p.vs_currency()?;
|
||||
let rate = crate::http::grin_rate(vs)?;
|
||||
let spendable = data
|
||||
.as_ref()
|
||||
.map(|d| d.info.amount_currently_spendable)
|
||||
.unwrap_or(0);
|
||||
let grin = spendable as f64 / 1_000_000_000.0;
|
||||
Some(format!(
|
||||
"≈ {} · 1ツ = {}",
|
||||
fmt_pairing(grin * rate, p),
|
||||
fmt_pairing(rate, p)
|
||||
))
|
||||
}
|
||||
|
||||
/// Format a value already in the pairing's unit (dollars, BTC, …) with the
|
||||
/// right symbol/precision. Sats scales the BTC value by 1e8.
|
||||
fn fmt_pairing(value: f64, p: crate::settings::Pairing) -> String {
|
||||
use crate::settings::Pairing;
|
||||
match p {
|
||||
Pairing::Usd => format!("${:.2}", value),
|
||||
Pairing::Eur => format!("€{:.2}", value),
|
||||
Pairing::Gbp => format!("£{:.2}", value),
|
||||
Pairing::Jpy => format!("¥{:.0}", value),
|
||||
Pairing::Cny => format!("CN¥{:.2}", value),
|
||||
Pairing::Btc => {
|
||||
let s = format!("{:.8}", value);
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.');
|
||||
format!("₿{}", if s.is_empty() { "0" } else { s })
|
||||
}
|
||||
Pairing::Sats => format!("{} sats", fmt_thousands((value * 1e8).round() as u64)),
|
||||
Pairing::Off => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The "≈ …" amount preview for the current pairing, or `None` when off / no
|
||||
/// rate yet. Shared by the Pay screen, the send flow, and the balance hero.
|
||||
fn pairing_preview(grin: f64) -> Option<String> {
|
||||
let p = crate::AppConfig::pairing();
|
||||
let vs = p.vs_currency()?;
|
||||
let rate = crate::http::grin_rate(vs)?;
|
||||
Some(format!("≈ {}", fmt_pairing(grin * rate, p)))
|
||||
}
|
||||
|
||||
/// Convert a bech32 npub to hex for short display fallbacks.
|
||||
|
||||
@@ -715,12 +715,12 @@ impl SendFlow {
|
||||
self.amount.clone()
|
||||
};
|
||||
w::amount_text_centered(ui, &display, 64.0);
|
||||
if let Some(rate) = crate::http::grin_usd_rate() {
|
||||
if let Ok(grin) = display.parse::<f64>() {
|
||||
if let Ok(grin) = display.parse::<f64>() {
|
||||
if let Some(preview) = super::pairing_preview(grin) {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(format!("≈ ${:.2}", grin * rate))
|
||||
RichText::new(preview)
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
|
||||
+1
-1
@@ -19,4 +19,4 @@ mod release;
|
||||
pub use release::*;
|
||||
|
||||
mod price;
|
||||
pub use price::grin_usd_rate;
|
||||
pub use price::grin_rate;
|
||||
|
||||
+44
-38
@@ -12,30 +12,31 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! GRIN/USD price preview, fetched over Tor and cached.
|
||||
//! GRIN price preview, fetched over Tor and cached per currency.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::tor::Tor;
|
||||
|
||||
/// Cache refresh interval (seconds).
|
||||
const REFRESH_SECS: i64 = 300;
|
||||
|
||||
lazy_static! {
|
||||
/// Cached (rate, fetched_at) and an in-flight flag.
|
||||
static ref RATE: RwLock<Option<(f64, i64)>> = RwLock::new(None);
|
||||
static ref FETCHING: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
static ref LAST_TRY: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
|
||||
}
|
||||
|
||||
/// Minimum delay between fetch attempts, so a failing fetch (e.g. Tor still
|
||||
/// bootstrapping) does not respawn a thread every frame.
|
||||
/// Minimum delay between fetch attempts for a currency, so a failing fetch
|
||||
/// (e.g. Tor still bootstrapping) does not respawn a thread every frame.
|
||||
const RETRY_SECS: i64 = 30;
|
||||
|
||||
lazy_static! {
|
||||
/// Cached GRIN rates per `vs_currency`: code -> (rate, fetched_at).
|
||||
static ref RATES: RwLock<HashMap<String, (f64, i64)>> = RwLock::new(HashMap::new());
|
||||
/// Currencies with a fetch currently in flight.
|
||||
static ref FETCHING: RwLock<HashSet<String>> = RwLock::new(HashSet::new());
|
||||
/// Last fetch attempt per currency (unix secs).
|
||||
static ref LAST_TRY: RwLock<HashMap<String, i64>> = RwLock::new(HashMap::new());
|
||||
}
|
||||
|
||||
fn now() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -43,60 +44,65 @@ fn now() -> i64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Get the cached GRIN/USD rate if fresh, triggering a refresh otherwise.
|
||||
/// Returns `None` until the first successful fetch or when fiat is disabled.
|
||||
pub fn grin_usd_rate() -> Option<f64> {
|
||||
if !AppConfig::fiat_preview() {
|
||||
return None;
|
||||
}
|
||||
let cached = { RATE.read().clone() };
|
||||
/// Get the cached GRIN rate against `vs` (e.g. "usd", "eur", "btc") if fresh,
|
||||
/// triggering a background refresh otherwise. Returns `None` until the first
|
||||
/// successful fetch for that currency.
|
||||
pub fn grin_rate(vs: &str) -> Option<f64> {
|
||||
let cached = { RATES.read().get(vs).cloned() };
|
||||
let needs_refresh = match cached {
|
||||
Some((_, ts)) => now() - ts > REFRESH_SECS,
|
||||
None => true,
|
||||
};
|
||||
if needs_refresh {
|
||||
trigger_refresh();
|
||||
trigger_refresh(vs.to_string());
|
||||
}
|
||||
cached.map(|(rate, _)| rate)
|
||||
}
|
||||
|
||||
/// Spawn a background refresh over Tor (deduplicated).
|
||||
fn trigger_refresh() {
|
||||
use std::sync::atomic::Ordering;
|
||||
/// Spawn a background refresh over Tor for one currency (deduplicated per code).
|
||||
fn trigger_refresh(vs: String) {
|
||||
let t = now();
|
||||
if t - LAST_TRY.load(Ordering::SeqCst) < RETRY_SECS {
|
||||
return;
|
||||
{
|
||||
let last = LAST_TRY.read().get(&vs).copied().unwrap_or(0);
|
||||
if t - last < RETRY_SECS {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if FETCHING.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
{
|
||||
let mut fetching = FETCHING.write();
|
||||
if fetching.contains(&vs) {
|
||||
return;
|
||||
}
|
||||
fetching.insert(vs.clone());
|
||||
}
|
||||
LAST_TRY.store(t, Ordering::SeqCst);
|
||||
std::thread::spawn(|| {
|
||||
LAST_TRY.write().insert(vs.clone(), t);
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
if let Some(rate) = fetch_rate().await {
|
||||
let mut w = RATE.write();
|
||||
*w = Some((rate, now()));
|
||||
if let Some(rate) = fetch_rate(&vs).await {
|
||||
RATES.write().insert(vs.clone(), (rate, now()));
|
||||
}
|
||||
});
|
||||
FETCHING.store(false, Ordering::SeqCst);
|
||||
FETCHING.write().remove(&vs);
|
||||
});
|
||||
}
|
||||
|
||||
/// Fetch the GRIN/USD rate from CoinGecko over Tor.
|
||||
async fn fetch_rate() -> Option<f64> {
|
||||
let url =
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies=usd".to_string();
|
||||
/// Fetch the GRIN/`vs` rate from CoinGecko over Tor.
|
||||
async fn fetch_rate(vs: &str) -> Option<f64> {
|
||||
let url = format!(
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={}",
|
||||
vs
|
||||
);
|
||||
// CoinGecko rejects requests without a User-Agent (403). A static,
|
||||
// non-identifying UA is fine over Tor.
|
||||
let headers = vec![("User-Agent".to_string(), "goblin-wallet".to_string())];
|
||||
let body = Tor::http_request("GET", url, None, headers).await?;
|
||||
let parsed: Option<f64> = serde_json::from_str::<serde_json::Value>(&body)
|
||||
.ok()
|
||||
.and_then(|doc| doc.get("grin")?.get("usd")?.as_f64());
|
||||
.and_then(|doc| doc.get("grin")?.get(vs)?.as_f64());
|
||||
if parsed.is_none() {
|
||||
log::warn!(
|
||||
"price: unexpected response from rate API (Tor exit blocked?): {}",
|
||||
|
||||
+98
-8
@@ -75,8 +75,10 @@ pub struct AppConfig {
|
||||
density: Option<String>,
|
||||
/// Identifier of the last opened wallet to boot into.
|
||||
last_wallet_id: Option<i64>,
|
||||
/// Show fiat (USD) preview alongside amounts.
|
||||
/// Show fiat (USD) preview alongside amounts (legacy; migrated to pairing).
|
||||
fiat_preview: Option<bool>,
|
||||
/// Amount pairing code: off|usd|eur|gbp|jpy|cny|btc|sats (default usd).
|
||||
pairing: Option<String>,
|
||||
|
||||
/// Flag to use proxy for network requests.
|
||||
use_proxy: Option<bool>,
|
||||
@@ -93,6 +95,85 @@ pub struct AppConfig {
|
||||
app_update: Option<AppUpdate>,
|
||||
}
|
||||
|
||||
/// What the amount preview is paired to: nothing, a fiat currency, or bitcoin.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Pairing {
|
||||
Off,
|
||||
Usd,
|
||||
Eur,
|
||||
Gbp,
|
||||
Jpy,
|
||||
Cny,
|
||||
Btc,
|
||||
Sats,
|
||||
}
|
||||
|
||||
impl Pairing {
|
||||
/// All variants, in picker order.
|
||||
pub const ALL: [Pairing; 8] = [
|
||||
Pairing::Off,
|
||||
Pairing::Usd,
|
||||
Pairing::Eur,
|
||||
Pairing::Gbp,
|
||||
Pairing::Jpy,
|
||||
Pairing::Cny,
|
||||
Pairing::Btc,
|
||||
Pairing::Sats,
|
||||
];
|
||||
|
||||
/// Stable config code.
|
||||
pub fn code(&self) -> &'static str {
|
||||
match self {
|
||||
Pairing::Off => "off",
|
||||
Pairing::Usd => "usd",
|
||||
Pairing::Eur => "eur",
|
||||
Pairing::Gbp => "gbp",
|
||||
Pairing::Jpy => "jpy",
|
||||
Pairing::Cny => "cny",
|
||||
Pairing::Btc => "btc",
|
||||
Pairing::Sats => "sats",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_code(s: &str) -> Option<Pairing> {
|
||||
Some(match s {
|
||||
"off" => Pairing::Off,
|
||||
"usd" => Pairing::Usd,
|
||||
"eur" => Pairing::Eur,
|
||||
"gbp" => Pairing::Gbp,
|
||||
"jpy" => Pairing::Jpy,
|
||||
"cny" => Pairing::Cny,
|
||||
"btc" => Pairing::Btc,
|
||||
"sats" => Pairing::Sats,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The CoinGecko `vs_currency` to price against (sats prices vs btc).
|
||||
/// `None` when pairing is off.
|
||||
pub fn vs_currency(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Pairing::Off => None,
|
||||
Pairing::Sats => Some("btc"),
|
||||
other => Some(other.code()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Human label for the picker / settings row.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Pairing::Off => "Off",
|
||||
Pairing::Usd => "USD",
|
||||
Pairing::Eur => "EUR",
|
||||
Pairing::Gbp => "GBP",
|
||||
Pairing::Jpy => "JPY",
|
||||
Pairing::Cny => "CNY",
|
||||
Pairing::Btc => "Bitcoin",
|
||||
Pairing::Sats => "Sats",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -112,6 +193,7 @@ impl Default for AppConfig {
|
||||
density: None,
|
||||
last_wallet_id: None,
|
||||
fiat_preview: None,
|
||||
pairing: None,
|
||||
use_proxy: None,
|
||||
use_socks_proxy: None,
|
||||
http_proxy_url: None,
|
||||
@@ -363,17 +445,25 @@ impl AppConfig {
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Check if fiat (USD) preview is enabled (default on).
|
||||
pub fn fiat_preview() -> bool {
|
||||
/// What amount previews are paired to (default USD). Migrates the legacy
|
||||
/// `fiat_preview = false` to `Off`.
|
||||
pub fn pairing() -> Pairing {
|
||||
let r_config = Settings::app_config_to_read();
|
||||
r_config.fiat_preview.unwrap_or(true)
|
||||
if let Some(code) = r_config.pairing.clone() {
|
||||
if let Some(p) = Pairing::from_code(&code) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
match r_config.fiat_preview {
|
||||
Some(false) => Pairing::Off,
|
||||
_ => Pairing::Usd,
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle fiat preview.
|
||||
pub fn toggle_fiat_preview() {
|
||||
let enabled = Self::fiat_preview();
|
||||
/// Save the amount pairing.
|
||||
pub fn set_pairing(p: Pairing) {
|
||||
let mut w_config = Settings::app_config_to_update();
|
||||
w_config.fiat_preview = Some(!enabled);
|
||||
w_config.pairing = Some(p.code().to_string());
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user