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 @@