diff --git a/package.json b/package.json
index 6d69138..2e53b20 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "homepage",
- "version": "1.26.2",
+ "version": "1.27.0",
"private": true,
"type": "module",
"scripts": {
@@ -59,7 +59,8 @@
"leaflet": "^1.9.4",
"mongoose": "^9.4.1",
"node-cron": "^4.2.1",
- "sharp": "^0.34.5"
+ "sharp": "^0.34.5",
+ "web-haptics": "^0.0.6"
},
"pnpm": {
"onlyBuiltDependencies": [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index aa43697..1ba5fc7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -53,6 +53,9 @@ importers:
sharp:
specifier: ^0.34.5
version: 0.34.5
+ web-haptics:
+ specifier: ^0.0.6
+ version: 0.0.6(svelte@5.55.1)
devDependencies:
'@playwright/test':
specifier: 1.56.1
@@ -2033,6 +2036,23 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
+ web-haptics@0.0.6:
+ resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ svelte: '>=4'
+ vue: '>=3'
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ svelte:
+ optional: true
+ vue:
+ optional: true
+
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -3765,6 +3785,10 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
+ web-haptics@0.0.6(svelte@5.55.1):
+ optionalDependencies:
+ svelte: 5.55.1
+
webidl-conversions@7.0.0: {}
webidl-conversions@8.0.0: {}
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 0c419b6..d6fe2b4 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "bocken"
-version = "0.4.0"
+version = "0.5.0"
edition = "2021"
[lib]
diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml
index 0f58e4d..a8c2333 100644
--- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml
+++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
diff --git a/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt b/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt
index 598f9c8..bad7c1e 100644
--- a/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt
+++ b/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt
@@ -6,6 +6,10 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
+import android.os.VibrationAttributes
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.os.VibratorManager
import android.speech.tts.TextToSpeech
import android.webkit.JavascriptInterface
import androidx.core.app.ActivityCompat
@@ -100,6 +104,36 @@ class AndroidBridge(private val context: Context) {
return LocationForegroundService.getIntervalState()
}
+ /**
+ * Force-vibrate bypassing silent/DND by using USAGE_ACCESSIBILITY attributes.
+ * Why: default web Vibration API uses USAGE_TOUCH which Android silences.
+ */
+ @JavascriptInterface
+ fun forceVibrate(durationMs: Long, intensityPct: Int) {
+ val vibrator: Vibrator? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ (context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
+ } else {
+ @Suppress("DEPRECATION")
+ context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
+ }
+ if (vibrator?.hasVibrator() != true) return
+
+ val amplitude = (intensityPct.coerceIn(1, 100) * 255 / 100).coerceAtLeast(1)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val effect = VibrationEffect.createOneShot(durationMs, amplitude)
+ val attrs = VibrationAttributes.Builder()
+ .setUsage(VibrationAttributes.USAGE_ACCESSIBILITY)
+ .build()
+ vibrator.vibrate(effect, attrs)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ vibrator.vibrate(VibrationEffect.createOneShot(durationMs, amplitude))
+ } else {
+ @Suppress("DEPRECATION")
+ vibrator.vibrate(durationMs)
+ }
+ }
+
/** Returns true if at least one TTS engine is installed on the device. */
@JavascriptInterface
fun hasTtsEngine(): Boolean {
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 8eeaeea..10dacb6 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"productName": "Bocken",
"identifier": "org.bocken.app",
- "version": "0.4.0",
+ "version": "0.5.0",
"build": {
"devUrl": "http://192.168.1.4:5173",
"frontendDist": "https://bocken.org"
diff --git a/src/lib/components/CounterButton.svelte b/src/lib/components/CounterButton.svelte
deleted file mode 100644
index 59f7b44..0000000
--- a/src/lib/components/CounterButton.svelte
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
diff --git a/src/lib/js/haptics.ts b/src/lib/js/haptics.ts
new file mode 100644
index 0000000..74a54ba
--- /dev/null
+++ b/src/lib/js/haptics.ts
@@ -0,0 +1,33 @@
+import { createWebHaptics } from 'web-haptics/svelte';
+
+export type HapticPulse = { duration: number; intensity?: number };
+
+type AndroidBridgeShape = {
+ forceVibrate?: (durationMs: number, intensityPct: number) => void;
+};
+
+function getBridge(): AndroidBridgeShape | undefined {
+ if (typeof window === 'undefined') return undefined;
+ return (window as unknown as { AndroidBridge?: AndroidBridgeShape }).AndroidBridge;
+}
+
+/**
+ * Progressively-enhanced haptics: uses the Tauri Android bridge (bypasses silent
+ * mode via VibrationAttributes.USAGE_ACCESSIBILITY) when running inside the
+ * installed app; falls back to web-haptics in the browser.
+ */
+export function createHaptics() {
+ const web = createWebHaptics();
+ const nativeAvailable = typeof getBridge()?.forceVibrate === 'function';
+
+ function trigger(pulse: HapticPulse = { duration: 30, intensity: 0.7 }): void {
+ const bridge = getBridge();
+ if (bridge?.forceVibrate) {
+ bridge.forceVibrate(pulse.duration, Math.round((pulse.intensity ?? 0.7) * 100));
+ return;
+ }
+ web.trigger([pulse]);
+ }
+
+ return { trigger, destroy: web.destroy, nativeAvailable };
+}
diff --git a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte
index 9c83584..159665f 100644
--- a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte
+++ b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte
@@ -1,5 +1,6 @@