1
0
forked from GRIN/grim

Copy buttons: haptic tick + transient 'Copied' confirmation

This commit is contained in:
2ro
2026-06-15 21:15:26 -04:00
parent 22292ef79c
commit b7c3b95f51
5 changed files with 59 additions and 0 deletions
@@ -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;
+4
View File
@@ -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! {
+3
View File
@@ -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) {}
}
+35
View File
@@ -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);
+1
View File
@@ -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);