summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-10-24 06:31:35 +0000
committerGitHub <noreply@github.com>2025-10-24 06:31:35 +0000
commitb4e16c83e2823f6348842c23c48de2caae52563a (patch)
treee4efaf0f2910491d039aab59d544a92836b77266 /packages
parentMerge pull request #16591 from misskey-dev/develop (diff)
parentRelease: 2025.10.1 (diff)
downloadmisskey-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')
-rw-r--r--packages/backend/migration/1760607435831-RoleBadgesRemoteUsers.js16
-rw-r--r--packages/backend/migration/1760790899857-unnecessary-null-default.js26
-rw-r--r--packages/backend/package.json54
-rw-r--r--packages/backend/src/core/DriveService.ts53
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts4
-rw-r--r--packages/backend/src/models/Meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts5
-rw-r--r--packages/frontend-builder/package.json12
-rw-r--r--packages/frontend-embed/package.json26
-rw-r--r--packages/frontend-shared/js/emojilist.ts11
-rw-r--r--packages/frontend-shared/package.json8
-rw-r--r--packages/frontend/package.json53
-rw-r--r--packages/frontend/src/accounts.ts10
-rw-r--r--packages/frontend/src/aiscript/api.ts2
-rw-r--r--packages/frontend/src/boot/common.ts13
-rw-r--r--packages/frontend/src/components/MkAnimBg.fragment.glsl111
-rw-r--r--packages/frontend/src/components/MkAnimBg.vertex.glsl15
-rw-r--r--packages/frontend/src/components/MkAnimBg.vue123
-rw-r--r--packages/frontend/src/components/MkImageEffectorDialog.vue19
-rw-r--r--packages/frontend/src/components/MkPoll.vue20
-rw-r--r--packages/frontend/src/components/MkPostForm.vue95
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.reaction.vue6
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue4
-rw-r--r--packages/frontend/src/components/MkUploaderDialog.vue1
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue24
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.vue2
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue11
-rw-r--r--packages/frontend/src/components/grid/MkGrid.vue262
-rw-r--r--packages/frontend/src/composables/use-uploader.ts4
-rw-r--r--packages/frontend/src/directives/adaptive-bg.ts6
-rw-r--r--packages/frontend/src/directives/adaptive-border.ts8
-rw-r--r--packages/frontend/src/directives/anim.ts8
-rw-r--r--packages/frontend/src/directives/appear.ts15
-rw-r--r--packages/frontend/src/directives/click-anime.ts6
-rw-r--r--packages/frontend/src/directives/follow-append.ts12
-rw-r--r--packages/frontend/src/directives/get-size.ts10
-rw-r--r--packages/frontend/src/directives/hotkey.ts11
-rw-r--r--packages/frontend/src/directives/index.ts66
-rw-r--r--packages/frontend/src/directives/panel.ts6
-rw-r--r--packages/frontend/src/directives/ripple.ts7
-rw-r--r--packages/frontend/src/directives/tooltip.ts49
-rw-r--r--packages/frontend/src/directives/user-preview.ts50
-rw-r--r--packages/frontend/src/local-storage.ts1
-rw-r--r--packages/frontend/src/pages/about-misskey.vue3
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue2
-rw-r--r--packages/frontend/src/pages/admin/performance.vue18
-rw-r--r--packages/frontend/src/pages/settings/apps.vue3
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue9
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue1
-rw-r--r--packages/frontend/src/pages/user/home.vue49
-rw-r--r--packages/frontend/src/shaders/snoise.glsl85
-rw-r--r--packages/frontend/src/theme.ts9
-rw-r--r--packages/frontend/src/type.ts8
-rw-r--r--packages/frontend/src/types/misc.ts6
-rw-r--r--packages/frontend/src/ui/_common_/navbar-h.vue10
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue10
-rw-r--r--packages/frontend/src/ui/deck.vue2
-rw-r--r--packages/frontend/src/ui/deck/chat-column.vue11
-rw-r--r--packages/frontend/src/ui/deck/column.vue8
-rw-r--r--packages/frontend/src/ui/deck/main-column.vue24
-rw-r--r--packages/frontend/src/utility/hotkey.ts4
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/blockNoise.glsl43
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/blockNoise.ts41
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/blur.glsl78
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/blur.ts76
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/checker.glsl43
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/checker.ts41
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/chromaticAberration.glsl49
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/chromaticAberration.ts46
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/colorAdjust.glsl82
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts79
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/colorClamp.glsl29
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/colorClamp.ts30
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/colorClampAdvanced.ts24
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/distort.glsl30
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/distort.ts28
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/fill.glsl50
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/fill.ts48
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/grayscale.glsl22
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/grayscale.ts20
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/invert.glsl23
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/invert.ts21
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/mirror.glsl26
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/mirror.ts29
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/pixelate.glsl68
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/pixelate.ts66
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/polkadot.glsl75
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/polkadot.ts73
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/stripe.glsl45
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/stripe.ts43
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/tearing.glsl33
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/tearing.ts31
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/threshold.glsl23
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/threshold.ts21
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.glsl147
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts135
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/zoomLines.glsl48
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/zoomLines.ts46
-rw-r--r--packages/frontend/src/utility/key-event.ts153
-rw-r--r--packages/frontend/src/utility/snowfall-effect.fragment.glsl23
-rw-r--r--packages/frontend/src/utility/snowfall-effect.ts57
-rw-r--r--packages/frontend/src/utility/snowfall-effect.vertex.glsl37
-rw-r--r--packages/frontend/src/utility/watermark.ts4
-rw-r--r--packages/frontend/src/utility/webgl.ts88
-rw-r--r--packages/frontend/tsconfig.json1
-rw-r--r--packages/frontend/vite.config.ts2
-rw-r--r--packages/icons-subsetter/package.json10
-rw-r--r--packages/misskey-bubble-game/package.json14
-rw-r--r--packages/misskey-js/generator/package.json10
-rw-r--r--packages/misskey-js/package.json12
-rw-r--r--packages/misskey-js/src/autogen/types.ts2
-rw-r--r--packages/misskey-reversi/package.json12
-rw-r--r--packages/sw/package.json4
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"
}