From 3bc81522c65d724de121cbe6265c60e48a8f8ae7 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 3 Jun 2025 07:31:19 +0900 Subject: enhance(frontend): IDにUUIDを使うのをやめる (#16138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update flash-edit.vue --- packages/frontend/src/components/MkImgWithBlurhash.vue | 4 ++-- packages/frontend/src/components/MkMiniChart.vue | 4 ++-- packages/frontend/src/components/MkUploaderDialog.vue | 4 ++-- packages/frontend/src/components/MkWidgets.vue | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index e3a0a371b4..361aeff4d0 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -82,7 +82,7 @@ const canvasPromise = new Promise(resol - diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index 87595a820b..4ea5756284 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -34,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only - -- cgit v1.2.3-freya From 4af8c7f8b0a753cb91d5145de2906a895e636668 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 3 Jun 2025 18:44:01 +0900 Subject: enhance(frontend): リアクションビューワーで使用可能なリアクションを優先して表示するオプション (#16149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): リアクションビューワーで使用可能なリアクションを優先して表示するオプション * Update Changelog * tweak * fix * enhance: リアクティブじゃなくする --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + packages/frontend-shared/js/emojilist.ts | 4 ++++ .../frontend/src/components/MkReactionsViewer.vue | 19 ++++++++++++++++++- packages/frontend/src/pages/settings/preferences.vue | 10 ++++++++++ packages/frontend/src/preferences/def.ts | 3 +++ 7 files changed, 41 insertions(+), 1 deletion(-) (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aeaa66f79..c91d602692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - ### Client +- Enhance: ノートのリアクション一覧で、押せるリアクションを優先して表示できるようにするオプションを追加 - Fix: ドライブファイルの選択が不安定な問題を修正 - Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正 - Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 73bcb2f1c8..0119b4ae5e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5821,6 +5821,10 @@ export interface Locale extends ILocale { * URLプレビューを表示する */ "showUrlPreview": string; + /** + * 利用できるリアクションを先頭に表示 + */ + "showAvailableReactionsFirstInNote": string; "_chat": { /** * 送信者の名前を表示 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c7971507aa..b61bbf4970 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1455,6 +1455,7 @@ _settings: contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。" contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。" showUrlPreview: "URLプレビューを表示する" + showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示" _chat: showSenderName: "送信者の名前を表示" diff --git a/packages/frontend-shared/js/emojilist.ts b/packages/frontend-shared/js/emojilist.ts index f8bbf39177..09bea06719 100644 --- a/packages/frontend-shared/js/emojilist.ts +++ b/packages/frontend-shared/js/emojilist.ts @@ -48,6 +48,10 @@ export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string { ?? char; } +export function isSupportedEmoji(char: string): boolean { + return unicodeEmojisMap.has(colorizeEmoji(char)) || unicodeEmojisMap.has(char); +} + export function getEmojiName(char: string): string { // Colorize it because emojilist.json assumes that const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char); diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 725978179e..bd9ef50157 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -33,7 +33,10 @@ import * as Misskey from 'misskey-js'; import { inject, watch, ref } from 'vue'; import { TransitionGroup } from 'vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue'; +import { $i } from '@/i.js'; import { prefer } from '@/preferences.js'; +import { customEmojisMap } from '@/custom-emojis.js'; +import { isSupportedEmoji } from '@@/js/emojilist.js'; import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ @@ -70,6 +73,12 @@ function onMockToggleReaction(emoji: string, count: number) { emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1])); } +function canReact(reaction: string) { + if (!$i) return false; + // TODO: CheckPermissions + return !reaction.match(/@\w/) && (customEmojisMap.has(reaction) || isSupportedEmoji(reaction)); +} + watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { let newReactions: [string, number][] = []; hasMoreReactions.value = Object.keys(newSource).length > maxNumber; @@ -86,7 +95,15 @@ watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) = newReactions = [ ...newReactions, ...Object.entries(newSource) - .sort(([, a], [, b]) => b - a) + .sort(([emojiA, countA], [emojiB, countB]) => { + if (prefer.s.showAvailableReactionsFirstInNote) { + if (!canReact(emojiA) && canReact(emojiB)) return 1; + if (canReact(emojiA) && !canReact(emojiB)) return -1; + return countB - countA; + } else { + return countB - countA; + } + }) .filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)), ]; diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index 678cfb93c0..00f810cc37 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -229,6 +229,14 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + @@ -824,6 +832,7 @@ const showFixedPostFormInChannel = prefer.model('showFixedPostFormInChannel'); const numberOfPageCache = prefer.model('numberOfPageCache'); const enableInfiniteScroll = prefer.model('enableInfiniteScroll'); const useReactionPickerForContextMenu = prefer.model('useReactionPickerForContextMenu'); +const showAvailableReactionsFirstInNote = prefer.model('showAvailableReactionsFirstInNote'); const useGroupedNotifications = prefer.model('useGroupedNotifications'); const alwaysConfirmFollow = prefer.model('alwaysConfirmFollow'); const confirmWhenRevealingSensitiveMedia = prefer.model('confirmWhenRevealingSensitiveMedia'); @@ -917,6 +926,7 @@ watch([ enableHorizontalSwipe, enablePullToRefresh, reduceAnimation, + showAvailableReactionsFirstInNote, ], async () => { await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index 5aadf835f2..6eb9b2408a 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -377,6 +377,9 @@ export const PREF_DEF = definePreferences({ showTitlebar: { default: false, }, + showAvailableReactionsFirstInNote: { + default: false, + }, plugins: { default: [] as Plugin[], mergeStrategy: (a, b) => { -- cgit v1.2.3-freya From cd9322a8243b12632db2dd9a29a702d7531a5aa0 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 3 Jun 2025 19:18:29 +0900 Subject: feat(frontend): 画像編集機能 (#16121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * wip * Update watermarker.ts * wip * wip * Update watermarker.ts * Update MkUploaderDialog.vue * wip * Update ImageEffector.ts * Update ImageEffector.ts * wip * wip * wip * wip * wip * wip * Update MkRange.vue * Update MkRange.vue * wip * wip * Update MkImageEffectorDialog.vue * Update MkImageEffectorDialog.Layer.vue * wip * Update zoomLines.ts * Update zoomLines.ts * wip * wip * Update ImageEffector.ts * wip * Update ImageEffector.ts * wip * Update ImageEffector.ts * swip * wip * Update ImageEffector.ts * wop * Update MkUploaderDialog.vue * Update ImageEffector.ts * wip * wip * wip * Update def.ts * Update def.ts * test * test * Update manager.ts * Update manager.ts * Update manager.ts * Update manager.ts * Update MkImageEffectorDialog.vue * wip * use WEBGL_lose_context * wip * Update MkUploaderDialog.vue * Update drive.vue * wip * Update MkUploaderDialog.vue * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip --- CHANGELOG.md | 1 + locales/index.d.ts | 178 ++++++++ locales/ja-JP.yml | 49 +++ packages/frontend/assets/sample/2-3.jpg | Bin 0 -> 306563 bytes packages/frontend/assets/sample/3-2.jpg | Bin 0 -> 419447 bytes .../src/components/MkImageEffectorDialog.Layer.vue | 78 ++++ .../src/components/MkImageEffectorDialog.vue | 302 +++++++++++++ .../frontend/src/components/MkPositionSelector.vue | 53 +++ packages/frontend/src/components/MkRange.vue | 68 ++- .../frontend/src/components/MkUploaderDialog.vue | 292 ++++++++++--- .../components/MkWatermarkEditorDialog.Layer.vue | 318 ++++++++++++++ .../src/components/MkWatermarkEditorDialog.vue | 455 ++++++++++++++++++++ .../src/pages/settings/drive.WatermarkItem.vue | 112 +++++ packages/frontend/src/pages/settings/drive.vue | 190 ++++++-- packages/frontend/src/preferences/def.ts | 28 ++ .../src/utility/image-effector/ImageEffector.ts | 476 +++++++++++++++++++++ .../frontend/src/utility/image-effector/fxs.ts | 37 ++ .../src/utility/image-effector/fxs/checker.ts | 87 ++++ .../image-effector/fxs/chromaticAberration.ts | 76 ++++ .../src/utility/image-effector/fxs/colorClamp.ts | 53 +++ .../image-effector/fxs/colorClampAdvanced.ts | 89 ++++ .../src/utility/image-effector/fxs/distort.ts | 71 +++ .../src/utility/image-effector/fxs/glitch.ts | 96 +++++ .../src/utility/image-effector/fxs/grayscale.ts | 37 ++ .../src/utility/image-effector/fxs/invert.ts | 53 +++ .../src/utility/image-effector/fxs/mirror.ts | 58 +++ .../src/utility/image-effector/fxs/polkadot.ts | 151 +++++++ .../src/utility/image-effector/fxs/stripe.ts | 98 +++++ .../src/utility/image-effector/fxs/threshold.ts | 62 +++ .../image-effector/fxs/watermarkPlacement.ts | 148 +++++++ .../src/utility/image-effector/fxs/zoomLines.ts | 97 +++++ packages/frontend/src/utility/snowfall-effect.ts | 2 +- packages/frontend/src/utility/watermark.ts | 180 ++++++++ 33 files changed, 3887 insertions(+), 108 deletions(-) create mode 100644 packages/frontend/assets/sample/2-3.jpg create mode 100644 packages/frontend/assets/sample/3-2.jpg create mode 100644 packages/frontend/src/components/MkImageEffectorDialog.Layer.vue create mode 100644 packages/frontend/src/components/MkImageEffectorDialog.vue create mode 100644 packages/frontend/src/components/MkPositionSelector.vue create mode 100644 packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue create mode 100644 packages/frontend/src/components/MkWatermarkEditorDialog.vue create mode 100644 packages/frontend/src/pages/settings/drive.WatermarkItem.vue create mode 100644 packages/frontend/src/utility/image-effector/ImageEffector.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/checker.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/chromaticAberration.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/colorClamp.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/colorClampAdvanced.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/distort.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/glitch.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/grayscale.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/invert.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/mirror.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/polkadot.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/stripe.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/threshold.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/zoomLines.ts create mode 100644 packages/frontend/src/utility/watermark.ts (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index c91d602692..659aaae694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - ### Client +- Feat: 画像にウォーターマークを付与できるようになりました - Enhance: ノートのリアクション一覧で、押せるリアクションを優先して表示できるようにするオプションを追加 - Fix: ドライブファイルの選択が不安定な問題を修正 - Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 0119b4ae5e..6f3d2b3853 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5481,6 +5481,14 @@ export interface Locale extends ILocale { * 全ての「ヒントとコツ」を非表示 */ "hideAllTips": string; + /** + * デフォルトの画像圧縮度 + */ + "defaultImageCompressionLevel": string; + /** + * 低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。 + */ + "defaultImageCompressionLevel_description": string; "_chat": { /** * まだメッセージはありません @@ -12024,6 +12032,176 @@ export interface Locale extends ILocale { */ "tip": string; }; + /** + * ウォーターマーク + */ + "watermark": string; + /** + * デフォルトのプリセット + */ + "defaultPreset": string; + "_watermarkEditor": { + /** + * 画像にクレジット情報などのウォーターマークを追加することができます。 + */ + "tip": string; + /** + * 保存せずに終了しますか? + */ + "quitWithoutSaveConfirm": string; + /** + * ウォーターマークの編集 + */ + "title": string; + /** + * 全体に被せる + */ + "cover": string; + /** + * 敷き詰める + */ + "repeat": string; + /** + * 不透明度 + */ + "opacity": string; + /** + * サイズ + */ + "scale": string; + /** + * テキスト + */ + "text": string; + /** + * 位置 + */ + "position": string; + /** + * タイプ + */ + "type": string; + /** + * 画像 + */ + "image": string; + /** + * 高度 + */ + "advanced": string; + /** + * ストライプ + */ + "stripe": string; + /** + * ラインの幅 + */ + "stripeWidth": string; + /** + * ラインの数 + */ + "stripeFrequency": string; + /** + * 角度 + */ + "angle": string; + /** + * ポルカドット + */ + "polkadot": string; + /** + * チェッカー + */ + "checker": string; + /** + * メインドットの不透明度 + */ + "polkadotMainDotOpacity": string; + /** + * メインドットの大きさ + */ + "polkadotMainDotRadius": string; + /** + * サブドットの不透明度 + */ + "polkadotSubDotOpacity": string; + /** + * サブドットの大きさ + */ + "polkadotSubDotRadius": string; + /** + * サブドットの数 + */ + "polkadotSubDotDivisions": string; + }; + "_imageEffector": { + /** + * エフェクト + */ + "title": string; + /** + * エフェクトを追加 + */ + "addEffect": string; + /** + * 変更を破棄して終了しますか? + */ + "discardChangesConfirm": string; + "_fxs": { + /** + * 色収差 + */ + "chromaticAberration": string; + /** + * グリッチ + */ + "glitch": string; + /** + * ミラー + */ + "mirror": string; + /** + * 色の反転 + */ + "invert": string; + /** + * 白黒 + */ + "grayscale": string; + /** + * 色の圧縮 + */ + "colorClamp": string; + /** + * 色の圧縮(高度) + */ + "colorClampAdvanced": string; + /** + * 歪み + */ + "distort": string; + /** + * 二値化 + */ + "threshold": string; + /** + * 集中線 + */ + "zoomLines": string; + /** + * ストライプ + */ + "stripe": string; + /** + * ポルカドット + */ + "polkadot": string; + /** + * チェッカー + */ + "checker": string; + }; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b61bbf4970..d4bd2c3116 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1365,6 +1365,8 @@ abort: "中止" tip: "ヒントとコツ" redisplayAllTips: "全ての「ヒントとコツ」を再表示" hideAllTips: "全ての「ヒントとコツ」を非表示" +defaultImageCompressionLevel: "デフォルトの画像圧縮度" +defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。" _chat: noMessagesYet: "まだメッセージはありません" @@ -3219,3 +3221,50 @@ _clip: _userLists: tip: "任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。" + +watermark: "ウォーターマーク" +defaultPreset: "デフォルトのプリセット" +_watermarkEditor: + tip: "画像にクレジット情報などのウォーターマークを追加することができます。" + quitWithoutSaveConfirm: "保存せずに終了しますか?" + title: "ウォーターマークの編集" + cover: "全体に被せる" + repeat: "敷き詰める" + opacity: "不透明度" + scale: "サイズ" + text: "テキスト" + position: "位置" + type: "タイプ" + image: "画像" + advanced: "高度" + stripe: "ストライプ" + stripeWidth: "ラインの幅" + stripeFrequency: "ラインの数" + angle: "角度" + polkadot: "ポルカドット" + checker: "チェッカー" + polkadotMainDotOpacity: "メインドットの不透明度" + polkadotMainDotRadius: "メインドットの大きさ" + polkadotSubDotOpacity: "サブドットの不透明度" + polkadotSubDotRadius: "サブドットの大きさ" + polkadotSubDotDivisions: "サブドットの数" + +_imageEffector: + title: "エフェクト" + addEffect: "エフェクトを追加" + discardChangesConfirm: "変更を破棄して終了しますか?" + + _fxs: + chromaticAberration: "色収差" + glitch: "グリッチ" + mirror: "ミラー" + invert: "色の反転" + grayscale: "白黒" + colorClamp: "色の圧縮" + colorClampAdvanced: "色の圧縮(高度)" + distort: "歪み" + threshold: "二値化" + zoomLines: "集中線" + stripe: "ストライプ" + polkadot: "ポルカドット" + checker: "チェッカー" diff --git a/packages/frontend/assets/sample/2-3.jpg b/packages/frontend/assets/sample/2-3.jpg new file mode 100644 index 0000000000..ee9bff6527 Binary files /dev/null and b/packages/frontend/assets/sample/2-3.jpg differ diff --git a/packages/frontend/assets/sample/3-2.jpg b/packages/frontend/assets/sample/3-2.jpg new file mode 100644 index 0000000000..400de1649d Binary files /dev/null and b/packages/frontend/assets/sample/3-2.jpg differ diff --git a/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue b/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue new file mode 100644 index 0000000000..0312017d86 --- /dev/null +++ b/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue @@ -0,0 +1,78 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue new file mode 100644 index 0000000000..997dd4d528 --- /dev/null +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -0,0 +1,302 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkPositionSelector.vue b/packages/frontend/src/components/MkPositionSelector.vue new file mode 100644 index 0000000000..002950cdf1 --- /dev/null +++ b/packages/frontend/src/components/MkPositionSelector.vue @@ -0,0 +1,53 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index f36e68b687..9a6a207c74 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -12,7 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
+
+
+
+
+
@@ -24,7 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only @mouseenter.passive="onMouseenter" @mousedown="onMousedown" @touchstart="onMousedown" - >
+ > +
+
@@ -63,6 +70,9 @@ const emit = defineEmits<{ const containerEl = useTemplateRef('containerEl'); const thumbEl = useTemplateRef('thumbEl'); +const maxRatio = computed(() => Math.abs(props.max) / (props.max + Math.abs(Math.min(0, props.min)))); +const minRatio = computed(() => Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min)))); + const rawValue = ref((props.modelValue - props.min) / (props.max - props.min)); const steppedRawValue = computed(() => { if (props.step) { @@ -222,15 +232,17 @@ function onMousedown(ev: MouseEvent | TouchEvent) { } } - $thumbHeight: 20px; - $thumbWidth: 20px; + $thumbHeight: 32px; + $thumbWidth: 32px; + $thumbInnerHeight: 19px; + $thumbInnerWidth: 19px; > .body { display: flex; align-items: center; justify-content: center; gap: 8px; - padding: 7px 12px; + padding: 0px 4px; background: var(--MI_THEME-panel); border: solid 1px var(--MI_THEME-panel); border-radius: 6px; @@ -256,10 +268,30 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .highlight { position: absolute; top: 0; - left: 0; height: 100%; - background: var(--MI_THEME-accent); - opacity: 0.5; + background: color(from var(--MI_THEME-buttonGradateA) srgb r g b / 0.5); + overflow: clip; + + > .shine { + position: absolute; + top: 0; + width: 64px; + height: 100%; + } + } + + > .highlight.right { + > .shine.right { + right: calc(#{$thumbInnerWidth} / 2); + background: linear-gradient(-90deg, var(--MI_THEME-buttonGradateB), color(from var(--MI_THEME-buttonGradateA) srgb r g b / 0)); + } + } + + > .highlight.left { + > .shine.left { + left: calc(#{$thumbInnerWidth} / 2); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateB), color(from var(--MI_THEME-buttonGradateA) srgb r g b / 0)); + } } } @@ -290,11 +322,25 @@ function onMousedown(ev: MouseEvent | TouchEvent) { width: $thumbWidth; height: $thumbHeight; cursor: grab; - background: var(--MI_THEME-accent); - border-radius: 999px; &:hover { - background: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); + > .thumbInner { + background: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); + } + } + + > .thumbInner { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: $thumbInnerWidth; + height: $thumbInnerHeight; + background: var(--MI_THEME-accent); + border-radius: 999px; + pointer-events: none; } } } diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index a0d25d08d3..b2e4896ed3 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="ctx in items" :key="ctx.id" v-panel - :class="[$style.item, ctx.waiting ? $style.itemWaiting : null, ctx.uploaded ? $style.itemCompleted : null, ctx.uploadFailed ? $style.itemFailed : null]" + :class="[$style.item, ctx.preprocessing ? $style.itemWaiting : null, ctx.uploaded ? $style.itemCompleted : null, ctx.uploadFailed ? $style.itemFailed : null]" :style="{ '--p': ctx.progress != null ? `${ctx.progress.value / ctx.progress.max * 100}%` : '0%' }" >
@@ -40,8 +40,8 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ ctx.name }}
{{ ctx.file.type }} - {{ bytes(ctx.file.size) }} ({{ i18n.tsx._uploader.compressedToX({ x: bytes(ctx.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - ctx.compressedSize / ctx.file.size) * 100) }) }}) + {{ bytes(ctx.file.size) }}
@@ -59,19 +59,6 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - -
{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}
@@ -93,7 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue new file mode 100644 index 0000000000..4cfb4a72bc --- /dev/null +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue @@ -0,0 +1,455 @@ + + + + + + + diff --git a/packages/frontend/src/pages/settings/drive.WatermarkItem.vue b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue new file mode 100644 index 0000000000..b466f35fc5 --- /dev/null +++ b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue @@ -0,0 +1,112 @@ + + + + + + + diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index d62e487341..0614b1242b 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -39,53 +39,122 @@ SPDX-License-Identifier: AGPL-3.0-only
- -
- - - {{ i18n.ts.uploadFolder }} - - + + + + +
+ + + {{ i18n.ts.uploadFolder }} + + + + + + + {{ i18n.ts.drivecleaner }} - - - {{ i18n.ts.drivecleaner }} - + + + + + + + + + + + + + + - - - - - + + + + - - - - - - - - - - - - - - - -
-
+
+
+
+ + + + + + +
+ + + + + + +
+
+ + + + + + + + + +
+ +
+ + + + + + + + +
+
+
+ + + + + + + + + +
+
+
+ diff --git a/packages/frontend/src/components/MkUploaderItems.vue b/packages/frontend/src/components/MkUploaderItems.vue new file mode 100644 index 0000000000..2d624cf344 --- /dev/null +++ b/packages/frontend/src/components/MkUploaderItems.vue @@ -0,0 +1,196 @@ + + + + + + + diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts new file mode 100644 index 0000000000..3f105dc201 --- /dev/null +++ b/packages/frontend/src/composables/use-uploader.ts @@ -0,0 +1,535 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; +import isAnimated from 'is-file-animated'; +import { EventEmitter } from 'eventemitter3'; +import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue'; +import type { MenuItem } from '@/types/menu.js'; +import { genId } from '@/utility/id.js'; +import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; +import { isWebpSupported } from '@/utility/isWebpSupported.js'; +import { uploadFile, UploadAbortedError } from '@/utility/drive.js'; +import * as os from '@/os.js'; +import { ensureSignin } from '@/i.js'; +import { WatermarkRenderer } from '@/utility/watermark.js'; + +export type UploaderFeatures = { + effect?: boolean; + watermark?: boolean; + crop?: boolean; +}; + +const COMPRESSION_SUPPORTED_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/svg+xml', +]; + +const CROPPING_SUPPORTED_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/webp', +]; + +const IMAGE_EDITING_SUPPORTED_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/webp', +]; + +const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES; + +const mimeTypeMap = { + 'image/webp': 'webp', + 'image/jpeg': 'jpg', + 'image/png': 'png', +} as const; + +export type UploaderItem = { + id: string; + name: string; + uploadName?: string; + progress: { max: number; value: number } | null; + thumbnail: string; + preprocessing: boolean; + uploading: boolean; + uploaded: Misskey.entities.DriveFile | null; + uploadFailed: boolean; + aborted: boolean; + compressionLevel: 0 | 1 | 2 | 3; + compressedSize?: number | null; + preprocessedFile?: Blob | null; + file: File; + watermarkPresetId: string | null; + abort?: (() => void) | null; +}; + +function getCompressionSettings(level: 0 | 1 | 2 | 3) { + if (level === 1) { + return { + maxWidth: 2000, + maxHeight: 2000, + }; + } else if (level === 2) { + return { + maxWidth: 2000 * 0.75, // =1500 + maxHeight: 2000 * 0.75, // =1500 + }; + } else if (level === 3) { + return { + maxWidth: 2000 * 0.75 * 0.75, // =1125 + maxHeight: 2000 * 0.75 * 0.75, // =1125 + }; + } else { + return null; + } +} + +export function useUploader(options: { + folderId?: string | null; + multiple?: boolean; + features?: UploaderFeatures; +} = {}) { + const $i = ensureSignin(); + + const events = new EventEmitter<{ + 'itemUploaded': (ctx: { item: UploaderItem; }) => void; + }>(); + + const uploaderFeatures = computed>(() => { + return { + effect: options.features?.effect ?? true, + watermark: options.features?.watermark ?? true, + crop: options.features?.crop ?? true, + }; + }); + + const items = ref([]); + + function initializeFile(file: File) { + const id = genId(); + const filename = file.name ?? 'untitled'; + const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; + items.value.push({ + id, + name: prefer.s.keepOriginalFilename ? filename : id + extension, + progress: null, + thumbnail: window.URL.createObjectURL(file), + preprocessing: false, + uploading: false, + aborted: false, + uploaded: null, + uploadFailed: false, + compressionLevel: prefer.s.defaultImageCompressionLevel, + watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null, + file: markRaw(file), + }); + const reactiveItem = items.value.at(-1)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); + }); + } + + function addFiles(newFiles: File[]) { + for (const file of newFiles) { + initializeFile(file); + } + } + + function removeItem(item: UploaderItem) { + URL.revokeObjectURL(item.thumbnail); + items.value.splice(items.value.indexOf(item), 1); + } + + function getMenu(item: UploaderItem): MenuItem[] { + const menu: MenuItem[] = []; + + if ( + !item.preprocessing && + !item.uploading && + !item.uploaded + ) { + menu.push({ + icon: 'ti ti-cursor-text', + text: i18n.ts.rename, + action: async () => { + const { result, canceled } = await os.inputText({ + type: 'text', + title: i18n.ts.rename, + placeholder: item.name, + default: item.name, + }); + if (canceled) return; + if (result.trim() === '') return; + + item.name = result; + }, + }); + } + + if ( + uploaderFeatures.value.crop && + CROPPING_SUPPORTED_TYPES.includes(item.file.type) && + !item.preprocessing && + !item.uploading && + !item.uploaded + ) { + menu.push({ + icon: 'ti ti-crop', + text: i18n.ts.cropImage, + action: async () => { + const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); + URL.revokeObjectURL(item.thumbnail); + items.value.splice(items.value.indexOf(item), 1, { + ...item, + file: markRaw(cropped), + thumbnail: window.URL.createObjectURL(cropped), + }); + const reactiveItem = items.value.find(x => x.id === item.id)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); + }); + }, + }); + } + + if ( + uploaderFeatures.value.effect && + IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && + !item.preprocessing && + !item.uploading && + !item.uploaded + ) { + menu.push({ + icon: 'ti ti-sparkles', + text: i18n.ts._imageEffector.title + ' (BETA)', + action: async () => { + const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), { + image: item.file, + }, { + ok: (file) => { + URL.revokeObjectURL(item.thumbnail); + items.value.splice(items.value.indexOf(item), 1, { + ...item, + file: markRaw(file), + thumbnail: window.URL.createObjectURL(file), + }); + const reactiveItem = items.value.find(x => x.id === item.id)!; + preprocess(reactiveItem).then(() => { + triggerRef(items); + }); + }, + closed: () => dispose(), + }); + }, + }); + } + + if ( + uploaderFeatures.value.watermark && + WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && + !item.preprocessing && + !item.uploading && + !item.uploaded + ) { + function changeWatermarkPreset(presetId: string | null) { + item.watermarkPresetId = presetId; + preprocess(item).then(() => { + triggerRef(items); + }); + } + + menu.push({ + icon: 'ti ti-copyright', + text: i18n.ts.watermark, + caption: computed(() => item.watermarkPresetId == null ? null : prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId)?.name), + type: 'parent', + children: [{ + type: 'radioOption', + text: i18n.ts.none, + active: computed(() => item.watermarkPresetId == null), + action: () => changeWatermarkPreset(null), + }, { + type: 'divider', + }, ...prefer.s.watermarkPresets.map(preset => ({ + type: 'radioOption' as const, + text: preset.name, + active: computed(() => item.watermarkPresetId === preset.id), + action: () => changeWatermarkPreset(preset.id), + })), ...(prefer.s.watermarkPresets.length > 0 ? [{ + type: 'divider' as const, + }] : []), { + type: 'button', + icon: 'ti ti-plus', + text: i18n.ts.add, + action: async () => { + const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkWatermarkEditorDialog.vue').then(x => x.default), { + image: item.file, + }, { + ok: (preset) => { + prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]); + changeWatermarkPreset(preset.id); + }, + closed: () => dispose(), + }); + }, + }], + }); + } + + if ( + COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && + !item.preprocessing && + !item.uploading && + !item.uploaded + ) { + function changeCompressionLevel(level: 0 | 1 | 2 | 3) { + item.compressionLevel = level; + preprocess(item).then(() => { + triggerRef(items); + }); + } + + menu.push({ + icon: 'ti ti-leaf', + text: computed(() => { + let text = i18n.ts.compress; + + if (item.compressionLevel === 0 || item.compressionLevel == null) { + text += `: ${i18n.ts.none}`; + } else if (item.compressionLevel === 1) { + text += `: ${i18n.ts.low}`; + } else if (item.compressionLevel === 2) { + text += `: ${i18n.ts.medium}`; + } else if (item.compressionLevel === 3) { + text += `: ${i18n.ts.high}`; + } + + return text; + }), + type: 'parent', + children: [{ + type: 'radioOption', + text: i18n.ts.none, + active: computed(() => item.compressionLevel === 0 || item.compressionLevel == null), + action: () => changeCompressionLevel(0), + }, { + type: 'divider', + }, { + type: 'radioOption', + text: i18n.ts.low, + active: computed(() => item.compressionLevel === 1), + action: () => changeCompressionLevel(1), + }, { + type: 'radioOption', + text: i18n.ts.medium, + active: computed(() => item.compressionLevel === 2), + action: () => changeCompressionLevel(2), + }, { + type: 'radioOption', + text: i18n.ts.high, + active: computed(() => item.compressionLevel === 3), + action: () => changeCompressionLevel(3), + }], + }); + } + + if (!item.preprocessing && !item.uploading && !item.uploaded) { + menu.push({ + type: 'divider', + }, { + icon: 'ti ti-upload', + text: i18n.ts.upload, + action: () => { + uploadOne(item); + }, + }, { + icon: 'ti ti-x', + text: i18n.ts.remove, + action: () => { + removeItem(item); + }, + }); + } else if (item.uploading) { + menu.push({ + type: 'divider', + }, { + icon: 'ti ti-cloud-pause', + text: i18n.ts.abort, + danger: true, + action: () => { + if (item.abort != null) { + item.abort(); + } + }, + }); + } + + return menu; + } + + async function uploadOne(item: UploaderItem): Promise { + item.uploadFailed = false; + item.uploading = true; + + const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, { + name: item.uploadName ?? item.name, + folderId: options.folderId, + onProgress: (progress) => { + if (item.progress == null) { + item.progress = { max: progress.total, value: progress.loaded }; + } else { + item.progress.value = progress.loaded; + item.progress.max = progress.total; + } + }, + }); + + item.abort = () => { + item.abort = null; + abort(); + item.uploading = false; + item.uploadFailed = true; + }; + + await filePromise.then((file) => { + item.uploaded = file; + item.abort = null; + events.emit('itemUploaded', { item }); + }).catch(err => { + item.uploadFailed = true; + item.progress = null; + if (!(err instanceof UploadAbortedError)) { + throw err; + } + }).finally(() => { + item.uploading = false; + }); + } + + async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる + items.value = items.value.map(item => ({ + ...item, + aborted: false, + uploadFailed: false, + uploading: false, + })); + + for (const item of items.value.filter(item => item.uploaded == null)) { + // アップロード処理途中で値が変わる場合(途中で全キャンセルされたりなど)もあるので、Array filterではなくここでチェック + if (item.aborted) { + continue; + } + + await uploadOne(item); + } + } + + function abortAll() { + for (const item of items.value) { + if (item.uploaded != null) { + continue; + } + + if (item.abort != null) { + item.abort(); + } + item.aborted = true; + item.uploadFailed = true; + } + } + + async function preprocess(item: UploaderItem): Promise { + item.preprocessing = true; + + let file: Blob | File = item.file; + const imageBitmap = await window.createImageBitmap(file); + + const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(file.type); + const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId); + if (needsWatermark && preset != null) { + const canvas = window.document.createElement('canvas'); + const renderer = new WatermarkRenderer({ + canvas: canvas, + renderWidth: imageBitmap.width, + renderHeight: imageBitmap.height, + image: imageBitmap, + }); + + await renderer.setLayers(preset.layers); + + renderer.render(); + + file = await new Promise((resolve) => { + canvas.toBlob((blob) => { + if (blob == null) { + throw new Error('Failed to convert canvas to blob'); + } + resolve(blob); + renderer.destroy(); + }, 'image/png'); + }); + } + + const compressionSettings = getCompressionSettings(item.compressionLevel); + const needsCompress = item.compressionLevel !== 0 && compressionSettings && COMPRESSION_SUPPORTED_TYPES.includes(file.type) && !(await isAnimated(file)); + + if (needsCompress) { + const config = { + mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg', + maxWidth: compressionSettings.maxWidth, + maxHeight: compressionSettings.maxHeight, + quality: isWebpSupported() ? 0.85 : 0.8, + }; + + try { + const result = await readAndCompressImage(file, config); + if (result.size < file.size || file.type === 'image/webp') { + // The compression may not always reduce the file size + // (and WebP is not browser safe yet) + file = result; + item.compressedSize = result.size; + item.uploadName = file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name; + } + } catch (err) { + console.error('Failed to resize image', err); + } + } else { + item.compressedSize = null; + item.uploadName = item.name; + } + + URL.revokeObjectURL(item.thumbnail); + item.thumbnail = window.URL.createObjectURL(file); + item.preprocessedFile = markRaw(file); + item.preprocessing = false; + + imageBitmap.close(); + } + + onUnmounted(() => { + for (const item of items.value) { + URL.revokeObjectURL(item.thumbnail); + } + }); + + return { + items, + addFiles, + removeItem, + abortAll, + upload, + getMenu, + uploading: computed(() => items.value.some(item => item.uploading)), + readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)), + allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)), + events, + }; +} + diff --git a/packages/frontend/src/tips.ts b/packages/frontend/src/tips.ts index a6850d0406..7218f4c19a 100644 --- a/packages/frontend/src/tips.ts +++ b/packages/frontend/src/tips.ts @@ -8,6 +8,7 @@ import { store } from '@/store.js'; export const TIPS = [ 'drive', 'uploader', + 'postFormUploader', 'clips', 'userLists', 'tl.home', -- cgit v1.2.3-freya From 4906f1f45c4724526d113af46fb9b35ba1376b15 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 7 Jun 2025 08:07:23 +0900 Subject: 🎨 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/components/MkPostForm.vue | 1 + packages/frontend/src/composables/use-uploader.ts | 1 + packages/frontend/src/pages/timeline.vue | 4 ++++ 3 files changed, 6 insertions(+) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 46893a0752..e319c9bacb 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -593,6 +593,7 @@ function showOtherSettings() { }, }, { type: 'divider' }, { type: 'switch', + icon: 'ti ti-eye', text: i18n.ts.preview, ref: showPreview, }, { diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts index 0dbc3052df..ab6db4568e 100644 --- a/packages/frontend/src/composables/use-uploader.ts +++ b/packages/frontend/src/composables/use-uploader.ts @@ -174,6 +174,7 @@ export function useUploader(options: { }, { type: 'switch', text: i18n.ts.sensitive, + icon: 'ti ti-eye-exclamation', ref: computed({ get: () => item.isSensitive ?? false, set: (value) => item.isSensitive = value, diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 5696d1dd89..b783f7ee0b 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -226,6 +226,7 @@ const headerActions = computed(() => { menuItems.push({ type: 'switch', + icon: 'ti ti-repeat', text: i18n.ts.showRenotes, ref: withRenotes, }); @@ -233,6 +234,7 @@ const headerActions = computed(() => { if (isBasicTimeline(src.value) && hasWithReplies(src.value)) { menuItems.push({ type: 'switch', + icon: 'ti ti-messages', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, disabled: onlyFiles, @@ -241,10 +243,12 @@ const headerActions = computed(() => { menuItems.push({ type: 'switch', + icon: 'ti ti-eye-exclamation', text: i18n.ts.withSensitive, ref: withSensitive, }, { type: 'switch', + icon: 'ti ti-photo', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false, -- cgit v1.2.3-freya From 9a3219f12ef95f4b99fba9d734f8330f5b32a7a1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:51:45 +0900 Subject: fix(frontend): Plugin:register_note_view_interruptor()によるノートの書き換えが機能しない問題を修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #16180 --- CHANGELOG.md | 1 + packages/frontend/src/components/MkNote.vue | 18 ++++++++---------- packages/frontend/src/components/MkNoteDetailed.vue | 18 ++++++++---------- 3 files changed, 17 insertions(+), 20 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index 446f74007f..dd2534adeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正 - Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正 - Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正 +- Fix: Plugin:register_note_view_interruptor()によるノートの書き換えが機能しない問題を修正 ### Server - Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 4a78d00665..040c2acdc2 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -268,17 +268,15 @@ let note = deepClone(props.note); // plugin const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); if (noteViewInterruptors.length > 0) { - onMounted(async () => { - let result: Misskey.entities.Note | null = deepClone(note); - for (const interruptor of noteViewInterruptors) { - try { - result = await interruptor.handler(result!) as Misskey.entities.Note | null; - } catch (err) { - console.error(err); - } + let result: Misskey.entities.Note | null = deepClone(note); + for (const interruptor of noteViewInterruptors) { + try { + result = await interruptor.handler(result!) as Misskey.entities.Note | null; + } catch (err) { + console.error(err); } - note = result as Misskey.entities.Note; - }); + } + note = result as Misskey.entities.Note; } const isRenote = Misskey.note.isPureRenote(note); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index e090901875..7a2090d171 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -289,17 +289,15 @@ let note = deepClone(props.note); // plugin const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); if (noteViewInterruptors.length > 0) { - onMounted(async () => { - let result: Misskey.entities.Note | null = deepClone(note); - for (const interruptor of noteViewInterruptors) { - try { - result = await interruptor.handler(result!) as Misskey.entities.Note | null; - } catch (err) { - console.error(err); - } + let result: Misskey.entities.Note | null = deepClone(note); + for (const interruptor of noteViewInterruptors) { + try { + result = await interruptor.handler(result!) as Misskey.entities.Note | null; + } catch (err) { + console.error(err); } - note = result as Misskey.entities.Note; - }); + } + note = result as Misskey.entities.Note; } const isRenote = Misskey.note.isPureRenote(note); -- cgit v1.2.3-freya From 63e8935c860c621ec25c944269f52683949ef182 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 11 Jun 2025 12:42:49 +0900 Subject: fix(frontend): disable note_view_interruptor temporary to prevent rendering glitch --- CHANGELOG.md | 3 +++ packages/frontend/src/components/MkNote.vue | 28 ++++++++++++---------- .../frontend/src/components/MkNoteDetailed.vue | 27 +++++++++++---------- 3 files changed, 32 insertions(+), 26 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2534adeb..86d1a71293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 2025.6.1 +### Note +- Misskey Webプラグインのnote_view_interruptorは不具合の影響により現在一時的に無効化されています。 + ### General - diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 040c2acdc2..3ca7455f0d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -265,19 +265,21 @@ const currentClip = inject | null>('currentClip', nul let note = deepClone(props.note); -// plugin -const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); -if (noteViewInterruptors.length > 0) { - let result: Misskey.entities.Note | null = deepClone(note); - for (const interruptor of noteViewInterruptors) { - try { - result = await interruptor.handler(result!) as Misskey.entities.Note | null; - } catch (err) { - console.error(err); - } - } - note = result as Misskey.entities.Note; -} +// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある +// https://github.com/aiscript-dev/aiscript/issues/937 +//// plugin +//const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); +//if (noteViewInterruptors.length > 0) { +// let result: Misskey.entities.Note | null = deepClone(note); +// for (const interruptor of noteViewInterruptors) { +// try { +// result = await interruptor.handler(result!) as Misskey.entities.Note | null; +// } catch (err) { +// console.error(err); +// } +// } +// note = result as Misskey.entities.Note; +//} const isRenote = Misskey.note.isPureRenote(note); const appearNote = getAppearNote(note); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 7a2090d171..cc26b0d0dc 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -286,19 +286,20 @@ const inChannel = inject('inChannel', null); let note = deepClone(props.note); -// plugin -const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); -if (noteViewInterruptors.length > 0) { - let result: Misskey.entities.Note | null = deepClone(note); - for (const interruptor of noteViewInterruptors) { - try { - result = await interruptor.handler(result!) as Misskey.entities.Note | null; - } catch (err) { - console.error(err); - } - } - note = result as Misskey.entities.Note; -} +// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある +//// plugin +//const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); +//if (noteViewInterruptors.length > 0) { +// let result: Misskey.entities.Note | null = deepClone(note); +// for (const interruptor of noteViewInterruptors) { +// try { +// result = await interruptor.handler(result!) as Misskey.entities.Note | null; +// } catch (err) { +// console.error(err); +// } +// } +// note = result as Misskey.entities.Note; +//} const isRenote = Misskey.note.isPureRenote(note); const appearNote = getAppearNote(note); -- cgit v1.2.3-freya From bc07b79a234a021ca2d1e3ea6186501c74a89493 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:36:42 +0900 Subject: fix(frontend): デッキのタイムラインカラムで新着ノート時のサウンドが再生されない問題を修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #16164 --- CHANGELOG.md | 1 + packages/frontend/src/components/MkStreamingNotesTimeline.vue | 10 +++++++++- packages/frontend/src/ui/deck/tl-column.vue | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eb197ca64..6f7c7e6ab9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正 - Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正 - Fix: ウェルカムタイムラインでリアクションが表示されない問題を修正 +- Fix: デッキのタイムラインカラムで新着ノート時のサウンドが再生されない問題を修正 ### Server - Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all) diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue index 576a0cf8cc..db9621e378 100644 --- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue +++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue @@ -62,6 +62,7 @@ import { useInterval } from '@@/js/use-interval.js'; import { getScrollContainer, scrollToTop } from '@@/js/scroll.js'; import type { BasicTimelineType } from '@/timelines.js'; import type { PagingCtx } from '@/composables/use-pagination.js'; +import type { SoundStore } from '@/preferences/def.js'; import { usePagination } from '@/composables/use-pagination.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; @@ -83,6 +84,7 @@ const props = withDefaults(defineProps<{ channel?: string; role?: string; sound?: boolean; + customSound?: SoundStore | null; withRenotes?: boolean; withReplies?: boolean; withSensitive?: boolean; @@ -92,6 +94,8 @@ const props = withDefaults(defineProps<{ withReplies: false, withSensitive: true, onlyFiles: false, + sound: false, + customSound: null, }); provide('inTimeline', true); @@ -190,7 +194,11 @@ function prepend(note: Misskey.entities.Note) { } if (props.sound) { - sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note'); + if (props.customSound) { + sound.playMisskeySfxFile(props.customSound); + } else { + sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note'); + } } } diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 97208f1c6a..37814f0914 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -26,6 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only :withReplies="withReplies" :withSensitive="withSensitive" :onlyFiles="onlyFiles" + :sound="true" + :customSound="soundSetting" /> -- cgit v1.2.3-freya From 32d721abf1929aa6b7f442c7ac6b695850717a8c Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:08:14 +0900 Subject: refactor(frontend): checkWordMuteの返り値が誤っている問題を修正 (#16188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(frontend): checkWordMuteの返り値が誤っている問題を修正 * fix lint --- packages/frontend/src/components/MkNote.vue | 24 ++++++++++++++-------- .../src/components/MkStreamingNotesTimeline.vue | 2 +- packages/frontend/src/utility/get-note-menu.ts | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 3ca7455f0d..794a091f30 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -321,20 +321,27 @@ const pleaseLoginContext = computed(() => ({ url: `https://${host}/notes/${appearNote.id}`, })); -/* Overload FunctionにLintが対応していないのでコメントアウト +/* eslint-disable no-redeclare */ +/** checkOnlyでは純粋なワードミュート結果をbooleanで返却する */ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly: true): boolean; -function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly: false): Array | false | 'sensitiveMute'; -*/ -function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly = false): Array | false | 'sensitiveMute' { +function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly?: false): Array | false | 'sensitiveMute'; + +function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly = false): Array | boolean | 'sensitiveMute' { if (mutedWords != null) { const result = checkWordMute(noteToCheck, $i, mutedWords); - if (Array.isArray(result)) return result; + if (Array.isArray(result)) { + return checkOnly ? (result.length > 0) : result; + } const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords); - if (Array.isArray(replyResult)) return replyResult; + if (Array.isArray(replyResult)) { + return checkOnly ? (replyResult.length > 0) : replyResult; + } const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords); - if (Array.isArray(renoteResult)) return renoteResult; + if (Array.isArray(renoteResult)) { + return checkOnly ? (renoteResult.length > 0) : renoteResult; + } } if (checkOnly) return false; @@ -345,6 +352,7 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array { @@ -417,7 +425,7 @@ if (!props.mock) { const users = renotes.map(x => x.user); - if (users.length < 1) return; + if (users.length < 1 || renoteButton.value == null) return; const { dispose } = os.popup(MkUsersTooltip, { showing, diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue index db9621e378..7e72840b7b 100644 --- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue +++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue @@ -428,7 +428,7 @@ defineExpose({ background: var(--MI_THEME-panel); } -.note { +.note:not(:empty) { border-bottom: solid 0.5px var(--MI_THEME-divider); } diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts index 5b99be5fdb..ea93444f08 100644 --- a/packages/frontend/src/utility/get-note-menu.ts +++ b/packages/frontend/src/utility/get-note-menu.ts @@ -542,7 +542,7 @@ function smallerVisibility(a: Visibility, b: Visibility): Visibility { export function getRenoteMenu(props: { note: Misskey.entities.Note; - renoteButton: ShallowRef; + renoteButton: ShallowRef; mock?: boolean; }) { const appearNote = getAppearNote(props.note); -- cgit v1.2.3-freya From 3dbfd80d65262bec24002c50cdfb2c1b962efcf5 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 15 Jun 2025 09:25:57 +0900 Subject: enhance(frontend/image-effector): tweak colorAdjust fx --- .../src/components/MkImageEffectorDialog.Layer.vue | 11 ++--- .../src/utility/image-effector/ImageEffector.ts | 2 + .../src/utility/image-effector/fxs/colorAdjust.ts | 49 ++++++++++++---------- 3 files changed, 35 insertions(+), 27 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue b/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue index ff3b9aff9b..d8466fa7ca 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.Layer.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-if="v.type === 'boolean'" v-model="layer.params[k]" > - + - + - +
@@ -55,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only :max="10000" :step="1" > - +
- + diff --git a/packages/frontend/src/utility/image-effector/ImageEffector.ts b/packages/frontend/src/utility/image-effector/ImageEffector.ts index 85dc5d5266..1028c57f35 100644 --- a/packages/frontend/src/utility/image-effector/ImageEffector.ts +++ b/packages/frontend/src/utility/image-effector/ImageEffector.ts @@ -19,6 +19,8 @@ type ParamTypeToPrimitive = { type ImageEffectorFxParamDefs = Record string; }>; export function defineImageEffectorFx(fx: ImageEffectorFx) { diff --git a/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts index cbb874852d..c38490e198 100644 --- a/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts +++ b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts @@ -72,7 +72,7 @@ void main() { vec3 color = in_color.rgb; color = color * u_brightness; - color += vec3(clamp(u_lightness, 0.0, 2.0) - 1.0); + color += vec3(u_lightness); color = (color - 0.5) * u_contrast + 0.5; vec3 hsl = rgb2hsl(color); @@ -92,45 +92,50 @@ export const FX_colorAdjust = defineImageEffectorFx({ params: { lightness: { type: 'number' as const, - default: 100, - min: 0, - max: 200, - step: 1, + default: 0, + min: -1, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', }, contrast: { type: 'number' as const, - default: 100, + default: 1, min: 0, - max: 200, - step: 1, + max: 4, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', }, hue: { type: 'number' as const, default: 0, - min: -360, - max: 360, - step: 1, + min: -1, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 180) + '°', }, brightness: { type: 'number' as const, - default: 100, + default: 1, min: 0, - max: 200, - step: 1, + max: 4, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', }, saturation: { type: 'number' as const, - default: 100, + default: 1, min: 0, - max: 200, - step: 1, + max: 4, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', }, }, main: ({ gl, u, params }) => { - gl.uniform1f(u.brightness, params.brightness / 100); - gl.uniform1f(u.contrast, params.contrast / 100); - gl.uniform1f(u.hue, params.hue / 360); - gl.uniform1f(u.lightness, params.lightness / 100); - gl.uniform1f(u.saturation, params.saturation / 100); + gl.uniform1f(u.brightness, params.brightness); + gl.uniform1f(u.contrast, params.contrast); + gl.uniform1f(u.hue, params.hue / 2); + gl.uniform1f(u.lightness, params.lightness); + gl.uniform1f(u.saturation, params.saturation); }, }); -- cgit v1.2.3-freya From fe805fb7f0a05ea201fafb5e7926cded33d53b31 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 15 Jun 2025 11:06:46 +0900 Subject: enhance(frontend/image-effector): tweak fxs --- locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + .../src/components/MkImageEffectorDialog.vue | 2 +- .../frontend/src/utility/image-effector/fxs.ts | 8 +- .../src/utility/image-effector/fxs/blockNoise.ts | 2 +- .../src/utility/image-effector/fxs/glitch.ts | 99 ---------------------- .../src/utility/image-effector/fxs/tearing.ts | 99 ++++++++++++++++++++++ 7 files changed, 109 insertions(+), 106 deletions(-) delete mode 100644 packages/frontend/src/utility/image-effector/fxs/glitch.ts create mode 100644 packages/frontend/src/utility/image-effector/fxs/tearing.ts (limited to 'packages/frontend/src/components') diff --git a/locales/index.d.ts b/locales/index.d.ts index 9bf4f95448..16003570a2 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -12224,6 +12224,10 @@ export interface Locale extends ILocale { * ブロックノイズ */ "blockNoise": string; + /** + * ティアリング + */ + "tearing": string; }; }; } diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index bc2c5ab51b..6ef92e0f2e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3274,3 +3274,4 @@ _imageEffector: polkadot: "ポルカドット" checker: "チェッカー" blockNoise: "ブロックノイズ" + tearing: "ティアリング" diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue index 42502ba449..2c6185fd33 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -96,7 +96,7 @@ watch(layers, async () => { }, { deep: true }); function addEffect(ev: MouseEvent) { - os.popupMenu(FXS.filter(fx => fx.id !== 'watermarkPlacement').map((fx) => ({ + os.popupMenu(FXS.map((fx) => ({ text: fx.name, action: () => { layers.push({ diff --git a/packages/frontend/src/utility/image-effector/fxs.ts b/packages/frontend/src/utility/image-effector/fxs.ts index 003b56efc4..1fa48aea15 100644 --- a/packages/frontend/src/utility/image-effector/fxs.ts +++ b/packages/frontend/src/utility/image-effector/fxs.ts @@ -10,21 +10,17 @@ import { FX_colorClamp } from './fxs/colorClamp.js'; import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js'; import { FX_distort } from './fxs/distort.js'; import { FX_polkadot } from './fxs/polkadot.js'; -import { FX_glitch } from './fxs/glitch.js'; +import { FX_tearing } from './fxs/tearing.js'; import { FX_grayscale } from './fxs/grayscale.js'; import { FX_invert } from './fxs/invert.js'; import { FX_mirror } from './fxs/mirror.js'; import { FX_stripe } from './fxs/stripe.js'; import { FX_threshold } from './fxs/threshold.js'; -import { FX_watermarkPlacement } from './fxs/watermarkPlacement.js'; import { FX_zoomLines } from './fxs/zoomLines.js'; import { FX_blockNoise } from './fxs/blockNoise.js'; import type { ImageEffectorFx } from './ImageEffector.js'; export const FXS = [ - FX_watermarkPlacement, - FX_chromaticAberration, - FX_glitch, FX_mirror, FX_invert, FX_grayscale, @@ -37,5 +33,7 @@ export const FXS = [ FX_stripe, FX_polkadot, FX_checker, + FX_chromaticAberration, + FX_tearing, FX_blockNoise, ] as const satisfies ImageEffectorFx[]; diff --git a/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts b/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts index 66ebbabc0c..bf7eaa8bda 100644 --- a/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts +++ b/packages/frontend/src/utility/image-effector/fxs/blockNoise.ts @@ -49,7 +49,7 @@ void main() { export const FX_blockNoise = defineImageEffectorFx({ id: 'blockNoise' as const, - name: i18n.ts._imageEffector._fxs.blockNoise, + name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.blockNoise, shader, uniforms: ['amount', 'channelShift'] as const, params: { diff --git a/packages/frontend/src/utility/image-effector/fxs/glitch.ts b/packages/frontend/src/utility/image-effector/fxs/glitch.ts deleted file mode 100644 index 702b476a0b..0000000000 --- a/packages/frontend/src/utility/image-effector/fxs/glitch.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import seedrandom from 'seedrandom'; -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_glitch = defineImageEffectorFx({ - id: 'glitch' as const, - name: i18n.ts._imageEffector._fxs.glitch, - shader, - uniforms: ['amount', 'channelShift'] as const, - params: { - amount: { - type: 'number' as const, - default: 3, - min: 1, - max: 100, - step: 1, - }, - strength: { - type: 'number' as const, - default: 0.05, - min: -1, - max: 1, - step: 0.01, - toViewValue: v => Math.round(v * 100) + '%', - }, - size: { - type: 'number' as const, - default: 0.2, - min: 0, - max: 1, - step: 0.01, - toViewValue: v => Math.round(v * 100) + '%', - }, - channelShift: { - type: 'number' as const, - default: 0.5, - min: 0, - max: 10, - step: 0.01, - toViewValue: v => Math.round(v * 100) + '%', - }, - seed: { - type: 'seed' as const, - default: 100, - }, - }, - main: ({ gl, program, u, params }) => { - gl.uniform1i(u.amount, params.amount); - gl.uniform1f(u.channelShift, params.channelShift); - - const rnd = seedrandom(params.seed.toString()); - - for (let i = 0; i < params.amount; i++) { - const o = gl.getUniformLocation(program, `u_shiftOrigins[${i.toString()}]`); - gl.uniform1f(o, rnd()); - - const s = gl.getUniformLocation(program, `u_shiftStrengths[${i.toString()}]`); - gl.uniform1f(s, (1 - (rnd() * 2)) * params.strength); - - const h = gl.getUniformLocation(program, `u_shiftHeights[${i.toString()}]`); - gl.uniform1f(h, rnd() * params.size); - } - }, -}); diff --git a/packages/frontend/src/utility/image-effector/fxs/tearing.ts b/packages/frontend/src/utility/image-effector/fxs/tearing.ts new file mode 100644 index 0000000000..d5f1e062ec --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/tearing.ts @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import seedrandom from 'seedrandom'; +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' as const, + name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.tearing, + shader, + uniforms: ['amount', 'channelShift'] as const, + params: { + amount: { + type: 'number' as const, + default: 3, + min: 1, + max: 100, + step: 1, + }, + strength: { + type: 'number' as const, + default: 0.05, + min: -1, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + size: { + type: 'number' as const, + default: 0.2, + min: 0, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + channelShift: { + type: 'number' as const, + default: 0.5, + min: 0, + max: 10, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + seed: { + type: 'seed' as const, + default: 100, + }, + }, + main: ({ gl, program, u, params }) => { + gl.uniform1i(u.amount, params.amount); + gl.uniform1f(u.channelShift, params.channelShift); + + const rnd = seedrandom(params.seed.toString()); + + for (let i = 0; i < params.amount; i++) { + const o = gl.getUniformLocation(program, `u_shiftOrigins[${i.toString()}]`); + gl.uniform1f(o, rnd()); + + const s = gl.getUniformLocation(program, `u_shiftStrengths[${i.toString()}]`); + gl.uniform1f(s, (1 - (rnd() * 2)) * params.strength); + + const h = gl.getUniformLocation(program, `u_shiftHeights[${i.toString()}]`); + gl.uniform1f(h, rnd() * params.size); + } + }, +}); -- cgit v1.2.3-freya