Merge branch 'feat/vibration' into 'develop'
feat: vibration See merge request firefish/firefish!10595
This commit is contained in:
commit
373a26d868
25 changed files with 77 additions and 18 deletions
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<button
|
||||
v-for="emoji in searchResultCustom"
|
||||
:key="emoji.id"
|
||||
v-vibrate="50"
|
||||
class="_button item"
|
||||
:title="emoji.name"
|
||||
tabindex="0"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<div>
|
||||
<div
|
||||
ref="itemsEl"
|
||||
v-vibrate="5"
|
||||
class="rrevdjwt _popup _shadow"
|
||||
:class="{ center: align === 'center', asDrawer }"
|
||||
:style="{
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)"
|
||||
>
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
11
packages/client/src/directives/vibrate.ts
Normal file
11
packages/client/src/directives/vibrate.ts
Normal 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;
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -126,6 +126,7 @@ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
|||
"syncDeviceDarkMode",
|
||||
"plugins",
|
||||
"mediaVolume",
|
||||
"vibrate",
|
||||
"sound_masterVolume",
|
||||
"sound_note",
|
||||
"sound_noteMy",
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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")),
|
||||
|
|
6
packages/client/src/scripts/vibrate.ts
Normal file
6
packages/client/src/scripts/vibrate.ts
Normal 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);
|
||||
}
|
|
@ -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 },
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue