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"
languageForTranslation: "Post translation language"
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"
_sensitiveMediaDetection:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import clickAnime from "./click-anime";
import panel from "./panel";
import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
import vibrate from "./vibrate";
export default function (app: App) {
app.directive("userPreview", userPreview);
@ -27,4 +28,5 @@ export default function (app: App) {
app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder);
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>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { onMounted, ref } from "vue";
import XForm from "./auth.form.vue";
import MkSignin from "@/components/MkSignin.vue";
import MkKeyValue from "@/components/MkKeyValue.vue";
import * as os from "@/os";
import { login } from "@/account";
import { $i, login } from "@/account";
import { i18n } from "@/i18n";
import { $i } from "@/account";
const props = defineProps<{
token: string;
@ -102,11 +101,7 @@ const accepted = () => {
const isMastodon = !!getUrlParams().mastodon;
if (session.value.app.callbackUrl && isMastodon) {
const redirectUri = decodeURIComponent(getUrlParams().redirect_uri);
if (
!session.value.app.callbackUrl
.split("\n")
.some((p) => p === redirectUri)
) {
if (!session.value.app.callbackUrl.split("\n").includes(redirectUri)) {
state.value = "fetch-session-error";
fetching.value = false;
throw new Error("Callback URI doesn't match registered app");

View file

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

View file

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

View file

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

View file

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

View file

@ -2,9 +2,11 @@ import { defineAsyncComponent } from "vue";
import { $i } from "@/account";
import { i18n } from "@/i18n";
import { popup } from "@/os";
import { vibrate } from "@/scripts/vibrate";
export function pleaseLogin(path?: string) {
if ($i) return;
vibrate(100);
popup(
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,
plugins: [] as Plugin[],
mediaVolume: 0.5,
vibrate: true,
sound_masterVolume: 0.3,
sound_note: { type: "none", volume: 0 },
sound_noteMy: { type: "syuilo/up", volume: 1 },

View file

@ -25,6 +25,7 @@
<button
v-if="!isDesktop && !isMobile"
v-vibrate="5"
class="widgetButton _button"
@click="widgetsShowing = true"
>
@ -33,6 +34,7 @@
<div v-if="isMobile" class="buttons">
<button
v-vibrate="5"
:aria-label="i18n.t('menu')"
class="button nav _button"
@click="drawerMenuShowing = true"
@ -48,6 +50,7 @@
</div>
</button>
<button
v-vibrate="5"
:aria-label="i18n.t('home')"
class="button home _button"
@click="
@ -65,6 +68,7 @@
</div>
</button>
<button
v-vibrate="5"
:aria-label="i18n.t('notifications')"
class="button notifications _button"
@click="
@ -73,6 +77,7 @@
"
>
<div
v-vibrate="5"
class="button-wrapper"
:class="buttonAnimIndex === 1 ? 'on' : ''"
>
@ -86,6 +91,7 @@
</div>
</button>
<button
v-vibrate="5"
:aria-label="i18n.t('messaging')"
class="button messaging _button"
@click="
@ -107,6 +113,7 @@
</div>
</button>
<button
v-vibrate="5"
:aria-label="i18n.t('_deck._columns.widgets')"
class="button widget _button"
@click="widgetsShowing = true"
@ -225,7 +232,7 @@ provideMetadataReceiver((info) => {
const menuIndicated = computed(() => {
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;
}
return false;