Copy buttons: haptic tick + transient 'Copied' confirmation
This commit is contained in:
@@ -534,6 +534,22 @@ public class MainActivity extends GameActivity {
|
||||
}
|
||||
}
|
||||
|
||||
// Called from native code to play a tiny "tick" haptic on a successful copy.
|
||||
public void vibrateCopy() {
|
||||
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||
return;
|
||||
}
|
||||
// One short, light tick — a confirmation, not an alert.
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK));
|
||||
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
} else {
|
||||
vibrator.vibrate(20);
|
||||
}
|
||||
}
|
||||
|
||||
// Called from native code to set status-bar icon color to contrast the
|
||||
// in-app theme. white = light icons for a dark background. The app draws
|
||||
// edge-to-edge, so the OS status-bar background is the app's own content;
|
||||
|
||||
@@ -224,6 +224,10 @@ impl PlatformCallbacks for Android {
|
||||
fn vibrate_error(&self) {
|
||||
let _ = self.call_java_method("vibrateError", "()V", &[]);
|
||||
}
|
||||
|
||||
fn vibrate_copy(&self) {
|
||||
let _ = self.call_java_method("vibrateCopy", "()V", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
||||
@@ -57,4 +57,7 @@ pub trait PlatformCallbacks {
|
||||
/// Play a short "error" haptic (e.g. a rejected over-balance payment).
|
||||
/// No-op off Android.
|
||||
fn vibrate_error(&self) {}
|
||||
|
||||
/// Play a tiny "tick" haptic confirming a successful copy. No-op off Android.
|
||||
fn vibrate_copy(&self) {}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@ pub struct GoblinWalletView {
|
||||
cancel_confirm: Option<u32>,
|
||||
/// Outcome of the last manual cancel, shown transiently on the receipt.
|
||||
cancel_msg: Option<(crate::nostr::CancelOutcome, std::time::Instant)>,
|
||||
/// Transient "Copied" flash for the settings backup card (npub/keys).
|
||||
copy_flash: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
/// Sub-pages of the Settings tab.
|
||||
@@ -190,6 +192,7 @@ impl Default for GoblinWalletView {
|
||||
avatar_msg: None,
|
||||
cancel_confirm: None,
|
||||
cancel_msg: None,
|
||||
copy_flash: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2023,6 +2026,7 @@ impl GoblinWalletView {
|
||||
};
|
||||
if w::big_action(ui, &label, false).clicked() && !npub.is_empty() {
|
||||
cb.copy_string_to_buffer(npub.clone());
|
||||
cb.vibrate_copy();
|
||||
self.receive_copied = Some((1, std::time::Instant::now()));
|
||||
}
|
||||
},
|
||||
@@ -2215,11 +2219,15 @@ impl GoblinWalletView {
|
||||
if !npub.is_empty() {
|
||||
if settings_row_btn(ui, &t!("goblin.settings.copy_npub"), COPY) {
|
||||
cb.copy_string_to_buffer(npub.clone());
|
||||
cb.vibrate_copy();
|
||||
self.copy_flash = Some(std::time::Instant::now());
|
||||
}
|
||||
// A real backup is the SECRET key (nsec), not the npub.
|
||||
if settings_row_btn(ui, &t!("goblin.settings.backup_nsec"), COPY) {
|
||||
if let Some(nsec) = wallet.nostr_service().and_then(|s| s.nsec()) {
|
||||
cb.copy_string_to_buffer(nsec);
|
||||
cb.vibrate_copy();
|
||||
self.copy_flash = Some(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
// Encrypted backup file: the identity JSON as stored
|
||||
@@ -2233,6 +2241,8 @@ impl GoblinWalletView {
|
||||
let json = serde_json::to_string_pretty(&*s.identity.read())
|
||||
.unwrap_or_default();
|
||||
cb.copy_string_to_buffer(json);
|
||||
cb.vibrate_copy();
|
||||
self.copy_flash = Some(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
if settings_row_danger(
|
||||
@@ -2253,6 +2263,28 @@ impl GoblinWalletView {
|
||||
}
|
||||
}
|
||||
});
|
||||
// Transient confirmation that the copy landed — pairs with the
|
||||
// haptic tick so the tap feels acknowledged.
|
||||
if let Some(at) = self.copy_flash {
|
||||
if at.elapsed().as_secs_f32() < 1.5 {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"{} {}",
|
||||
crate::gui::icons::CHECK,
|
||||
t!("goblin.receipt.copied")
|
||||
))
|
||||
.font(FontId::new(13.0, fonts::medium()))
|
||||
.color(t.pos),
|
||||
);
|
||||
});
|
||||
ui.ctx()
|
||||
.request_repaint_after(std::time::Duration::from_millis(120));
|
||||
} else {
|
||||
self.copy_flash = None;
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.settings.backup_note"))
|
||||
@@ -2389,6 +2421,7 @@ impl GoblinWalletView {
|
||||
if let Some(s) = wallet.nostr_service() {
|
||||
let json = s.store.export_json(&s.npub());
|
||||
cb.copy_string_to_buffer(json);
|
||||
cb.vibrate_copy();
|
||||
}
|
||||
}
|
||||
if settings_row_btn(
|
||||
@@ -3172,6 +3205,7 @@ impl GoblinWalletView {
|
||||
ui.add_space(10.0);
|
||||
if w::big_action(ui, &t!("goblin.settings.sp_copy"), false).clicked() {
|
||||
cb.copy_string_to_buffer(result);
|
||||
cb.vibrate_copy();
|
||||
}
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
@@ -3459,6 +3493,7 @@ impl GoblinWalletView {
|
||||
if w::big_action_on_card(ui, &t!("goblin.settings.copy_new_nsec")).clicked() {
|
||||
if let Some(nsec) = wallet.nostr_service().and_then(|s| s.nsec()) {
|
||||
cb.copy_string_to_buffer(nsec);
|
||||
cb.vibrate_copy();
|
||||
}
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
|
||||
@@ -537,6 +537,7 @@ impl OnboardingContent {
|
||||
ui.add_space(14.0);
|
||||
} else if w::chip(ui, &t!("goblin.onboarding.words.copy_clipboard"), false).clicked() {
|
||||
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
|
||||
cb.vibrate_copy();
|
||||
}
|
||||
if !restore {
|
||||
ui.add_space(14.0);
|
||||
|
||||
Reference in New Issue
Block a user