feat(frontend): introduce haptic feedback as experimental feature

#16410
This commit is contained in:
syuilo 2025-08-18 10:49:27 +09:00
parent 14cc42e305
commit fcde6789ff
9 changed files with 41 additions and 0 deletions

View File

@ -52,6 +52,7 @@
"icons-subsetter": "workspace:*", "icons-subsetter": "workspace:*",
"idb-keyval": "6.2.2", "idb-keyval": "6.2.2",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"ios-haptics": "0.1.0",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"magic-string": "0.30.17", "magic-string": "0.30.17",

View File

@ -141,6 +141,7 @@ import { $i } from '@/i.js';
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import { haptic } from '@/utility/haptic.js';
const router = useRouter(); const router = useRouter();
@ -431,6 +432,8 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
const key = getKey(emoji); const key = getKey(emoji);
emit('chosen', key); emit('chosen', key);
haptic();
// 使 // 使
if (!pinned.value?.includes(key)) { if (!pinned.value?.includes(key)) {
let recents = store.s.recentlyUsedEmojis; let recents = store.s.recentlyUsedEmojis;

View File

@ -46,6 +46,7 @@ import { claimAchievement } from '@/utility/achievements.js';
import { pleaseLogin } from '@/utility/please-login.js'; import { pleaseLogin } from '@/utility/please-login.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { haptic } from '@/utility/haptic.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed, user: Misskey.entities.UserDetailed,
@ -84,6 +85,8 @@ async function onClick() {
wait.value = true; wait.value = true;
haptic();
try { try {
if (isFollowing.value) { if (isFollowing.value) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({

View File

@ -27,6 +27,7 @@ import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
import { getScrollContainer } from '@@/js/scroll.js'; import { getScrollContainer } from '@@/js/scroll.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { isHorizontalSwipeSwiping } from '@/utility/touch.js'; import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
import { haptic } from '@/utility/haptic.js';
const SCROLL_STOP = 10; const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity; const MAX_PULL_DISTANCE = Infinity;
@ -203,6 +204,8 @@ function moving(event: MouseEvent | TouchEvent) {
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE); pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD; isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD;
if (isPulledEnough.value) haptic();
} }
/** /**

View File

@ -30,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { toRefs } from 'vue'; import { toRefs } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import XButton from '@/components/MkSwitch.button.vue'; import XButton from '@/components/MkSwitch.button.vue';
import { haptic } from '@/utility/haptic.js';
const props = defineProps<{ const props = defineProps<{
modelValue: boolean | Ref<boolean>; modelValue: boolean | Ref<boolean>;
@ -48,6 +49,8 @@ const toggle = () => {
if (props.disabled) return; if (props.disabled) return;
emit('update:modelValue', !checked.value); emit('update:modelValue', !checked.value);
emit('change', !checked.value); emit('change', !checked.value);
haptic();
}; };
</script> </script>

View File

@ -99,6 +99,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="enableFolderPageView"> <MkSwitch v-model="enableFolderPageView">
<template #label>Enable folder page view</template> <template #label>Enable folder page view</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="enableHapticFeedback">
<template #label>Enable haptic feedback</template>
</MkSwitch>
</div> </div>
</MkFolder> </MkFolder>
</SearchMarker> </SearchMarker>
@ -173,6 +176,7 @@ const skipNoteRender = prefer.model('skipNoteRender');
const devMode = prefer.model('devMode'); const devMode = prefer.model('devMode');
const stackingRouterView = prefer.model('experimental.stackingRouterView'); const stackingRouterView = prefer.model('experimental.stackingRouterView');
const enableFolderPageView = prefer.model('experimental.enableFolderPageView'); const enableFolderPageView = prefer.model('experimental.enableFolderPageView');
const enableHapticFeedback = prefer.model('experimental.enableHapticFeedback');
watch(skipNoteRender, () => { watch(skipNoteRender, () => {
suggestReload(); suggestReload();

View File

@ -498,4 +498,7 @@ export const PREF_DEF = definePreferences({
'experimental.enableFolderPageView': { 'experimental.enableFolderPageView': {
default: false, default: false,
}, },
'experimental.enableHapticFeedback': {
default: false,
},
}); });

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { haptic as _haptic } from 'ios-haptics';
import { prefer } from '@/preferences.js';
export function haptic() {
if (prefer.s['experimental.enableHapticFeedback']) {
_haptic();
}
}

View File

@ -811,6 +811,9 @@ importers:
insert-text-at-cursor: insert-text-at-cursor:
specifier: 0.3.0 specifier: 0.3.0
version: 0.3.0 version: 0.3.0
ios-haptics:
specifier: 0.1.0
version: 0.1.0
is-file-animated: is-file-animated:
specifier: 1.0.2 specifier: 1.0.2
version: 1.0.2 version: 1.0.2
@ -7179,6 +7182,9 @@ packages:
resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==}
engines: {node: '>=12.22.0'} engines: {node: '>=12.22.0'}
ios-haptics@0.1.0:
resolution: {integrity: sha512-Fk0RApBYJeZNZ9pW3Wx3WcunhdLlpEnVNy/BOn85tx39eZDOHLGhXEb7medoIURGBUjXatOZf5Ozy0+OG466YA==}
ip-address@9.0.5: ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
@ -18221,6 +18227,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
ios-haptics@0.1.0: {}
ip-address@9.0.5: ip-address@9.0.5:
dependencies: dependencies:
jsbn: 1.1.0 jsbn: 1.1.0