feat: vibration

This commit is contained in:
Kainoa Kanter 2023-09-17 21:59:09 +00:00
parent e0cc251a1e
commit 490abe7275
25 changed files with 77 additions and 18 deletions

View File

@ -1142,6 +1142,7 @@ indexable: "Indexable"
indexableDescription: "Allow built-in search to show your public posts" indexableDescription: "Allow built-in search to show your public posts"
languageForTranslation: "Post translation language" languageForTranslation: "Post translation language"
detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages" detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages"
vibrate: "Play vibrations"
openServerInfo: "Show server information by clicking the server ticker on a post" openServerInfo: "Show server information by clicking the server ticker on a post"
_sensitiveMediaDetection: _sensitiveMediaDetection:

View File

@ -28,6 +28,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, ref } from "vue"; import { nextTick, onMounted, ref } from "vue";
import { vibrate } from "@/scripts/vibrate";
const props = defineProps<{ const props = defineProps<{
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
@ -93,6 +94,8 @@ function onMousedown(evt: MouseEvent): void {
circleCenterY, circleCenterY,
); );
vibrate(10);
window.setTimeout(() => { window.setTimeout(() => {
ripple.style.transform = "scale(" + scale / 2 + ")"; ripple.style.transform = "scale(" + scale / 2 + ")";
}, 1); }, 1);

View File

@ -23,6 +23,7 @@
<button <button
v-for="emoji in searchResultCustom" v-for="emoji in searchResultCustom"
:key="emoji.id" :key="emoji.id"
v-vibrate="50"
class="_button item" class="_button item"
:title="emoji.name" :title="emoji.name"
tabindex="0" tabindex="0"

View File

@ -4,6 +4,7 @@
<div class="title"><slot name="header"></slot></div> <div class="title"><slot name="header"></slot></div>
<div class="divider"></div> <div class="divider"></div>
<button <button
v-vibrate="5"
class="_button" class="_button"
:aria-expanded="showBody" :aria-expanded="showBody"
:aria-controls="bodyId" :aria-controls="bodyId"

View File

@ -69,6 +69,7 @@ import { i18n } from "@/i18n";
import { $i } from "@/account"; import { $i } from "@/account";
import { getUserMenu } from "@/scripts/get-user-menu"; import { getUserMenu } from "@/scripts/get-user-menu";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { vibrate } from "@/scripts/vibrate";
const router = useRouter(); const router = useRouter();
@ -154,6 +155,7 @@ async function onClick() {
await os.api("following/create", { await os.api("following/create", {
userId: props.user.id, userId: props.user.id,
}); });
vibrate([30, 40, 100]);
hasPendingFollowRequestFromYou.value = true; hasPendingFollowRequestFromYou.value = true;
} }
} }

View File

@ -8,6 +8,7 @@
<div> <div>
<div <div
ref="itemsEl" ref="itemsEl"
v-vibrate="5"
class="rrevdjwt _popup _shadow" class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ :style="{

View File

@ -6,6 +6,7 @@
ref="el" ref="el"
v-hotkey="keymap" v-hotkey="keymap"
v-size="{ max: [500, 350] }" v-size="{ max: [500, 350] }"
v-vibrate="5"
:aria-label="accessibleLabel" :aria-label="accessibleLabel"
class="tkcbzcuz note-container" class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : null"
@ -225,9 +226,9 @@
isForeignLanguage && isForeignLanguage &&
translation == null translation == null
" "
v-tooltip.noDelay.bottom="i18n.ts.translate"
class="button _button" class="button _button"
@click.stop="translate" @click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
> >
<i class="ph-translate ph-bold ph-lg"></i> <i class="ph-translate ph-bold ph-lg"></i>
</button> </button>
@ -385,8 +386,8 @@ const isForeignLanguage: boolean =
async function translate_(noteId, targetLang: string) { async function translate_(noteId, targetLang: string) {
return await os.api("notes/translate", { return await os.api("notes/translate", {
noteId: noteId, noteId,
targetLang: targetLang, targetLang,
}); });
} }

View File

@ -130,9 +130,9 @@
isForeignLanguage && isForeignLanguage &&
translation == null translation == null
" "
v-tooltip.noDelay.bottom="i18n.ts.translate"
class="button _button" class="button _button"
@click.stop="translate" @click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
> >
<i class="ph-translate ph-bold ph-lg"></i> <i class="ph-translate ph-bold ph-lg"></i>
</button> </button>
@ -306,8 +306,8 @@ const isForeignLanguage: boolean =
async function translate_(noteId, targetLang: string) { async function translate_(noteId, targetLang: string) {
return await os.api("notes/translate", { return await os.api("notes/translate", {
noteId: noteId, noteId,
targetLang: targetLang, targetLang,
}); });
} }

View File

