diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2025-10-24 06:31:35 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-24 06:31:35 +0000 |
| commit | b4e16c83e2823f6348842c23c48de2caae52563a (patch) | |
| tree | e4efaf0f2910491d039aab59d544a92836b77266 /packages | |
| parent | Merge pull request #16591 from misskey-dev/develop (diff) | |
| parent | Release: 2025.10.1 (diff) | |
| download | misskey-b4e16c83e2823f6348842c23c48de2caae52563a.tar.gz misskey-b4e16c83e2823f6348842c23c48de2caae52563a.tar.bz2 misskey-b4e16c83e2823f6348842c23c48de2caae52563a.zip | |
Merge pull request #16629 from misskey-dev/develop
Release: 2025.10.1
Diffstat (limited to 'packages')
114 files changed, 1985 insertions, 1773 deletions
diff --git a/packages/backend/migration/1760607435831-RoleBadgesRemoteUsers.js b/packages/backend/migration/1760607435831-RoleBadgesRemoteUsers.js new file mode 100644 index 0000000000..483d35a91b --- /dev/null +++ b/packages/backend/migration/1760607435831-RoleBadgesRemoteUsers.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RoleBadgesRemoteUsers1760607435831 { + name = 'RoleBadgesRemoteUsers1760607435831' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "showRoleBadgesOfRemoteUsers" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "showRoleBadgesOfRemoteUsers"`); + } +} diff --git a/packages/backend/migration/1760790899857-unnecessary-null-default.js b/packages/backend/migration/1760790899857-unnecessary-null-default.js new file mode 100644 index 0000000000..d34758315f --- /dev/null +++ b/packages/backend/migration/1760790899857-unnecessary-null-default.js @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UnnecessaryNullDefault1760790899857 { + name = 'UnnecessaryNullDefault1760790899857' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "userId" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "systemWebhookId" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "urlPreviewUserAgent" DROP DEFAULT`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "urlPreviewUserAgent" SET DEFAULT NULL`); + await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "systemWebhookId" SET DEFAULT NULL`); + await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "userId" SET DEFAULT NULL`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 07a80abc0f..3228d7b096 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -39,17 +39,17 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.13.19", - "@swc/core-darwin-x64": "1.13.19", + "@swc/core-darwin-arm64": "1.13.20", + "@swc/core-darwin-x64": "1.13.20", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.13.19", - "@swc/core-linux-arm64-gnu": "1.13.19", - "@swc/core-linux-arm64-musl": "1.13.19", - "@swc/core-linux-x64-gnu": "1.13.19", - "@swc/core-linux-x64-musl": "1.13.19", - "@swc/core-win32-arm64-msvc": "1.13.19", - "@swc/core-win32-ia32-msvc": "1.13.19", - "@swc/core-win32-x64-msvc": "1.13.19", + "@swc/core-linux-arm-gnueabihf": "1.13.20", + "@swc/core-linux-arm64-gnu": "1.13.20", + "@swc/core-linux-arm64-musl": "1.13.20", + "@swc/core-linux-x64-gnu": "1.13.20", + "@swc/core-linux-x64-musl": "1.13.20", + "@swc/core-win32-arm64-msvc": "1.13.20", + "@swc/core-win32-ia32-msvc": "1.13.20", + "@swc/core-win32-x64-msvc": "1.13.20", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.0.9", @@ -69,10 +69,10 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.896.0", - "@aws-sdk/lib-storage": "3.895.0", + "@aws-sdk/client-s3": "3.908.0", + "@aws-sdk/lib-storage": "3.908.0", "@discordapp/twemoji": "16.0.1", - "@fastify/accepts": "5.0.2", + "@fastify/accepts": "5.0.3", "@fastify/cookie": "11.0.2", "@fastify/cors": "10.1.0", "@fastify/express": "4.0.2", @@ -81,7 +81,7 @@ "@fastify/static": "8.2.0", "@fastify/view": "10.0.2", "@misskey-dev/sharp-read-bmp": "1.2.0", - "@misskey-dev/summaly": "5.2.3", + "@misskey-dev/summaly": "5.2.4", "@napi-rs/canvas": "0.1.80", "@nestjs/common": "11.1.6", "@nestjs/core": "11.1.6", @@ -103,7 +103,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.58.8", + "bullmq": "5.61.0", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.6.2", @@ -120,12 +120,12 @@ "file-type": "19.6.0", "fluent-ffmpeg": "2.1.3", "form-data": "4.0.4", - "got": "14.4.9", - "happy-dom": "16.8.1", + "got": "14.5.0", + "happy-dom": "20.0.7", "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", - "ioredis": "5.8.0", + "ioredis": "5.8.1", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", @@ -134,7 +134,7 @@ "json5": "2.2.3", "jsonld": "8.3.3", "jsrsasign": "11.1.0", - "juice": "11.0.1", + "juice": "11.0.3", "meilisearch": "0.53.0", "mfm-js": "0.25.0", "microformats-parser": "2.0.4", @@ -145,7 +145,7 @@ "nanoid": "5.1.6", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.10.1", + "nodemailer": "7.0.9", "nsfwjs": "4.2.0", "oauth": "0.10.2", "oauth2orize": "1.12.0", @@ -171,17 +171,17 @@ "sanitize-html": "2.17.0", "secure-json-parse": "3.0.2", "sharp": "0.33.5", - "semver": "7.7.2", + "semver": "7.7.3", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.27.10", + "systeminformation": "5.27.11", "tinycolor2": "1.6.0", "tmp": "0.2.5", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", "typeorm": "0.3.27", - "typescript": "5.9.2", + "typescript": "5.9.3", "ulid": "2.4.0", "vary": "1.1.2", "web-push": "3.6.7", @@ -210,8 +210,8 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "22.18.6", - "@types/nodemailer": "6.4.19", + "@types/node": "22.18.10", + "@types/nodemailer": "6.4.20", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", @@ -231,8 +231,8 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", "aws-sdk-client-mock": "4.1.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.32.0", diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 567bad2a2d..816f83ec93 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -517,40 +517,43 @@ export class DriveService { this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`); //#region Check drive usage and mime type - if (user && !isLink) { + if (user != null && !isLink) { const isLocalUser = this.userEntityService.isLocalUser(user); - const policies = await this.roleService.getUserPolicies(user.id); + const isModerator = isLocalUser ? await this.roleService.isModerator(user) : false; + if (!isModerator) { + const policies = await this.roleService.getUserPolicies(user.id); - const allowedMimeTypes = policies.uploadableFileTypes; - const isAllowed = allowedMimeTypes.some((mimeType) => { - if (mimeType === '*' || mimeType === '*/*') return true; - if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1)); - return info.type.mime === mimeType; - }); - if (!isAllowed) { - throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', `Unallowed file type: ${info.type.mime}`); - } + const allowedMimeTypes = policies.uploadableFileTypes; + const isAllowed = allowedMimeTypes.some((mimeType) => { + if (mimeType === '*' || mimeType === '*/*') return true; + if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1)); + return info.type.mime === mimeType; + }); + if (!isAllowed) { + throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', `Unallowed file type: ${info.type.mime}`); + } - const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; - const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb; + const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; + const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb; - if (maxFileSize < info.size) { - if (isLocalUser) { - throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.'); + if (maxFileSize < info.size) { + if (isLocalUser) { + throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.'); + } } - } - const usage = await this.driveFileEntityService.calcDriveUsageOf(user); + const usage = await this.driveFileEntityService.calcDriveUsageOf(user); - this.registerLogger.debug('drive capacity override applied'); - this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + this.registerLogger.debug('drive capacity override applied'); + this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); - // If usage limit exceeded - if (driveCapacity < usage + info.size) { - if (isLocalUser) { - throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); + // If usage limit exceeded + if (driveCapacity < usage + info.size) { + if (isLocalUser) { + throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); + } + await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size); } - await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size); } } //#endregion diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 47021359e1..ac5b855096 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -512,8 +512,8 @@ export class UserEntityService implements OnModuleInit { } : undefined) : undefined, emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), - // パフォーマンス上の理由でローカルユーザーのみ - badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs + // パフォーマンス上の理由で、明示的に設定しない場合はローカルユーザーのみ取得 + badgeRoles: (this.meta.showRoleBadgesOfRemoteUsers || user.host == null) ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs .filter((r) => r.isPublic || iAmModerator) .sort((a, b) => b.displayOrder - a.displayOrder) .map((r) => ({ diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index f8021a7a84..205c9eeb89 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -717,6 +717,11 @@ export class MiMeta { }) public remoteNotesCleaningExpiryDaysForEachNotes: number; + @Column('boolean', { + default: false, + }) + public showRoleBadgesOfRemoteUsers: boolean; + @Column('jsonb', { default: { }, }) diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 21099c0a8c..2c7f793584 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -593,6 +593,10 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + showRoleBadgesOfRemoteUsers: { + type: 'boolean', + optional: false, nullable: false, + }, }, }, } as const; @@ -748,6 +752,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning, remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes, remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes, + showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index a1a2a99d6e..b3c2cecc67 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -209,6 +209,7 @@ export const paramDef = { enableRemoteNotesCleaning: { type: 'boolean' }, remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' }, remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' }, + showRoleBadgesOfRemoteUsers: { type: 'boolean' }, }, required: [], } as const; @@ -743,6 +744,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes; } + if (ps.showRoleBadgesOfRemoteUsers !== undefined) { + set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index bdaf0d4027..320e8b9cec 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -11,15 +11,15 @@ }, "devDependencies": { "@types/estree": "1.0.8", - "@types/node": "22.18.6", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", - "rollup": "4.52.2", - "typescript": "5.9.2" + "@types/node": "22.18.10", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "rollup": "4.52.4", + "typescript": "5.9.3" }, "dependencies": { "estree-walker": "3.0.3", "magic-string": "0.30.19", - "vite": "7.1.7" + "vite": "7.1.9" } } diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index cd5e5071a6..d4c922695b 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -26,47 +26,47 @@ "mfm-js": "0.25.0", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.52.2", + "rollup": "4.52.4", "sass": "1.93.2", "shiki": "3.13.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", - "typescript": "5.9.2", - "uuid": "11.1.0", - "vite": "7.1.7", + "typescript": "5.9.3", + "uuid": "13.0.0", + "vite": "7.1.9", "vue": "3.5.22" }, "devDependencies": { - "@misskey-dev/summaly": "5.2.3", + "@misskey-dev/summaly": "5.2.4", "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/estree": "1.0.8", "@types/micromatch": "4.0.9", - "@types/node": "22.18.6", + "@types/node": "22.18.10", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", "@vitest/coverage-v8": "3.2.4", "@vue/runtime-core": "3.5.22", "acorn": "8.15.0", - "cross-env": "10.0.0", + "cross-env": "10.1.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.5.0", "fast-glob": "3.3.3", - "happy-dom": "18.0.1", + "happy-dom": "20.0.7", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.11.3", + "msw": "2.11.5", "nodemon": "3.1.10", "prettier": "3.6.2", "start-server-and-test": "2.1.2", "tsx": "4.20.6", "vite-plugin-turbosnap": "1.0.3", - "vue-component-type-helpers": "3.0.8", + "vue-component-type-helpers": "3.1.1", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.0.8" + "vue-tsc": "3.1.1" } } diff --git a/packages/frontend-shared/js/emojilist.ts b/packages/frontend-shared/js/emojilist.ts index 09bea06719..20ddd0f7d7 100644 --- a/packages/frontend-shared/js/emojilist.ts +++ b/packages/frontend-shared/js/emojilist.ts @@ -39,13 +39,18 @@ for (let i = 0; i < emojilist.length; i++) { export const emojiCharByCategory = _charGroupByCategory; -export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string { +export function getUnicodeEmojiOrNull(char: string): UnicodeEmojiDef | null { // Colorize it because emojilist.json assumes that return unicodeEmojisMap.get(colorizeEmoji(char)) // カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする ?? unicodeEmojisMap.get(char) - // それでも見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する) - ?? char; + // それでも見つからない場合はnullを返す + ?? null; +} + +export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string { + // 絵文字が見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する) + return getUnicodeEmojiOrNull(char) ?? char; } export function isSupportedEmoji(char: string): boolean { diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 46f39496b1..981e4b07f3 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -21,13 +21,13 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "22.18.6", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", + "@types/node": "22.18.10", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", "esbuild": "0.25.10", "eslint-plugin-vue": "10.5.0", "nodemon": "3.1.10", - "typescript": "5.9.2", + "typescript": "5.9.3", "vue-eslint-parser": "10.2.0" }, "files": [ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 0200269fcd..e67c1e9b33 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,7 +24,7 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", "@rollup/pluginutils": "5.3.0", - "@sentry/vue": "10.15.0", + "@sentry/vue": "10.19.0", "@syuilo/aiscript": "1.1.2", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@twemoji/parser": "16.0.0", @@ -36,12 +36,12 @@ "broadcast-channel": "7.1.0", "buraha": "0.0.1", "canvas-confetti": "1.9.3", - "chart.js": "4.5.0", + "chart.js": "4.5.1", "chartjs-adapter-date-fns": "3.0.0", "chartjs-chart-matrix": "3.0.0", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.2.0", - "chromatic": "13.2.1", + "chromatic": "13.3.0", "compare-versions": "6.1.1", "cropperjs": "2.0.1", "date-fns": "4.1.0", @@ -57,7 +57,7 @@ "json5": "2.2.3", "magic-string": "0.30.19", "matter-js": "0.20.0", - "mediabunny": "1.21.0", + "mediabunny": "1.23.0", "mfm-js": "0.25.0", "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", @@ -66,7 +66,7 @@ "punycode.js": "2.3.1", "qr-code-styling": "1.9.2", "qr-scanner": "1.4.2", - "rollup": "4.52.2", + "rollup": "4.52.4", "sanitize-html": "2.17.0", "sass": "1.93.2", "shiki": "3.13.0", @@ -77,18 +77,18 @@ "tinycolor2": "1.6.0", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", - "typescript": "5.9.2", + "typescript": "5.9.3", "v-code-diff": "1.13.1", - "vite": "7.1.7", + "vite": "7.1.9", "vue": "3.5.22", "vuedraggable": "next", "wanakana": "5.3.1" }, "devDependencies": { - "@misskey-dev/summaly": "5.2.3", + "@misskey-dev/summaly": "5.2.4", "@storybook/addon-essentials": "8.6.14", "@storybook/addon-interactions": "8.6.14", - "@storybook/addon-links": "9.1.8", + "@storybook/addon-links": "9.1.10", "@storybook/addon-mdx-gfm": "8.6.14", "@storybook/addon-storysource": "8.6.14", "@storybook/blocks": "8.6.14", @@ -96,57 +96,58 @@ "@storybook/core-events": "8.6.14", "@storybook/manager-api": "8.6.14", "@storybook/preview-api": "8.6.14", - "@storybook/react": "9.1.8", - "@storybook/react-vite": "9.1.8", + "@storybook/react": "9.1.10", + "@storybook/react-vite": "9.1.10", "@storybook/test": "8.6.14", "@storybook/theming": "8.6.14", "@storybook/types": "8.6.14", - "@storybook/vue3": "9.1.8", - "@storybook/vue3-vite": "9.1.8", + "@storybook/vue3": "9.1.10", + "@storybook/vue3-vite": "9.1.10", "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", "@types/estree": "1.0.8", "@types/matter-js": "0.20.2", "@types/micromatch": "4.0.9", - "@types/node": "22.18.6", + "@types/node": "22.18.10", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/sanitize-html": "2.16.0", "@types/seedrandom": "3.0.8", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", "@vitest/coverage-v8": "3.2.4", "@vue/compiler-core": "3.5.22", "@vue/runtime-core": "3.5.22", "acorn": "8.15.0", - "cross-env": "10.0.0", - "cypress": "14.5.4", + "cross-env": "10.1.0", + "cypress": "15.4.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.5.0", "fast-glob": "3.3.3", - "happy-dom": "18.0.1", + "happy-dom": "20.0.7", "intersection-observer": "0.12.2", "micromatch": "4.0.8", "minimatch": "10.0.3", - "msw": "2.11.3", - "msw-storybook-addon": "2.0.5", + "msw": "2.11.5", + "msw-storybook-addon": "2.0.6", "nodemon": "3.1.10", "prettier": "3.6.2", - "react": "19.1.1", - "react-dom": "19.1.1", + "react": "19.2.0", + "react-dom": "19.2.0", "seedrandom": "3.0.5", "start-server-and-test": "2.1.2", - "storybook": "9.1.8", + "storybook": "9.1.10", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "tsx": "4.20.6", + "vite-plugin-glsl": "1.5.4", "vite-plugin-turbosnap": "1.0.3", "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5", - "vue-component-type-helpers": "3.0.8", + "vue-component-type-helpers": "3.1.1", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.0.8" + "vue-tsc": "3.1.1" } } diff --git a/packages/frontend/src/accounts.ts b/packages/frontend/src/accounts.ts index 60f7cd0b4b..79086c2b39 100644 --- a/packages/frontend/src/accounts.ts +++ b/packages/frontend/src/accounts.ts @@ -211,13 +211,13 @@ export async function switchAccount(host: string, id: string) { } } -export async function openAccountMenu(opts: { +export async function getAccountMenu(opts: { includeCurrentAccount?: boolean; withExtraOperation: boolean; active?: Misskey.entities.User['id']; onChoose?: (account: Misskey.entities.MeDetailed) => void; -}, ev: MouseEvent) { - if (!$i) return; +}) { + if ($i == null) throw new Error('No current account'); const me = $i; const callback = opts.onChoose; @@ -338,9 +338,7 @@ export async function openAccountMenu(opts: { menuItems.push(...accountItems); } - popupMenu(menuItems, ev.currentTarget ?? ev.target, { - align: 'left', - }); + return menuItems; } export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> { diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts index 0549ab76a0..dc84925375 100644 --- a/packages/frontend/src/aiscript/api.ts +++ b/packages/frontend/src/aiscript/api.ts @@ -66,7 +66,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string }) }); return confirm.canceled ? values.FALSE : values.TRUE; }), - 'Mk:toast': values.FN_NATIVE(async ([text]) => { + 'Mk:toast': values.FN_NATIVE(([text]) => { utils.assertString(text); os.toast(text.value); return values.NULL; diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 4becf32ab5..f9783cb65c 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -69,9 +69,6 @@ export async function common(createVue: () => Promise<App<Element>>) { if (lastVersion !== version) { miLocalStorage.setItem('lastVersion', version); - // テーマリビルドするため - miLocalStorage.removeItem('theme'); - try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため if (lastVersion != null && compareVersions(version, lastVersion) === 1) { isClientUpdated = true; @@ -176,7 +173,7 @@ export async function common(createVue: () => Promise<App<Element>>) { })(); applyTheme(theme); - }, { immediate: isSafeMode || miLocalStorage.getItem('theme') == null }); + }, { immediate: true }); window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light'; @@ -195,14 +192,6 @@ export async function common(createVue: () => Promise<App<Element>>) { applyTheme(theme ?? defaultLightTheme); } }); - } - - if (!isSafeMode) { - if (prefer.s.darkTheme && store.s.darkMode) { - if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme); - } else if (prefer.s.lightTheme && !store.s.darkMode) { - if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme); - } fetchInstanceMetaPromise.then(() => { // TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア diff --git a/packages/frontend/src/components/MkAnimBg.fragment.glsl b/packages/frontend/src/components/MkAnimBg.fragment.glsl new file mode 100644 index 0000000000..d40872bb7a --- /dev/null +++ b/packages/frontend/src/components/MkAnimBg.fragment.glsl @@ -0,0 +1,111 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec2 mod289(vec2 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec3 permute(vec3 x) { + return mod289(((x*34.0)+1.0)*x); +} + +float snoise(vec2 v) { + const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); + + vec2 i = floor(v + dot(v, C.yy)); + vec2 x0 = v - i + dot(i, C.xx); + + vec2 i1; + i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + + i = mod289(i); + vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); + + vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); + m = m*m; + m = m*m; + + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + + m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); + + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); +} + +in vec2 in_uv; +uniform float u_time; +uniform vec2 u_resolution; +uniform float u_spread; +uniform float u_speed; +uniform float u_warp; +uniform float u_focus; +uniform float u_itensity; +out vec4 out_color; + +float circle(in vec2 _pos, in vec2 _origin, in float _radius) { + float SPREAD = 0.7 * u_spread; + float SPEED = 0.00055 * u_speed; + float WARP = 1.5 * u_warp; + float FOCUS = 1.15 * u_focus; + + vec2 dist = _pos - _origin; + + float distortion = snoise(vec2( + _pos.x * 1.587 * WARP + u_time * SPEED * 0.5, + _pos.y * 1.192 * WARP + u_time * SPEED * 0.3 + )) * 0.5 + 0.5; + + float feather = 0.01 + SPREAD * pow(distortion, FOCUS); + + return 1.0 - smoothstep( + _radius - (_radius * feather), + _radius + (_radius * feather), + dot( dist, dist ) * 4.0 + ); +} + +void main() { + vec3 green = vec3(1.0) - vec3(153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0); + vec3 purple = vec3(1.0) - vec3(195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0); + vec3 orange = vec3(1.0) - vec3(255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0); + + float ratio = u_resolution.x / u_resolution.y; + + vec2 uv = vec2(in_uv.x, in_uv.y / ratio) * 0.5 + 0.5; + + vec3 color = vec3(0.0); + + float greenMix = snoise(in_uv * 1.31 + u_time * 0.8 * 0.00017) * 0.5 + 0.5; + float purpleMix = snoise(in_uv * 1.26 + u_time * 0.8 * -0.0001) * 0.5 + 0.5; + float orangeMix = snoise(in_uv * 1.34 + u_time * 0.8 * 0.00015) * 0.5 + 0.5; + + float alphaOne = 0.35 + 0.65 * pow(snoise(vec2(u_time * 0.00012, uv.x)) * 0.5 + 0.5, 1.2); + float alphaTwo = 0.35 + 0.65 * pow(snoise(vec2((u_time + 1561.0) * 0.00014, uv.x )) * 0.5 + 0.5, 1.2); + float alphaThree = 0.35 + 0.65 * pow(snoise(vec2((u_time + 3917.0) * 0.00013, uv.x )) * 0.5 + 0.5, 1.2); + + color += vec3(circle(uv, vec2(0.22 + sin(u_time * 0.000201) * 0.06, 0.80 + cos(u_time * 0.000151) * 0.06), 0.15)) * alphaOne * (purple * purpleMix + orange * orangeMix); + color += vec3(circle(uv, vec2(0.90 + cos(u_time * 0.000166) * 0.06, 0.42 + sin(u_time * 0.000138) * 0.06), 0.18)) * alphaTwo * (green * greenMix + purple * purpleMix); + color += vec3(circle(uv, vec2(0.19 + sin(u_time * 0.000112) * 0.06, 0.25 + sin(u_time * 0.000192) * 0.06), 0.09)) * alphaThree * (orange * orangeMix); + + color *= u_itensity + 1.0 * pow(snoise(vec2(in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009)) * 0.5 + 0.5, 2.0); + + vec3 inverted = vec3(1.0) - color; + out_color = vec4(color, max(max(color.x, color.y), color.z)); +} diff --git a/packages/frontend/src/components/MkAnimBg.vertex.glsl b/packages/frontend/src/components/MkAnimBg.vertex.glsl new file mode 100644 index 0000000000..56d6b017b1 --- /dev/null +++ b/packages/frontend/src/components/MkAnimBg.vertex.glsl @@ -0,0 +1,15 @@ +#version 300 es + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 position; +uniform vec2 u_scale; +out vec2 in_uv; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); + in_uv = position / u_scale; +} diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue index 0e1018dcbf..bcdc604bb8 100644 --- a/packages/frontend/src/components/MkAnimBg.vue +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -10,6 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import isChromatic from 'chromatic/isChromatic'; +import vertexShaderSource from './MkAnimBg.vertex.glsl'; +import fragmentShaderSource from './MkAnimBg.fragment.glsl'; import { initShaderProgram } from '@/utility/webgl.js'; const canvasEl = useTemplateRef('canvasEl'); @@ -42,126 +44,7 @@ onMounted(() => { const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - const shaderProgram = initShaderProgram(gl, `#version 300 es - in vec2 position; - uniform vec2 u_scale; - out vec2 in_uv; - - void main() { - gl_Position = vec4(position, 0.0, 1.0); - in_uv = position / u_scale; - } - `, `#version 300 es - precision mediump float; - - vec3 mod289(vec3 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; - } - - vec2 mod289(vec2 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; - } - - vec3 permute(vec3 x) { - return mod289(((x*34.0)+1.0)*x); - } - - float snoise(vec2 v) { - const vec4 C = vec4(0.211324865405187, - 0.366025403784439, - -0.577350269189626, - 0.024390243902439); - - vec2 i = floor(v + dot(v, C.yy) ); - vec2 x0 = v - i + dot(i, C.xx); - - vec2 i1; - i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); - vec4 x12 = x0.xyxy + C.xxzz; - x12.xy -= i1; - - i = mod289(i); - vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) - + i.x + vec3(0.0, i1.x, 1.0 )); - - vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); - m = m*m ; - m = m*m ; - - vec3 x = 2.0 * fract(p * C.www) - 1.0; - vec3 h = abs(x) - 0.5; - vec3 ox = floor(x + 0.5); - vec3 a0 = x - ox; - - m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); - - vec3 g; - g.x = a0.x * x0.x + h.x * x0.y; - g.yz = a0.yz * x12.xz + h.yz * x12.yw; - return 130.0 * dot(m, g); - } - - in vec2 in_uv; - uniform float u_time; - uniform vec2 u_resolution; - uniform float u_spread; - uniform float u_speed; - uniform float u_warp; - uniform float u_focus; - uniform float u_itensity; - out vec4 out_color; - - float circle( in vec2 _pos, in vec2 _origin, in float _radius ) { - float SPREAD = 0.7 * u_spread; - float SPEED = 0.00055 * u_speed; - float WARP = 1.5 * u_warp; - float FOCUS = 1.15 * u_focus; - - vec2 dist = _pos - _origin; - - float distortion = snoise( vec2( - _pos.x * 1.587 * WARP + u_time * SPEED * 0.5, - _pos.y * 1.192 * WARP + u_time * SPEED * 0.3 - ) ) * 0.5 + 0.5; - - float feather = 0.01 + SPREAD * pow( distortion, FOCUS ); - - return 1.0 - smoothstep( - _radius - ( _radius * feather ), - _radius + ( _radius * feather ), - dot( dist, dist ) * 4.0 - ); - } - - void main() { - vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 ); - vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 ); - vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 ); - - float ratio = u_resolution.x / u_resolution.y; - - vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5; - - vec3 color = vec3( 0.0 ); - - float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5; - float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5; - float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5; - - float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 ); - float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 ); - float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 ); - - color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix ); - color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix ); - color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix ); - - color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 ); - - vec3 inverted = vec3( 1.0 ) - color; - out_color = vec4(color, max(max(color.x, color.y), color.z)); - } - `); + const shaderProgram = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource); if (shaderProgram == null) return; gl.useProgram(shaderProgram); diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue index 5ce514f93e..0fc8fa9dcc 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <div :class="$style.container"> <div :class="$style.preview"> - <canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas> + <canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown.prevent.stop="onImagePointerdown"></canvas> <div :class="$style.previewContainer"> <div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div> <div class="_acrylic" :class="$style.editControls"> @@ -257,8 +257,12 @@ function onImagePointerdown(ev: PointerEvent) { xOffset /= 2; yOffset /= 2; - let startX = ev.offsetX - xOffset; - let startY = ev.offsetY - yOffset; + const rect = canvasEl.value.getBoundingClientRect(); + const pointerOffsetX = ev.clientX - rect.left; + const pointerOffsetY = ev.clientY - rect.top; + + let startX = pointerOffsetX - xOffset; + let startY = pointerOffsetY - yOffset; if (AW / AH < BW / BH) { // 横長 startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1)); @@ -311,9 +315,11 @@ function onImagePointerdown(ev: PointerEvent) { }); } - _move(ev.offsetX, ev.offsetY); + _move(ev.clientX, ev.clientY); - function _move(pointerX: number, pointerY: number) { + function _move(pointerClientX: number, pointerClientY: number) { + const pointerX = pointerClientX - rect.left; + const pointerY = pointerClientY - rect.top; let x = pointerX - xOffset; let y = pointerY - yOffset; @@ -340,7 +346,7 @@ function onImagePointerdown(ev: PointerEvent) { } function move(ev: PointerEvent) { - _move(ev.offsetX, ev.offsetY); + _move(ev.clientX, ev.clientY); } function up() { @@ -448,6 +454,7 @@ function onImagePointerdown(ev: PointerEvent) { margin: 20px; box-sizing: border-box; object-fit: contain; + touch-action: none; } .controls { diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 76c65397ae..9fc9c98493 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -57,15 +57,8 @@ const remaining = computed(() => { return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000); }); -const remainingWatchStop = watch(remaining, (to) => { - if (to <= 0) { - showResult.value = true; - remainingWatchStop(); - } -}, { immediate: true }); - const total = computed(() => sum(props.choices.map(x => x.votes))); -const closed = computed(() => remaining.value === 0); +const closed = computed(() => remaining.value <= 0); const isVoted = computed(() => !props.multiple && props.choices.some(c => c.isVoted)); const timer = computed(() => i18n.tsx._poll[ remaining.value >= 86400 ? 'remainingDays' : @@ -78,7 +71,16 @@ const timer = computed(() => i18n.tsx._poll[ d: Math.floor(remaining.value / 86400), })); -const showResult = ref(props.readOnly || isVoted.value); +const showResult = ref(props.readOnly || isVoted.value || closed.value); + +if (!closed.value) { + const closedWatchStop = watch(closed, (isNowClosed) => { + if (isNowClosed) { + showResult.value = true; + closedWatchStop(); + } + }); +} const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index c1b950a6c8..afa70cdbae 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu"> <img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/> </button> - <button v-if="$i.policies.noteDraftLimit > 0" v-tooltip="(postAccount != null && postAccount.id !== $i.id) ? null : i18n.ts.draftsAndScheduledNotes" class="_button" :class="$style.draftButton" :disabled="postAccount != null && postAccount.id !== $i.id" @click="showDraftMenu"><i class="ti ti-list"></i></button> </div> <div :class="$style.headerRight"> <template v-if="!(targetChannel != null && fixed)"> @@ -141,7 +140,7 @@ import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { ensureSignin, notesCount, incNotesCount } from '@/i.js'; -import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { getAccounts, getAccountMenu } from '@/accounts.js'; import { deepClone } from '@/utility/clone.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { miLocalStorage } from '@/local-storage.js'; @@ -620,6 +619,19 @@ function showOtherSettings() { action: () => { toggleReactionAcceptance(); }, + }, { type: 'divider' }, { + type: 'button', + text: i18n.ts._drafts.saveToDraft, + icon: 'ti ti-cloud-upload', + action: async () => { + if (!canSaveAsServerDraft.value) { + return os.alert({ + type: 'error', + text: i18n.ts._drafts.cannotCreateDraft, + }); + } + saveServerDraft(); + }, }, ...($i.policies.scheduledNoteLimit > 0 ? [{ icon: 'ti ti-calendar-time', text: i18n.ts.schedulePost + '...', @@ -1159,34 +1171,9 @@ function showActions(ev: MouseEvent) { const postAccount = ref<Misskey.entities.UserDetailed | null>(null); -function openAccountMenu(ev: MouseEvent) { +async function openAccountMenu(ev: MouseEvent) { if (props.mock) return; - openAccountMenu_({ - withExtraOperation: false, - includeCurrentAccount: true, - active: postAccount.value != null ? postAccount.value.id : $i.id, - onChoose: (account) => { - if (account.id === $i.id) { - postAccount.value = null; - } else { - postAccount.value = account; - } - }, - }, ev); -} - -function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) { - const menu = uploader.getMenu(item); - os.popupMenu(menu, ev.currentTarget ?? ev.target); -} - -function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) { - const menu = uploader.getMenu(item); - os.contextMenu(menu, ev); -} - -function showDraftMenu(ev: MouseEvent) { function showDraftsDialog(scheduled: boolean) { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), { scheduled, @@ -1244,34 +1231,44 @@ function showDraftMenu(ev: MouseEvent) { }); } - os.popupMenu([{ - type: 'button', - text: i18n.ts._drafts.saveToDraft, - icon: 'ti ti-cloud-upload', - action: async () => { - if (!canSaveAsServerDraft.value) { - return os.alert({ - type: 'error', - text: i18n.ts._drafts.cannotCreateDraft, - }); + const items = await getAccountMenu({ + withExtraOperation: false, + includeCurrentAccount: true, + active: postAccount.value != null ? postAccount.value.id : $i.id, + onChoose: (account) => { + if (account.id === $i.id) { + postAccount.value = null; + } else { + postAccount.value = account; } - saveServerDraft(); }, - }, { + }); + + os.popupMenu([{ type: 'button', text: i18n.ts._drafts.listDrafts, icon: 'ti ti-cloud-download', action: () => { showDraftsDialog(false); }, - }, { type: 'divider' }, { + }, { type: 'button', text: i18n.ts._drafts.listScheduledNotes, icon: 'ti ti-clock-down', action: () => { showDraftsDialog(true); }, - }], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + }, { type: 'divider' }, ...items], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); +} + +function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) { + const menu = uploader.getMenu(item); + os.popupMenu(menu, ev.currentTarget ?? ev.target); +} + +function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) { + const menu = uploader.getMenu(item); + os.contextMenu(menu, ev); } async function schedule() { @@ -1422,20 +1419,6 @@ defineExpose({ margin: auto; } -.draftButton { - padding: 8px; - font-size: 90%; - border-radius: 6px; - - &:hover { - background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05)); - } - - &:disabled { - background: none; - } -} - .headerRight { display: flex; min-height: 48px; diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index d96f0e2420..7c60288883 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject, onMounted, useTemplateRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; -import { getUnicodeEmoji } from '@@/js/emojilist.js'; +import { getUnicodeEmojiOrNull } from '@@/js/emojilist.js'; import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; import type { MenuItem } from '@/types/menu'; import XDetails from '@/components/MkReactionsViewer.details.vue'; @@ -60,11 +60,11 @@ const buttonEl = useTemplateRef('buttonEl'); const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, '')); const canToggle = computed(() => { - const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction); + const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmojiOrNull(props.reaction); // TODO //return !props.reaction.match(/@\w/) && $i && emoji && checkReactionPermissions($i, props.note, emoji); - return !props.reaction.match(/@\w/) && $i && emoji; + return props.reaction.match(/@\w/) == null && $i != null && emoji != null; }); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.'); diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index dbc673333c..236afa127c 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -64,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> +import type { Awaitable } from '@/types/misc.js'; + export type SuperMenuDef = { title?: string; items: ({ @@ -80,7 +82,7 @@ export type SuperMenuDef = { text: string; danger?: boolean; active?: boolean; - action: (ev: MouseEvent) => void | Promise<void>; + action: (ev: MouseEvent) => Awaitable<void>; } | { type?: 'link'; to: string; diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index ce098d71e4..8849fa447d 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- クライアントで検出するMIME typeとサーバーで検出するMIME typeが異なる場合があり、混乱の元になるのでとりあえず隠しとく --> <!-- https://github.com/misskey-dev/misskey/issues/16091 --> + <!-- https://github.com/misskey-dev/misskey/issues/16663 --> <!--<div>{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}</div>--> </div> </div> diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue index 288293db3f..b34181e5cc 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue @@ -65,6 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="layer.repeat"> <template #label>{{ i18n.ts._watermarkEditor.repeat }}</template> </MkSwitch> + + <MkSwitch v-model="layerPreserveBoundingRect"> + <template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template> + </MkSwitch> </template> <template v-else-if="layer.type === 'image'"> @@ -129,6 +133,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="layer.cover"> <template #label>{{ i18n.ts._watermarkEditor.cover }}</template> </MkSwitch> + + <MkSwitch v-model="layerPreserveBoundingRect"> + <template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template> + </MkSwitch> </template> <template v-else-if="layer.type === 'qr'"> @@ -335,7 +343,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { ref, onMounted } from 'vue'; +import { ref, onMounted, computed } from 'vue'; import * as Misskey from 'misskey-js'; import type { WatermarkPreset } from '@/utility/watermark.js'; import { i18n } from '@/i18n.js'; @@ -351,6 +359,20 @@ import { misskeyApi } from '@/utility/misskey-api.js'; const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true }); +const layerPreserveBoundingRect = computed({ + get: () => { + if (layer.value.type === 'text' || layer.value.type === 'image') { + return !layer.value.noBoundingBoxExpansion; + } + return false; + }, + set: (v: boolean) => { + if (layer.value.type === 'text' || layer.value.type === 'image') { + layer.value.noBoundingBoxExpansion = !v; + } + }, +}); + const driveFile = ref<Misskey.entities.DriveFile | null>(null); const driveFileError = ref(false); onMounted(async () => { diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue index 0d0488d9bc..3b3f20d8d1 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue @@ -90,6 +90,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] { angle: 0, opacity: 0.75, repeat: false, + noBoundingBoxExpansion: false, }; } @@ -104,6 +105,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] { angle: 0, opacity: 0.75, repeat: false, + noBoundingBoxExpansion: false, cover: false, }; } diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 2f4de840db..c4adf440cb 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -62,9 +62,10 @@ import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'v import { scrollToTop } from '@@/js/scroll.js'; import XTabs from './MkPageHeader.tabs.vue'; import { globalEvents } from '@/events.js'; -import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { getAccountMenu } from '@/accounts.js'; import { $i } from '@/i.js'; import { DI } from '@/di.js'; +import * as os from '@/os.js'; const props = withDefaults(defineProps<PageHeaderProps>(), { tabs: () => ([] as Tab[]), @@ -99,10 +100,12 @@ const top = () => { } }; -function openAccountMenu(ev: MouseEvent) { - openAccountMenu_({ +async function openAccountMenu(ev: MouseEvent) { + const menuItems = await getAccountMenu({ withExtraOperation: true, - }, ev); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } function onTabClick(): void { diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index a175485a7e..96d9e35773 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -71,7 +71,7 @@ import { import * as os from '@/os.js'; import { createColumn } from '@/components/grid/column.js'; import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js'; -import { handleKeyEvent } from '@/utility/key-event.js'; +import { makeHotkey } from '@/utility/hotkey.js'; type RowHolder = { row: GridRow, @@ -289,161 +289,143 @@ function onKeyDown(ev: KeyboardEvent) { const max = availableBounds.value; const bounds = rangedBounds.value; - handleKeyEvent(ev, [ - { - code: 'Delete', handler: () => { - if (rangedRows.value.length > 0) { - if (rowSetting.events.delete) { - rowSetting.events.delete(rangedRows.value); - } - } else { - const context = createContext(); - removeDataFromGrid(context, (cell) => { - emitCellValue(cell, undefined); - }); + makeHotkey({ + 'delete': () => { + if (rangedRows.value.length > 0) { + if (rowSetting.events.delete) { + rowSetting.events.delete(rangedRows.value); } - }, - }, - { - code: 'KeyC', modifiers: ['Control'], handler: () => { - const context = createContext(); - copyGridDataToClipboard(data.value, context); - }, - }, - { - code: 'KeyV', modifiers: ['Control'], handler: async () => { - const _cells = cells.value; + } else { const context = createContext(); - await pasteToGridFromClipboard(context, (row, col, parsedValue) => { - emitCellValue(_cells[row.index].cells[col.index], parsedValue); + removeDataFromGrid(context, (cell) => { + emitCellValue(cell, undefined); }); - }, + } }, - { - code: 'ArrowRight', modifiers: ['Control', 'Shift'], handler: () => { - updateSelectionRange({ - leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row }, - rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row }, - }); - }, + 'ctrl+c|meta+c': () => { + const context = createContext(); + copyGridDataToClipboard(data.value, context); }, - { - code: 'ArrowLeft', modifiers: ['Control', 'Shift'], handler: () => { - updateSelectionRange({ - leftTop: { col: max.leftTop.col, row: bounds.leftTop.row }, - rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row }, - }); - }, + 'ctrl+v|meta+v': async () => { + const _cells = cells.value; + const context = createContext(); + await pasteToGridFromClipboard(context, (row, col, parsedValue) => { + emitCellValue(_cells[row.index].cells[col.index], parsedValue); + }); }, - { - code: 'ArrowUp', modifiers: ['Control', 'Shift'], handler: () => { - updateSelectionRange({ - leftTop: { col: bounds.leftTop.col, row: max.leftTop.row }, - rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row }, - }); - }, + 'ctrl+shift+right|meta+shift+right': () => { + updateSelectionRange({ + leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row }, + rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row }, + }); }, - { - code: 'ArrowDown', modifiers: ['Control', 'Shift'], handler: () => { - updateSelectionRange({ - leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row }, - rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row }, - }); - }, + 'ctrl+shift+left|meta+shift+left': () => { + updateSelectionRange({ + leftTop: { col: max.leftTop.col, row: bounds.leftTop.row }, + rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row }, + }); }, - { - code: 'ArrowRight', modifiers: ['Shift'], handler: () => { - updateSelectionRange({ - leftTop: { - col: bounds.leftTop.col < selectedCellAddress.col - ? bounds.leftTop.col + 1 - : selectedCellAddress.col, - row: bounds.leftTop.row, - }, - rightBottom: { - col: (bounds.rightBottom.col > selectedCellAddress.col || bounds.leftTop.col === selectedCellAddress.col) - ? bounds.rightBottom.col + 1 - : selectedCellAddress.col, - row: bounds.rightBottom.row, - }, - }); - }, + 'ctrl+shift+up|meta+shift+up': () => { + updateSelectionRange({ + leftTop: { col: bounds.leftTop.col, row: max.leftTop.row }, + rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row }, + }); }, - { - code: 'ArrowLeft', modifiers: ['Shift'], handler: () => { - updateSelectionRange({ - leftTop: { - col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col) - ? bounds.leftTop.col - 1 - : selectedCellAddress.col, - row: bounds.leftTop.row, - }, - rightBottom: { - col: bounds.rightBottom.col > selectedCellAddress.col - ? bounds.rightBottom.col - 1 - : selectedCellAddress.col, - row: bounds.rightBottom.row, - }, - }); - }, + 'ctrl+shift+down|meta+shift+down': () => { + updateSelectionRange({ + leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row }, + rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row }, + }); }, - { - code: 'ArrowUp', modifiers: ['Shift'], handler: () => { - updateSelectionRange({ - leftTop: { - col: bounds.leftTop.col, - row: (bounds.leftTop.row < selectedCellAddress.row || bounds.rightBottom.row === selectedCellAddress.row) - ? bounds.leftTop.row - 1 - : selectedCellAddress.row, - }, - rightBottom: { - col: bounds.rightBottom.col, - row: bounds.rightBottom.row > selectedCellAddress.row - ? bounds.rightBottom.row - 1 - : selectedCellAddress.row, - }, - }); - }, + 'ctrl+right|meta+right': () => { + selectionCell({ col: max.rightBottom.col, row: selectedCellAddress.row }); }, - { - code: 'ArrowDown', modifiers: ['Shift'], handler: () => { - updateSelectionRange({ - leftTop: { - col: bounds.leftTop.col, - row: bounds.leftTop.row < selectedCellAddress.row - ? bounds.leftTop.row + 1 - : selectedCellAddress.row, - }, - rightBottom: { - col: bounds.rightBottom.col, - row: (bounds.rightBottom.row > selectedCellAddress.row || bounds.leftTop.row === selectedCellAddress.row) - ? bounds.rightBottom.row + 1 - : selectedCellAddress.row, - }, - }); - }, + 'ctrl+left|meta+left': () => { + selectionCell({ col: max.leftTop.col, row: selectedCellAddress.row }); + }, + 'ctrl+up|meta+up': () => { + selectionCell({ col: selectedCellAddress.col, row: max.leftTop.row }); + }, + 'ctrl+down|meta+down': () => { + selectionCell({ col: selectedCellAddress.col, row: max.rightBottom.row }); + }, + 'shift+right': () => { + updateSelectionRange({ + leftTop: { + col: bounds.leftTop.col < selectedCellAddress.col + ? bounds.leftTop.col + 1 + : selectedCellAddress.col, + row: bounds.leftTop.row, + }, + rightBottom: { + col: (bounds.rightBottom.col > selectedCellAddress.col || bounds.leftTop.col === selectedCellAddress.col) + ? bounds.rightBottom.col + 1 + : selectedCellAddress.col, + row: bounds.rightBottom.row, + }, + }); + }, + 'shift+left': () => { + updateSelectionRange({ + leftTop: { + col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col) + ? bounds.leftTop.col - 1 + : selectedCellAddress.col, + row: bounds.leftTop.row, + }, + rightBottom: { + col: bounds.rightBottom.col > selectedCellAddress.col + ? bounds.rightBottom.col - 1 + : selectedCellAddress.col, + row: bounds.rightBottom.row, + }, + }); + }, + 'shift+up': () => { + updateSelectionRange({ + leftTop: { + col: bounds.leftTop.col, + row: (bounds.leftTop.row < selectedCellAddress.row || bounds.rightBottom.row === selectedCellAddress.row) + ? bounds.leftTop.row - 1 + : selectedCellAddress.row, + }, + rightBottom: { + col: bounds.rightBottom.col, + row: bounds.rightBottom.row > selectedCellAddress.row + ? bounds.rightBottom.row - 1 + : selectedCellAddress.row, + }, + }); + }, + 'shift+down': () => { + updateSelectionRange({ + leftTop: { + col: bounds.leftTop.col, + row: bounds.leftTop.row < selectedCellAddress.row + ? bounds.leftTop.row + 1 + : selectedCellAddress.row, + }, + rightBottom: { + col: bounds.rightBottom.col, + row: (bounds.rightBottom.row > selectedCellAddress.row || bounds.leftTop.row === selectedCellAddress.row) + ? bounds.rightBottom.row + 1 + : selectedCellAddress.row, + }, + }); }, - { - code: 'ArrowDown', handler: () => { - selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 }); - }, + 'down': () => { + selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 }); }, - { - code: 'ArrowUp', handler: () => { - selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 }); - }, + 'up': () => { + selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 }); }, - { - code: 'ArrowRight', handler: () => { - selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row }); - }, + 'right': () => { + selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row }); }, - { - code: 'ArrowLeft', handler: () => { - selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row }); - }, + 'left': () => { + selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row }); }, - ]); + }, [])(ev); break; } diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index 12b6e85940..e4aa1fda53 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -634,7 +634,9 @@ export function useUploader(options: { bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW, }, audio: { - bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW, + // Explicitly keep audio (don't discard) and copy it if possible + // without re-encoding to avoid WebCodecs limitations on iOS Safari + discard: false, }, }); diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts index a68cd1b18b..25e9ae1c9e 100644 --- a/packages/frontend/src/directives/adaptive-bg.ts +++ b/packages/frontend/src/directives/adaptive-bg.ts @@ -6,8 +6,8 @@ import type { Directive } from 'vue'; import { getBgColor } from '@/utility/get-bg-color.js'; -export default { - mounted(src, binding, vn) { +export const adaptiveBgDirective = { + mounted(src) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; @@ -18,4 +18,4 @@ export default { src.style.backgroundColor = myBg; } }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts index 8072a1ffd9..749861fd94 100644 --- a/packages/frontend/src/directives/adaptive-border.ts +++ b/packages/frontend/src/directives/adaptive-border.ts @@ -9,8 +9,8 @@ import { globalEvents } from '@/events.js'; const handlerMap = new WeakMap<any, any>(); -export default { - mounted(src, binding, vn) { +export const adaptiveBorderDirective = { + mounted(src) { function calc() { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; @@ -30,7 +30,7 @@ export default { globalEvents.on('themeChanged', calc); }, - unmounted(src, binding, vn) { + unmounted(src) { globalEvents.off('themeChanged', handlerMap.get(src)); }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/anim.ts b/packages/frontend/src/directives/anim.ts index ad0cb5ed81..a165fa11e0 100644 --- a/packages/frontend/src/directives/anim.ts +++ b/packages/frontend/src/directives/anim.ts @@ -5,8 +5,8 @@ import type { Directive } from 'vue'; -export default { - beforeMount(src, binding, vn) { +export const animDirective = { + beforeMount(src) { src.style.opacity = '0'; src.style.transform = 'scale(0.9)'; // ページネーションと相性が悪いので @@ -14,10 +14,10 @@ export default { src.classList.add('_zoom'); }, - mounted(src, binding, vn) { + mounted(src) { window.setTimeout(() => { src.style.opacity = '1'; src.style.transform = 'none'; }, 1); }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/appear.ts b/packages/frontend/src/directives/appear.ts index f5fec108dc..117dc397da 100644 --- a/packages/frontend/src/directives/appear.ts +++ b/packages/frontend/src/directives/appear.ts @@ -5,13 +5,18 @@ import { throttle } from 'throttle-debounce'; import type { Directive } from 'vue'; +import type { Awaitable } from '@/types/misc.js'; -export default { - mounted(src, binding, vn) { +interface HTMLElementWithObserver extends HTMLElement { + _observer_?: IntersectionObserver; +} + +export const appearDirective = { + mounted(src, binding) { const fn = binding.value; if (fn == null) return; - const check = throttle(1000, (entries) => { + const check = throttle<IntersectionObserverCallback>(1000, (entries) => { if (entries.some(entry => entry.isIntersecting)) { fn(); } @@ -24,7 +29,7 @@ export default { src._observer_ = observer; }, - unmounted(src, binding, vn) { + unmounted(src) { if (src._observer_) src._observer_.disconnect(); }, -} as Directive; +} as Directive<HTMLElementWithObserver, (() => Awaitable<void>) | null | undefined>; diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts index c34f351fb3..7891e8092c 100644 --- a/packages/frontend/src/directives/click-anime.ts +++ b/packages/frontend/src/directives/click-anime.ts @@ -6,8 +6,8 @@ import type { Directive } from 'vue'; import { prefer } from '@/preferences.js'; -export default { - mounted(el: HTMLElement, binding, vn) { +export const clickAnimeDirective = { + mounted(el) { if (!prefer.s.animation) return; const target = el.children[0]; @@ -37,4 +37,4 @@ export default { target.classList.add('_anime_bounce_standBy'); }); }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts index f3eaac10e3..303dcb842a 100644 --- a/packages/frontend/src/directives/follow-append.ts +++ b/packages/frontend/src/directives/follow-append.ts @@ -6,8 +6,12 @@ import type { Directive } from 'vue'; import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js'; -export default { - mounted(src, binding, vn) { +interface HTMLElementWithRO extends HTMLElement { + _ro_?: ResizeObserver; +} + +export const followAppendDirective = { + mounted(src, binding) { if (binding.value === false) return; let isBottom = true; @@ -34,7 +38,7 @@ export default { src._ro_ = ro; }, - unmounted(src, binding, vn) { + unmounted(src) { if (src._ro_) src._ro_.unobserve(src); }, -} as Directive; +} as Directive<HTMLElementWithRO, boolean>; diff --git a/packages/frontend/src/directives/get-size.ts b/packages/frontend/src/directives/get-size.ts index 488f201a0d..42660987dd 100644 --- a/packages/frontend/src/directives/get-size.ts +++ b/packages/frontend/src/directives/get-size.ts @@ -37,8 +37,10 @@ function calc(src: Element) { info.fn(width, height); } -export default { - mounted(src, binding, vn) { +type SizeCallback = (w: number, h: number) => void; + +export const getSizeDirective = { + mounted(src, binding) { const resize = new ResizeObserver((entries, observer) => { calc(src); }); @@ -48,7 +50,7 @@ export default { calc(src); }, - unmounted(src, binding, vn) { + unmounted(src, binding) { binding.value(0, 0); const info = mountings.get(src); if (!info) return; @@ -56,4 +58,4 @@ export default { if (info.intersection) info.intersection.disconnect(); mountings.delete(src); }, -} as Directive<Element, (w: number, h: number) => void>; +} as Directive<Element, SizeCallback>; diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts index 63637ab2ba..d8fdfe647a 100644 --- a/packages/frontend/src/directives/hotkey.ts +++ b/packages/frontend/src/directives/hotkey.ts @@ -5,8 +5,14 @@ import type { Directive } from 'vue'; import { makeHotkey } from '@/utility/hotkey.js'; +import type { Keymap } from '@/utility/hotkey.js'; -export default { +interface HTMLElementWithHotkey extends HTMLElement { + _hotkey_global?: boolean; + _keyHandler?: (ev: KeyboardEvent) => void; +} + +export const hotkeyDirective = { mounted(el, binding) { el._hotkey_global = binding.modifiers.global === true; @@ -20,10 +26,11 @@ export default { }, unmounted(el) { + if (el._keyHandler == null) return; if (el._hotkey_global) { window.document.removeEventListener('keydown', el._keyHandler); } else { el.removeEventListener('keydown', el._keyHandler); } }, -} as Directive; +} as Directive<HTMLElementWithHotkey, Keymap>; diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts index 9555045afe..07b756b95d 100644 --- a/packages/frontend/src/directives/index.ts +++ b/packages/frontend/src/directives/index.ts @@ -3,19 +3,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { App } from 'vue'; +import type { App, Directive } from 'vue'; -import userPreview from './user-preview.js'; -import getSize from './get-size.js'; -import ripple from './ripple.js'; -import tooltip from './tooltip.js'; -import hotkey from './hotkey.js'; -import appear from './appear.js'; -import anim from './anim.js'; -import clickAnime from './click-anime.js'; -import panel from './panel.js'; -import adaptiveBorder from './adaptive-border.js'; -import adaptiveBg from './adaptive-bg.js'; +import { userPreviewDirective } from './user-preview.js'; +import { getSizeDirective } from './get-size.js'; +import { rippleDirective } from './ripple.js'; +import { tooltipDirective } from './tooltip.js'; +import { hotkeyDirective } from './hotkey.js'; +import { appearDirective } from './appear.js'; +import { animDirective } from './anim.js'; +import { clickAnimeDirective } from './click-anime.js'; +import { panelDirective } from './panel.js'; +import { adaptiveBorderDirective } from './adaptive-border.js'; +import { adaptiveBgDirective } from './adaptive-bg.js'; export default function(app: App) { for (const [key, value] of Object.entries(directives)) { @@ -24,16 +24,32 @@ export default function(app: App) { } export const directives = { - 'userPreview': userPreview, - 'user-preview': userPreview, - 'get-size': getSize, - 'ripple': ripple, - 'tooltip': tooltip, - 'hotkey': hotkey, - 'appear': appear, - 'anim': anim, - 'click-anime': clickAnime, - 'panel': panel, - 'adaptive-border': adaptiveBorder, - 'adaptive-bg': adaptiveBg, -}; + 'userPreview': userPreviewDirective, + 'user-preview': userPreviewDirective, + 'get-size': getSizeDirective, + 'ripple': rippleDirective, + 'tooltip': tooltipDirective, + 'hotkey': hotkeyDirective, + 'appear': appearDirective, + 'anim': animDirective, + 'click-anime': clickAnimeDirective, + 'panel': panelDirective, + 'adaptive-border': adaptiveBorderDirective, + 'adaptive-bg': adaptiveBgDirective, +} as Record<string, Directive>; + +declare module 'vue' { + export interface ComponentCustomProperties { + vUserPreview: typeof userPreviewDirective; + vGetSize: typeof getSizeDirective; + vRipple: typeof rippleDirective; + vTooltip: typeof tooltipDirective; + vHotkey: typeof hotkeyDirective; + vAppear: typeof appearDirective; + vAnim: typeof animDirective; + vClickAnime: typeof clickAnimeDirective; + vPanel: typeof panelDirective; + vAdaptiveBorder: typeof adaptiveBorderDirective; + vAdaptiveBg: typeof adaptiveBgDirective; + } +} diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts index 0af19e6ca3..7913baefe4 100644 --- a/packages/frontend/src/directives/panel.ts +++ b/packages/frontend/src/directives/panel.ts @@ -6,8 +6,8 @@ import type { Directive } from 'vue'; import { getBgColor } from '@/utility/get-bg-color.js'; -export default { - mounted(src, binding, vn) { +export const panelDirective = { + mounted(src) { const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'); @@ -18,4 +18,4 @@ export default { src.style.backgroundColor = 'var(--MI_THEME-panel)'; } }, -} as Directive; +} as Directive<HTMLElement>; diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts index 614cd37011..bacf49ab72 100644 --- a/packages/frontend/src/directives/ripple.ts +++ b/packages/frontend/src/directives/ripple.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { Directive } from 'vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { prefer } from '@/preferences.js'; import { popup } from '@/os.js'; -export default { - mounted(el, binding, vn) { +export const rippleDirective = { + mounted(el, binding) { // 明示的に false であればバインドしない if (binding.value === false) return; if (!prefer.s.animation) return; @@ -24,4 +25,4 @@ export default { }); }); }, -}; +} as Directive<HTMLElement, boolean | undefined>; diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts index 62aecbc87c..8839d9a939 100644 --- a/packages/frontend/src/directives/tooltip.ts +++ b/packages/frontend/src/directives/tooltip.ts @@ -14,13 +14,30 @@ import { popup, alert } from '@/os.js'; const start = isTouchUsing ? 'touchstart' : 'mouseenter'; const end = isTouchUsing ? 'touchend' : 'mouseleave'; -export default { - mounted(el: HTMLElement, binding, vn) { +type TooltipDirectiveState = { + text: string | null | undefined; + _close: null | (() => void); + showTimer: number | null; + hideTimer: number | null; + checkTimer: number | null; + show: () => void; + close: () => void; +}; + +interface TooltipDirectiveElement extends HTMLElement { + _tooltipDirective_?: TooltipDirectiveState; +} + +type TooltipDirectiveModifiers = 'left' | 'right' | 'top' | 'bottom' | 'mfm' | 'noDelay'; +type TooltipDirectiveArg = 'dialog'; + +export const tooltipDirective = { + mounted(el, binding) { const delay = binding.modifiers.noDelay ? 0 : 100; - const self = (el as any)._tooltipDirective_ = {} as any; + const self = el._tooltipDirective_ = {} as TooltipDirectiveState; - self.text = binding.value as string; + self.text = binding.value; self._close = null; self.showTimer = null; self.hideTimer = null; @@ -28,7 +45,7 @@ export default { self.close = () => { if (self._close) { - window.clearInterval(self.checkTimer); + if (self.checkTimer) window.clearInterval(self.checkTimer); self._close(); self._close = null; } @@ -36,6 +53,7 @@ export default { if (binding.arg === 'dialog') { el.addEventListener('click', (ev) => { + if (binding.value == null) return; ev.preventDefault(); ev.stopPropagation(); alert({ @@ -72,8 +90,8 @@ export default { }); el.addEventListener(start, (ev) => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); + if (self.showTimer) window.clearTimeout(self.showTimer); + if (self.hideTimer) window.clearTimeout(self.hideTimer); if (delay === 0) { self.show(); } else { @@ -82,8 +100,8 @@ export default { }, { passive: true }); el.addEventListener(end, () => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); + if (self.showTimer) window.clearTimeout(self.showTimer); + if (self.hideTimer) window.clearTimeout(self.hideTimer); if (delay === 0) { self.close(); } else { @@ -92,18 +110,23 @@ export default { }, { passive: true }); el.addEventListener('click', () => { - window.clearTimeout(self.showTimer); + if (self.showTimer) window.clearTimeout(self.showTimer); self.close(); }); }, updated(el, binding) { const self = el._tooltipDirective_; + if (self == null) return; self.text = binding.value as string; }, - unmounted(el, binding, vn) { + unmounted(el) { const self = el._tooltipDirective_; - window.clearInterval(self.checkTimer); + if (self == null) return; + if (self.showTimer) window.clearTimeout(self.showTimer); + if (self.hideTimer) window.clearTimeout(self.hideTimer); + if (self.checkTimer) window.clearTimeout(self.checkTimer); + self.close(); }, -} as Directive; +} as Directive<TooltipDirectiveElement, string | null | undefined, TooltipDirectiveModifiers, TooltipDirectiveArg>; diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts index b11ef8f088..f81f5dbef8 100644 --- a/packages/frontend/src/directives/user-preview.ts +++ b/packages/frontend/src/directives/user-preview.ts @@ -5,18 +5,19 @@ import { defineAsyncComponent, ref } from 'vue'; import type { Directive } from 'vue'; +import * as Misskey from 'misskey-js'; import { popup } from '@/os.js'; import { isTouchUsing } from '@/utility/touch.js'; export class UserPreview { - private el; - private user; - private showTimer; - private hideTimer; - private checkTimer; - private promise; + private el: HTMLElement; + private user: string | Misskey.entities.UserDetailed; + private showTimer: number | null = null; + private hideTimer: number | null = null; + private checkTimer: number | null = null; + private promise: null | { cancel: () => void } = null; - constructor(el, user) { + constructor(el: HTMLElement, user: string | Misskey.entities.UserDetailed) { this.el = el; this.user = user; @@ -43,10 +44,10 @@ export class UserPreview { source: this.el, }, { mouseover: () => { - window.clearTimeout(this.hideTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); }, mouseleave: () => { - window.clearTimeout(this.showTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); this.hideTimer = window.setTimeout(this.close, 500); }, closed: () => dispose(), @@ -60,8 +61,8 @@ export class UserPreview { this.checkTimer = window.setInterval(() => { if (!window.document.body.contains(this.el)) { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); this.close(); } }, 1000); @@ -69,26 +70,26 @@ export class UserPreview { private close() { if (this.promise) { - window.clearInterval(this.checkTimer); + if (this.checkTimer) window.clearInterval(this.checkTimer); this.promise.cancel(); this.promise = null; } } private onMouseover() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); this.showTimer = window.setTimeout(this.show, 500); } private onMouseleave() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); + if (this.hideTimer) window.clearTimeout(this.hideTimer); this.hideTimer = window.setTimeout(this.close, 500); } private onClick() { - window.clearTimeout(this.showTimer); + if (this.showTimer) window.clearTimeout(this.showTimer); this.close(); } @@ -105,8 +106,14 @@ export class UserPreview { } } -export default { - mounted(el: HTMLElement, binding, vn) { +interface UserPreviewDirectiveElement extends HTMLElement { + _userPreviewDirective_?: { + preview: UserPreview; + }; +} + +export const userPreviewDirective = { + mounted(el, binding) { if (binding.value == null) return; if (isTouchUsing) return; @@ -117,10 +124,11 @@ export default { self.preview = new UserPreview(el, binding.value); }, - unmounted(el, binding, vn) { + unmounted(el, binding) { if (binding.value == null) return; const self = el._userPreviewDirective_; + if (self == null) return; self.preview.detach(); }, -} as Directive; +} as Directive<UserPreviewDirectiveElement, string | Misskey.entities.UserDetailed | null | undefined>; diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 687983bcdb..f79f62f80e 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -25,6 +25,7 @@ export type Keys = ( 'bootloaderLocales' | 'theme' | 'themeId' | + 'themeCachedVersion' | 'customCss' | 'chatMessageDrafts' | 'scratchpad' | diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index a481972174..08a4e80494 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -295,6 +295,9 @@ const patronsWithIcon = [{ }, { name: 'しゃどかの', icon: 'https://assets.misskey-hub.net/patrons/5bec3c6b402942619e03f7a2ae76d69e.jpg', +}, { + name: '大賀愛一郎', + icon: 'https://assets.misskey-hub.net/patrons/c701a797d1df4125970f25d3052250ac.jpg', }]; const patrons = [ diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue index a380bd133e..cbe863f184 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -503,7 +503,7 @@ function refreshGridItems() { name: it.name, host: it.host ?? '', category: it.category ?? '', - aliases: it.aliases.join(','), + aliases: it.aliases.join(' '), license: it.license ?? '', isSensitive: it.isSensitive, localOnly: it.localOnly, diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index e3021778e7..c5f3c2d4f0 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -54,6 +54,15 @@ SPDX-License-Identifier: AGPL-3.0-only </SearchMarker> <SearchMarker> + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="showRoleBadgesOfRemoteUsers" @change="onChange_showRoleBadgesOfRemoteUsers"> + <template #label><SearchLabel>{{ i18n.ts.showRoleBadgesOfRemoteUsers }}</SearchLabel></template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + </SearchMarker> + + <SearchMarker> <MkFolder :defaultOpen="true"> <template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template> <template #label><SearchLabel>Misskey® Fan-out Timeline Technology™ (FTT)</SearchLabel></template> @@ -188,6 +197,7 @@ const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration); const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser); const enableStatsForFederatedInstances = ref(meta.enableStatsForFederatedInstances); const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances); +const showRoleBadgesOfRemoteUsers = ref(meta.showRoleBadgesOfRemoteUsers); function onChange_enableServerMachineStats(value: boolean) { os.apiWithDialog('admin/update-meta', { @@ -229,6 +239,14 @@ function onChange_enableChartsForFederatedInstances(value: boolean) { }); } +function onChange_showRoleBadgesOfRemoteUsers(value: boolean) { + os.apiWithDialog('admin/update-meta', { + showRoleBadgesOfRemoteUsers: value, + }).then(() => { + fetchInstance(true); + }); +} + const fttForm = useForm({ enableFanoutTimeline: meta.enableFanoutTimeline, enableFanoutTimelineDbFallback: meta.enableFanoutTimelineDbFallback, diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index 54e214241b..10901f737b 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-for="token in items" :key="token.id" :defaultOpen="true"> <template #icon> <img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/> - <i v-else class="ti ti-plug"/> + <i v-else class="ti ti-plug"></i> </template> <template #label>{{ token.name }}</template> <template #caption>{{ token.description }}</template> @@ -86,6 +86,7 @@ definePage(() => ({ <style lang="scss" module> .appIcon { + display: block; width: 20px; height: 20px; border-radius: 4px; diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index c8cbc0977f..d25708dcb4 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -67,7 +67,6 @@ import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { prefer } from '@/preferences.js'; -import { PREF_DEF } from '@/preferences/def.js'; import { getInitialPrefValue } from '@/preferences/manager.js'; import { genId } from '@/utility/id.js'; @@ -77,12 +76,13 @@ const items = ref(prefer.s.menu.map(x => ({ id: genId(), type: x, }))); +const itemTypeValues = computed(() => items.value.map(x => x.type)); const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); const showNavbarSubButtons = prefer.model('showNavbarSubButtons'); async function addItem() { - const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k)); + const menu = Object.keys(navbarItemDef).filter(k => !itemTypeValues.value.includes(k)); const { canceled, result: item } = await os.select({ title: i18n.ts.addItem, items: [...menu.map(k => ({ @@ -102,8 +102,9 @@ function removeItem(index: number) { items.value.splice(index, 1); } -async function save() { - prefer.commit('menu', items.value.map(x => x.type)); +function save() { + prefer.commit('menu', itemTypeValues.value); + os.success(); } function reset() { diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index c622647b4f..5e3f148710 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -475,6 +475,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPreferenceContainer k="disableShowingAnimatedImages"> <MkSwitch v-model="disableShowingAnimatedImages"> <template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template> + <template #caption>{{ i18n.ts.disableShowingAnimatedImages_caption }}</template> </MkSwitch> </MkPreferenceContainer> </SearchMarker> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 7094aca7c0..ae7893a7a8 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -368,27 +368,19 @@ onDeactivated(disposeBannerParallaxResizeObserver); > .banner-container { position: relative; - height: 250px; + --bannerHeight: 250px; + height: var(--bannerHeight); overflow: clip; - background-size: cover; - background-position: center; - view-timeline-name: --bannerParallax; - view-timeline-inset: var(--bannerParallaxInset, auto); - view-timeline-axis: block; > .banner { - position: absolute; - top: 50%; - left: 0; width: 100%; - height: 300%; + height: 100%; + background-size: cover; background-color: #4c5e6d; background-repeat: repeat-y; - background-position: center; - will-change: transform; - animation: bannerParallaxKeyframes linear both; - animation-timeline: --bannerParallax; - animation-range: cover; + background-position-x: center; + background-position-y: 50%; + will-change: background-position-y; } > .fade { @@ -681,7 +673,8 @@ onDeactivated(disposeBannerParallaxResizeObserver); > .main { > .profile > .main { > .banner-container { - height: 140px; + --bannerHeight: 140px; + height: var(--bannerHeight); > .fade { display: none; @@ -745,12 +738,32 @@ onDeactivated(disposeBannerParallaxResizeObserver); } } +@supports (view-timeline-name: --name) { + .ftskorzw { + > .main { + > .profile > .main { + > .banner-container { + view-timeline-name: --bannerParallax; + view-timeline-inset: var(--bannerParallaxInset, auto); + view-timeline-axis: block; + + > .banner { + animation: bannerParallaxKeyframes linear both; + animation-timeline: --bannerParallax; + animation-range: cover; + } + } + } + } + } +} + @keyframes bannerParallaxKeyframes { from { - transform: translateY(-50%); + background-position-y: 50%; } to { - transform: translateY(-30%); + background-position-y: calc(50% + var(--bannerHeight, 250px) / 3); } } </style> diff --git a/packages/frontend/src/shaders/snoise.glsl b/packages/frontend/src/shaders/snoise.glsl new file mode 100644 index 0000000000..89a91eec75 --- /dev/null +++ b/packages/frontend/src/shaders/snoise.glsl @@ -0,0 +1,85 @@ +// Description : Array and textureless GLSL 2D/3D/4D simplex +// noise functions. +// Author : Ian McEwan, Ashima Arts. +// Maintainer : stegu +// Lastmod : 20201014 (stegu) +// License : Copyright (C) 2011 Ashima Arts. All rights reserved. +// Distributed under the MIT License. See LICENSE file. +// https://github.com/ashima/webgl-noise +// https://github.com/stegu/webgl-noise + +vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 mod289(vec4 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 permute(vec4 x) { + return mod289(((x * 34.0) + 10.0) * x); +} + +vec4 taylorInvSqrt(vec4 r) { + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) { + const vec2 C = vec2(1.0/6.0, 1.0/3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + vec3 x1 = x0 - i1 + C.xxx; + vec3 x2 = x0 - i2 + C.yyy; + vec3 x3 = x0 - D.yyy; + + i = mod289(i); + vec4 p = permute(permute(permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0)) + + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + float n_ = 0.142857142857; + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + vec4 m = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 105.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); +} diff --git a/packages/frontend/src/theme.ts b/packages/frontend/src/theme.ts index 4d03b1d0e9..13f5dbf60e 100644 --- a/packages/frontend/src/theme.ts +++ b/packages/frontend/src/theme.ts @@ -10,6 +10,7 @@ import tinycolor from 'tinycolor2'; import lightTheme from '@@/themes/_light.json5'; import darkTheme from '@@/themes/_dark.json5'; import JSON5 from 'json5'; +import { version } from '@@/js/config.js'; import type { Ref } from 'vue'; import type { BundledTheme } from 'shiki/themes'; import { deepClone } from '@/utility/clone.js'; @@ -123,6 +124,7 @@ function applyThemeInternal(theme: Theme, persist: boolean) { if (persist) { miLocalStorage.setItem('theme', JSON.stringify(props)); miLocalStorage.setItem('themeId', theme.id); + miLocalStorage.setItem('themeCachedVersion', version); miLocalStorage.setItem('colorScheme', colorScheme); } @@ -131,7 +133,7 @@ function applyThemeInternal(theme: Theme, persist: boolean) { } let timeout: number | null = null; -let currentTheme: Theme | null = null; +let currentThemeId = miLocalStorage.getItem('themeId'); export function applyTheme(theme: Theme, persist = true) { if (timeout) { @@ -139,9 +141,8 @@ export function applyTheme(theme: Theme, persist = true) { timeout = null; } - if (deepEqual(currentTheme, theme)) return; - // リアクティビティ解除 - currentTheme = deepClone(theme); + if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return; + currentThemeId = theme.id; if (window.document.startViewTransition != null) { window.document.documentElement.classList.add('_themeChanging_'); diff --git a/packages/frontend/src/type.ts b/packages/frontend/src/type.ts deleted file mode 100644 index 5ff27158d2..0000000000 --- a/packages/frontend/src/type.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }; - -export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> }; diff --git a/packages/frontend/src/types/misc.ts b/packages/frontend/src/types/misc.ts new file mode 100644 index 0000000000..3ddd732531 --- /dev/null +++ b/packages/frontend/src/types/misc.ts @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type Awaitable <T> = T | Promise<T>; diff --git a/packages/frontend/src/ui/_common_/navbar-h.vue b/packages/frontend/src/ui/_common_/navbar-h.vue index a78bdd52d1..b025dd4858 100644 --- a/packages/frontend/src/ui/_common_/navbar-h.vue +++ b/packages/frontend/src/ui/_common_/navbar-h.vue @@ -55,7 +55,7 @@ import MkButton from '@/components/MkButton.vue'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; -import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { getAccountMenu } from '@/accounts.js'; import { $i } from '@/i.js'; import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js'; @@ -84,10 +84,12 @@ async function more(ev: MouseEvent) { }); } -function openAccountMenu(ev: MouseEvent) { - openAccountMenu_({ +async function openAccountMenu(ev: MouseEvent) { + const menuItems = await getAccountMenu({ withExtraOperation: true, - }, ev); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } onMounted(() => { diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 2e21587fcb..b0e45eafcd 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -109,7 +109,7 @@ import { instance } from '@/instance.js'; import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js'; import { useRouter } from '@/router.js'; import { prefer } from '@/preferences.js'; -import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { getAccountMenu } from '@/accounts.js'; import { $i } from '@/i.js'; const router = useRouter(); @@ -170,10 +170,12 @@ function toggleRealtimeMode(ev: MouseEvent) { }], ev.currentTarget ?? ev.target); } -function openAccountMenu(ev: MouseEvent) { - openAccountMenu_({ +async function openAccountMenu(ev: MouseEvent) { + const menuItems = await getAccountMenu({ withExtraOperation: true, - }, ev); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } async function more(ev: MouseEvent) { diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index e2ee4b658e..ff8e91663a 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -167,7 +167,7 @@ const columnsEl = useTemplateRef('columnsEl'); const addColumn = async (ev) => { const { canceled, result: column } = await os.select({ title: i18n.ts._deck.addColumn, - items: columnTypes.map(column => ({ + items: columnTypes.filter(column => column !== 'chat' || $i == null || $i.policies.chatAvailability !== 'unavailable').map(column => ({ value: column, label: i18n.ts._deck._columns[column], })), }); diff --git a/packages/frontend/src/ui/deck/chat-column.vue b/packages/frontend/src/ui/deck/chat-column.vue index 791af2e44c..0015447e22 100644 --- a/packages/frontend/src/ui/deck/chat-column.vue +++ b/packages/frontend/src/ui/deck/chat-column.vue @@ -7,21 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only <XColumn :column="column" :isStacked="isStacked"> <template #header><i class="ti ti-messages" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.chat }}</template> - <div style="padding: 8px;"> - <MkChatHistories/> + <div style="padding: 8px;" class="_gaps"> + <MkInfo v-if="$i.policies.chatAvailability === 'readonly'">{{ i18n.ts._chat.chatIsReadOnlyForThisAccountOrServer }}</MkInfo> + <MkInfo v-else-if="$i.policies.chatAvailability === 'unavailable'" warn>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo> + <MkChatHistories v-if="$i.policies.chatAvailability !== 'unavailable'"/> </div> </XColumn> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ensureSignin } from '@/i.js'; import { i18n } from '../../i18n.js'; import XColumn from './column.vue'; import type { Column } from '@/deck.js'; +import MkInfo from '@/components/MkInfo.vue'; import MkChatHistories from '@/components/MkChatHistories.vue'; defineProps<{ column: Column; isStacked: boolean; }>(); + +const $i = ensureSignin(); </script> diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 11937fda24..312ca51c83 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -62,15 +62,18 @@ const props = withDefaults(defineProps<{ column: Column; isStacked?: boolean; naked?: boolean; + handleScrollToTop?: boolean; menu?: MenuItem[]; refresher?: () => Promise<void>; }>(), { isStacked: false, naked: false, + handleScrollToTop: true, }); const emit = defineEmits<{ (ev: 'headerWheel', ctx: WheelEvent): void; + (ev: 'headerClick', ctx: MouseEvent): void; }>(); const body = useTemplateRef('body'); @@ -252,7 +255,10 @@ function onContextmenu(ev: MouseEvent) { os.contextMenu(getMenu(), ev); } -function goTop() { +function goTop(ev: MouseEvent) { + emit('headerClick', ev); + if (!props.handleScrollToTop) return; + if (body.value) { body.value.scrollTo({ top: 0, diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 78454d2e49..1388cbdc18 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked"> +<XColumn + v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'" + :column="column" + :isStacked="isStacked" + :handleScrollToTop="false" + @headerClick="onHeaderClick" +> <template #header> <template v-if="pageMetadata"> <i :class="pageMetadata.icon"></i> @@ -12,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </template> - <div style="height: 100%;"> + <div ref="rootEl" style="height: 100%;"> <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" @contextmenu.stop="onContextmenu"/> <RouterView v-else @contextmenu.stop="onContextmenu"/> </div> @@ -20,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { provide, shallowRef, ref } from 'vue'; +import { provide, useTemplateRef, ref } from 'vue'; import { isLink } from '@@/js/is-link.js'; import XColumn from './column.vue'; import type { Column } from '@/deck.js'; @@ -38,6 +44,7 @@ defineProps<{ }>(); const pageMetadata = ref<null | PageMetadata>(null); +const rootEl = useTemplateRef('rootEl'); provide(DI.router, mainRouter); provideMetadataReceiver((metadataGetter) => { @@ -69,4 +76,15 @@ function onContextmenu(ev: MouseEvent) { }, }], ev); } + +function onHeaderClick() { + if (!rootEl.value) return; + const scrollEl = rootEl.value.querySelector<HTMLElement>('._pageScrollable,._pageScrollableReversed'); + if (scrollEl) { + scrollEl.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } +} </script> diff --git a/packages/frontend/src/utility/hotkey.ts b/packages/frontend/src/utility/hotkey.ts index d728cdfcb0..9c1e66a22e 100644 --- a/packages/frontend/src/utility/hotkey.ts +++ b/packages/frontend/src/utility/hotkey.ts @@ -50,12 +50,12 @@ let latestHotkey: Pattern & { callback: CallbackFunction } | null = null; //#endregion //#region impl -export const makeHotkey = (keymap: Keymap) => { +export const makeHotkey = (keymap: Keymap, ignoreElements = IGNORE_ELEMENTS) => { const actions = parseKeymap(keymap); return (ev: KeyboardEvent) => { if ('pswp' in window && window.pswp != null) return; if (window.document.activeElement != null) { - if (IGNORE_ELEMENTS.includes(window.document.activeElement.tagName.toLowerCase())) return; + if (ignoreElements.includes(window.document.activeElement.tagName.toLowerCase())) return; if (getHTMLElementOrNull(window.document.activeElement)?.isContentEditable) return; } for (const action of actions) { diff --git a/packages/frontend/src/utility/image-effector/fxs/blockNoise.glsl b/packages/frontend/src/utility/image-effector/fxs/blockNoise.glsl new file mode 100644 index 0000000000..84c4ecbed4 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/blockNoise.glsl @@ -0,0 +1,43 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform int u_amount; +uniform float u_shiftStrengths[128]; +uniform vec2 u_shiftOrigins[128]; +uniform vec2 u_shiftSizes[128]; +uniform float u_channelShift; +out vec4 out_color; + +void main() { + // TODO: ピクセル毎に計算する必要はないのでuniformにする + float aspect_ratio = min(in_resolution.x, in_resolution.y) / max(in_resolution.x, in_resolution.y); + float aspect_ratio_x = in_resolution.x > in_resolution.y ? 1.0 : aspect_ratio; + float aspect_ratio_y = in_resolution.x < in_resolution.y ? 1.0 : aspect_ratio; + + float v = 0.0; + + for (int i = 0; i < u_amount; i++) { + if ( + in_uv.x * aspect_ratio_x > ((u_shiftOrigins[i].x * aspect_ratio_x) - u_shiftSizes[i].x) && + in_uv.x * aspect_ratio_x < ((u_shiftOrigins[i].x * aspect_ratio_x) + u_shiftSizes[i].x) && + in_uv.y * aspect_ratio_y > ((u_shiftOrigins[i].y * aspect_ratio_y) - u_shiftSizes[i].y) && + in_uv.y * aspect_ratio_y < ((u_shiftOrigins[i].y * aspect_ratio_y) + u_shiftSizes[i].y) + ) { + v += u_shiftStrengths[i]; + } + } + + float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r; + float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g; + float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b; + float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a; + out_color = vec4(r, g, b, a); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts b/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts index 7e09524c10..355ab4536c 100644 --- a/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts +++ b/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts @@ -4,49 +4,10 @@ */ import seedrandom from 'seedrandom'; +import shader from './blockNoise.glsl'; import { defineImageEffectorFx } from '../ImageEffector.js'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform int u_amount; -uniform float u_shiftStrengths[128]; -uniform vec2 u_shiftOrigins[128]; -uniform vec2 u_shiftSizes[128]; -uniform float u_channelShift; -out vec4 out_color; - -void main() { - // TODO: ピクセル毎に計算する必要はないのでuniformにする - float aspect_ratio = min(in_resolution.x, in_resolution.y) / max(in_resolution.x, in_resolution.y); - float aspect_ratio_x = in_resolution.x > in_resolution.y ? 1.0 : aspect_ratio; - float aspect_ratio_y = in_resolution.x < in_resolution.y ? 1.0 : aspect_ratio; - - float v = 0.0; - - for (int i = 0; i < u_amount; i++) { - if ( - in_uv.x * aspect_ratio_x > ((u_shiftOrigins[i].x * aspect_ratio_x) - u_shiftSizes[i].x) && - in_uv.x * aspect_ratio_x < ((u_shiftOrigins[i].x * aspect_ratio_x) + u_shiftSizes[i].x) && - in_uv.y * aspect_ratio_y > ((u_shiftOrigins[i].y * aspect_ratio_y) - u_shiftSizes[i].y) && - in_uv.y * aspect_ratio_y < ((u_shiftOrigins[i].y * aspect_ratio_y) + u_shiftSizes[i].y) - ) { - v += u_shiftStrengths[i]; - } - } - - float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r; - float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g; - float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b; - float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a; - out_color = vec4(r, g, b, a); -} -`; - export const FX_blockNoise = defineImageEffectorFx({ id: 'blockNoise', name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.blockNoise, diff --git a/packages/frontend/src/utility/image-effector/fxs/blur.glsl b/packages/frontend/src/utility/image-effector/fxs/blur.glsl new file mode 100644 index 0000000000..e591267887 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/blur.glsl @@ -0,0 +1,78 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; +const float TWO_PI = 6.283185307179586; +const float HALF_PI = 1.5707963267948966; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform vec2 u_offset; +uniform vec2 u_scale; +uniform bool u_ellipse; +uniform float u_angle; +uniform float u_radius; +uniform int u_samples; +out vec4 out_color; + +void main() { + float angle = -(u_angle * PI); + 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; + + bool isInside = false; + if (u_ellipse) { + vec2 norm = (rotatedUV - u_offset) / u_scale; + isInside = dot(norm, norm) <= 1.0; + } else { + isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y; + } + + if (!isInside) { + out_color = texture(in_texture, in_uv); + return; + } + + vec4 result = vec4(0.0); + 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); + + // 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); + + // Scale by radius to get the actual sampling offset + vec2 offset = vec2(normalizedX, normalizedY) * blurOffset; + 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; + } + } + } + + out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/blur.ts b/packages/frontend/src/utility/image-effector/fxs/blur.ts index fa215fd3e4..40f51fa646 100644 --- a/packages/frontend/src/utility/image-effector/fxs/blur.ts +++ b/packages/frontend/src/utility/image-effector/fxs/blur.ts @@ -4,83 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './blur.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform vec2 u_offset; -uniform vec2 u_scale; -uniform bool u_ellipse; -uniform float u_angle; -uniform float u_radius; -uniform int u_samples; -out vec4 out_color; - -void main() { - float angle = -(u_angle * PI); - 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; - - bool isInside = false; - if (u_ellipse) { - vec2 norm = (rotatedUV - u_offset) / u_scale; - isInside = dot(norm, norm) <= 1.0; - } else { - isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y; - } - - if (!isInside) { - out_color = texture(in_texture, in_uv); - return; - } - - vec4 result = vec4(0.0); - 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); - - // 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); - - // Scale by radius to get the actual sampling offset - vec2 offset = vec2(normalizedX, normalizedY) * blurOffset; - 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; - } - } - } - - out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv); -} -`; - export const FX_blur = defineImageEffectorFx({ id: 'blur', name: i18n.ts._imageEffector._fxs.blur, diff --git a/packages/frontend/src/utility/image-effector/fxs/checker.glsl b/packages/frontend/src/utility/image-effector/fxs/checker.glsl new file mode 100644 index 0000000000..09d11c15d2 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/checker.glsl @@ -0,0 +1,43 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; +const float TWO_PI = 6.283185307179586; +const float HALF_PI = 1.5707963267948966; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_angle; +uniform float u_scale; +uniform vec3 u_color; +uniform float u_opacity; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + float angle = -(u_angle * PI); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); + vec2 rotatedUV = vec2( + centeredUv.x * cos(angle) - centeredUv.y * sin(angle), + centeredUv.x * sin(angle) + centeredUv.y * cos(angle) + ); + + float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0); + float fin = max(sign(fmodResult), 0.0); + + out_color = vec4( + mix(in_color.r, u_color.r, fin * u_opacity), + mix(in_color.g, u_color.g, fin * u_opacity), + mix(in_color.b, u_color.b, fin * u_opacity), + in_color.a + ); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/checker.ts b/packages/frontend/src/utility/image-effector/fxs/checker.ts index c48f73acbd..7d1938eeb7 100644 --- a/packages/frontend/src/utility/image-effector/fxs/checker.ts +++ b/packages/frontend/src/utility/image-effector/fxs/checker.ts @@ -4,48 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './checker.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_angle; -uniform float u_scale; -uniform vec3 u_color; -uniform float u_opacity; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); - float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); - - float angle = -(u_angle * PI); - vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); - vec2 rotatedUV = vec2( - centeredUv.x * cos(angle) - centeredUv.y * sin(angle), - centeredUv.x * sin(angle) + centeredUv.y * cos(angle) - ); - - float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0); - float fin = max(sign(fmodResult), 0.0); - - out_color = vec4( - mix(in_color.r, u_color.r, fin * u_opacity), - mix(in_color.g, u_color.g, fin * u_opacity), - mix(in_color.b, u_color.b, fin * u_opacity), - in_color.a - ); -} -`; - export const FX_checker = defineImageEffectorFx({ id: 'checker', name: i18n.ts._imageEffector._fxs.checker, diff --git a/packages/frontend/src/utility/image-effector/fxs/chromaticAberration.glsl b/packages/frontend/src/utility/image-effector/fxs/chromaticAberration.glsl new file mode 100644 index 0000000000..60bb4f5318 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/chromaticAberration.glsl @@ -0,0 +1,49 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +out vec4 out_color; +uniform float u_amount; +uniform float u_start; +uniform bool u_normalize; + +void main() { + int samples = 64; + float r_strength = 1.0; + float g_strength = 1.5; + float b_strength = 2.0; + + vec2 size = vec2(in_resolution.x, in_resolution.y); + + vec4 accumulator = vec4(0.0); + float normalisedValue = length((in_uv - 0.5) * 2.0); + float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0); + + vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5)); + vec2 velocity = vector * strength * u_amount; + + vec2 rOffset = -vector * strength * (u_amount * r_strength); + vec2 gOffset = -vector * strength * (u_amount * g_strength); + vec2 bOffset = -vector * strength * (u_amount * b_strength); + + for (int i = 0; i < samples; i++) { + accumulator.r += texture(in_texture, in_uv + rOffset).r; + rOffset -= velocity / float(samples); + + accumulator.g += texture(in_texture, in_uv + gOffset).g; + gOffset -= velocity / float(samples); + + accumulator.b += texture(in_texture, in_uv + bOffset).b; + bOffset -= velocity / float(samples); + } + + out_color = vec4(vec3(accumulator / float(samples)), 1.0); +} + diff --git a/packages/frontend/src/utility/image-effector/fxs/chromaticAberration.ts b/packages/frontend/src/utility/image-effector/fxs/chromaticAberration.ts index 4adb7ce91e..ed4d134251 100644 --- a/packages/frontend/src/utility/image-effector/fxs/chromaticAberration.ts +++ b/packages/frontend/src/utility/image-effector/fxs/chromaticAberration.ts @@ -4,53 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './chromaticAberration.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -out vec4 out_color; -uniform float u_amount; -uniform float u_start; -uniform bool u_normalize; - -void main() { - int samples = 64; - float r_strength = 1.0; - float g_strength = 1.5; - float b_strength = 2.0; - - vec2 size = vec2(in_resolution.x, in_resolution.y); - - vec4 accumulator = vec4(0.0); - float normalisedValue = length((in_uv - 0.5) * 2.0); - float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0); - - vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5)); - vec2 velocity = vector * strength * u_amount; - - vec2 rOffset = -vector * strength * (u_amount * r_strength); - vec2 gOffset = -vector * strength * (u_amount * g_strength); - vec2 bOffset = -vector * strength * (u_amount * b_strength); - - for (int i = 0; i < samples; i++) { - accumulator.r += texture(in_texture, in_uv + rOffset).r; - rOffset -= velocity / float(samples); - - accumulator.g += texture(in_texture, in_uv + gOffset).g; - gOffset -= velocity / float(samples); - - accumulator.b += texture(in_texture, in_uv + bOffset).b; - bOffset -= velocity / float(samples); - } - - out_color = vec4(vec3(accumulator / float(samples)), 1.0); -} -`; - export const FX_chromaticAberration = defineImageEffectorFx({ id: 'chromaticAberration', name: i18n.ts._imageEffector._fxs.chromaticAberration, diff --git a/packages/frontend/src/utility/image-effector/fxs/colorAdjust.glsl b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.glsl new file mode 100644 index 0000000000..2d0c87ce95 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.glsl @@ -0,0 +1,82 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_brightness; +uniform float u_contrast; +uniform float u_hue; +uniform float u_lightness; +uniform float u_saturation; +out vec4 out_color; + +// RGB to HSL +vec3 rgb2hsl(vec3 c) { + float maxc = max(max(c.r, c.g), c.b); + float minc = min(min(c.r, c.g), c.b); + float l = (maxc + minc) * 0.5; + float s = 0.0; + float h = 0.0; + if (maxc != minc) { + float d = maxc - minc; + s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc); + if (maxc == c.r) { + h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0); + } else if (maxc == c.g) { + h = (c.b - c.r) / d + 2.0; + } else { + h = (c.r - c.g) / d + 4.0; + } + h /= 6.0; + } + return vec3(h, s, l); +} + +// HSL to RGB +float hue2rgb(float p, float q, float t) { + if (t < 0.0) t += 1.0; + if (t > 1.0) t -= 1.0; + if (t < 1.0/6.0) return p + (q - p) * 6.0 * t; + if (t < 1.0/2.0) return q; + if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0; + return p; +} + +vec3 hsl2rgb(vec3 hsl) { + float r, g, b; + float h = hsl.x; + float s = hsl.y; + float l = hsl.z; + if (s == 0.0) { + r = g = b = l; + } else { + float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; + float p = 2.0 * l - q; + r = hue2rgb(p, q, h + 1.0/3.0); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1.0/3.0); + } + return vec3(r, g, b); +} + +void main() { + vec4 in_color = texture(in_texture, in_uv); + vec3 color = in_color.rgb; + + color = color * u_brightness; + color += vec3(u_lightness); + color = (color - 0.5) * u_contrast + 0.5; + + vec3 hsl = rgb2hsl(color); + hsl.x = mod(hsl.x + u_hue, 1.0); + hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0); + + color = hsl2rgb(hsl); + out_color = vec4(color, in_color.a); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts index 8cfbbcb516..989ca79a2c 100644 --- a/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts +++ b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts @@ -4,86 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './colorAdjust.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_brightness; -uniform float u_contrast; -uniform float u_hue; -uniform float u_lightness; -uniform float u_saturation; -out vec4 out_color; - -// RGB to HSL -vec3 rgb2hsl(vec3 c) { - float maxc = max(max(c.r, c.g), c.b); - float minc = min(min(c.r, c.g), c.b); - float l = (maxc + minc) * 0.5; - float s = 0.0; - float h = 0.0; - if (maxc != minc) { - float d = maxc - minc; - s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc); - if (maxc == c.r) { - h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0); - } else if (maxc == c.g) { - h = (c.b - c.r) / d + 2.0; - } else { - h = (c.r - c.g) / d + 4.0; - } - h /= 6.0; - } - return vec3(h, s, l); -} - -// HSL to RGB -float hue2rgb(float p, float q, float t) { - if (t < 0.0) t += 1.0; - if (t > 1.0) t -= 1.0; - if (t < 1.0/6.0) return p + (q - p) * 6.0 * t; - if (t < 1.0/2.0) return q; - if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0; - return p; -} -vec3 hsl2rgb(vec3 hsl) { - float r, g, b; - float h = hsl.x; - float s = hsl.y; - float l = hsl.z; - if (s == 0.0) { - r = g = b = l; - } else { - float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; - float p = 2.0 * l - q; - r = hue2rgb(p, q, h + 1.0/3.0); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1.0/3.0); - } - return vec3(r, g, b); -} - -void main() { - vec4 in_color = texture(in_texture, in_uv); - vec3 color = in_color.rgb; - - color = color * u_brightness; - color += vec3(u_lightness); - color = (color - 0.5) * u_contrast + 0.5; - - vec3 hsl = rgb2hsl(color); - hsl.x = mod(hsl.x + u_hue, 1.0); - hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0); - - color = hsl2rgb(hsl); - out_color = vec4(color, in_color.a); -} -`; - export const FX_colorAdjust = defineImageEffectorFx({ id: 'colorAdjust', name: i18n.ts._imageEffector._fxs.colorAdjust, diff --git a/packages/frontend/src/utility/image-effector/fxs/colorClamp.glsl b/packages/frontend/src/utility/image-effector/fxs/colorClamp.glsl new file mode 100644 index 0000000000..bf37f5ab43 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/colorClamp.glsl @@ -0,0 +1,29 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// colorClamp, colorClampAdvanced共通 +// colorClampではmax, minがすべて同じ値となる + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_rMax; +uniform float u_rMin; +uniform float u_gMax; +uniform float u_gMin; +uniform float u_bMax; +uniform float u_bMin; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float r = min(max(in_color.r, u_rMin), u_rMax); + float g = min(max(in_color.g, u_gMin), u_gMax); + float b = min(max(in_color.b, u_bMin), u_bMax); + out_color = vec4(r, g, b, in_color.a); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/colorClamp.ts b/packages/frontend/src/utility/image-effector/fxs/colorClamp.ts index 4f18eb63c4..f3513011fa 100644 --- a/packages/frontend/src/utility/image-effector/fxs/colorClamp.ts +++ b/packages/frontend/src/utility/image-effector/fxs/colorClamp.ts @@ -4,32 +4,14 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './colorClamp.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_max; -uniform float u_min; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float r = min(max(in_color.r, u_min), u_max); - float g = min(max(in_color.g, u_min), u_max); - float b = min(max(in_color.b, u_min), u_max); - out_color = vec4(r, g, b, in_color.a); -} -`; - export const FX_colorClamp = defineImageEffectorFx({ id: 'colorClamp', name: i18n.ts._imageEffector._fxs.colorClamp, shader, - uniforms: ['max', 'min'] as const, + uniforms: ['rMax', 'rMin', 'gMax', 'gMin', 'bMax', 'bMin'] as const, params: { max: { label: i18n.ts._imageEffector._fxProps.max, @@ -51,7 +33,11 @@ export const FX_colorClamp = defineImageEffectorFx({ }, }, main: ({ gl, u, params }) => { - gl.uniform1f(u.max, params.max); - gl.uniform1f(u.min, 1.0 + params.min); + gl.uniform1f(u.rMax, params.max); + gl.uniform1f(u.rMin, 1.0 + params.min); + gl.uniform1f(u.gMax, params.max); + gl.uniform1f(u.gMin, 1.0 + params.min); + gl.uniform1f(u.bMax, params.max); + gl.uniform1f(u.bMin, 1.0 + params.min); }, }); diff --git a/packages/frontend/src/utility/image-effector/fxs/colorClampAdvanced.ts b/packages/frontend/src/utility/image-effector/fxs/colorClampAdvanced.ts index 7e793061cf..397e16c1ba 100644 --- a/packages/frontend/src/utility/image-effector/fxs/colorClampAdvanced.ts +++ b/packages/frontend/src/utility/image-effector/fxs/colorClampAdvanced.ts @@ -4,31 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './colorClamp.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_rMax; -uniform float u_rMin; -uniform float u_gMax; -uniform float u_gMin; -uniform float u_bMax; -uniform float u_bMin; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float r = min(max(in_color.r, u_rMin), u_rMax); - float g = min(max(in_color.g, u_gMin), u_gMax); - float b = min(max(in_color.b, u_bMin), u_bMax); - out_color = vec4(r, g, b, in_color.a); -} -`; - export const FX_colorClampAdvanced = defineImageEffectorFx({ id: 'colorClampAdvanced', name: i18n.ts._imageEffector._fxs.colorClampAdvanced, diff --git a/packages/frontend/src/utility/image-effector/fxs/distort.glsl b/packages/frontend/src/utility/image-effector/fxs/distort.glsl new file mode 100644 index 0000000000..7e0d1e3252 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/distort.glsl @@ -0,0 +1,30 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; +const float TWO_PI = 6.283185307179586; +const float HALF_PI = 1.5707963267948966; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_phase; +uniform float u_frequency; +uniform float u_strength; +uniform int u_direction; // 0: vertical, 1: horizontal +out vec4 out_color; + +void main() { + float v = u_direction == 0 ? + sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.y * u_frequency) * u_strength : + sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.x * u_frequency) * u_strength; + vec4 in_color = u_direction == 0 ? + texture(in_texture, vec2(in_uv.x + v, in_uv.y)) : + texture(in_texture, vec2(in_uv.x, in_uv.y + v)); + out_color = in_color; +} diff --git a/packages/frontend/src/utility/image-effector/fxs/distort.ts b/packages/frontend/src/utility/image-effector/fxs/distort.ts index 7b5ec45f4b..3ea93a0266 100644 --- a/packages/frontend/src/utility/image-effector/fxs/distort.ts +++ b/packages/frontend/src/utility/image-effector/fxs/distort.ts @@ -4,35 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './distort.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_phase; -uniform float u_frequency; -uniform float u_strength; -uniform int u_direction; // 0: vertical, 1: horizontal -out vec4 out_color; - -void main() { - float v = u_direction == 0 ? - sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.y * u_frequency) * u_strength : - sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.x * u_frequency) * u_strength; - vec4 in_color = u_direction == 0 ? - texture(in_texture, vec2(in_uv.x + v, in_uv.y)) : - texture(in_texture, vec2(in_uv.x, in_uv.y + v)); - out_color = in_color; -} -`; - export const FX_distort = defineImageEffectorFx({ id: 'distort', name: i18n.ts._imageEffector._fxs.distort, diff --git a/packages/frontend/src/utility/image-effector/fxs/fill.glsl b/packages/frontend/src/utility/image-effector/fxs/fill.glsl new file mode 100644 index 0000000000..f04dc5545a --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/fill.glsl @@ -0,0 +1,50 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; +const float TWO_PI = 6.283185307179586; +const float HALF_PI = 1.5707963267948966; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform vec2 u_offset; +uniform vec2 u_scale; +uniform bool u_ellipse; +uniform float u_angle; +uniform vec3 u_color; +uniform float u_opacity; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + //float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + //float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + float angle = -(u_angle * PI); + 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; + + bool isInside = false; + if (u_ellipse) { + vec2 norm = (rotatedUV - u_offset) / u_scale; + isInside = dot(norm, norm) <= 1.0; + } else { + isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y; + } + + out_color = isInside ? vec4( + mix(in_color.r, u_color.r, u_opacity), + mix(in_color.g, u_color.g, u_opacity), + mix(in_color.b, u_color.b, u_opacity), + in_color.a + ) : in_color; +} diff --git a/packages/frontend/src/utility/image-effector/fxs/fill.ts b/packages/frontend/src/utility/image-effector/fxs/fill.ts index 35dee594e3..772cd76cf7 100644 --- a/packages/frontend/src/utility/image-effector/fxs/fill.ts +++ b/packages/frontend/src/utility/image-effector/fxs/fill.ts @@ -4,55 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './fill.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform vec2 u_offset; -uniform vec2 u_scale; -uniform bool u_ellipse; -uniform float u_angle; -uniform vec3 u_color; -uniform float u_opacity; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - //float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); - //float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); - - float angle = -(u_angle * PI); - 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; - - bool isInside = false; - if (u_ellipse) { - vec2 norm = (rotatedUV - u_offset) / u_scale; - isInside = dot(norm, norm) <= 1.0; - } else { - isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y; - } - - out_color = isInside ? vec4( - mix(in_color.r, u_color.r, u_opacity), - mix(in_color.g, u_color.g, u_opacity), - mix(in_color.b, u_color.b, u_opacity), - in_color.a - ) : in_color; -} -`; - export const FX_fill = defineImageEffectorFx({ id: 'fill', name: i18n.ts._imageEffector._fxs.fill, diff --git a/packages/frontend/src/utility/image-effector/fxs/grayscale.glsl b/packages/frontend/src/utility/image-effector/fxs/grayscale.glsl new file mode 100644 index 0000000000..54ca719976 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/grayscale.glsl @@ -0,0 +1,22 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +out vec4 out_color; + +float getBrightness(vec4 color) { + return (color.r + color.g + color.b) / 3.0; +} + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float brightness = getBrightness(in_color); + out_color = vec4(brightness, brightness, brightness, in_color.a); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/grayscale.ts b/packages/frontend/src/utility/image-effector/fxs/grayscale.ts index e1a288fc85..055e8b4618 100644 --- a/packages/frontend/src/utility/image-effector/fxs/grayscale.ts +++ b/packages/frontend/src/utility/image-effector/fxs/grayscale.ts @@ -4,27 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './grayscale.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -out vec4 out_color; - -float getBrightness(vec4 color) { - return (color.r + color.g + color.b) / 3.0; -} - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float brightness = getBrightness(in_color); - out_color = vec4(brightness, brightness, brightness, in_color.a); -} -`; - export const FX_grayscale = defineImageEffectorFx({ id: 'grayscale', name: i18n.ts._imageEffector._fxs.grayscale, diff --git a/packages/frontend/src/utility/image-effector/fxs/invert.glsl b/packages/frontend/src/utility/image-effector/fxs/invert.glsl new file mode 100644 index 0000000000..a2d1574f5b --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/invert.glsl @@ -0,0 +1,23 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform bool u_r; +uniform bool u_g; +uniform bool u_b; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + out_color.r = u_r ? 1.0 - in_color.r : in_color.r; + out_color.g = u_g ? 1.0 - in_color.g : in_color.g; + out_color.b = u_b ? 1.0 - in_color.b : in_color.b; + out_color.a = in_color.a; +} diff --git a/packages/frontend/src/utility/image-effector/fxs/invert.ts b/packages/frontend/src/utility/image-effector/fxs/invert.ts index 1c662ae849..9417047931 100644 --- a/packages/frontend/src/utility/image-effector/fxs/invert.ts +++ b/packages/frontend/src/utility/image-effector/fxs/invert.ts @@ -4,28 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './invert.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform bool u_r; -uniform bool u_g; -uniform bool u_b; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - out_color.r = u_r ? 1.0 - in_color.r : in_color.r; - out_color.g = u_g ? 1.0 - in_color.g : in_color.g; - out_color.b = u_b ? 1.0 - in_color.b : in_color.b; - out_color.a = in_color.a; -} -`; - export const FX_invert = defineImageEffectorFx({ id: 'invert', name: i18n.ts._imageEffector._fxs.invert, diff --git a/packages/frontend/src/utility/image-effector/fxs/mirror.glsl b/packages/frontend/src/utility/image-effector/fxs/mirror.glsl new file mode 100644 index 0000000000..b27934e9ef --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/mirror.glsl @@ -0,0 +1,26 @@ +#version 300 es +precision mediump float; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform int u_h; +uniform int u_v; +out vec4 out_color; + +void main() { + vec2 uv = in_uv; + if (u_h == -1 && in_uv.x > 0.5) { + uv.x = 1.0 - uv.x; + } + if (u_h == 1 && in_uv.x < 0.5) { + uv.x = 1.0 - uv.x; + } + if (u_v == -1 && in_uv.y > 0.5) { + uv.y = 1.0 - uv.y; + } + if (u_v == 1 && in_uv.y < 0.5) { + uv.y = 1.0 - uv.y; + } + out_color = texture(in_texture, uv); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/mirror.ts b/packages/frontend/src/utility/image-effector/fxs/mirror.ts index 3d7893f8b0..6515454ead 100644 --- a/packages/frontend/src/utility/image-effector/fxs/mirror.ts +++ b/packages/frontend/src/utility/image-effector/fxs/mirror.ts @@ -4,36 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './mirror.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform int u_h; -uniform int u_v; -out vec4 out_color; - -void main() { - vec2 uv = in_uv; - if (u_h == -1 && in_uv.x > 0.5) { - uv.x = 1.0 - uv.x; - } - if (u_h == 1 && in_uv.x < 0.5) { - uv.x = 1.0 - uv.x; - } - if (u_v == -1 && in_uv.y > 0.5) { - uv.y = 1.0 - uv.y; - } - if (u_v == 1 && in_uv.y < 0.5) { - uv.y = 1.0 - uv.y; - } - out_color = texture(in_texture, uv); -} -`; - export const FX_mirror = defineImageEffectorFx({ id: 'mirror', name: i18n.ts._imageEffector._fxs.mirror, diff --git a/packages/frontend/src/utility/image-effector/fxs/pixelate.glsl b/packages/frontend/src/utility/image-effector/fxs/pixelate.glsl new file mode 100644 index 0000000000..4de3f27397 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/pixelate.glsl @@ -0,0 +1,68 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; +const float TWO_PI = 6.283185307179586; +const float HALF_PI = 1.5707963267948966; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform vec2 u_offset; +uniform vec2 u_scale; +uniform bool u_ellipse; +uniform float u_angle; +uniform int u_samples; +uniform float u_strength; +out vec4 out_color; + +// TODO: pixelateの中心を画像中心ではなく範囲の中心にする +// TODO: 画像のアスペクト比に関わらず各画素は正方形にする + +void main() { + if (u_strength <= 0.0) { + out_color = texture(in_texture, in_uv); + return; + } + + float angle = -(u_angle * PI); + 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; + + bool isInside = false; + if (u_ellipse) { + vec2 norm = (rotatedUV - u_offset) / u_scale; + isInside = dot(norm, norm) <= 1.0; + } else { + isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y; + } + + if (!isInside) { + out_color = texture(in_texture, in_uv); + return; + } + + float dx = u_strength / 1.0; + float dy = u_strength / 1.0; + 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)); + + vec4 result = vec4(0.0); + float totalSamples = 0.0; + + // TODO: より多くのサンプリング + result += texture(in_texture, new_uv); + totalSamples += 1.0; + + out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/pixelate.ts b/packages/frontend/src/utility/image-effector/fxs/pixelate.ts index d9a5f454f3..e3eef49b23 100644 --- a/packages/frontend/src/utility/image-effector/fxs/pixelate.ts +++ b/packages/frontend/src/utility/image-effector/fxs/pixelate.ts @@ -4,73 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './pixelate.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform vec2 u_offset; -uniform vec2 u_scale; -uniform bool u_ellipse; -uniform float u_angle; -uniform int u_samples; -uniform float u_strength; -out vec4 out_color; - -// TODO: pixelateの中心を画像中心ではなく範囲の中心にする -// TODO: 画像のアスペクト比に関わらず各画素は正方形にする - -void main() { - if (u_strength <= 0.0) { - out_color = texture(in_texture, in_uv); - return; - } - - float angle = -(u_angle * PI); - 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; - - bool isInside = false; - if (u_ellipse) { - vec2 norm = (rotatedUV - u_offset) / u_scale; - isInside = dot(norm, norm) <= 1.0; - } else { - isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y; - } - - if (!isInside) { - out_color = texture(in_texture, in_uv); - return; - } - - float dx = u_strength / 1.0; - float dy = u_strength / 1.0; - 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)); - - vec4 result = vec4(0.0); - float totalSamples = 0.0; - - // TODO: より多くのサンプリング - result += texture(in_texture, new_uv); - totalSamples += 1.0; - - out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv); -} -`; - export const FX_pixelate = defineImageEffectorFx({ id: 'pixelate', name: i18n.ts._imageEffector._fxs.pixelate, diff --git a/packages/frontend/src/utility/image-effector/fxs/polkadot.glsl b/packages/frontend/src/utility/image-effector/fxs/polkadot.glsl new file mode 100644 index 0000000000..39ecad34b5 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/polkadot.glsl @@ -0,0 +1,75 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; +const float TWO_PI = 6.283185307179586; +const float HALF_PI = 1.5707963267948966; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_angle; +uniform float u_scale; +uniform float u_major_radius; +uniform float u_major_opacity; +uniform float u_minor_divisions; +uniform float u_minor_radius; +uniform float u_minor_opacity; +uniform vec3 u_color; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + float angle = -(u_angle * PI); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); + vec2 rotatedUV = vec2( + centeredUv.x * cos(angle) - centeredUv.y * sin(angle), + centeredUv.x * sin(angle) + centeredUv.y * cos(angle) + ); + + float major_modX = mod(rotatedUV.x, (1.0 / u_scale)); + float major_modY = mod(rotatedUV.y, (1.0 / u_scale)); + float major_threshold = ((u_major_radius / 2.0) / u_scale); + if ( + length(vec2(major_modX, major_modY)) < major_threshold || + length(vec2((1.0 / u_scale) - major_modX, major_modY)) < major_threshold || + length(vec2(major_modX, (1.0 / u_scale) - major_modY)) < major_threshold || + length(vec2((1.0 / u_scale) - major_modX, (1.0 / u_scale) - major_modY)) < major_threshold + ) { + out_color = vec4( + mix(in_color.r, u_color.r, u_major_opacity), + mix(in_color.g, u_color.g, u_major_opacity), + mix(in_color.b, u_color.b, u_major_opacity), + in_color.a + ); + return; + } + + float minor_modX = mod(rotatedUV.x, (1.0 / u_scale / u_minor_divisions)); + float minor_modY = mod(rotatedUV.y, (1.0 / u_scale / u_minor_divisions)); + float minor_threshold = ((u_minor_radius / 2.0) / (u_minor_divisions * u_scale)); + if ( + length(vec2(minor_modX, minor_modY)) < minor_threshold || + length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, minor_modY)) < minor_threshold || + length(vec2(minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold || + length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold + ) { + out_color = vec4( + mix(in_color.r, u_color.r, u_minor_opacity), + mix(in_color.g, u_color.g, u_minor_opacity), + mix(in_color.b, u_color.b, u_minor_opacity), + in_color.a + ); + return; + } + + out_color = in_color; +} diff --git a/packages/frontend/src/utility/image-effector/fxs/polkadot.ts b/packages/frontend/src/utility/image-effector/fxs/polkadot.ts index 1685601bd2..521e08cc7b 100644 --- a/packages/frontend/src/utility/image-effector/fxs/polkadot.ts +++ b/packages/frontend/src/utility/image-effector/fxs/polkadot.ts @@ -4,80 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './polkadot.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_angle; -uniform float u_scale; -uniform float u_major_radius; -uniform float u_major_opacity; -uniform float u_minor_divisions; -uniform float u_minor_radius; -uniform float u_minor_opacity; -uniform vec3 u_color; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); - float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); - - float angle = -(u_angle * PI); - vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); - vec2 rotatedUV = vec2( - centeredUv.x * cos(angle) - centeredUv.y * sin(angle), - centeredUv.x * sin(angle) + centeredUv.y * cos(angle) - ); - - float major_modX = mod(rotatedUV.x, (1.0 / u_scale)); - float major_modY = mod(rotatedUV.y, (1.0 / u_scale)); - float major_threshold = ((u_major_radius / 2.0) / u_scale); - if ( - length(vec2(major_modX, major_modY)) < major_threshold || - length(vec2((1.0 / u_scale) - major_modX, major_modY)) < major_threshold || - length(vec2(major_modX, (1.0 / u_scale) - major_modY)) < major_threshold || - length(vec2((1.0 / u_scale) - major_modX, (1.0 / u_scale) - major_modY)) < major_threshold - ) { - out_color = vec4( - mix(in_color.r, u_color.r, u_major_opacity), - mix(in_color.g, u_color.g, u_major_opacity), - mix(in_color.b, u_color.b, u_major_opacity), - in_color.a - ); - return; - } - - float minor_modX = mod(rotatedUV.x, (1.0 / u_scale / u_minor_divisions)); - float minor_modY = mod(rotatedUV.y, (1.0 / u_scale / u_minor_divisions)); - float minor_threshold = ((u_minor_radius / 2.0) / (u_minor_divisions * u_scale)); - if ( - length(vec2(minor_modX, minor_modY)) < minor_threshold || - length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, minor_modY)) < minor_threshold || - length(vec2(minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold || - length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold - ) { - out_color = vec4( - mix(in_color.r, u_color.r, u_minor_opacity), - mix(in_color.g, u_color.g, u_minor_opacity), - mix(in_color.b, u_color.b, u_minor_opacity), - in_color.a - ); - return; - } - - out_color = in_color; -} -`; - // Primarily used for watermark export const FX_polkadot = defineImageEffectorFx({ id: 'polkadot', diff --git a/packages/frontend/src/utility/image-effector/fxs/stripe.glsl b/packages/frontend/src/utility/image-effector/fxs/stripe.glsl new file mode 100644 index 0000000000..bb18d8fcb8 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/stripe.glsl @@ -0,0 +1,45 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; +const float TWO_PI = 6.283185307179586; +const float HALF_PI = 1.5707963267948966; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_angle; +uniform float u_frequency; +uniform float u_phase; +uniform float u_threshold; +uniform vec3 u_color; +uniform float u_opacity; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + float angle = -(u_angle * PI); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); + vec2 rotatedUV = vec2( + centeredUv.x * cos(angle) - centeredUv.y * sin(angle), + centeredUv.x * sin(angle) + centeredUv.y * cos(angle) + ); + + float phase = u_phase * TWO_PI; + float value = (1.0 + sin((rotatedUV.x * u_frequency - HALF_PI) + phase)) / 2.0; + value = value < u_threshold ? 1.0 : 0.0; + out_color = vec4( + mix(in_color.r, u_color.r, value * u_opacity), + mix(in_color.g, u_color.g, value * u_opacity), + mix(in_color.b, u_color.b, value * u_opacity), + in_color.a + ); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/stripe.ts b/packages/frontend/src/utility/image-effector/fxs/stripe.ts index 1c054c1aaa..3a6ecf970c 100644 --- a/packages/frontend/src/utility/image-effector/fxs/stripe.ts +++ b/packages/frontend/src/utility/image-effector/fxs/stripe.ts @@ -4,50 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './stripe.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_angle; -uniform float u_frequency; -uniform float u_phase; -uniform float u_threshold; -uniform vec3 u_color; -uniform float u_opacity; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); - float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); - - float angle = -(u_angle * PI); - vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); - vec2 rotatedUV = vec2( - centeredUv.x * cos(angle) - centeredUv.y * sin(angle), - centeredUv.x * sin(angle) + centeredUv.y * cos(angle) - ); - - float phase = u_phase * TWO_PI; - float value = (1.0 + sin((rotatedUV.x * u_frequency - HALF_PI) + phase)) / 2.0; - value = value < u_threshold ? 1.0 : 0.0; - out_color = vec4( - mix(in_color.r, u_color.r, value * u_opacity), - mix(in_color.g, u_color.g, value * u_opacity), - mix(in_color.b, u_color.b, value * u_opacity), - in_color.a - ); -} -`; - // Primarily used for watermark export const FX_stripe = defineImageEffectorFx({ id: 'stripe', diff --git a/packages/frontend/src/utility/image-effector/fxs/tearing.glsl b/packages/frontend/src/utility/image-effector/fxs/tearing.glsl new file mode 100644 index 0000000000..3fb2fc2cad --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/tearing.glsl @@ -0,0 +1,33 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform int u_amount; +uniform float u_shiftStrengths[128]; +uniform float u_shiftOrigins[128]; +uniform float u_shiftHeights[128]; +uniform float u_channelShift; +out vec4 out_color; + +void main() { + float v = 0.0; + + for (int i = 0; i < u_amount; i++) { + if (in_uv.y > (u_shiftOrigins[i] - u_shiftHeights[i]) && in_uv.y < (u_shiftOrigins[i] + u_shiftHeights[i])) { + v += u_shiftStrengths[i]; + } + } + + float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r; + float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g; + float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b; + float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a; + out_color = vec4(r, g, b, a); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/tearing.ts b/packages/frontend/src/utility/image-effector/fxs/tearing.ts index a1d5178d24..453b16bb19 100644 --- a/packages/frontend/src/utility/image-effector/fxs/tearing.ts +++ b/packages/frontend/src/utility/image-effector/fxs/tearing.ts @@ -4,39 +4,10 @@ */ import seedrandom from 'seedrandom'; +import shader from './tearing.glsl'; import { defineImageEffectorFx } from '../ImageEffector.js'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform int u_amount; -uniform float u_shiftStrengths[128]; -uniform float u_shiftOrigins[128]; -uniform float u_shiftHeights[128]; -uniform float u_channelShift; -out vec4 out_color; - -void main() { - float v = 0.0; - - for (int i = 0; i < u_amount; i++) { - if (in_uv.y > (u_shiftOrigins[i] - u_shiftHeights[i]) && in_uv.y < (u_shiftOrigins[i] + u_shiftHeights[i])) { - v += u_shiftStrengths[i]; - } - } - - float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r; - float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g; - float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b; - float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a; - out_color = vec4(r, g, b, a); -} -`; - export const FX_tearing = defineImageEffectorFx({ id: 'tearing', name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.tearing, diff --git a/packages/frontend/src/utility/image-effector/fxs/threshold.glsl b/packages/frontend/src/utility/image-effector/fxs/threshold.glsl new file mode 100644 index 0000000000..5ca8c46c39 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/threshold.glsl @@ -0,0 +1,23 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_r; +uniform float u_g; +uniform float u_b; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float r = in_color.r < u_r ? 0.0 : 1.0; + float g = in_color.g < u_g ? 0.0 : 1.0; + float b = in_color.b < u_b ? 0.0 : 1.0; + out_color = vec4(r, g, b, in_color.a); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/threshold.ts b/packages/frontend/src/utility/image-effector/fxs/threshold.ts index 3e591fc939..d0bb8305ae 100644 --- a/packages/frontend/src/utility/image-effector/fxs/threshold.ts +++ b/packages/frontend/src/utility/image-effector/fxs/threshold.ts @@ -4,28 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; +import shader from './threshold.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform float u_r; -uniform float u_g; -uniform float u_b; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float r = in_color.r < u_r ? 0.0 : 1.0; - float g = in_color.g < u_g ? 0.0 : 1.0; - float b = in_color.b < u_b ? 0.0 : 1.0; - out_color = vec4(r, g, b, in_color.a); -} -`; - export const FX_threshold = defineImageEffectorFx({ id: 'threshold', name: i18n.ts._imageEffector._fxs.threshold, diff --git a/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.glsl b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.glsl new file mode 100644 index 0000000000..d6a1ef1820 --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.glsl @@ -0,0 +1,147 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const float PI = 3.141592653589793; + +in vec2 in_uv; // 0..1 +uniform sampler2D in_texture; // 背景 +uniform vec2 in_resolution; // 出力解像度(px) + +uniform sampler2D u_watermark; // ウォーターマーク +uniform vec2 u_wmResolution; // ウォーターマーク元解像度(px) + +uniform float u_opacity; // 0..1 +uniform float u_scale; // watermarkのスケール +uniform float u_angle; // -1..1 (PI倍) +uniform bool u_cover; // cover基準 or fit基準 +uniform bool u_repeat; // タイル敷き詰め +uniform int u_alignX; // 0:left 1:center 2:right +uniform int u_alignY; // 0:top 1:center 2:bottom +uniform float u_margin; // 余白(比率) +uniform float u_repeatMargin; // 敷き詰め時の余白(比率) +uniform bool u_noBBoxExpansion; // 回転時のBounding Box拡張を抑止 +uniform bool u_wmEnabled; // watermark有効 + +out vec4 out_color; + +mat2 rot(float a) { + float c = cos(a), s = sin(a); + return mat2(c, -s, s, c); +} + +// cover/fitとscaleから、最終的なサイズ(px)を計算 +vec2 computeWmSize(vec2 outSize, vec2 wmSize, bool cover, float scale) { + float wmAspect = wmSize.x / wmSize.y; + float outAspect = outSize.x / outSize.y; + vec2 size; + if (cover) { + if (wmAspect >= outAspect) { + size.y = outSize.y * scale; + size.x = size.y * wmAspect; + } else { + size.x = outSize.x * scale; + size.y = size.x / wmAspect; + } + } else { + if (wmAspect >= outAspect) { + size.x = outSize.x * scale; + size.y = size.x / wmAspect; + } else { + size.y = outSize.y * scale; + size.x = size.y * wmAspect; + } + } + return size; +} + +void main() { + vec2 outSize = in_resolution; + vec2 p = in_uv * outSize; // 出力のピクセル座標 + vec4 base = texture(in_texture, in_uv); + + if (!u_wmEnabled) { + out_color = base; + return; + } + + float theta = u_angle * PI; // ラジアン + vec2 wmSize = computeWmSize(outSize, u_wmResolution, u_cover, u_scale); + vec2 margin = u_repeat ? wmSize * u_repeatMargin : outSize * u_margin; + + // アライメントに基づく回転中心を計算 + float rotateX = 0.0; + float rotateY = 0.0; + if (abs(theta) > 1e-6 && !u_noBBoxExpansion) { + rotateX = abs(abs(wmSize.x * cos(theta)) + abs(wmSize.y * sin(theta)) - wmSize.x) * 0.5; + rotateY = abs(abs(wmSize.x * sin(theta)) + abs(wmSize.y * cos(theta)) - wmSize.y) * 0.5; + } + + float x; + if (u_alignX == 1) { + x = (outSize.x - wmSize.x) * 0.5; + } else if (u_alignX == 0) { + x = rotateX + margin.x; + } else { + x = outSize.x - wmSize.x - margin.x - rotateX; + } + + float y; + if (u_alignY == 1) { + y = (outSize.y - wmSize.y) * 0.5; + } else if (u_alignY == 0) { + y = rotateY + margin.y; + } else { + y = outSize.y - wmSize.y - margin.y - rotateY; + } + + vec2 rectMin = vec2(x, y); + vec2 rectMax = rectMin + wmSize; + vec2 rectCenter = (rectMin + rectMax) * 0.5; + + vec4 wmCol = vec4(0.0); + + if (u_repeat) { + // アライメントに基づく中心で回転 + vec2 q = rectCenter + rot(theta) * (p - rectCenter); + + // タイルグリッドの原点をrectMin(アライメント位置)に設定 + vec2 gridOrigin = rectMin - margin; + vec2 qFromOrigin = q - gridOrigin; + + // タイルサイズ(ウォーターマーク + マージン)で正規化 + vec2 tile = wmSize + margin * 2.0; + vec2 tileUv = qFromOrigin / tile; + + // タイル内のローカル座標(0..1)を取得 + vec2 localUv = fract(tileUv); + + // ローカル座標をピクセル単位に変換 + vec2 localPos = localUv * tile; + + // マージン領域内かチェック + bool inMargin = any(lessThan(localPos, margin)) || any(greaterThanEqual(localPos, margin + wmSize)); + + if (!inMargin) { + // ウォーターマーク領域内: UV座標を計算 + vec2 uvWm = (localPos - margin) / wmSize; + wmCol = texture(u_watermark, uvWm); + } + // マージン領域の場合は透明(wmCol = vec4(0.0))のまま + } else { + // アライメントと回転に従い一枚だけ描画 + vec2 q = rectCenter + rot(theta) * (p - rectCenter); + bool inside = all(greaterThanEqual(q, rectMin)) && all(lessThan(q, rectMax)); + if (inside) { + vec2 uvWm = (q - rectMin) / wmSize; + wmCol = texture(u_watermark, uvWm); + } + } + + float a = clamp(wmCol.a * u_opacity, 0.0, 1.0); + out_color = mix(base, vec4(wmCol.rgb, 1.0), a); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts index f79acb44b0..bb51ed796b 100644 --- a/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts +++ b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts @@ -4,93 +4,13 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; - -const shader = `#version 300 es -precision mediump float; - -const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform sampler2D u_texture_watermark; -uniform vec2 u_resolution_watermark; -uniform float u_scale; -uniform float u_angle; -uniform float u_opacity; -uniform bool u_repeat; -uniform int u_alignX; // 0: left, 1: center, 2: right -uniform int u_alignY; // 0: top, 1: center, 2: bottom -uniform float u_alignMargin; -uniform int u_fitMode; // 0: contain, 1: cover -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - float in_x_ratio = max(in_resolution.x / in_resolution.y, 1.0); - float in_y_ratio = max(in_resolution.y / in_resolution.x, 1.0); - - bool contain = u_fitMode == 0; - - float x_ratio = u_resolution_watermark.x / in_resolution.x; - float y_ratio = u_resolution_watermark.y / in_resolution.y; - - float aspect_ratio = contain ? - (min(x_ratio, y_ratio) / max(x_ratio, y_ratio)) : - (max(x_ratio, y_ratio) / min(x_ratio, y_ratio)); - - float x_scale = contain ? - (x_ratio > y_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) : - (x_ratio > y_ratio ? aspect_ratio * u_scale : 1.0 * u_scale); - - float y_scale = contain ? - (y_ratio > x_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) : - (y_ratio > x_ratio ? aspect_ratio * u_scale : 1.0 * u_scale); - - float x_offset = u_alignX == 0 ? x_scale / 2.0 : u_alignX == 2 ? 1.0 - (x_scale / 2.0) : 0.5; - float y_offset = u_alignY == 0 ? y_scale / 2.0 : u_alignY == 2 ? 1.0 - (y_scale / 2.0) : 0.5; - - x_offset += (u_alignX == 0 ? 1.0 : u_alignX == 2 ? -1.0 : 0.0) * u_alignMargin; - y_offset += (u_alignY == 0 ? 1.0 : u_alignY == 2 ? -1.0 : 0.0) * u_alignMargin; - - float angle = -(u_angle * PI); - vec2 center = vec2(x_offset, y_offset); - //vec2 centeredUv = (in_uv - center) * vec2(in_x_ratio, in_y_ratio); - vec2 centeredUv = (in_uv - center); - vec2 rotatedUV = vec2( - centeredUv.x * cos(angle) - centeredUv.y * sin(angle), - centeredUv.x * sin(angle) + centeredUv.y * cos(angle) - ) + center; - - // trim - if (!u_repeat) { - bool isInside = rotatedUV.x > x_offset - (x_scale / 2.0) && rotatedUV.x < x_offset + (x_scale / 2.0) && - rotatedUV.y > y_offset - (y_scale / 2.0) && rotatedUV.y < y_offset + (y_scale / 2.0); - if (!isInside) { - out_color = in_color; - return; - } - } - - vec4 watermark_color = texture(u_texture_watermark, vec2( - (rotatedUV.x - (x_offset - (x_scale / 2.0))) / x_scale, - (rotatedUV.y - (y_offset - (y_scale / 2.0))) / y_scale - )); - - out_color.r = mix(in_color.r, watermark_color.r, u_opacity * watermark_color.a); - out_color.g = mix(in_color.g, watermark_color.g, u_opacity * watermark_color.a); - out_color.b = mix(in_color.b, watermark_color.b, u_opacity * watermark_color.a); - out_color.a = in_color.a * (1.0 - u_opacity * watermark_color.a) + watermark_color.a * u_opacity; -} -`; +import shader from './watermarkPlacement.glsl'; export const FX_watermarkPlacement = defineImageEffectorFx({ id: 'watermarkPlacement', name: '(internal)', shader, - uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'alignMargin', 'fitMode'] as const, + uniforms: ['opacity', 'scale', 'angle', 'cover', 'repeat', 'alignX', 'alignY', 'margin', 'repeatMargin', 'noBBoxExpansion', 'wmResolution', 'wmEnabled', 'watermark'] as const, params: { cover: { type: 'boolean', @@ -125,29 +45,50 @@ export const FX_watermarkPlacement = defineImageEffectorFx({ max: 1.0, step: 0.01, }, + noBoundingBoxExpansion: { + type: 'boolean', + default: false, + }, watermark: { type: 'texture', default: null, }, }, main: ({ gl, u, params, textures }) => { - if (textures.watermark == null) { - return; - } + // 基本パラメータ + gl.uniform1f(u.opacity, params.opacity ?? 1.0); + gl.uniform1f(u.scale, params.scale ?? 0.3); + gl.uniform1f(u.angle, params.angle ?? 0.0); + gl.uniform1i(u.cover, params.cover ? 1 : 0); + gl.uniform1i(u.repeat, params.repeat ? 1 : 0); + const ax = params.align?.x === 'left' ? 0 : params.align?.x === 'center' ? 1 : 2; + const ay = params.align?.y === 'top' ? 0 : params.align?.y === 'center' ? 1 : 2; + gl.uniform1i(u.alignX, ax); + gl.uniform1i(u.alignY, ay); + gl.uniform1f(u.margin, (params.align?.margin ?? 0)); + gl.uniform1f(u.repeatMargin, (params.align?.margin ?? 0)); + gl.uniform1i(u.noBBoxExpansion, params.noBoundingBoxExpansion ? 1 : 0); - gl.activeTexture(gl.TEXTURE1); - gl.bindTexture(gl.TEXTURE_2D, textures.watermark.texture); - gl.uniform1i(u.texture_watermark, 1); + // ウォーターマークテクスチャ + const wm = textures.watermark; + if (wm) { + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, wm.texture); - gl.uniform2fv(u.resolution_watermark, [textures.watermark.width, textures.watermark.height]); - gl.uniform1f(u.scale, params.scale); + // リピートモードに応じてWRAP属性を設定 + if (params.repeat) { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + } else { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + } - gl.uniform1f(u.opacity, params.opacity); - gl.uniform1f(u.angle, params.angle); - gl.uniform1i(u.repeat, params.repeat ? 1 : 0); - gl.uniform1i(u.alignX, params.align.x === 'left' ? 0 : params.align.x === 'right' ? 2 : 1); - gl.uniform1i(u.alignY, params.align.y === 'top' ? 0 : params.align.y === 'bottom' ? 2 : 1); - gl.uniform1f(u.alignMargin, params.align.margin ?? 0); - gl.uniform1i(u.fitMode, params.cover ? 1 : 0); + gl.uniform1i(u.watermark, 1); + gl.uniform2f(u.wmResolution, wm.width, wm.height); + gl.uniform1i(u.wmEnabled, 1); + } else { + gl.uniform1i(u.wmEnabled, 0); + } }, }); diff --git a/packages/frontend/src/utility/image-effector/fxs/zoomLines.glsl b/packages/frontend/src/utility/image-effector/fxs/zoomLines.glsl new file mode 100644 index 0000000000..a0f11fcb5b --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/zoomLines.glsl @@ -0,0 +1,48 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// エイリアスを解決してくれないので、プロジェクトルートからの絶対パスにする必要がある +#include /src/shaders/snoise; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform vec2 u_pos; +uniform float u_frequency; +uniform bool u_thresholdEnabled; +uniform float u_threshold; +uniform float u_maskSize; +uniform bool u_black; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)); + vec2 uv = centeredUv; + + float seed = 1.0; + float time = 0.0; + + vec2 noiseUV = (uv - u_pos) / distance((uv - u_pos), vec2(0.0)); + float noiseX = (noiseUV.x + seed) * u_frequency; + float noiseY = (noiseUV.y + seed) * u_frequency; + float noise = (1.0 + snoise(vec3(noiseX, noiseY, time))) / 2.0; + + float t = noise; + if (u_thresholdEnabled) t = t < u_threshold ? 1.0 : 0.0; + + // TODO: マスクの形自体も揺らぎを与える + float d = distance(uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0)); + float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0))); + out_color = vec4( + mix(in_color.r, u_black ? 0.0 : 1.0, t * mask), + mix(in_color.g, u_black ? 0.0 : 1.0, t * mask), + mix(in_color.b, u_black ? 0.0 : 1.0, t * mask), + in_color.a + ); +} diff --git a/packages/frontend/src/utility/image-effector/fxs/zoomLines.ts b/packages/frontend/src/utility/image-effector/fxs/zoomLines.ts index 4ea28658dd..8c0956d24e 100644 --- a/packages/frontend/src/utility/image-effector/fxs/zoomLines.ts +++ b/packages/frontend/src/utility/image-effector/fxs/zoomLines.ts @@ -4,53 +4,9 @@ */ import { defineImageEffectorFx } from '../ImageEffector.js'; -import { GLSL_LIB_SNOISE } from '@/utility/webgl.js'; +import shader from './zoomLines.glsl'; import { i18n } from '@/i18n.js'; -const shader = `#version 300 es -precision mediump float; - -${GLSL_LIB_SNOISE} - -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform vec2 u_pos; -uniform float u_frequency; -uniform bool u_thresholdEnabled; -uniform float u_threshold; -uniform float u_maskSize; -uniform bool u_black; -out vec4 out_color; - -void main() { - vec4 in_color = texture(in_texture, in_uv); - vec2 centeredUv = (in_uv - vec2(0.5, 0.5)); - vec2 uv = centeredUv; - - float seed = 1.0; - float time = 0.0; - - vec2 noiseUV = (uv - u_pos) / distance((uv - u_pos), vec2(0.0)); - float noiseX = (noiseUV.x + seed) * u_frequency; - float noiseY = (noiseUV.y + seed) * u_frequency; - float noise = (1.0 + snoise(vec3(noiseX, noiseY, time))) / 2.0; - - float t = noise; - if (u_thresholdEnabled) t = t < u_threshold ? 1.0 : 0.0; - - // TODO: マスクの形自体も揺らぎを与える - float d = distance(uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0)); - float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0))); - out_color = vec4( - mix(in_color.r, u_black ? 0.0 : 1.0, t * mask), - mix(in_color.g, u_black ? 0.0 : 1.0, t * mask), - mix(in_color.b, u_black ? 0.0 : 1.0, t * mask), - in_color.a - ); -} -`; - export const FX_zoomLines = defineImageEffectorFx({ id: 'zoomLines', name: i18n.ts._imageEffector._fxs.zoomLines, diff --git a/packages/frontend/src/utility/key-event.ts b/packages/frontend/src/utility/key-event.ts deleted file mode 100644 index 020a6c2174..0000000000 --- a/packages/frontend/src/utility/key-event.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する - * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values - */ -export type KeyCode = ( - | 'Backspace' - | 'Tab' - | 'Enter' - | 'Shift' - | 'Control' - | 'Alt' - | 'Pause' - | 'CapsLock' - | 'Escape' - | 'Space' - | 'PageUp' - | 'PageDown' - | 'End' - | 'Home' - | 'ArrowLeft' - | 'ArrowUp' - | 'ArrowRight' - | 'ArrowDown' - | 'Insert' - | 'Delete' - | 'Digit0' - | 'Digit1' - | 'Digit2' - | 'Digit3' - | 'Digit4' - | 'Digit5' - | 'Digit6' - | 'Digit7' - | 'Digit8' - | 'Digit9' - | 'KeyA' - | 'KeyB' - | 'KeyC' - | 'KeyD' - | 'KeyE' - | 'KeyF' - | 'KeyG' - | 'KeyH' - | 'KeyI' - | 'KeyJ' - | 'KeyK' - | 'KeyL' - | 'KeyM' - | 'KeyN' - | 'KeyO' - | 'KeyP' - | 'KeyQ' - | 'KeyR' - | 'KeyS' - | 'KeyT' - | 'KeyU' - | 'KeyV' - | 'KeyW' - | 'KeyX' - | 'KeyY' - | 'KeyZ' - | 'MetaLeft' - | 'MetaRight' - | 'ContextMenu' - | 'F1' - | 'F2' - | 'F3' - | 'F4' - | 'F5' - | 'F6' - | 'F7' - | 'F8' - | 'F9' - | 'F10' - | 'F11' - | 'F12' - | 'NumLock' - | 'ScrollLock' - | 'Semicolon' - | 'Equal' - | 'Comma' - | 'Minus' - | 'Period' - | 'Slash' - | 'Backquote' - | 'BracketLeft' - | 'Backslash' - | 'BracketRight' - | 'Quote' - | 'Meta' - | 'AltGraph' -); - -/** - * 修飾キーを表す文字列。不足分は適宜追加する。 - */ -export type KeyModifier = ( - | 'Shift' - | 'Control' - | 'Alt' - | 'Meta' -); - -/** - * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。 - */ -export type KeyState = ( - | 'composing' - | 'repeat' -); - -export type KeyEventHandler = { - modifiers?: KeyModifier[]; - states?: KeyState[]; - code: KeyCode | 'any'; - handler: (event: KeyboardEvent) => void; -}; - -export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) { - function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) { - if (modifiers) { - return modifiers.every(modifier => ev.getModifierState(modifier)); - } - return true; - } - - function checkState(ev: KeyboardEvent, states?: KeyState[]) { - if (states) { - return states.every(state => ev.getModifierState(state)); - } - return true; - } - - let hit = false; - for (const handler of handlers.filter(it => it.code === event.code)) { - if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) { - handler.handler(event); - hit = true; - break; - } - } - - if (!hit) { - for (const handler of handlers.filter(it => it.code === 'any')) { - handler.handler(event); - } - } -} diff --git a/packages/frontend/src/utility/snowfall-effect.fragment.glsl b/packages/frontend/src/utility/snowfall-effect.fragment.glsl new file mode 100644 index 0000000000..560af039e8 --- /dev/null +++ b/packages/frontend/src/utility/snowfall-effect.fragment.glsl @@ -0,0 +1,23 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec4 v_color; +in float v_rotation; +uniform sampler2D u_texture; +out vec4 out_color; + +void main() { + vec2 rotated = vec2( + cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5, + cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5 + ); + + vec4 snowflake = texture(u_texture, rotated); + + out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a); +} diff --git a/packages/frontend/src/utility/snowfall-effect.ts b/packages/frontend/src/utility/snowfall-effect.ts index 65398e6a43..cefa720ebf 100644 --- a/packages/frontend/src/utility/snowfall-effect.ts +++ b/packages/frontend/src/utility/snowfall-effect.ts @@ -3,59 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export class SnowfallEffect { - private VERTEX_SOURCE = `#version 300 es - in vec4 a_position; - in vec4 a_color; - in vec3 a_rotation; - in vec3 a_speed; - in float a_size; - out vec4 v_color; - out float v_rotation; - uniform float u_time; - uniform mat4 u_projection; - uniform vec3 u_worldSize; - uniform float u_gravity; - uniform float u_wind; - uniform float u_spin_factor; - uniform float u_turbulence; - - void main() { - v_color = a_color; - v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y; - - vec3 pos = a_position.xyz; - - pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x; - pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y; - - pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z; - pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z; - - gl_Position = u_projection * vec4(pos.xyz, a_position.w); - gl_PointSize = (a_size / gl_Position.w) * 100.0; - } - `; +import vertexSource from './snowfall-effect.vertex.glsl'; +import fragmentSource from './snowfall-effect.fragment.glsl'; - private FRAGMENT_SOURCE = `#version 300 es - precision mediump float; - - in vec4 v_color; - in float v_rotation; - uniform sampler2D u_texture; - out vec4 out_color; - - void main() { - vec2 rotated = vec2( - cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5, - cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5 - ); - - vec4 snowflake = texture(u_texture, rotated); - - out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a); - } - `; +export class SnowfallEffect { + private VERTEX_SOURCE = vertexSource; + private FRAGMENT_SOURCE = fragmentSource; private gl: WebGLRenderingContext; private program: WebGLProgram; diff --git a/packages/frontend/src/utility/snowfall-effect.vertex.glsl b/packages/frontend/src/utility/snowfall-effect.vertex.glsl new file mode 100644 index 0000000000..bfca1a76ee --- /dev/null +++ b/packages/frontend/src/utility/snowfall-effect.vertex.glsl @@ -0,0 +1,37 @@ +#version 300 es + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec4 a_position; +in vec4 a_color; +in vec3 a_rotation; +in vec3 a_speed; +in float a_size; +out vec4 v_color; +out float v_rotation; +uniform float u_time; +uniform mat4 u_projection; +uniform vec3 u_worldSize; +uniform float u_gravity; +uniform float u_wind; +uniform float u_spin_factor; +uniform float u_turbulence; + +void main() { + v_color = a_color; + v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y; + + vec3 pos = a_position.xyz; + + pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x; + pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y; + + pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z; + pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z; + + gl_Position = u_projection * vec4(pos.xyz, a_position.w); + gl_PointSize = (a_size / gl_Position.w) * 100.0; +} diff --git a/packages/frontend/src/utility/watermark.ts b/packages/frontend/src/utility/watermark.ts index b3525f158f..1b46721a2b 100644 --- a/packages/frontend/src/utility/watermark.ts +++ b/packages/frontend/src/utility/watermark.ts @@ -27,6 +27,7 @@ export type WatermarkPreset = { type: 'text'; text: string; repeat: boolean; + noBoundingBoxExpansion: boolean; scale: number; angle: number; align: Align; @@ -38,6 +39,7 @@ export type WatermarkPreset = { imageId: string | null; cover: boolean; repeat: boolean; + noBoundingBoxExpansion: boolean; scale: number; angle: number; align: Align; @@ -106,6 +108,7 @@ export class WatermarkRenderer { id: layer.id, params: { repeat: layer.repeat, + noBoundingBoxExpansion: layer.noBoundingBoxExpansion, scale: layer.scale, align: layer.align, angle: layer.angle, @@ -123,6 +126,7 @@ export class WatermarkRenderer { id: layer.id, params: { repeat: layer.repeat, + noBoundingBoxExpansion: layer.noBoundingBoxExpansion, scale: layer.scale, align: layer.align, angle: layer.angle, diff --git a/packages/frontend/src/utility/webgl.ts b/packages/frontend/src/utility/webgl.ts index dee2103ecf..ae595b605c 100644 --- a/packages/frontend/src/utility/webgl.ts +++ b/packages/frontend/src/utility/webgl.ts @@ -38,91 +38,3 @@ export function initShaderProgram(gl: WebGL2RenderingContext, vsSource: string, return shaderProgram; } - -export const GLSL_LIB_SNOISE = ` -// Description : Array and textureless GLSL 2D/3D/4D simplex -// noise functions. -// Author : Ian McEwan, Ashima Arts. -// Maintainer : stegu -// Lastmod : 20201014 (stegu) -// License : Copyright (C) 2011 Ashima Arts. All rights reserved. -// Distributed under the MIT License. See LICENSE file. -// https://github.com/ashima/webgl-noise -// https://github.com/stegu/webgl-noise - -vec3 mod289(vec3 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 mod289(vec4 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 permute(vec4 x) { - return mod289(((x * 34.0) + 10.0) * x); -} - -vec4 taylorInvSqrt(vec4 r) { - return 1.79284291400159 - 0.85373472095314 * r; -} - -float snoise(vec3 v) { - const vec2 C = vec2(1.0/6.0, 1.0/3.0); - const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); - - vec3 i = floor(v + dot(v, C.yyy)); - vec3 x0 = v - i + dot(i, C.xxx); - - vec3 g = step(x0.yzx, x0.xyz); - vec3 l = 1.0 - g; - vec3 i1 = min(g.xyz, l.zxy); - vec3 i2 = max(g.xyz, l.zxy); - - vec3 x1 = x0 - i1 + C.xxx; - vec3 x2 = x0 - i2 + C.yyy; - vec3 x3 = x0 - D.yyy; - - i = mod289(i); - vec4 p = permute(permute(permute( - i.z + vec4(0.0, i1.z, i2.z, 1.0)) - + i.y + vec4(0.0, i1.y, i2.y, 1.0)) - + i.x + vec4(0.0, i1.x, i2.x, 1.0)); - - float n_ = 0.142857142857; - vec3 ns = n_ * D.wyz - D.xzx; - - vec4 j = p - 49.0 * floor(p * ns.z * ns.z); - - vec4 x_ = floor(j * ns.z); - vec4 y_ = floor(j - 7.0 * x_); - - vec4 x = x_ * ns.x + ns.yyyy; - vec4 y = y_ * ns.x + ns.yyyy; - vec4 h = 1.0 - abs(x) - abs(y); - - vec4 b0 = vec4(x.xy, y.xy); - vec4 b1 = vec4(x.zw, y.zw); - - vec4 s0 = floor(b0) * 2.0 + 1.0; - vec4 s1 = floor(b1) * 2.0 + 1.0; - vec4 sh = -step(h, vec4(0.0)); - - vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; - vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; - - vec3 p0 = vec3(a0.xy, h.x); - vec3 p1 = vec3(a0.zw, h.y); - vec3 p2 = vec3(a1.xy, h.z); - vec3 p3 = vec3(a1.zw, h.w); - - vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); - p0 *= norm.x; - p1 *= norm.y; - p2 *= norm.z; - p3 *= norm.w; - - vec4 m = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); - m = m * m; - return 105.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); -} -`; diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index ab606bff09..135bcc04cb 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -36,6 +36,7 @@ ], "types": [ "vite/client", + "vite-plugin-glsl/ext", "vitest/importMeta", ], "lib": [ diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 456ff150f6..6f320e99c9 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -1,6 +1,7 @@ import path from 'path'; import pluginReplace from '@rollup/plugin-replace'; import pluginVue from '@vitejs/plugin-vue'; +import pluginGlsl from 'vite-plugin-glsl'; import { defineConfig } from 'vite'; import type { UserConfig } from 'vite'; import * as yaml from 'js-yaml'; @@ -117,6 +118,7 @@ export function getConfig(): UserConfig { pluginRemoveUnrefI18n(), pluginUnwindCssModuleClassName(), pluginJson5(), + pluginGlsl({ minify: true }), ...process.env.NODE_ENV === 'production' ? [ pluginReplace({ diff --git a/packages/icons-subsetter/package.json b/packages/icons-subsetter/package.json index dda3e575b0..284bced499 100644 --- a/packages/icons-subsetter/package.json +++ b/packages/icons-subsetter/package.json @@ -11,17 +11,17 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "22.18.6", + "@types/node": "22.18.10", "@types/wawoff2": "1.0.2", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1" + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1" }, "dependencies": { "@tabler/icons-webfont": "3.35.0", - "harfbuzzjs": "0.4.12", + "harfbuzzjs": "0.4.13", "tiny-glob": "0.2.9", "tsx": "4.20.6", - "typescript": "5.9.2", + "typescript": "5.9.3", "wawoff2": "2.0.1" }, "files": [ diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json index 26eea6aaf2..ef05d67eff 100644 --- a/packages/misskey-bubble-game/package.json +++ b/packages/misskey-bubble-game/package.json @@ -23,15 +23,15 @@ }, "devDependencies": { "@types/matter-js": "0.20.2", + "@types/node": "22.18.10", "@types/seedrandom": "3.0.8", - "@types/node": "22.18.6", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", - "nodemon": "3.1.10", - "execa": "9.6.0", - "typescript": "5.9.2", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", "esbuild": "0.25.10", - "glob": "11.0.3" + "execa": "9.6.0", + "glob": "11.0.3", + "nodemon": "3.1.10", + "typescript": "5.9.3" }, "files": [ "built" diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json index f203426ec0..fca323eac9 100644 --- a/packages/misskey-js/generator/package.json +++ b/packages/misskey-js/generator/package.json @@ -7,15 +7,15 @@ "generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix" }, "devDependencies": { - "@readme/openapi-parser": "5.0.1", - "@types/node": "22.18.6", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", + "@readme/openapi-parser": "5.0.2", + "@types/node": "22.18.10", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", "openapi-types": "12.1.3", "openapi-typescript": "7.9.1", "ts-case-convert": "2.1.0", "tsx": "4.20.6", - "typescript": "5.9.2" + "typescript": "5.9.3" }, "files": [ "built" diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 664526e948..e98f1b4262 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.10.0", + "version": "2025.10.1", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", @@ -35,10 +35,10 @@ "directory": "packages/misskey-js" }, "devDependencies": { - "@microsoft/api-extractor": "7.52.13", - "@types/node": "22.18.6", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", + "@microsoft/api-extractor": "7.53.1", + "@types/node": "22.18.10", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", "@vitest/coverage-v8": "3.2.4", "esbuild": "0.25.10", "execa": "9.6.0", @@ -46,7 +46,7 @@ "ncp": "2.0.0", "nodemon": "3.1.10", "tsd": "0.33.0", - "typescript": "5.9.2", + "typescript": "5.9.3", "vitest": "3.2.4", "vitest-websocket-mock": "0.5.0" }, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 7edd43bf9b..3e95651071 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -9460,6 +9460,7 @@ export interface operations { enableRemoteNotesCleaning: boolean; remoteNotesCleaningExpiryDaysForEachNotes: number; remoteNotesCleaningMaxProcessingDurationInMinutes: number; + showRoleBadgesOfRemoteUsers: boolean; }; }; }; @@ -12780,6 +12781,7 @@ export interface operations { enableRemoteNotesCleaning?: boolean; remoteNotesCleaningExpiryDaysForEachNotes?: number; remoteNotesCleaningMaxProcessingDurationInMinutes?: number; + showRoleBadgesOfRemoteUsers?: boolean; }; }; }; diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json index 4fdd55868c..f050cf1fd9 100644 --- a/packages/misskey-reversi/package.json +++ b/packages/misskey-reversi/package.json @@ -22,14 +22,14 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "22.18.6", - "@typescript-eslint/eslint-plugin": "8.44.1", - "@typescript-eslint/parser": "8.44.1", + "@types/node": "22.18.10", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "esbuild": "0.25.10", "execa": "9.6.0", + "glob": "11.0.3", "nodemon": "3.1.10", - "typescript": "5.9.2", - "esbuild": "0.25.10", - "glob": "11.0.3" + "typescript": "5.9.3" }, "files": [ "built" diff --git a/packages/sw/package.json b/packages/sw/package.json index 08a901d6af..893701a7b4 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -14,11 +14,11 @@ "misskey-js": "workspace:*" }, "devDependencies": { - "@typescript-eslint/parser": "8.44.1", + "@typescript-eslint/parser": "8.46.1", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74", "eslint-plugin-import": "2.32.0", "nodemon": "3.1.10", - "typescript": "5.9.2" + "typescript": "5.9.3" }, "type": "module" } |