summaryrefslogtreecommitdiff
path: root/packages/frontend/src/utility
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/utility')
-rw-r--r--packages/frontend/src/utility/admin-lookup.ts10
-rw-r--r--packages/frontend/src/utility/autocomplete.ts50
-rw-r--r--packages/frontend/src/utility/chart-vline.ts2
-rw-r--r--packages/frontend/src/utility/check-word-mute.ts2
-rw-r--r--packages/frontend/src/utility/collect-page-vars.ts73
-rw-r--r--packages/frontend/src/utility/deep-equal.ts2
-rw-r--r--packages/frontend/src/utility/drive.ts30
-rw-r--r--packages/frontend/src/utility/element-contains.ts (renamed from packages/frontend/src/utility/contains.ts)5
-rw-r--r--packages/frontend/src/utility/file-drop.ts22
-rw-r--r--packages/frontend/src/utility/form.ts54
-rw-r--r--packages/frontend/src/utility/get-drive-file-menu.ts7
-rw-r--r--packages/frontend/src/utility/get-embed-code.ts13
-rw-r--r--packages/frontend/src/utility/get-note-menu.ts2
-rw-r--r--packages/frontend/src/utility/get-user-environment.ts2
-rw-r--r--packages/frontend/src/utility/image-compositor-functions/blur.glsl55
-rw-r--r--packages/frontend/src/utility/image-compositor-functions/blur.ts4
-rw-r--r--packages/frontend/src/utility/image-compositor-functions/fill.glsl11
-rw-r--r--packages/frontend/src/utility/image-compositor-functions/pixelate.glsl34
-rw-r--r--packages/frontend/src/utility/image-frame-renderer/ImageFrameRenderer.ts2
-rw-r--r--packages/frontend/src/utility/is-birthday.ts28
-rw-r--r--packages/frontend/src/utility/mfm-function-picker.ts48
-rw-r--r--packages/frontend/src/utility/paginator.ts6
-rw-r--r--packages/frontend/src/utility/please-login.ts6
-rw-r--r--packages/frontend/src/utility/sensitive-file.ts33
-rw-r--r--packages/frontend/src/utility/snowfall-effect.ts22
-rw-r--r--packages/frontend/src/utility/sound.ts2
-rw-r--r--packages/frontend/src/utility/storage.ts14
-rw-r--r--packages/frontend/src/utility/timeline-date-separate.ts2
-rw-r--r--packages/frontend/src/utility/tour.ts2
29 files changed, 279 insertions, 264 deletions
diff --git a/packages/frontend/src/utility/admin-lookup.ts b/packages/frontend/src/utility/admin-lookup.ts
index 18eebaa8f8..74485a11d7 100644
--- a/packages/frontend/src/utility/admin-lookup.ts
+++ b/packages/frontend/src/utility/admin-lookup.ts
@@ -14,7 +14,7 @@ export async function lookupUser() {
});
if (canceled || result == null) return;
- const show = (user) => {
+ const show = (user: Misskey.entities.UserDetailed) => {
os.pageWindow(`/admin/user/${user.id}`);
};
@@ -36,7 +36,7 @@ export async function lookupUser() {
notFound();
}
});
- idPromise.then(show).catch(err => {
+ idPromise.then(show).catch(_ => {
notFound();
});
}
@@ -71,12 +71,8 @@ export async function lookupFile() {
});
if (canceled) return;
- const show = (file) => {
- os.pageWindow(`/admin/file/${file.id}`);
- };
-
misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
- show(file);
+ os.pageWindow(`/admin/file/${file.id}`);
}).catch(err => {
if (err.code === 'NO_SUCH_FILE') {
os.alert({
diff --git a/packages/frontend/src/utility/autocomplete.ts b/packages/frontend/src/utility/autocomplete.ts
index 82109af1a0..a44bf7c1ae 100644
--- a/packages/frontend/src/utility/autocomplete.ts
+++ b/packages/frontend/src/utility/autocomplete.ts
@@ -12,6 +12,15 @@ import { popup } from '@/os.js';
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
+type CompleteProps<T extends keyof CompleteInfo> = {
+ type: T;
+ value: CompleteInfo[T]['payload'];
+};
+
+function isCompleteType<T extends keyof CompleteInfo>(expectedType: T, props: CompleteProps<keyof CompleteInfo>): props is CompleteProps<T> {
+ return props.type === expectedType;
+}
+
export class Autocomplete {
private suggestion: {
x: Ref<number>;
@@ -194,7 +203,7 @@ export class Autocomplete {
this.currentType = type;
//#region サジェストを表示すべき位置を計算
- const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
+ const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart ?? 0);
const rect = this.textarea.getBoundingClientRect();
@@ -213,10 +222,11 @@ export class Autocomplete {
const _y = ref(y);
const _q = ref(q);
- const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
textarea: this.textarea,
close: this.close,
type: type,
+ //@ts-expect-error popupは今のところジェネリック型のコンポーネントに対応していない
q: _q,
x: _x,
y: _y,
@@ -252,19 +262,19 @@ export class Autocomplete {
/**
* オートコンプリートする
*/
- private complete<T extends keyof CompleteInfo>({ type, value }: { type: T; value: CompleteInfo[T]['payload'] }) {
+ private complete<T extends keyof CompleteInfo>(props: CompleteProps<T>) {
this.close();
const caret = Number(this.textarea.selectionStart);
- if (type === 'user') {
+ if (isCompleteType('user', props)) {
const source = this.text;
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
const after = source.substring(caret);
- const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
+ const acct = props.value.host === null ? props.value.username : `${props.value.username}@${toASCII(props.value.host)}`;
// 挿入
this.text = `${trimmedBefore}@${acct} ${after}`;
@@ -275,7 +285,7 @@ export class Autocomplete {
const pos = trimmedBefore.length + (acct.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
- } else if (type === 'hashtag') {
+ } else if (isCompleteType('hashtag', props)) {
const source = this.text;
const before = source.substring(0, caret);
@@ -283,15 +293,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
- this.text = `${trimmedBefore}#${value} ${after}`;
+ this.text = `${trimmedBefore}#${props.value} ${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
- const pos = trimmedBefore.length + (value.length + 2);
+ const pos = trimmedBefore.length + (props.value.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
- } else if (type === 'emoji') {
+ } else if (isCompleteType('emoji', props)) {
const source = this.text;
const before = source.substring(0, caret);
@@ -299,15 +309,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
- this.text = trimmedBefore + value + after;
+ this.text = trimmedBefore + props.value + after;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
- const pos = trimmedBefore.length + value.length;
+ const pos = trimmedBefore.length + props.value.length;
this.textarea.setSelectionRange(pos, pos);
});
- } else if (type === 'emojiComplete') {
+ } else if (isCompleteType('emojiComplete', props)) {
const source = this.text;
const before = source.substring(0, caret);
@@ -315,15 +325,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
- this.text = trimmedBefore + value + after;
+ this.text = trimmedBefore + props.value + after;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
- const pos = trimmedBefore.length + value.length;
+ const pos = trimmedBefore.length + props.value.length;
this.textarea.setSelectionRange(pos, pos);
});
- } else if (type === 'mfmTag') {
+ } else if (isCompleteType('mfmTag', props)) {
const source = this.text;
const before = source.substring(0, caret);
@@ -331,15 +341,15 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
- this.text = `${trimmedBefore}$[${value} ]${after}`;
+ this.text = `${trimmedBefore}$[${props.value} ]${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
- const pos = trimmedBefore.length + (value.length + 3);
+ const pos = trimmedBefore.length + (props.value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});
- } else if (type === 'mfmParam') {
+ } else if (isCompleteType('mfmParam', props)) {
const source = this.text;
const before = source.substring(0, caret);
@@ -347,12 +357,12 @@ export class Autocomplete {
const after = source.substring(caret);
// 挿入
- this.text = `${trimmedBefore}.${value}${after}`;
+ this.text = `${trimmedBefore}.${props.value}${after}`;
// キャレットを戻す
nextTick(() => {
this.textarea.focus();
- const pos = trimmedBefore.length + (value.length + 1);
+ const pos = trimmedBefore.length + (props.value.length + 1);
this.textarea.setSelectionRange(pos, pos);
});
}
diff --git a/packages/frontend/src/utility/chart-vline.ts b/packages/frontend/src/utility/chart-vline.ts
index 2fe4bdb83b..1097c66d0e 100644
--- a/packages/frontend/src/utility/chart-vline.ts
+++ b/packages/frontend/src/utility/chart-vline.ts
@@ -11,7 +11,7 @@ export const chartVLine = (vLineColor: string) => ({
const tooltip = chart.tooltip as any;
if (tooltip?._active?.length) {
const ctx = chart.ctx;
- const xs = tooltip._active.map(a => a.element.x);
+ const xs = tooltip._active.map((a: any) => a.element.x) as number[];
const x = xs.reduce((a, b) => a + b, 0) / xs.length;
const topY = chart.scales.y.top;
const bottomY = chart.scales.y.bottom;
diff --git a/packages/frontend/src/utility/check-word-mute.ts b/packages/frontend/src/utility/check-word-mute.ts
index 98fea1bced..eafc939c80 100644
--- a/packages/frontend/src/utility/check-word-mute.ts
+++ b/packages/frontend/src/utility/check-word-mute.ts
@@ -29,7 +29,7 @@ export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.
try {
return new RegExp(regexp[1], regexp[2]).test(text);
- } catch (err) {
+ } catch (_) {
// This should never happen due to input sanitisation.
return false;
}
diff --git a/packages/frontend/src/utility/collect-page-vars.ts b/packages/frontend/src/utility/collect-page-vars.ts
deleted file mode 100644
index 5096c0669e..0000000000
--- a/packages/frontend/src/utility/collect-page-vars.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-interface StringPageVar {
- name: string,
- type: 'string',
- value: string
-}
-
-interface NumberPageVar {
- name: string,
- type: 'number',
- value: number
-}
-
-interface BooleanPageVar {
- name: string,
- type: 'boolean',
- value: boolean
-}
-
-type PageVar = StringPageVar | NumberPageVar | BooleanPageVar;
-
-export function collectPageVars(content): PageVar[] {
- const pageVars: PageVar[] = [];
- const collect = (xs: any[]): void => {
- for (const x of xs) {
- if (x.type === 'textInput') {
- pageVars.push({
- name: x.name,
- type: 'string',
- value: x.default || '',
- });
- } else if (x.type === 'textareaInput') {
- pageVars.push({
- name: x.name,
- type: 'string',
- value: x.default || '',
- });
- } else if (x.type === 'numberInput') {
- pageVars.push({
- name: x.name,
- type: 'number',
- value: x.default || 0,
- });
- } else if (x.type === 'switch') {
- pageVars.push({
- name: x.name,
- type: 'boolean',
- value: x.default || false,
- });
- } else if (x.type === 'counter') {
- pageVars.push({
- name: x.name,
- type: 'number',
- value: 0,
- });
- } else if (x.type === 'radioButton') {
- pageVars.push({
- name: x.name,
- type: 'string',
- value: x.default || '',
- });
- } else if (x.children) {
- collect(x.children);
- }
- }
- };
- collect(content);
- return pageVars;
-}
diff --git a/packages/frontend/src/utility/deep-equal.ts b/packages/frontend/src/utility/deep-equal.ts
index 2859641dc7..ac2c2e68da 100644
--- a/packages/frontend/src/utility/deep-equal.ts
+++ b/packages/frontend/src/utility/deep-equal.ts
@@ -31,7 +31,7 @@ export function deepEqual(a: JsonLike, b: JsonLike): boolean {
if (aks.length !== bks.length) return false;
for (let i = 0; i < aks.length; i++) {
const k = aks[i];
- if (!deepEqual(a[k], (b as { [key: string]: JsonLike })[k])) return false;
+ if (!deepEqual((a as { [key: string]: JsonLike })[k], (b as { [key: string]: JsonLike })[k])) return false;
}
return true;
}
diff --git a/packages/frontend/src/utility/drive.ts b/packages/frontend/src/utility/drive.ts
index 64079d125a..7578ed36e6 100644
--- a/packages/frontend/src/utility/drive.ts
+++ b/packages/frontend/src/utility/drive.ts
@@ -180,8 +180,9 @@ export function chooseFileFromPcAndUpload(
export function chooseDriveFile(options: {
multiple?: boolean;
} = {}): Promise<Misskey.entities.DriveFile[]> {
- return new Promise(async resolve => {
- const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkDriveFileSelectDialog.vue').then(x => x.default), {
+ return new Promise((resolve, rej) => {
+ let dispose: () => void;
+ os.popupAsyncWithDialog(import('@/components/MkDriveFileSelectDialog.vue').then(x => x.default), {
multiple: options.multiple ?? false,
}, {
done: files => {
@@ -190,7 +191,7 @@ export function chooseDriveFile(options: {
}
},
closed: () => dispose(),
- });
+ }).then((d) => dispose = d.dispose, rej);
});
}
@@ -300,15 +301,28 @@ export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFi
});
}
-export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<(Misskey.entities.DriveFolder | null)[]> {
- return new Promise(async resolve => {
- const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkDriveFolderSelectDialog.vue').then(x => x.default), {
+export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<{
+ canceled: false;
+ folders: (Misskey.entities.DriveFolder | null)[];
+} | {
+ canceled: true;
+ folders: undefined;
+}> {
+ return new Promise((resolve, reject) => {
+ let dispose: () => void;
+ os.popupAsyncWithDialog(import('@/components/MkDriveFolderSelectDialog.vue').then(x => x.default), {
initialFolder,
}, {
done: folders => {
- resolve(folders);
+ resolve(folders == null ? {
+ canceled: true,
+ folders: undefined,
+ } : {
+ canceled: false,
+ folders,
+ });
},
closed: () => dispose(),
- });
+ }).then(d => dispose = d.dispose, reject);
});
}
diff --git a/packages/frontend/src/utility/contains.ts b/packages/frontend/src/utility/element-contains.ts
index 6137c06e85..8389d49278 100644
--- a/packages/frontend/src/utility/contains.ts
+++ b/packages/frontend/src/utility/element-contains.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-export default (parent, child, checkSame = true) => {
+export function elementContains(parent: Element | null, child: Element | null, checkSame = true) {
+ if (parent === null || child === null) return false;
if (checkSame && parent === child) return true;
let node = child.parentNode;
while (node) {
@@ -11,4 +12,4 @@ export default (parent, child, checkSame = true) => {
node = node.parentNode;
}
return false;
-};
+}
diff --git a/packages/frontend/src/utility/file-drop.ts b/packages/frontend/src/utility/file-drop.ts
index 4259fe25e9..ffc024e8f3 100644
--- a/packages/frontend/src/utility/file-drop.ts
+++ b/packages/frontend/src/utility/file-drop.ts
@@ -75,20 +75,18 @@ export async function readDataTransferItems(itemList: DataTransferItemList): Pro
});
}
- function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
- return new Promise(async (resolve) => {
- const allEntries = Array.of<FileSystemEntry>();
- const reader = fileSystemDirectoryEntry.createReader();
- while (true) {
- const entries = await new Promise<FileSystemEntry[]>((res, rej) => reader.readEntries(res, rej));
- if (entries.length === 0) {
- break;
- }
- allEntries.push(...entries);
+ async function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
+ const allEntries = Array.of<FileSystemEntry>();
+ const reader = fileSystemDirectoryEntry.createReader();
+ while (true) {
+ const entries = await new Promise<FileSystemEntry[]>((res, rej) => reader.readEntries(res, rej));
+ if (entries.length === 0) {
+ break;
}
+ allEntries.push(...entries);
+ }
- resolve(await Promise.all(allEntries.map(readEntry)));
- });
+ return await Promise.all(allEntries.map(readEntry));
}
// 扱いにくいので配列に変換
diff --git a/packages/frontend/src/utility/form.ts b/packages/frontend/src/utility/form.ts
index cb4a227f67..43dee37a0e 100644
--- a/packages/frontend/src/utility/form.ts
+++ b/packages/frontend/src/utility/form.ts
@@ -4,7 +4,7 @@
*/
import * as Misskey from 'misskey-js';
-import type { OptionValue } from '@/components/MkSelect.vue';
+import type { OptionValue } from '@/types/option-value.js';
export type EnumItem = string | {
label: string;
@@ -25,6 +25,7 @@ export interface StringFormItem extends FormItemBase {
required?: boolean;
multiline?: boolean;
treatAsMfm?: boolean;
+ manualSave?: boolean;
}
export interface NumberFormItem extends FormItemBase {
@@ -33,6 +34,7 @@ export interface NumberFormItem extends FormItemBase {
description?: string;
required?: boolean;
step?: number;
+ manualSave?: boolean;
}
export interface BooleanFormItem extends FormItemBase {
@@ -43,18 +45,18 @@ export interface BooleanFormItem extends FormItemBase {
export interface EnumFormItem extends FormItemBase {
type: 'enum';
- default?: string | null;
+ default?: OptionValue | null;
required?: boolean;
enum: EnumItem[];
}
export interface RadioFormItem extends FormItemBase {
type: 'radio';
- default?: unknown | null;
+ default?: OptionValue | null;
required?: boolean;
options: {
label: string;
- value: unknown;
+ value: OptionValue;
}[];
}
@@ -82,7 +84,7 @@ export interface ArrayFormItem extends FormItemBase {
export interface ButtonFormItem extends FormItemBase {
type: 'button';
content?: string;
- action: (ev: MouseEvent, v: any) => void;
+ action: (ev: PointerEvent, v: any) => void;
}
export interface DriveFileFormItem extends FormItemBase {
@@ -124,24 +126,32 @@ type NonNullableIfRequired<T, Item extends FormItem> =
type GetItemType<Item extends FormItem> =
Item extends StringFormItem
? NonNullableIfRequired<InferDefault<Item, string>, Item>
- : Item extends NumberFormItem
- ? NonNullableIfRequired<InferDefault<Item, number>, Item>
- : Item extends BooleanFormItem
- ? boolean
- : Item extends RadioFormItem
- ? GetRadioItemType<Item>
- : Item extends RangeFormItem
- ? NonNullableIfRequired<InferDefault<Item, number>, Item>
- : Item extends EnumFormItem
- ? GetEnumItemType<Item>
- : Item extends ArrayFormItem
- ? NonNullableIfRequired<InferDefault<Item, unknown[]>, Item>
- : Item extends ObjectFormItem
- ? NonNullableIfRequired<InferDefault<Item, Record<string, unknown>>, Item>
- : Item extends DriveFileFormItem
- ? Misskey.entities.DriveFile | undefined
- : never;
+ : Item extends NumberFormItem
+ ? NonNullableIfRequired<InferDefault<Item, number>, Item>
+ : Item extends BooleanFormItem
+ ? boolean
+ : Item extends RadioFormItem
+ ? GetRadioItemType<Item>
+ : Item extends RangeFormItem
+ ? NonNullableIfRequired<InferDefault<Item, number>, Item>
+ : Item extends EnumFormItem
+ ? GetEnumItemType<Item>
+ : Item extends ArrayFormItem
+ ? NonNullableIfRequired<InferDefault<Item, unknown[]>, Item>
+ : Item extends ObjectFormItem
+ ? NonNullableIfRequired<InferDefault<Item, Record<string, unknown>>, Item>
+ : Item extends DriveFileFormItem
+ ? Misskey.entities.DriveFile | undefined
+ : never;
export type GetFormResultType<F extends Form> = {
[P in keyof F]: GetItemType<F[P]>;
};
+
+export function getDefaultFormValues<F extends FormWithDefault>(form: F): GetFormResultType<F> {
+ const result = {} as GetFormResultType<F>;
+ for (const key of Object.keys(form) as (keyof F)[]) {
+ result[key] = form[key].default as GetItemType<F[typeof key]>;
+ }
+ return result;
+}
diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts
index 040cf8f976..fea7f8f1d3 100644
--- a/packages/frontend/src/utility/get-drive-file-menu.ts
+++ b/packages/frontend/src/utility/get-drive-file-menu.ts
@@ -44,10 +44,11 @@ async function describe(file: Misskey.entities.DriveFile) {
}
function move(file: Misskey.entities.DriveFile) {
- selectDriveFolder(null).then(folder => {
+ selectDriveFolder(null).then(({ canceled, folders }) => {
+ if (canceled) return;
misskeyApi('drive/files/update', {
fileId: file.id,
- folderId: folder[0] ? folder[0].id : null,
+ folderId: folders[0] ? folders[0].id : null,
});
});
}
@@ -89,7 +90,7 @@ async function deleteFile(file: Misskey.entities.DriveFile) {
}
export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
- const isImage = file.type.startsWith('image/');
+ const _isImage = file.type.startsWith('image/');
const menuItems: MenuItem[] = [];
diff --git a/packages/frontend/src/utility/get-embed-code.ts b/packages/frontend/src/utility/get-embed-code.ts
index 5ccd46cfe2..5817d7ece8 100644
--- a/packages/frontend/src/utility/get-embed-code.ts
+++ b/packages/frontend/src/utility/get-embed-code.ts
@@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineAsyncComponent } from 'vue';
-import { genId } from '@/utility/id.js';
import { url } from '@@/js/config.js';
import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js';
import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js';
+import { genId } from '@/utility/id.js';
import * as os from '@/os.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
@@ -21,19 +21,20 @@ export function normalizeEmbedParams(params: EmbedParams): Record<string, string
// paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す
const normalizedParams: Record<string, string> = {};
for (const key in params) {
+ const k = key as keyof EmbedParams;
// デフォルトの値と同じならparamsに含めない
- if (params[key] == null || params[key] === defaultEmbedParams[key]) {
+ if (params[k] == null || params[k] === defaultEmbedParams[k]) {
continue;
}
- switch (typeof params[key]) {
+ switch (typeof params[k]) {
case 'number':
- normalizedParams[key] = params[key].toString();
+ normalizedParams[k] = params[k].toString();
break;
case 'boolean':
- normalizedParams[key] = params[key] ? 'true' : 'false';
+ normalizedParams[k] = params[k] ? 'true' : 'false';
break;
default:
- normalizedParams[key] = params[key];
+ normalizedParams[k] = params[k];
break;
}
}
diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts
index fc165ea898..78176970f1 100644
--- a/packages/frontend/src/utility/get-note-menu.ts
+++ b/packages/frontend/src/utility/get-note-menu.ts
@@ -262,7 +262,7 @@ export function getNoteMenu(props: {
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
}
- async function promote(): Promise<void> {
+ async function _promote(): Promise<void> {
const { canceled, result: days } = await os.inputNumber({
title: i18n.ts.numberOfDays,
});
diff --git a/packages/frontend/src/utility/get-user-environment.ts b/packages/frontend/src/utility/get-user-environment.ts
index 3b8d43fb2c..ebae0492b1 100644
--- a/packages/frontend/src/utility/get-user-environment.ts
+++ b/packages/frontend/src/utility/get-user-environment.ts
@@ -39,7 +39,7 @@ export async function getUserEnvironment(): Promise<UserEnvironment> {
}
}
- const browserData = uaData.fullVersionList.find((item) => !/^\s*not.+a.+brand\s*$/i.test(item.brand));
+ const browserData = uaData.fullVersionList.find((item: any) => !/^\s*not.+a.+brand\s*$/i.test(item.brand));
return {
os: `${uaData.platform} ${osVersion}`,
browser: browserData ? `${browserData.brand} v${browserData.version}` : 'Unknown',
diff --git a/packages/frontend/src/utility/image-compositor-functions/blur.glsl b/packages/frontend/src/utility/image-compositor-functions/blur.glsl
index e591267887..dc48c2ae94 100644
--- a/packages/frontend/src/utility/image-compositor-functions/blur.glsl
+++ b/packages/frontend/src/utility/image-compositor-functions/blur.glsl
@@ -21,13 +21,20 @@ uniform float u_radius;
uniform int u_samples;
out vec4 out_color;
+float rand(vec2 value) {
+ return fract(sin(dot(value, vec2(12.9898, 78.233))) * 43758.5453);
+}
+
void main() {
float angle = -(u_angle * PI);
+ float aspect = in_resolution.x / max(in_resolution.y, 1.0);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
- vec2 rotatedUV = vec2(
- centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
- centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
- ) + u_offset;
+ vec2 scaledUv = vec2(centeredUv.x * aspect, centeredUv.y);
+ vec2 rotatedScaledUv = vec2(
+ scaledUv.x * cos(angle) - scaledUv.y * sin(angle),
+ scaledUv.x * sin(angle) + scaledUv.y * cos(angle)
+ );
+ vec2 rotatedUV = vec2(rotatedScaledUv.x / aspect, rotatedScaledUv.y) + u_offset;
bool isInside = false;
if (u_ellipse) {
@@ -46,31 +53,29 @@ void main() {
float totalSamples = 0.0;
// Make blur radius resolution-independent by using a percentage of image size
- // This ensures consistent visual blur regardless of image resolution
float referenceSize = min(in_resolution.x, in_resolution.y);
- float normalizedRadius = u_radius / 100.0; // Convert radius to percentage (0-15 -> 0-0.15)
- vec2 blurOffset = vec2(normalizedRadius) / in_resolution * referenceSize;
-
- // Calculate how many samples to take in each direction
- // This determines the grid density, not the blur extent
- int sampleRadius = int(sqrt(float(u_samples)) / 2.0);
+ float normalizedRadius = u_radius / 100.0;
+ float radiusPx = normalizedRadius * referenceSize;
+ vec2 texelSize = 1.0 / in_resolution;
- // Sample in a grid pattern within the specified radius
- for (int x = -sampleRadius; x <= sampleRadius; x++) {
- for (int y = -sampleRadius; y <= sampleRadius; y++) {
- // Normalize the grid position to [-1, 1] range
- float normalizedX = float(x) / float(sampleRadius);
- float normalizedY = float(y) / float(sampleRadius);
+ int sampleCount = max(u_samples, 1);
+ float sampleCountF = float(sampleCount);
+ float jitter = rand(in_uv * in_resolution);
+ float goldenAngle = 2.39996323;
- // Scale by radius to get the actual sampling offset
- vec2 offset = vec2(normalizedX, normalizedY) * blurOffset;
- vec2 sampleUV = in_uv + offset;
+ // Sample in a circular pattern to avoid axis-aligned artifacts
+ for (int i = 0; i < sampleCount; i++) {
+ float fi = float(i);
+ float radius = sqrt((fi + 0.5) / sampleCountF);
+ float theta = (fi + jitter) * goldenAngle;
+ vec2 direction = vec2(cos(theta), sin(theta));
+ vec2 offset = direction * (radiusPx * radius) * texelSize;
+ vec2 sampleUV = in_uv + offset;
- // Only sample if within texture bounds
- if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
- result += texture(in_texture, sampleUV);
- totalSamples += 1.0;
- }
+ if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
+ float weight = exp(-radius * radius * 4.0);
+ result += texture(in_texture, sampleUV) * weight;
+ totalSamples += weight;
}
}
diff --git a/packages/frontend/src/utility/image-compositor-functions/blur.ts b/packages/frontend/src/utility/image-compositor-functions/blur.ts
index 1ab8eee6ba..72711445cc 100644
--- a/packages/frontend/src/utility/image-compositor-functions/blur.ts
+++ b/packages/frontend/src/utility/image-compositor-functions/blur.ts
@@ -84,9 +84,9 @@ export const uiDefinition = {
radius: {
label: i18n.ts._imageEffector._fxProps.strength,
type: 'number',
- default: 3.0,
+ default: 10.0,
min: 0.0,
- max: 10.0,
+ max: 20.0,
step: 0.5,
},
},
diff --git a/packages/frontend/src/utility/image-compositor-functions/fill.glsl b/packages/frontend/src/utility/image-compositor-functions/fill.glsl
index f04dc5545a..02e5e3a071 100644
--- a/packages/frontend/src/utility/image-compositor-functions/fill.glsl
+++ b/packages/frontend/src/utility/image-compositor-functions/fill.glsl
@@ -27,11 +27,14 @@ void main() {
//float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
float angle = -(u_angle * PI);
+ float aspect = in_resolution.x / max(in_resolution.y, 1.0);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
- vec2 rotatedUV = vec2(
- centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
- centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
- ) + u_offset;
+ vec2 scaledUv = vec2(centeredUv.x * aspect, centeredUv.y);
+ vec2 rotatedScaledUv = vec2(
+ scaledUv.x * cos(angle) - scaledUv.y * sin(angle),
+ scaledUv.x * sin(angle) + scaledUv.y * cos(angle)
+ );
+ vec2 rotatedUV = vec2(rotatedScaledUv.x / aspect, rotatedScaledUv.y) + u_offset;
bool isInside = false;
if (u_ellipse) {
diff --git a/packages/frontend/src/utility/image-compositor-functions/pixelate.glsl b/packages/frontend/src/utility/image-compositor-functions/pixelate.glsl
index 4de3f27397..b08a3d798f 100644
--- a/packages/frontend/src/utility/image-compositor-functions/pixelate.glsl
+++ b/packages/frontend/src/utility/image-compositor-functions/pixelate.glsl
@@ -21,8 +21,6 @@ uniform int u_samples;
uniform float u_strength;
out vec4 out_color;
-// TODO: pixelateの中心を画像中心ではなく範囲の中心にする
-// TODO: 画像のアスペクト比に関わらず各画素は正方形にする
void main() {
if (u_strength <= 0.0) {
@@ -31,11 +29,14 @@ void main() {
}
float angle = -(u_angle * PI);
+ float aspect = in_resolution.x / max(in_resolution.y, 1.0);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
- vec2 rotatedUV = vec2(
- centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
- centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
- ) + u_offset;
+ vec2 scaledUv = vec2(centeredUv.x * aspect, centeredUv.y);
+ vec2 rotatedScaledUv = vec2(
+ scaledUv.x * cos(angle) - scaledUv.y * sin(angle),
+ scaledUv.x * sin(angle) + scaledUv.y * cos(angle)
+ );
+ vec2 rotatedUV = vec2(rotatedScaledUv.x / aspect, rotatedScaledUv.y) + u_offset;
bool isInside = false;
if (u_ellipse) {
@@ -50,19 +51,24 @@ void main() {
return;
}
- float dx = u_strength / 1.0;
- float dy = u_strength / 1.0;
+ float baseResolution = (in_resolution.x + in_resolution.y) * 0.5;
+ float dx = (u_strength * baseResolution) / max(in_resolution.x, 1.0);
+ float dy = (u_strength * baseResolution) / max(in_resolution.y, 1.0);
+ vec2 centerUv = vec2(0.5, 0.5) + u_offset;
vec2 new_uv = vec2(
- (dx * (floor((in_uv.x - 0.5 - (dx / 2.0)) / dx) + 0.5)),
- (dy * (floor((in_uv.y - 0.5 - (dy / 2.0)) / dy) + 0.5))
- ) + vec2(0.5 + (dx / 2.0), 0.5 + (dy / 2.0));
+ (dx * (floor((in_uv.x - centerUv.x - (dx / 2.0)) / dx) + 0.5)),
+ (dy * (floor((in_uv.y - centerUv.y - (dy / 2.0)) / dy) + 0.5))
+ ) + vec2(centerUv.x + (dx / 2.0), centerUv.y + (dy / 2.0));
vec4 result = vec4(0.0);
float totalSamples = 0.0;
- // TODO: より多くのサンプリング
- result += texture(in_texture, new_uv);
- totalSamples += 1.0;
+ vec2 halfStep = vec2(dx, dy) * 0.25;
+ result += texture(in_texture, new_uv + vec2(-halfStep.x, -halfStep.y));
+ result += texture(in_texture, new_uv + vec2(halfStep.x, -halfStep.y));
+ result += texture(in_texture, new_uv + vec2(-halfStep.x, halfStep.y));
+ result += texture(in_texture, new_uv + vec2(halfStep.x, halfStep.y));
+ totalSamples += 4.0;
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}
diff --git a/packages/frontend/src/utility/image-frame-renderer/ImageFrameRenderer.ts b/packages/frontend/src/utility/image-frame-renderer/ImageFrameRenderer.ts
index 9e97728785..591a94b855 100644
--- a/packages/frontend/src/utility/image-frame-renderer/ImageFrameRenderer.ts
+++ b/packages/frontend/src/utility/image-frame-renderer/ImageFrameRenderer.ts
@@ -201,7 +201,7 @@ export class ImageFrameRenderer {
qrSize,
);
qrImageBitmap.close();
- } catch (err) {
+ } catch (_) {
// nop
}
}
diff --git a/packages/frontend/src/utility/is-birthday.ts b/packages/frontend/src/utility/is-birthday.ts
new file mode 100644
index 0000000000..ff875281a2
--- /dev/null
+++ b/packages/frontend/src/utility/is-birthday.ts
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+export function isBirthday(user: Misskey.entities.UserDetailed, now = new Date()): boolean {
+ if (user.birthday == null) return false;
+
+ const [_, bm, bd] = user.birthday.split('-').map((v) => parseInt(v, 10));
+ if (isNaN(bm) || isNaN(bd)) return false;
+
+ const y = now.getFullYear();
+ const m = now.getMonth() + 1;
+ const d = now.getDate();
+
+ // 閏日生まれで平年の場合は3月1日を誕生日として扱う
+ if (bm === 2 && bd === 29 && m === 3 && d === 1 && !isLeapYear(y)) {
+ return true;
+ }
+
+ return m === bm && d === bd;
+}
+
+function isLeapYear(year: number): boolean {
+ return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
+}
diff --git a/packages/frontend/src/utility/mfm-function-picker.ts b/packages/frontend/src/utility/mfm-function-picker.ts
index 09802d580b..5580435db1 100644
--- a/packages/frontend/src/utility/mfm-function-picker.ts
+++ b/packages/frontend/src/utility/mfm-function-picker.ts
@@ -3,55 +3,27 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { nextTick } from 'vue';
import { MFM_TAGS } from '@@/js/const.js';
-import type { Ref } from 'vue';
-import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
/**
* MFMの装飾のリストを表示する
*/
-export function mfmFunctionPicker(anchorElement: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
+export function mfmFunctionPicker(anchorElement: HTMLElement | EventTarget | null, onChosen: (tag: string) => void, onClosed?: () => void) {
os.popupMenu([{
text: i18n.ts.addMfmFunction,
type: 'label',
- }, ...getFunctionList(textArea, textRef)], anchorElement);
-}
-
-function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>): MenuItem[] {
- return MFM_TAGS.map(tag => ({
+ }, ...MFM_TAGS.map(tag => ({
text: tag,
icon: 'ti ti-icons',
- action: () => add(textArea, textRef, tag),
- }));
-}
-
-function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, type: string) {
- const caretStart: number = textArea.selectionStart as number;
- const caretEnd: number = textArea.selectionEnd as number;
-
- MFM_TAGS.forEach(tag => {
- if (type === tag) {
- if (caretStart === caretEnd) {
- // 単純にFunctionを追加
- const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ]${textRef.value.substring(caretEnd)}`;
- textRef.value = trimmedText;
- } else {
- // 選択範囲を囲むようにFunctionを追加
- const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ${textRef.value.substring(caretStart, caretEnd)}]${textRef.value.substring(caretEnd)}`;
- textRef.value = trimmedText;
- }
- }
- });
-
- const nextCaretStart: number = caretStart + 3 + type.length;
- const nextCaretEnd: number = caretEnd + 3 + type.length;
-
- // キャレットを戻す
- nextTick(() => {
- textArea.focus();
- textArea.setSelectionRange(nextCaretStart, nextCaretEnd);
+ action: () => {
+ onChosen(tag);
+ },
+ }))], anchorElement, {
+ onClosed: () => {
+ if (onClosed) onClosed();
+ },
});
}
+
diff --git a/packages/frontend/src/utility/paginator.ts b/packages/frontend/src/utility/paginator.ts
index 59ae1e431a..45054acfd0 100644
--- a/packages/frontend/src/utility/paginator.ts
+++ b/packages/frontend/src/utility/paginator.ts
@@ -213,7 +213,7 @@ export class Paginator<
} : {}),
};
- const apiRes = (await misskeyApi(this.endpoint, data).catch(err => {
+ const apiRes = (await misskeyApi(this.endpoint, data).catch(_ => {
this.error.value = true;
this.fetching.value = false;
return null;
@@ -273,7 +273,7 @@ export class Paginator<
}),
};
- const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(err => {
+ const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(_ => {
return null;
})) as T[] | null;
@@ -326,7 +326,7 @@ export class Paginator<
}),
};
- const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(err => {
+ const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(_ => {
return null;
})) as T[] | null;
diff --git a/packages/frontend/src/utility/please-login.ts b/packages/frontend/src/utility/please-login.ts
index 737e7d7c6e..8120a8d1af 100644
--- a/packages/frontend/src/utility/please-login.ts
+++ b/packages/frontend/src/utility/please-login.ts
@@ -48,8 +48,8 @@ export async function pleaseLogin(opts: {
path?: string;
message?: string;
openOnRemote?: OpenOnRemoteOptions;
-} = {}) {
- if ($i) return;
+} = {}): Promise<boolean> {
+ if ($i != null) return true;
let _openOnRemote: OpenOnRemoteOptions | undefined = undefined;
@@ -71,5 +71,5 @@ export async function pleaseLogin(opts: {
closed: () => dispose(),
});
- throw new Error('signin required');
+ return false;
}
diff --git a/packages/frontend/src/utility/sensitive-file.ts b/packages/frontend/src/utility/sensitive-file.ts
new file mode 100644
index 0000000000..f1fc909e4a
--- /dev/null
+++ b/packages/frontend/src/utility/sensitive-file.ts
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import * as os from '@/os.js';
+import { prefer } from '@/preferences.js';
+import { i18n } from '@/i18n.js';
+
+export function shouldHideFileByDefault(file: Misskey.entities.DriveFile): boolean {
+ if (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) {
+ return true;
+ }
+
+ if (file.isSensitive && prefer.s.nsfw !== 'ignore') {
+ return true;
+ }
+
+ return false;
+}
+
+export async function canRevealFile(file: Misskey.entities.DriveFile): Promise<boolean> {
+ if (file.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
+ const { canceled } = await os.confirm({
+ type: 'question',
+ text: i18n.ts.sensitiveMediaRevealConfirm,
+ });
+ if (canceled) return false;
+ }
+
+ return true;
+}
diff --git a/packages/frontend/src/utility/snowfall-effect.ts b/packages/frontend/src/utility/snowfall-effect.ts
index cefa720ebf..aa86db6bd1 100644
--- a/packages/frontend/src/utility/snowfall-effect.ts
+++ b/packages/frontend/src/utility/snowfall-effect.ts
@@ -21,7 +21,7 @@ export class SnowfallEffect {
}>;
private uniforms: Record<string, {
type: string;
- value: number[] | Float32Array;
+ value: number | number[] | Float32Array;
location: WebGLUniformLocation;
}>;
private texture: WebGLTexture;
@@ -44,9 +44,9 @@ export class SnowfallEffect {
start: number;
previous: number;
} = {
- start: 0,
- previous: 0,
- };
+ start: 0,
+ previous: 0,
+ };
private raf = 0;
private density: number = 1 / 90;
@@ -90,7 +90,7 @@ export class SnowfallEffect {
mat2: 'uniformMatrix2fv',
mat3: 'uniformMatrix3fv',
mat4: 'uniformMatrix4fv',
- };
+ } as const;
private CAMERA = {
fov: 60,
@@ -167,7 +167,7 @@ export class SnowfallEffect {
return { ...this.WIND };
}
- private initShader(type, source): WebGLShader {
+ private initShader(type: number, source: string): WebGLShader {
const { gl } = this;
const shader = gl.createShader(type);
if (shader == null) throw new Error('Failed to create shader');
@@ -224,7 +224,7 @@ export class SnowfallEffect {
}
}
- private setBuffer(name: string, value?) {
+ private setBuffer(name: string, value?: number[] | undefined) {
const { gl, buffers } = this;
const buffer = buffers[name];
@@ -253,18 +253,18 @@ export class SnowfallEffect {
}
}
- private setUniform(name: string, value?) {
+ private setUniform(name: string, value?: number | number[] | Float32Array<ArrayBufferLike> | undefined) {
const { gl, uniforms } = this;
const uniform = uniforms[name];
- const setter = this.UNIFORM_SETTERS[uniform.type];
+ const setter = this.UNIFORM_SETTERS[uniform.type as keyof typeof this.UNIFORM_SETTERS];
const isMatrix = /^mat[2-4]$/i.test(uniform.type);
uniform.value = value ?? uniform.value;
if (isMatrix) {
- gl[setter](uniform.location, false, uniform.value);
+ (gl as any)[setter](uniform.location, false, uniform.value);
} else {
- gl[setter](uniform.location, uniform.value);
+ (gl as any)[setter](uniform.location, uniform.value);
}
}
diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts
index 8e79841647..303244d126 100644
--- a/packages/frontend/src/utility/sound.ts
+++ b/packages/frontend/src/utility/sound.ts
@@ -111,7 +111,7 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
try {
response = await window.fetch(url);
- } catch (err) {
+ } catch (_) {
return;
}
diff --git a/packages/frontend/src/utility/storage.ts b/packages/frontend/src/utility/storage.ts
index 9df3a251e6..42743f78ea 100644
--- a/packages/frontend/src/utility/storage.ts
+++ b/packages/frontend/src/utility/storage.ts
@@ -3,14 +3,24 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { computed, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
+import { readonly, ref } from 'vue';
import * as os from '@/os.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
-export const storagePersisted = ref(await navigator.storage.persisted());
+export const storagePersistenceSupported = window.isSecureContext && 'storage' in navigator;
+const storagePersisted = ref(false);
+
+export async function getStoragePersistenceStatusRef() {
+ if (storagePersistenceSupported) {
+ storagePersisted.value = await navigator.storage.persisted().catch(() => false);
+ }
+
+ return readonly(storagePersisted);
+}
export async function enableStoragePersistence() {
+ if (!storagePersistenceSupported) return;
try {
const persisted = await navigator.storage.persist();
if (persisted) {
diff --git a/packages/frontend/src/utility/timeline-date-separate.ts b/packages/frontend/src/utility/timeline-date-separate.ts
index 33ddea048b..de71b8ce11 100644
--- a/packages/frontend/src/utility/timeline-date-separate.ts
+++ b/packages/frontend/src/utility/timeline-date-separate.ts
@@ -104,7 +104,7 @@ export function makeDateGroupedTimelineComputedRef<T extends { id: string; creat
for (let i = 0; i < items.value.length; i++) {
const item = items.value[i];
const date = new Date(item.createdAt);
- const nextDate = items.value[i + 1] ? new Date(items.value[i + 1].createdAt) : null;
+ const _nextDate = items.value[i + 1] ? new Date(items.value[i + 1].createdAt) : null;
if (tl.length === 0 || (
span === 'day' && tl[tl.length - 1].date.getTime() !== date.getTime()
diff --git a/packages/frontend/src/utility/tour.ts b/packages/frontend/src/utility/tour.ts
index c6bfa35a66..b14486e953 100644
--- a/packages/frontend/src/utility/tour.ts
+++ b/packages/frontend/src/utility/tour.ts
@@ -13,7 +13,7 @@ type TourStep = {
};
export function startTour(steps: TourStep[]) {
- return new Promise<void>(async (resolve) => {
+ return new Promise<void>((resolve) => {
const currentStepIndex = ref(0);
const titleRef = ref(steps[0].title);
const descriptionRef = ref(steps[0].description);