@ -275,6 +275,7 @@ import { uploadFile } from "@/scripts/upload";
import { deepClone } from "@/scripts/clone"; import { deepClone } from "@/scripts/clone";
import XCheatSheet from "@/components/MkCheatSheetDialog.vue"; import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
import { preprocess } from "@/scripts/preprocess"; import { preprocess } from "@/scripts/preprocess";
import { vibrate } from "@/scripts/vibrate";
const modal = inject("modal"); const modal = inject("modal");
@ -937,6 +938,7 @@ async function post() {
text: err.message + "\n" + (err as any).id, text: err.message + "\n" + (err as any).id,
}); });
}); });
vibrate([10, 20, 10, 20, 10, 20, 60]);
} }
function cancel() { function cancel() {

View File

@ -3,6 +3,7 @@
v-if="count > 0" v-if="count > 0"
ref="buttonRef" ref="buttonRef"
v-ripple="canToggle" v-ripple="canToggle"
v-vibrate="[10, 30, 40]"
class="hkzvhatu _button" class="hkzvhatu _button"
:class="{ :class="{
reacted: note.myReaction == reaction, reacted: note.myReaction == reaction,

View File

@ -32,6 +32,7 @@ import { useTooltip } from "@/scripts/use-tooltip";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import type { MenuItem } from "@/types/menu"; import type { MenuItem } from "@/types/menu";
import { vibrate } from "@/scripts/vibrate";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -197,6 +198,7 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
icon: "ph-hand-fist ph-bold ph-lg", icon: "ph-hand-fist ph-bold ph-lg",
danger: false, danger: false,
action: () => { action: () => {
vibrate([30, 30, 60]);
os.api( os.api(
"notes/create", "notes/create",
props.note.visibility === "specified" props.note.visibility === "specified"

View File

@ -1,6 +1,7 @@
<template> <template>
<button <button
v-tooltip.noDelay.bottom="i18n.ts._gallery.like" v-tooltip.noDelay.bottom="i18n.ts._gallery.like"
v-vibrate="[30, 50, 50]"
class="button _button" class="button _button"
@click.stop="star($event)" @click.stop="star($event)"
> >

View File

@ -2,6 +2,7 @@
<button <button
ref="buttonRef" ref="buttonRef"
v-tooltip.noDelay.bottom="i18n.ts._gallery.like" v-tooltip.noDelay.bottom="i18n.ts._gallery.like"
v-vibrate="[30, 50, 50]"
class="button _button" class="button _button"
:class="$style.root" :class="$style.root"
@click.stop="toggleStar($event)" @click.stop="toggleStar($event)"

View File

@ -12,6 +12,7 @@
<button <button
v-if="displayBackButton" v-if="displayBackButton"
v-tooltip.noDelay="i18n.ts.goBack" v-tooltip.noDelay="i18n.ts.goBack"
v-vibrate="5"
class="_buttonIcon button icon backButton" class="_buttonIcon button icon backButton"
@click.stop="goBack()" @click.stop="goBack()"
@touchstart="preventDrag" @touchstart="preventDrag"
@ -20,6 +21,7 @@
</button> </button>
<MkAvatar <MkAvatar
v-if="narrow && props.displayMyAvatar && $i" v-if="narrow && props.displayMyAvatar && $i"
v-vibrate="5"
class="avatar button" class="avatar button"
:user="$i" :user="$i"
:disable-preview="true" :disable-preview="true"
@ -77,6 +79,7 @@
v-for="tab in tabs" v-for="tab in tabs"
:ref="(el) => (tabRefs[tab.key] = el)" :ref="(el) => (tabRefs[tab.key] = el)"
v-tooltip.noDelay="tab.title" v-tooltip.noDelay="tab.title"
v-vibrate="5"
class="tab _button" class="tab _button"
:class="{ :class="{
active: tab.key != null && tab.key === props.tab, active: tab.key != null && tab.key === props.tab,
@ -108,6 +111,7 @@
<template v-for="action in actions"> <template v-for="action in actions">
<button <button
v-tooltip.noDelay="action.text" v-tooltip.noDelay="action.text"
v-vibrate="5"
class="_buttonIcon button" class="_buttonIcon button"
:class="{ highlighted: action.highlighted }" :class="{ highlighted: action.highlighted }"
@click.stop="action.handler" @click.stop="action.handler"

View File

@ -12,6 +12,7 @@ import clickAnime from "./click-anime";
import panel from "./panel"; import panel from "./panel";
import adaptiveBorder from "./adaptive-border"; import adaptiveBorder from "./adaptive-border";
import focus from "./focus"; import focus from "./focus";
import vibrate from "./vibrate";
export default function (app: App) { export default function (app: App) {
app.directive("userPreview", userPreview); app.directive("userPreview", userPreview);
@ -27,4 +28,5 @@ export default function (app: App) {
app.directive("panel", panel); app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder); app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus); app.directive("focus", focus);
app.directive("vibrate", vibrate);
} }

View File

@ -0,0 +1,11 @@
import type { Directive } from "vue";
import { vibrate } from "../scripts/vibrate";
export default {
mounted(el, binding) {
const pattern = (binding.value as VibratePattern) ?? 20;
el.addEventListener("mousedown", () => {
vibrate(pattern);
});
},
} as Directive;

View File

@ -46,14 +46,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from "vue"; import { onMounted, ref } from "vue";
import XForm from "./auth.form.vue"; import XForm from "./auth.form.vue";
import MkSignin from "@/components/MkSignin.vue"; import MkSignin from "@/components/MkSignin.vue";
import MkKeyValue from "@/components/MkKeyValue.vue"; import MkKeyValue from "@/components/MkKeyValue.vue";
import * as os from "@/os"; import * as os from "@/os";
import { login } from "@/account"; import { $i, login } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { $i } from "@/account";
const props = defineProps<{ const props = defineProps<{
token: string; token: string;
@ -102,11 +101,7 @@ const accepted = () => {
const isMastodon = !!getUrlParams().mastodon; const isMastodon = !!getUrlParams().mastodon;
if (session.value.app.callbackUrl && isMastodon) { if (session.value.app.callbackUrl && isMastodon) {
const redirectUri = decodeURIComponent(getUrlParams().redirect_uri); const redirectUri = decodeURIComponent(getUrlParams().redirect_uri);
if ( if (!session.value.app.callbackUrl.split("\n").includes(redirectUri)) {
!session.value.app.callbackUrl
.split("\n")
.some((p) => p === redirectUri)
) {
state.value = "fetch-session-error"; state.value = "fetch-session-error";
fetching.value = false; fetching.value = false;
throw new Error("Callback URI doesn't match registered app"); throw new Error("Callback URI doesn't match registered app");

View File

@ -120,6 +120,7 @@ import {
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { stream } from "@/stream";
import * as sound from "@/scripts/sound"; import * as sound from "@/scripts/sound";
import { vibrate } from "@/scripts/vibrate";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { $i } from "@/account"; import { $i } from "@/account";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
@ -251,6 +252,7 @@ function onDrop(ev: DragEvent): void {
function onMessage(message) { function onMessage(message) {
sound.play("chat"); sound.play("chat");
vibrate([30, 30, 30]);
const _isBottom = isBottomVisible(rootEl.value, 64); const _isBottom = isBottomVisible(rootEl.value, 64);

View File

@ -136,6 +136,12 @@
class="_formBlock" class="_formBlock"
>{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch >{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch
> >
<FormSwitch
v-model="vibrate"
class="_formBlock"
@click="demoVibrate"
>{{ i18n.ts.vibrate }}
</FormSwitch>
<FormRadios v-model="fontSize" class="_formBlock"> <FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ i18n.ts.fontSize }}</template> <template #label>{{ i18n.ts.fontSize }}</template>
<option :value="null"> <option :value="null">
@ -273,7 +279,7 @@ import FormSection from "@/components/form/section.vue";
import FormLink from "@/components/form/link.vue"; import FormLink from "@/components/form/link.vue";
import MkLink from "@/components/MkLink.vue"; import MkLink from "@/components/MkLink.vue";
import { langs } from "@/config"; import { langs } from "@/config";
import { defaultStore } from "@/store"; import { ColdDeviceStorage, defaultStore } from "@/store";
import * as os from "@/os"; import * as os from "@/os";
import { unisonReload } from "@/scripts/unison-reload"; import { unisonReload } from "@/scripts/unison-reload";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -295,6 +301,10 @@ async function reloadAsk() {
unisonReload(); unisonReload();
} }
function demoVibrate() {
window.navigator.vibrate(100);
}
const overridedDeviceKind = computed( const overridedDeviceKind = computed(
defaultStore.makeGetterSetter("overridedDeviceKind"), defaultStore.makeGetterSetter("overridedDeviceKind"),
); );
@ -331,6 +341,7 @@ const disableDrawer = computed(defaultStore.makeGetterSetter("disableDrawer"));
const disableShowingAnimatedImages = computed( const disableShowingAnimatedImages = computed(
defaultStore.makeGetterSetter("disableShowingAnimatedImages"), defaultStore.makeGetterSetter("disableShowingAnimatedImages"),
); );
const vibrate = computed(ColdDeviceStorage.makeGetterSetter("vibrate"));
const loadRawImages = computed(defaultStore.makeGetterSetter("loadRawImages")); const loadRawImages = computed(defaultStore.makeGetterSetter("loadRawImages"));
const imageNewTab = computed(defaultStore.makeGetterSetter("imageNewTab")); const imageNewTab = computed(defaultStore.makeGetterSetter("imageNewTab"));
const nsfw = computed(defaultStore.makeGetterSetter("nsfw")); const nsfw = computed(defaultStore.makeGetterSetter("nsfw"));

View File

@ -126,6 +126,7 @@ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"syncDeviceDarkMode", "syncDeviceDarkMode",
"plugins", "plugins",
"mediaVolume", "mediaVolume",
"vibrate",
"sound_masterVolume", "sound_masterVolume",
"sound_note", "sound_note",
"sound_noteMy", "sound_noteMy",

View File

@ -239,8 +239,8 @@ export function getNoteMenu(props: {
async function translate_(noteId: number, targetLang: string) { async function translate_(noteId: number, targetLang: string) {
return await os.api("notes/translate", { return await os.api("notes/translate", {
noteId: noteId, noteId,
targetLang: targetLang, targetLang,
}); });
} }

View File

@ -2,9 +2,11 @@ import { defineAsyncComponent } from "vue";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { popup } from "@/os"; import { popup } from "@/os";
import { vibrate } from "@/scripts/vibrate";
export function pleaseLogin(path?: string) { export function pleaseLogin(path?: string) {
if ($i) return; if ($i) return;
vibrate(100);
popup( popup(
defineAsyncComponent(() => import("@/components/MkSigninDialog.vue")), defineAsyncComponent(() => import("@/components/MkSigninDialog.vue")),

View File

@ -0,0 +1,6 @@
import { ColdDeviceStorage } from "@/store";
export function vibrate(pattern: VibratePattern) {
if (!ColdDeviceStorage.get("vibrate") || !window.navigator.vibrate) return;
window.navigator.vibrate(pattern);
}

View File

@ -382,6 +382,7 @@ export class ColdDeviceStorage {
syncDeviceDarkMode: true, syncDeviceDarkMode: true,
plugins: [] as Plugin[], plugins: [] as Plugin[],
mediaVolume: 0.5, mediaVolume: 0.5,
vibrate: true,
sound_masterVolume: 0.3, sound_masterVolume: 0.3,
sound_note: { type: "none", volume: 0 }, sound_note: { type: "none", volume: 0 },
sound_noteMy: { type: "syuilo/up", volume: 1 }, sound_noteMy: { type: "syuilo/up", volume: 1 },

View File

@ -25,6 +25,7 @@
<button <button
v-if="!isDesktop && !isMobile" v-if="!isDesktop && !isMobile"
v-vibrate="5"
class="widgetButton _button" class="widgetButton _button"
@click="widgetsShowing = true" @click="widgetsShowing = true"
> >
@ -33,6 +34,7 @@
<div v-if="isMobile" class="buttons"> <div v-if="isMobile" class="buttons">
<button <button
v-vibrate="5"
:aria-label="i18n.t('menu')" :aria-label="i18n.t('menu')"
class="button nav _button" class="button nav _button"
@click="drawerMenuShowing = true" @click="drawerMenuShowing = true"
@ -48,6 +50,7 @@
</div> </div>
</button> </button>
<button <button
v-vibrate="5"
:aria-label="i18n.t('home')" :aria-label="i18n.t('home')"
class="button home _button" class="button home _button"
@click=" @click="
@ -65,6 +68,7 @@
</div> </div>
</button> </button>
<button <button
v-vibrate="5"
:aria-label="i18n.t('notifications')" :aria-label="i18n.t('notifications')"
class="button notifications _button" class="button notifications _button"
@click=" @click="
@ -73,6 +77,7 @@
" "
> >
<div <div
v-vibrate="5"
class="button-wrapper" class="button-wrapper"
:class="buttonAnimIndex === 1 ? 'on' : ''" :class="buttonAnimIndex === 1 ? 'on' : ''"
> >
@ -86,6 +91,7 @@
</div> </div>
</button> </button>
<button <button
v-vibrate="5"
:aria-label="i18n.t('messaging')" :aria-label="i18n.t('messaging')"
class="button messaging _button" class="button messaging _button"
@click=" @click="
@ -107,6 +113,7 @@
</div> </div>
</button> </button>
<button <button
v-vibrate="5"
:aria-label="i18n.t('_deck._columns.widgets')" :aria-label="i18n.t('_deck._columns.widgets')"
class="button widget _button" class="button widget _button"
@click="widgetsShowing = true" @click="widgetsShowing = true"
@ -225,7 +232,7 @@ provideMetadataReceiver((info) => {
const menuIndicated = computed(() => { const menuIndicated = computed(() => {
for (const def in navbarItemDef) { for (const def in navbarItemDef) {
if (def === "notifications") continue; // if (def === "notifications" || def === "messaging") continue; // Notifications & Messaging are bottom nav buttons and thus shouldn't be highlighted in the sidebar
if (navbarItemDef[def].indicated) return true; if (navbarItemDef[def].indicated) return true;
} }
return false; return false;