diff options
Diffstat (limited to 'packages/frontend/src/utility')
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); |