From c548ec9906947c72743e611254a6557e8e8d057c Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:01:44 +0900 Subject: refactor(frontend): verbatimModuleSyntaxを有効化 (#15323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * wip * revert unnecessary changes * wip * refactor(frontend): enforce verbatimModuleSyntax * fix * refactor(frontend-shared): enforce verbatimModuleSyntax * wip * refactor(frontend-embed): enforce verbatimModuleSyntax * enforce consistent-type-imports * fix lint config * attemt to fix ci * fix lint * fix * fix * fix --- .../components/MkAbuseReportWindow.stories.impl.ts | 2 +- .../src/components/MkAccountMoved.stories.impl.ts | 2 +- .../src/components/MkAchievements.stories.impl.ts | 2 +- .../src/components/MkAnalogClock.stories.impl.ts | 2 +- .../MkAnnouncementDialog.stories.impl.ts | 2 +- .../src/components/MkAntennaEditor.stories.impl.ts | 2 +- .../MkAntennaEditorDialog.stories.impl.ts | 2 +- packages/frontend/src/components/MkAsUi.vue | 5 +- .../src/components/MkAutocomplete.stories.impl.ts | 2 +- .../frontend/src/components/MkAutocomplete.vue | 3 +- .../src/components/MkAvatars.stories.impl.ts | 2 +- .../src/components/MkButton.stories.impl.ts | 2 +- .../MkChannelFollowButton.stories.impl.ts | 2 +- .../src/components/MkChannelList.stories.impl.ts | 2 +- packages/frontend/src/components/MkChannelList.vue | 3 +- .../components/MkChannelPreview.stories.impl.ts | 2 +- .../src/components/MkChart.stories.impl.ts | 2 +- packages/frontend/src/components/MkChartLegend.vue | 3 +- .../src/components/MkClickerGame.stories.impl.ts | 2 +- .../src/components/MkClipPreview.stories.impl.ts | 2 +- .../frontend/src/components/MkCode.stories.impl.ts | 2 +- .../src/components/MkCodeEditor.stories.impl.ts | 2 +- .../src/components/MkCodeInline.stories.impl.ts | 2 +- .../src/components/MkColorInput.stories.impl.ts | 2 +- .../src/components/MkContextMenu.stories.impl.ts | 2 +- .../src/components/MkCropperDialog.stories.impl.ts | 2 +- .../MkCustomEmojiDetailedDialog.stories.impl.ts | 2 +- .../src/components/MkCwButton.stories.impl.ts | 2 +- .../src/components/MkDateSeparatedList.vue | 5 +- .../src/components/MkDialog.stories.impl.ts | 2 +- .../src/components/MkDigitalClock.stories.impl.ts | 2 +- .../src/components/MkDonation.stories.impl.ts | 2 +- .../src/components/MkDrive.file.stories.impl.ts | 2 +- .../src/components/MkDrive.folder.stories.impl.ts | 2 +- .../src/components/MkDrive.stories.impl.ts | 2 +- .../MkDriveFileThumbnail.stories.impl.ts | 2 +- .../src/components/MkEmojiPicker.section.vue | 6 +- .../src/components/MkEmojiPicker.stories.impl.ts | 2 +- packages/frontend/src/components/MkEmojiPicker.vue | 6 +- .../MkExtensionInstaller.stories.impl.ts | 2 +- .../src/components/MkFlashPreview.stories.impl.ts | 2 +- .../MkGalleryPostPreview.stories.impl.ts | 2 +- packages/frontend/src/components/MkInput.vue | 6 +- .../components/MkInstanceCardMini.stories.impl.ts | 2 +- .../frontend/src/components/MkInstanceStats.vue | 3 +- .../frontend/src/components/MkInstanceTicker.vue | 3 +- .../src/components/MkInviteCode.stories.impl.ts | 2 +- packages/frontend/src/components/MkLink.vue | 2 +- packages/frontend/src/components/MkMediaAudio.vue | 2 +- packages/frontend/src/components/MkMediaRange.vue | 5 +- packages/frontend/src/components/MkMediaVideo.vue | 2 +- packages/frontend/src/components/MkMention.vue | 2 +- packages/frontend/src/components/MkMenu.vue | 4 +- packages/frontend/src/components/MkModal.vue | 2 +- packages/frontend/src/components/MkNote.vue | 8 ++- .../frontend/src/components/MkNoteDetailed.vue | 8 ++- .../frontend/src/components/MkNoteMediaGrid.vue | 70 +++++++++++----------- packages/frontend/src/components/MkNotes.vue | 3 +- .../src/components/MkNotificationSelectWindow.vue | 3 +- .../frontend/src/components/MkNotifications.vue | 2 +- packages/frontend/src/components/MkPageWindow.vue | 3 +- packages/frontend/src/components/MkPagination.vue | 5 +- packages/frontend/src/components/MkPostForm.vue | 6 +- packages/frontend/src/components/MkRadios.vue | 3 +- .../components/MkRoleSelectDialog.stories.impl.ts | 2 +- packages/frontend/src/components/MkSelect.vue | 3 +- packages/frontend/src/components/MkSignin.vue | 3 +- .../src/components/MkSignupDialog.form.vue | 3 +- .../MkSignupDialog.rules.stories.impl.ts | 2 +- .../frontend/src/components/MkSortOrderEditor.vue | 4 +- packages/frontend/src/components/MkSuperMenu.vue | 2 +- .../frontend/src/components/MkSwitch.button.vue | 3 +- packages/frontend/src/components/MkSwitch.vue | 3 +- .../src/components/MkSystemWebhookEditor.vue | 2 +- .../src/components/MkTagItem.stories.impl.ts | 2 +- packages/frontend/src/components/MkTextarea.vue | 3 +- packages/frontend/src/components/MkTimeline.vue | 2 +- packages/frontend/src/components/MkUserList.vue | 3 +- .../MkUserSetupDialog.Follow.stories.impl.ts | 2 +- .../src/components/MkUserSetupDialog.Follow.vue | 3 +- .../MkUserSetupDialog.Privacy.stories.impl.ts | 2 +- .../MkUserSetupDialog.Profile.stories.impl.ts | 2 +- .../MkUserSetupDialog.User.stories.impl.ts | 2 +- .../components/MkUserSetupDialog.stories.impl.ts | 2 +- .../src/components/global/MkA.stories.impl.ts | 2 +- .../src/components/global/MkAcct.stories.impl.ts | 2 +- .../src/components/global/MkAd.stories.impl.ts | 2 +- .../src/components/global/MkAvatar.stories.impl.ts | 2 +- .../global/MkCondensedLine.stories.impl.ts | 2 +- .../global/MkCustomEmoji.stories.impl.ts | 2 +- .../components/global/MkEllipsis.stories.impl.ts | 2 +- .../src/components/global/MkEmoji.stories.impl.ts | 2 +- .../src/components/global/MkError.stories.impl.ts | 2 +- .../src/components/global/MkError.stories.meta.ts | 2 +- .../components/global/MkLoading.stories.impl.ts | 2 +- .../src/components/global/MkMfm.stories.impl.ts | 4 +- packages/frontend/src/components/global/MkMfm.ts | 6 +- .../components/global/MkPageHeader.stories.impl.ts | 2 +- .../src/components/global/MkPageHeader.vue | 3 +- .../src/components/global/MkStickyContainer.vue | 3 +- .../src/components/global/MkTime.stories.impl.ts | 2 +- .../src/components/global/MkUrl.stories.impl.ts | 2 +- packages/frontend/src/components/global/MkUrl.vue | 2 +- .../components/global/MkUserName.stories.impl.ts | 2 +- .../frontend/src/components/global/RouterView.vue | 2 +- .../frontend/src/components/grid/MkDataCell.vue | 7 ++- .../frontend/src/components/grid/MkDataRow.vue | 7 ++- .../src/components/grid/MkGrid.stories.impl.ts | 8 +-- packages/frontend/src/components/grid/MkGrid.vue | 17 ++++-- .../frontend/src/components/grid/MkHeaderCell.vue | 5 +- .../frontend/src/components/grid/MkHeaderRow.vue | 7 ++- .../frontend/src/components/grid/MkNumberCell.vue | 3 +- .../src/components/grid/cell-validators.ts | 6 +- packages/frontend/src/components/grid/cell.ts | 12 ++-- packages/frontend/src/components/grid/column.ts | 12 ++-- .../frontend/src/components/grid/grid-event.ts | 10 ++-- .../frontend/src/components/grid/grid-utils.ts | 14 +++-- packages/frontend/src/components/grid/grid.ts | 6 +- packages/frontend/src/components/grid/row.ts | 10 ++-- packages/frontend/src/components/index.ts | 2 +- 120 files changed, 259 insertions(+), 214 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts index 9df957f3ec..b62096bbe9 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts +++ b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { userDetailed } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; diff --git a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts index cad26de6e2..b907b5b25a 100644 --- a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts +++ b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { commonHandlers } from '../../.storybook/mocks.js'; import { userDetailed } from '../../.storybook/fakes.js'; diff --git a/packages/frontend/src/components/MkAchievements.stories.impl.ts b/packages/frontend/src/components/MkAchievements.stories.impl.ts index 7614da51da..bbd3f69d7c 100644 --- a/packages/frontend/src/components/MkAchievements.stories.impl.ts +++ b/packages/frontend/src/components/MkAchievements.stories.impl.ts @@ -4,7 +4,7 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { userDetailed } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts index 270ca40825..a01d91ad20 100644 --- a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts +++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts @@ -4,7 +4,7 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import isChromatic from 'chromatic/isChromatic'; import MkAnalogClock from './MkAnalogClock.vue'; export const Default = { diff --git a/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts b/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts index bf3ddb935b..627cb0c4ff 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkAnnouncementDialog from './MkAnnouncementDialog.vue'; diff --git a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts index 1749e07a4e..4d921a4c48 100644 --- a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts +++ b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkAntennaEditor from './MkAntennaEditor.vue'; diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts index 1c6ca83b47..5878b52fb9 100644 --- a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue'; diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 365b767bd6..5c4d887e0c 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -63,14 +63,15 @@ SPDX-License-Identifier: AGPL-3.0-only + diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 1336b61356..33fe82b759 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -22,20 +22,24 @@ SPDX-License-Identifier: AGPL-3.0-only -

{{ 16 - props.modelValue.length }}/16

+

+ {{ 16 - props.modelValue.length }}/16 +

diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index e6b4a0b222..0791c1343b 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -63,6 +63,7 @@ import { dateString } from '@/filters/date.js'; import MkClipPreview from '@/components/MkClipPreview.vue'; import { defaultStore } from '@/store.js'; import { pleaseLogin } from '@/scripts/please-login.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; import { serverContext, assertServerContext } from '@/server-context.js'; import { $i } from '@/account.js'; @@ -132,10 +133,11 @@ function fetchNote() { noteId: props.noteId, }).then(res => { note.value = res; + const appearNote = getAppearNote(res); // 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く - if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) { + if ((appearNote.clippedCount ?? 0) > 0 || new Date(appearNote.createdAt).getTime() < new Date('2023-10-01').getTime()) { misskeyApi('notes/clips', { - noteId: note.value.id, + noteId: appearNote.id, }).then((_clips) => { clips.value = _clips; }); @@ -170,7 +172,7 @@ definePageMetadata(() => ({ avatar: note.value.user, path: `/notes/${note.value.id}`, share: { - title: i18n.tsx.noteOf({ user: note.value.user.name }), + title: i18n.tsx.noteOf({ user: note.value.user.name ?? note.value.user.username }), text: note.value.text, }, } : {}, -- cgit v1.2.3-freya From ec8381522789a7fc4329a627770d80a0cd7774a4 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:32:39 +0900 Subject: fix(frontend): MkSelectの初期値が表示されない場合がある (#15559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/frontend/src/components/MkSelect.vue | 130 +++++++++++++++++++------ packages/frontend/src/pages/settings/theme.vue | 83 +++++++++++++--- 3 files changed, 168 insertions(+), 46 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0461fc8322..426727104d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Fix: カスタム絵文字管理画面(beta)にてisSensitive/localOnlyの絞り込みが上手くいかない問題の修正 ( #15445 ) - Fix: ユーザのサジェスト中に@を入力してもサジェスト結果が消えないように `#14385` - Fix: CWの注釈が100文字を超えている場合、ノート投稿ボタンを非アクティブに +- Fix: テーマ選択で現在のテーマが初期表示されていない問題を修正 - 翻訳の更新 ### Server diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 83a79d0c9f..15717802ec 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -41,11 +41,28 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/locales/index.d.ts b/locales/index.d.ts index 947b577792..6810d204cb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4971,7 +4971,7 @@ export interface Locale extends ILocale { */ "disableStreamingTimeline": string; /** - * 通知をグルーピングして表示する + * 通知をグルーピング */ "useGroupedNotifications": string; /** @@ -5270,6 +5270,14 @@ export interface Locale extends ILocale { * このメディアのセンシティブ指定を解除しますか? */ "unmarkAsSensitiveConfirm": string; + /** + * 環境設定 + */ + "preferences": string; + /** + * アクセシビリティ + */ + "accessibility": string; "_accountSettings": { /** * コンテンツの表示にログインを必須にする diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index fbe4d98896..7a5d2f795e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1238,7 +1238,7 @@ releaseToRefresh: "離してリロード" refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" -useGroupedNotifications: "通知をグルーピングして表示する" +useGroupedNotifications: "通知をグルーピング" signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" doReaction: "リアクションする" @@ -1313,6 +1313,8 @@ confirmOnReact: "リアクションする際に確認する" reactAreYouSure: "\" {emoji} \" をリアクションしますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?" +preferences: "環境設定" +accessibility: "アクセシビリティ" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index 9f318cf449..c1119c2523 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -39,6 +39,10 @@ const config = { if (~replacePluginForIsChromatic) { config.plugins?.splice(replacePluginForIsChromatic, 1); } + + //pluginsからcreateSearchIndexを削除、複数あるかもしれないので全て削除 + config.plugins = config.plugins?.filter((plugin: Plugin) => plugin && plugin.name !== 'createSearchIndex') ?? []; + return mergeConfig(config, { plugins: [ { diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts new file mode 100644 index 0000000000..509eb804cb --- /dev/null +++ b/packages/frontend/lib/vite-plugin-create-search-index.ts @@ -0,0 +1,1496 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { parse as vueSfcParse } from 'vue/compiler-sfc'; +import type { LogOptions, Plugin } from 'vite'; +import fs from 'node:fs'; +import { glob } from 'glob'; +import JSON5 from 'json5'; +import MagicString from 'magic-string'; +import path from 'node:path' +import { hash, toBase62 } from '../vite.config'; +import { createLogger } from 'vite'; + +interface VueAstNode { + type: number; + tag?: string; + loc?: { + start: { offset: number, line: number, column: number }, + end: { offset: number, line: number, column: number }, + source?: string + }; + props?: Array<{ + name: string; + type: number; + value?: { content?: string }; + arg?: { content?: string }; + exp?: { content?: string; loc?: any }; + }>; + children?: VueAstNode[]; + content?: any; + __markerId?: string; + __children?: string[]; +} + +export type AnalysisResult = { + filePath: string; + usage: SearchIndexItem[]; +} + +export type SearchIndexItem = { + id: string; + path?: string; + label: string; + keywords: string | string[]; + icon?: string; + inlining?: string[]; + children?: SearchIndexItem[]; +}; + +export type Options = { + targetFilePaths: string[], + exportFilePath: string, + verbose?: boolean, +}; + +// 関連するノードタイプの定数化 +const NODE_TYPES = { + ELEMENT: 1, + EXPRESSION: 2, + TEXT: 3, + INTERPOLATION: 5, // Mustache +}; + +// マーカー関係を表す型 +interface MarkerRelation { + parentId?: string; + markerId: string; + node: VueAstNode; +} + +// ロガー +let logger = { + info: (msg: string, options?: LogOptions) => { }, + warn: (msg: string, options?: LogOptions) => { }, + error: (msg: string, options?: LogOptions) => { }, +}; +let loggerInitialized = false; + +function initLogger(options: Options) { + if (loggerInitialized) return; + loggerInitialized = true; + const viteLogger = createLogger(options.verbose ? 'info' : 'warn'); + + logger.info = (msg, options) => { + msg = `[create-search-index] ${msg}`; + viteLogger.info(msg, options); + } + + logger.warn = (msg, options) => { + msg = `[create-search-index] ${msg}`; + viteLogger.warn(msg, options); + } + + logger.error = (msg, options) => { + msg = `[create-search-index] ${msg}`; + viteLogger.error(msg, options); + } +} + +/** + * 解析結果をTypeScriptファイルとして出力する + */ +function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { + logger.info(`Processing ${analysisResults.length} files for output`); + + // 新しいツリー構造を構築 + const allMarkers = new Map(); + + // 1. すべてのマーカーを一旦フラットに収集 + for (const file of analysisResults) { + logger.info(`Processing file: ${file.filePath} with ${file.usage.length} markers`); + + for (const marker of file.usage) { + if (marker.id) { + // キーワードとchildren処理を共通化 + const processedMarker = { + ...marker, + keywords: processMarkerProperty(marker.keywords, 'keywords'), + children: processMarkerProperty(marker.children || [], 'children') + }; + + allMarkers.set(marker.id, processedMarker); + } + } + } + + logger.info(`Collected total ${allMarkers.size} unique markers`); + + // 2. 子マーカーIDの収集 + const childIds = collectChildIds(allMarkers); + logger.info(`Found ${childIds.size} child markers`); + + // 3. ルートマーカーの特定(他の誰かの子でないマーカー) + const rootMarkers = identifyRootMarkers(allMarkers, childIds); + logger.info(`Found ${rootMarkers.length} root markers`); + + // 4. 子マーカーの参照を解決 + const resolvedRootMarkers = resolveChildReferences(rootMarkers, allMarkers); + + // 5. デバッグ情報を生成 + const { totalMarkers, totalChildren } = countMarkers(resolvedRootMarkers); + logger.info(`Total markers in tree: ${totalMarkers} (${resolvedRootMarkers.length} roots + ${totalChildren} nested children)`); + + // 6. 結果をTS形式で出力 + writeOutputFile(outputPath, resolvedRootMarkers); +} + +/** + * マーカーのプロパティ(keywordsやchildren)を処理する + */ +function processMarkerProperty(propValue: any, propType: 'keywords' | 'children'): any { + // 文字列の配列表現を解析 + if (typeof propValue === 'string' && propValue.startsWith('[') && propValue.endsWith(']')) { + try { + // JSON5解析を試みる + return JSON5.parse(propValue.replace(/'/g, '"')); + } catch (e) { + // 解析に失敗した場合 + logger.warn(`Could not parse ${propType}: ${propValue}, using ${propType === 'children' ? 'empty array' : 'as is'}`); + return propType === 'children' ? [] : propValue; + } + } + + return propValue; +} + +/** + * 全マーカーから子IDを収集する + */ +function collectChildIds(allMarkers: Map): Set { + const childIds = new Set(); + + allMarkers.forEach((marker, id) => { + // 通常のchildren処理 + const children = marker.children; + if (Array.isArray(children)) { + children.forEach(childId => { + if (typeof childId === 'string') { + if (!allMarkers.has(childId)) { + logger.warn(`Warning: Child marker ID ${childId} referenced but not found`); + } else { + childIds.add(childId); + } + } + }); + } + + // inlining処理を追加 + if (marker.inlining) { + let inliningIds: string[] = []; + + // 文字列の場合は配列に変換 + if (typeof marker.inlining === 'string') { + try { + const inliningStr = (marker.inlining as string).trim(); + if (inliningStr.startsWith('[') && inliningStr.endsWith(']')) { + inliningIds = JSON5.parse(inliningStr.replace(/'/g, '"')); + logger.info(`Parsed inlining string to array: ${inliningStr} -> ${JSON.stringify(inliningIds)}`); + } else { + inliningIds = [inliningStr]; + } + } catch (e) { + logger.error(`Failed to parse inlining string: ${marker.inlining}`, e); + } + } + // 既に配列の場合 + else if (Array.isArray(marker.inlining)) { + inliningIds = marker.inlining; + } + + // inliningで指定されたIDを子セットに追加 + for (const inlineId of inliningIds) { + if (typeof inlineId === 'string') { + if (!allMarkers.has(inlineId)) { + logger.warn(`Warning: Inlining marker ID ${inlineId} referenced but not found`); + } else { + // inliningで参照されているマーカーも子として扱う + childIds.add(inlineId); + logger.info(`Added inlined marker ${inlineId} as child in collectChildIds`); + } + } + } + } + }); + + return childIds; +} + +/** + * ルートマーカー(他の子でないマーカー)を特定する + */ +function identifyRootMarkers( + allMarkers: Map, + childIds: Set +): SearchIndexItem[] { + const rootMarkers: SearchIndexItem[] = []; + + allMarkers.forEach((marker, id) => { + if (!childIds.has(id)) { + rootMarkers.push(marker); + logger.info(`Added root marker to output: ${id} with label ${marker.label}`); + } + }); + + return rootMarkers; +} + +/** + * 子マーカーの参照をIDから実際のオブジェクトに解決する + */ +function resolveChildReferences( + rootMarkers: SearchIndexItem[], + allMarkers: Map +): SearchIndexItem[] { + function resolveChildrenForMarker(marker: SearchIndexItem): SearchIndexItem { + // マーカーのディープコピーを作成 + const resolvedMarker = { ...marker }; + // 明示的に子マーカー配列を作成 + const resolvedChildren: SearchIndexItem[] = []; + + // 通常のchildren処理 + if (Array.isArray(marker.children)) { + for (const childId of marker.children) { + if (typeof childId === 'string') { + const childMarker = allMarkers.get(childId); + if (childMarker) { + // 子マーカーの子も再帰的に解決 + const resolvedChild = resolveChildrenForMarker(childMarker); + resolvedChildren.push(resolvedChild); + logger.info(`Resolved regular child ${childId} for parent ${marker.id}`); + } + } + } + } + + // inlining属性の処理 + let inliningIds: string[] = []; + + // 文字列の場合は配列に変換。例: "['2fa']" -> ['2fa'] + if (typeof marker.inlining === 'string') { + try { + // 文字列形式の配列を実際の配列に変換 + const inliningStr = (marker.inlining as string).trim(); + if (inliningStr.startsWith('[') && inliningStr.endsWith(']')) { + inliningIds = JSON5.parse(inliningStr.replace(/'/g, '"')); + logger.info(`Converted string inlining to array: ${inliningStr} -> ${JSON.stringify(inliningIds)}`); + } else { + // 単一値の場合は配列に + inliningIds = [inliningStr]; + logger.info(`Converted single string inlining to array: ${inliningStr}`); + } + } catch (e) { + logger.error(`Failed to parse inlining string: ${marker.inlining}`, e); + } + } + // 既に配列の場合はそのまま使用 + else if (Array.isArray(marker.inlining)) { + inliningIds = marker.inlining; + } + + // インライン指定されたマーカーを子として追加 + for (const inlineId of inliningIds) { + if (typeof inlineId === 'string') { + const inlineMarker = allMarkers.get(inlineId); + if (inlineMarker) { + // インライン指定されたマーカーを再帰的に解決 + const resolvedInline = resolveChildrenForMarker(inlineMarker); + delete resolvedInline.path + resolvedChildren.push(resolvedInline); + logger.info(`Added inlined marker ${inlineId} as child to ${marker.id}`); + } else { + logger.warn(`Inlining target not found: ${inlineId} referenced by ${marker.id}`); + } + } + } + + // 解決した子が存在する場合のみchildrenプロパティを設定 + if (resolvedChildren.length > 0) { + resolvedMarker.children = resolvedChildren; + } else { + delete resolvedMarker.children; + } + + return resolvedMarker; + } + + // すべてのルートマーカーの子を解決 + return rootMarkers.map(marker => resolveChildrenForMarker(marker)); +} + +/** + * マーカー数を数える(デバッグ用) + */ +function countMarkers(markers: SearchIndexItem[]): { totalMarkers: number, totalChildren: number } { + let totalMarkers = markers.length; + let totalChildren = 0; + + function countNested(items: SearchIndexItem[]): void { + for (const marker of items) { + if (marker.children && Array.isArray(marker.children)) { + totalChildren += marker.children.length; + totalMarkers += marker.children.length; + countNested(marker.children as SearchIndexItem[]); + } + } + } + + countNested(markers); + return { totalMarkers, totalChildren }; +} + +/** + * 最終的なTypeScriptファイルを出力 + */ +function writeOutputFile(outputPath: string, resolvedRootMarkers: SearchIndexItem[]): void { + try { + const tsOutput = generateTypeScriptCode(resolvedRootMarkers); + fs.writeFileSync(outputPath, tsOutput, 'utf-8'); + // 強制的に出力させるためにViteロガーを使わない + console.log(`Successfully wrote search index to ${outputPath} with ${resolvedRootMarkers.length} root entries`); + } catch (error) { + logger.error('[create-search-index]: error writing output: ', error); + } +} + +/** + * TypeScriptコード生成 + */ +function generateTypeScriptCode(resolvedRootMarkers: SearchIndexItem[]): string { + return ` +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// This file was automatically generated by create-search-index. +// Do not edit this file. + +import { i18n } from '@/i18n.js'; + +export type SearchIndexItem = { + id: string; + path?: string; + label: string; + keywords: string[]; + icon?: string; + children?: SearchIndexItem[]; +}; + +export const searchIndexes: SearchIndexItem[] = ${customStringify(resolvedRootMarkers)} as const; + +export type SearchIndex = typeof searchIndexes; +`; +} + +/** + * オブジェクトを特殊な形式の文字列に変換する + * i18n参照を保持しつつ適切な形式に変換 + */ +function customStringify(obj: any, depth = 0): string { + const INDENT_STR = '\t'; + + // 配列の処理 + if (Array.isArray(obj)) { + if (obj.length === 0) return '[]'; + const indent = INDENT_STR.repeat(depth); + const childIndent = INDENT_STR.repeat(depth + 1); + + // 配列要素の処理 + const items = obj.map(item => { + // オブジェクト要素 + if (typeof item === 'object' && item !== null) { + return `${childIndent}${customStringify(item, depth + 1)}`; + } + + // i18n参照を含む文字列要素 + if (typeof item === 'string' && item.includes('i18n.ts.')) { + return `${childIndent}${item}`; // クォートなしでそのまま出力 + } + + // その他の要素 + return `${childIndent}${JSON5.stringify(item)}`; + }).join(',\n'); + + return `[\n${items},\n${indent}]`; + } + + // null または非オブジェクト + if (obj === null || typeof obj !== 'object') { + return JSON5.stringify(obj); + } + + // オブジェクトの処理 + const indent = INDENT_STR.repeat(depth); + const childIndent = INDENT_STR.repeat(depth + 1); + + const entries = Object.entries(obj) + // 不要なプロパティを除去 + .filter(([key, value]) => { + if (value === undefined) return false; + if (key === 'children' && Array.isArray(value) && value.length === 0) return false; + if (key === 'inlining') return false; + return true; + }) + // 各プロパティを変換 + .map(([key, value]) => { + // 子要素配列の特殊処理 + if (key === 'children' && Array.isArray(value) && value.length > 0) { + return `${childIndent}${key}: ${customStringify(value, depth + 1)}`; + } + + // ラベルやその他プロパティを処理 + return `${childIndent}${key}: ${formatSpecialProperty(key, value)}`; + }); + + if (entries.length === 0) return '{}'; + return `{\n${entries.join(',\n')},\n${indent}}`; +} + +/** + * 特殊プロパティの書式設定 + */ +function formatSpecialProperty(key: string, value: any): string { + // 値がundefinedの場合は空文字列を返す + if (value === undefined) { + return '""'; + } + + // childrenが配列の場合は特別に処理 + if (key === 'children' && Array.isArray(value)) { + return customStringify(value); + } + + // keywordsが配列の場合、特別に処理 + if (key === 'keywords' && Array.isArray(value)) { + return `[${formatArrayForOutput(value)}]`; + } + + // 文字列値の場合の特別処理 + if (typeof value === 'string') { + // i18n.ts 参照を含む場合 - クォートなしでそのまま出力 + if (isI18nReference(value)) { + logger.info(`Preserving i18n reference in output: ${value}`); + return value; + } + + // keywords が配列リテラルの形式の場合 + if (key === 'keywords' && value.startsWith('[') && value.endsWith(']')) { + return value; + } + } + + // 上記以外は通常の JSON5 文字列として返す + return JSON5.stringify(value); +} + +/** + * 配列式の文字列表現を生成 + */ +function formatArrayForOutput(items: any[]): string { + return items.map(item => { + // i18n.ts. 参照の文字列はそのままJavaScript式として出力 + if (typeof item === 'string' && isI18nReference(item)) { + logger.info(`Preserving i18n reference in array: ${item}`); + return item; // クォートなしでそのまま + } + + // その他の値はJSON5形式で文字列化 + return JSON5.stringify(item); + }).join(', '); +} + +/** + * 要素ノードからテキスト内容を抽出する + * 各抽出方法を分離して可読性を向上 + */ +function extractElementText(node: VueAstNode): string | null { + if (!node) return null; + + logger.info(`Extracting text from node type=${node.type}, tag=${node.tag || 'unknown'}`); + + // 1. 直接コンテンツの抽出を試行 + const directContent = extractDirectContent(node); + if (directContent) return directContent; + + // 子要素がない場合は終了 + if (!node.children || !Array.isArray(node.children)) { + return null; + } + + // 2. インターポレーションノードを検索 + const interpolationContent = extractInterpolationContent(node.children); + if (interpolationContent) return interpolationContent; + + // 3. 式ノードを検索 + const expressionContent = extractExpressionContent(node.children); + if (expressionContent) return expressionContent; + + // 4. テキストノードを検索 + const textContent = extractTextContent(node.children); + if (textContent) return textContent; + + // 5. 再帰的に子ノードを探索 + return extractNestedContent(node.children); +} +/** + * ノードから直接コンテンツを抽出 + */ +function extractDirectContent(node: VueAstNode): string | null { + if (!node.content) return null; + + const content = typeof node.content === 'string' + ? node.content.trim() + : (node.content.content ? node.content.content.trim() : null); + + if (!content) return null; + + logger.info(`Direct node content found: ${content}`); + + // Mustache構文のチェック + const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; + const mustacheMatch = content.match(mustachePattern); + + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + const extractedContent = mustacheMatch[1].trim(); + logger.info(`Extracted i18n reference from mustache: ${extractedContent}`); + return extractedContent; + } + + // 直接i18n参照を含む場合 + if (isI18nReference(content)) { + logger.info(`Direct i18n reference found: ${content}`); + return content; + } + + // その他のコンテンツ + return content; +} + +/** + * インターポレーションノード(Mustache)からコンテンツを抽出 + */ +function extractInterpolationContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.type === NODE_TYPES.INTERPOLATION) { + logger.info(`Found interpolation node (Mustache): ${JSON.stringify(child.content).substring(0, 100)}...`); + + if (child.content && child.content.type === 4 && child.content.content) { + const content = child.content.content.trim(); + logger.info(`Interpolation content: ${content}`); + + if (isI18nReference(content)) { + return content; + } + } else if (child.content && typeof child.content === 'object') { + // オブジェクト形式のcontentを探索 + logger.info(`Complex interpolation node: ${JSON.stringify(child.content).substring(0, 100)}...`); + + if (child.content.content) { + const content = child.content.content.trim(); + + if (isI18nReference(content)) { + logger.info(`Found i18n reference in complex interpolation: ${content}`); + return content; + } + } + } + } + } + + return null; +} + +/** + * 式ノードからコンテンツを抽出 + */ +function extractExpressionContent(children: VueAstNode[]): string | null { + // i18n.ts. 参照パターンを持つものを優先 + for (const child of children) { + if (child.type === NODE_TYPES.EXPRESSION && child.content) { + const expr = child.content.trim(); + + if (isI18nReference(expr)) { + logger.info(`Found i18n reference in expression node: ${expr}`); + return expr; + } + } + } + + // その他の式 + for (const child of children) { + if (child.type === NODE_TYPES.EXPRESSION && child.content) { + const expr = child.content.trim(); + logger.info(`Found expression: ${expr}`); + return expr; + } + } + + return null; +} + +/** + * テキストノードからコンテンツを抽出 + */ +function extractTextContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.type === NODE_TYPES.TEXT && child.content) { + const text = child.content.trim(); + + if (text) { + logger.info(`Found text node: ${text}`); + + // Mustache構文のチェック + const mustachePattern = /^\s*{{\s*(.*?)\s*}}\s*$/; + const mustacheMatch = text.match(mustachePattern); + + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + logger.info(`Extracted i18n ref from text mustache: ${mustacheMatch[1]}`); + return mustacheMatch[1].trim(); + } + + return text; + } + } + } + + return null; +} + +/** + * 子ノードを再帰的に探索してコンテンツを抽出 + */ +function extractNestedContent(children: VueAstNode[]): string | null { + for (const child of children) { + if (child.children && Array.isArray(child.children) && child.children.length > 0) { + const nestedContent = extractElementText(child); + + if (nestedContent) { + logger.info(`Found nested content: ${nestedContent}`); + return nestedContent; + } + } else if (child.type === NODE_TYPES.ELEMENT) { + // childrenがなくても内部を調査 + const nestedContent = extractElementText(child); + + if (nestedContent) { + logger.info(`Found content in childless element: ${nestedContent}`); + return nestedContent; + } + } + } + + return null; +} + + +/** + * SearchLabelとSearchKeywordを探して抽出する関数 + */ +function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null, keywords: any[] } { + let label: string | null = null; + const keywords: any[] = []; + + logger.info(`Extracting labels and keywords from ${nodes.length} nodes`); + + // 再帰的にSearchLabelとSearchKeywordを探索(ネストされたSearchMarkerは処理しない) + function findComponents(nodes: VueAstNode[]) { + for (const node of nodes) { + if (node.type === NODE_TYPES.ELEMENT) { + logger.info(`Checking element: ${node.tag}`); + + // SearchMarkerの場合は、その子要素は別スコープなのでスキップ + if (node.tag === 'SearchMarker') { + logger.info(`Found nested SearchMarker - skipping its content to maintain scope isolation`); + continue; // このSearchMarkerの中身は処理しない (スコープ分離) + } + + // SearchLabelの処理 + if (node.tag === 'SearchLabel') { + logger.info(`Found SearchLabel node, structure: ${JSON.stringify(node).substring(0, 200)}...`); + + // まず完全なノード内容の抽出を試みる + const content = extractElementText(node); + if (content) { + label = content; + logger.info(`SearchLabel content extracted: ${content}`); + } else { + logger.info(`SearchLabel found but extraction failed, trying direct children inspection`); + + // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 + if (node.children && Array.isArray(node.children)) { + for (const child of node.children) { + // Mustacheインターポレーション + if (child.type === NODE_TYPES.INTERPOLATION && child.content) { + // content内の式を取り出す + const expression = child.content.content || + (child.content.type === 4 ? child.content.content : null) || + JSON.stringify(child.content); + + logger.info(`Interpolation expression: ${expression}`); + if (typeof expression === 'string' && isI18nReference(expression)) { + label = expression.trim(); + logger.info(`Found i18n in interpolation: ${label}`); + break; + } + } + // 式ノード + else if (child.type === NODE_TYPES.EXPRESSION && child.content && isI18nReference(child.content)) { + label = child.content.trim(); + logger.info(`Found i18n in expression: ${label}`); + break; + } + // テキストノードでもMustache構文を探す + else if (child.type === NODE_TYPES.TEXT && child.content) { + const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + label = mustacheMatch[1].trim(); + logger.info(`Found i18n in text mustache: ${label}`); + break; + } + } + } + } + } + } + // SearchKeywordの処理 + else if (node.tag === 'SearchKeyword') { + logger.info(`Found SearchKeyword node`); + + // まず完全なノード内容の抽出を試みる + const content = extractElementText(node); + if (content) { + keywords.push(content); + logger.info(`SearchKeyword content extracted: ${content}`); + } else { + logger.info(`SearchKeyword found but extraction failed, trying direct children inspection`); + + // バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認 + if (node.children && Array.isArray(node.children)) { + for (const child of node.children) { + // Mustacheインターポレーション + if (child.type === NODE_TYPES.INTERPOLATION && child.content) { + // content内の式を取り出す + const expression = child.content.content || + (child.content.type === 4 ? child.content.content : null) || + JSON.stringify(child.content); + + logger.info(`Keyword interpolation: ${expression}`); + if (typeof expression === 'string' && isI18nReference(expression)) { + const keyword = expression.trim(); + keywords.push(keyword); + logger.info(`Found i18n keyword in interpolation: ${keyword}`); + break; + } + } + // 式ノード + else if (child.type === NODE_TYPES.EXPRESSION && child.content && isI18nReference(child.content)) { + const keyword = child.content.trim(); + keywords.push(keyword); + logger.info(`Found i18n keyword in expression: ${keyword}`); + break; + } + // テキストノードでもMustache構文を探す + else if (child.type === NODE_TYPES.TEXT && child.content) { + const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/); + if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) { + const keyword = mustacheMatch[1].trim(); + keywords.push(keyword); + logger.info(`Found i18n keyword in text mustache: ${keyword}`); + break; + } + } + } + } + } + } + + // 子要素を再帰的に調査(ただしSearchMarkerは除外) + if (node.children && Array.isArray(node.children)) { + findComponents(node.children); + } + } + } + } + + findComponents(nodes); + + // デバッグ情報 + logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}]`); + return { label, keywords }; +} + + +function extractUsageInfoFromTemplateAst( + templateAst: any, + id: string, +): SearchIndexItem[] { + const allMarkers: SearchIndexItem[] = []; + const markerMap = new Map(); + const childrenIds = new Set(); + const normalizedId = id.replace(/\\/g, '/'); + + if (!templateAst) return allMarkers; + + // マーカーの基本情報を収集 + function collectMarkers(node: VueAstNode, parentId: string | null = null) { + if (node.type === 1 && node.tag === 'SearchMarker') { + // マーカーID取得 + const markerIdProp = node.props?.find((p: any) => p.name === 'markerId'); + const markerId = markerIdProp?.value?.content || + node.__markerId; + + // SearchMarkerにマーカーIDがない場合はエラー + if (markerId == null) { + logger.error(`Marker ID not found for node: ${JSON.stringify(node)}`); + throw new Error(`Marker ID not found in file ${id}`); + } + + // マーカー基本情報 + const markerInfo: SearchIndexItem = { + id: markerId, + children: [], + label: '', // デフォルト値 + keywords: [], + }; + + // 静的プロパティを取得 + if (node.props && Array.isArray(node.props)) { + for (const prop of node.props) { + if (prop.type === 6 && prop.name && prop.name !== 'markerId') { + if (prop.name === 'path') markerInfo.path = prop.value?.content || ''; + else if (prop.name === 'icon') markerInfo.icon = prop.value?.content || ''; + else if (prop.name === 'label') markerInfo.label = prop.value?.content || ''; + } + } + } + + // バインドプロパティを取得 + const bindings = extractNodeBindings(node); + if (bindings.path) markerInfo.path = bindings.path; + if (bindings.icon) markerInfo.icon = bindings.icon; + if (bindings.label) markerInfo.label = bindings.label; + if (bindings.children) markerInfo.children = bindings.children; + if (bindings.inlining) { + markerInfo.inlining = bindings.inlining; + logger.info(`Added inlining ${JSON.stringify(bindings.inlining)} to marker ${markerId}`); + } + if (bindings.keywords) { + if (Array.isArray(bindings.keywords)) { + markerInfo.keywords = bindings.keywords; + } else { + markerInfo.keywords = bindings.keywords || []; + } + } + + //pathがない場合はファイルパスを設定 + if (markerInfo.path == null && parentId == null) { + markerInfo.path = normalizedId.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1]; + } + + // SearchLabelとSearchKeywordを抽出 (AST全体を探索) + if (node.children && Array.isArray(node.children)) { + logger.info(`Processing marker ${markerId} for labels and keywords`); + const extracted = extractLabelsAndKeywords(node.children); + + // SearchLabelからのラベル取得は最優先で適用 + if (extracted.label) { + markerInfo.label = extracted.label; + logger.info(`Using extracted label for ${markerId}: ${extracted.label}`); + } else if (markerInfo.label) { + logger.info(`Using existing label for ${markerId}: ${markerInfo.label}`); + } else { + markerInfo.label = 'Unnamed marker'; + logger.info(`No label found for ${markerId}, using default`); + } + + // SearchKeywordからのキーワード取得を追加 + if (extracted.keywords.length > 0) { + const existingKeywords = Array.isArray(markerInfo.keywords) ? + [...markerInfo.keywords] : + (markerInfo.keywords ? [markerInfo.keywords] : []); + + // i18n参照のキーワードは最優先で追加 + const combinedKeywords = [...existingKeywords]; + for (const kw of extracted.keywords) { + combinedKeywords.push(kw); + logger.info(`Added extracted keyword to ${markerId}: ${kw}`); + } + + markerInfo.keywords = combinedKeywords; + } + } + + // マーカーを登録 + markerMap.set(markerId, markerInfo); + allMarkers.push(markerInfo); + + // 親子関係を記録 + if (parentId) { + const parent = markerMap.get(parentId); + if (parent) { + childrenIds.add(markerId); + } + } + + // 子ノードを処理 + if (node.children && Array.isArray(node.children)) { + node.children.forEach((child: VueAstNode) => { + collectMarkers(child, markerId); + }); + } + + return markerId; + } + // SearchMarkerでない場合は再帰的に子ノードを処理 + else if (node.children && Array.isArray(node.children)) { + node.children.forEach((child: VueAstNode) => { + collectMarkers(child, parentId); + }); + } + + return null; + } + + // AST解析開始 + collectMarkers(templateAst); + return allMarkers; +} + +// バインドプロパティの処理を修正する関数 +function extractNodeBindings(node: VueAstNode): Record { + const bindings: Record = {}; + + if (!node.props || !Array.isArray(node.props)) return bindings; + + // バインド式を収集 + for (const prop of node.props) { + if (prop.type === 7 && prop.name === 'bind' && prop.arg?.content) { + const propName = prop.arg.content; + const propContent = prop.exp?.content || ''; + + logger.info(`Processing bind prop ${propName}: ${propContent}`); + + // inliningプロパティの処理を追加 + if (propName === 'inlining') { + try { + const content = propContent.trim(); + + // 配列式の場合 + if (content.startsWith('[') && content.endsWith(']')) { + // 配列要素を解析 + const elements = parseArrayExpression(content); + if (elements.length > 0) { + bindings.inlining = elements; + logger.info(`Parsed inlining array: ${JSON5.stringify(elements)}`); + } else { + bindings.inlining = []; + } + } + // 文字列の場合は配列に変換 + else if (content) { + bindings.inlining = [content]; // 単一の値を配列に + logger.info(`Converting inlining to array: [${content}]`); + } + } catch (e) { + logger.error(`Failed to parse inlining binding: ${propContent}`, e); + } + } + // keywordsの特殊処理 + if (propName === 'keywords') { + try { + const content = propContent.trim(); + + // 配列式の場合 + if (content.startsWith('[') && content.endsWith(']')) { + // i18n参照や特殊な式を保持するため、各要素を個別に解析 + const elements = parseArrayExpression(content); + if (elements.length > 0) { + bindings.keywords = elements; + logger.info(`Parsed keywords array: ${JSON5.stringify(elements)}`); + } else { + bindings.keywords = []; + logger.info('Empty keywords array'); + } + } + // その他の式(非配列) + else if (content) { + bindings.keywords = content; // 式をそのまま保持 + logger.info(`Keeping keywords as expression: ${content}`); + } else { + bindings.keywords = []; + logger.info('No keywords provided'); + } + } catch (e) { + logger.error(`Failed to parse keywords binding: ${propContent}`, e); + // エラーが起きても何らかの値を設定 + bindings.keywords = propContent || []; + } + } + // その他のプロパティ + else if (propName === 'label') { + // ラベルの場合も式として保持 + bindings[propName] = propContent; + logger.info(`Set label from bind expression: ${propContent}`); + } + else { + bindings[propName] = propContent; + } + } + } + + return bindings; +} + +// 配列式をパースする補助関数(文字列リテラル処理を改善) +function parseArrayExpression(expr: string): any[] { + try { + // 単純なケースはJSON5でパースを試みる + return JSON5.parse(expr.replace(/'/g, '"')); + } catch (e) { + // 複雑なケース(i18n.ts.xxx などの式を含む場合)は手動パース + logger.info(`Complex array expression, trying manual parsing: ${expr}`); + + // "["と"]"を取り除く + const content = expr.substring(1, expr.length - 1).trim(); + if (!content) return []; + + const result: any[] = []; + let currentItem = ''; + let depth = 0; + let inString = false; + let stringChar = ''; + + // カンマで区切る(ただし文字列内や入れ子の配列内のカンマは無視) + for (let i = 0; i < content.length; i++) { + const char = content[i]; + + if (inString) { + if (char === stringChar && content[i - 1] !== '\\') { + inString = false; + } + currentItem += char; + } else if (char === '"' || char === "'") { + inString = true; + stringChar = char; + currentItem += char; + } else if (char === '[') { + depth++; + currentItem += char; + } else if (char === ']') { + depth--; + currentItem += char; + } else if (char === ',' && depth === 0) { + // 項目の区切りを検出 + const trimmed = currentItem.trim(); + + // 純粋な文字列リテラルの場合、実際の値に変換 + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + try { + result.push(JSON5.parse(trimmed)); + } catch (err) { + result.push(trimmed); + } + } else { + // それ以外の式はそのまま(i18n.ts.xxx など) + result.push(trimmed); + } + + currentItem = ''; + } else { + currentItem += char; + } + } + + // 最後の項目を処理 + if (currentItem.trim()) { + const trimmed = currentItem.trim(); + + // 純粋な文字列リテラルの場合、実際の値に変換 + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || + (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + try { + result.push(JSON5.parse(trimmed)); + } catch (err) { + result.push(trimmed); + } + } else { + // それ以外の式はそのまま(i18n.ts.xxx など) + result.push(trimmed); + } + } + + logger.info(`Parsed complex array expression: ${expr} -> ${JSON.stringify(result)}`); + return result; + } +} + +export async function analyzeVueProps(options: Options & { + transformedCodeCache: Record, +}): Promise { + initLogger(options); + + const allMarkers: SearchIndexItem[] = []; + + // 対象ファイルパスを glob で展開 + const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => { + const matchedFiles = glob.sync(filePathPattern); + return [...acc, ...matchedFiles]; + }, []); + + logger.info(`Found ${filePaths.length} matching files to analyze`); + + for (const filePath of filePaths) { + const absolutePath = path.join(process.cwd(), filePath); + const id = absolutePath.replace(/\\/g, '/'); // 絶対パスに変換 + const code = options.transformedCodeCache[id]; // options 経由でキャッシュ参照 + if (!code) { // キャッシュミスの場合 + logger.error(`Error: No cached code found for: ${id}.`); // エラーログ + throw new Error(`No cached code found for: ${id}.`); // エラーを投げる + } + + try { + const { descriptor, errors } = vueSfcParse(options.transformedCodeCache[id], { + filename: filePath, + }); + + if (errors.length > 0) { + logger.error(`Compile Error: ${filePath}, ${errors}`); + continue; // エラーが発生したファイルはスキップ + } + + const fileMarkers = extractUsageInfoFromTemplateAst(descriptor.template?.ast, id); + + if (fileMarkers && fileMarkers.length > 0) { + allMarkers.push(...fileMarkers); // すべてのマーカーを収集 + logger.info(`Successfully extracted ${fileMarkers.length} markers from ${filePath}`); + } else { + logger.info(`No markers found in ${filePath}`); + } + } catch (error) { + logger.error(`Error analyzing file ${filePath}:`, error); + } + } + + // 収集したすべてのマーカー情報を使用 + const analysisResult: AnalysisResult[] = [ + { + filePath: "combined-markers", // すべてのファイルのマーカーを1つのエントリとして扱う + usage: allMarkers, + } + ]; + + outputAnalysisResultAsTS(options.exportFilePath, analysisResult); // すべてのマーカー情報を渡す +} + +interface MarkerRelation { + parentId?: string; + markerId: string; + node: VueAstNode; +} + +async function processVueFile( + code: string, + id: string, + options: Options, + transformedCodeCache: Record +): Promise<{ + code: string, + map: any, + transformedCodeCache: Record +}> { + const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化 + // すでにキャッシュに存在する場合は、そのまま返す + if (transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) { + logger.info(`Using cached version for ${id}`); + return { + code: transformedCodeCache[normalizedId], + map: null, + transformedCodeCache + }; + } + + const s = new MagicString(code); // magic-string のインスタンスを作成 + const parsed = vueSfcParse(code, { filename: id }); + if (!parsed.descriptor.template) { + return { + code, + map: null, + transformedCodeCache + }; + } + const ast = parsed.descriptor.template.ast; // テンプレート AST を取得 + const markerRelations: MarkerRelation[] = []; // MarkerRelation 配列を初期化 + + if (ast) { + function traverse(node: any, currentParent?: any) { + if (node.type === 1 && node.tag === 'SearchMarker') { + // 行番号はコード先頭からの改行数で取得 + const lineNumber = code.slice(0, node.loc.start.offset).split('\n').length; + // ファイルパスと行番号からハッシュ値を生成 + // この際実行環境で差が出ないようにファイルパスを正規化 + const idKey = id.replace(/\\/g, '/').split('packages/frontend/')[1] + const generatedMarkerId = toBase62(hash(`${idKey}:${lineNumber}`)); + + const props = node.props || []; + const hasMarkerIdProp = props.some((prop: any) => prop.type === 6 && prop.name === 'markerId'); + const nodeMarkerId = hasMarkerIdProp + ? props.find((prop: any) => prop.type === 6 && prop.name === 'markerId')?.value?.content as string + : generatedMarkerId; + node.__markerId = nodeMarkerId; + + // 子マーカーの場合、親ノードに __children を設定しておく + if (currentParent && currentParent.type === 1 && currentParent.tag === 'SearchMarker') { + currentParent.__children = currentParent.__children || []; + currentParent.__children.push(nodeMarkerId); + } + + const parentMarkerId = currentParent && currentParent.__markerId; + markerRelations.push({ + parentId: parentMarkerId, + markerId: nodeMarkerId, + node: node, + }); + + if (!hasMarkerIdProp) { + const nodeStart = node.loc.start.offset; + let endOfStartTag; + + if (node.children && node.children.length > 0) { + // 子要素がある場合、最初の子要素の開始位置を基準にする + endOfStartTag = code.lastIndexOf('>', node.children[0].loc.start.offset); + } else if (node.loc.end.offset > nodeStart) { + // 子要素がない場合、自身の終了位置から逆算 + const nodeSource = code.substring(nodeStart, node.loc.end.offset); + // 自己終了タグか通常の終了タグかを判断 + if (nodeSource.includes('/>')) { + endOfStartTag = code.indexOf('/>', nodeStart) - 1; + } else { + endOfStartTag = code.indexOf('>', nodeStart); + } + } + + if (endOfStartTag !== undefined && endOfStartTag !== -1) { + // markerId が既に存在しないことを確認 + const tagText = code.substring(nodeStart, endOfStartTag + 1); + const markerIdRegex = /\s+markerId\s*=\s*["'][^"']*["']/; + + if (!markerIdRegex.test(tagText)) { + s.appendRight(endOfStartTag, ` markerId="${generatedMarkerId}" data-in-app-search-marker-id="${generatedMarkerId}"`); + logger.info(`Adding markerId="${generatedMarkerId}" to ${id}:${lineNumber}`); + } else { + logger.info(`markerId already exists in ${id}:${lineNumber}`); + } + } + } + } + + const newParent = node.type === 1 && node.tag === 'SearchMarker' ? node : currentParent; + if (node.children && Array.isArray(node.children)) { + node.children.forEach(child => traverse(child, newParent)); + } + } + + traverse(ast); // AST を traverse (1段階目: ID 生成と親子関係記録) + + // 2段階目: :children 属性の追加 + // 最初に親マーカーごとに子マーカーIDを集約する処理を追加 + const parentChildrenMap = new Map(); + + // 1. まず親ごとのすべての子マーカーIDを収集 + markerRelations.forEach(relation => { + if (relation.parentId) { + if (!parentChildrenMap.has(relation.parentId)) { + parentChildrenMap.set(relation.parentId, []); + } + parentChildrenMap.get(relation.parentId)?.push(relation.markerId); + } + }); + + // 2. 親ごとにまとめて :children 属性を処理 + for (const [parentId, childIds] of parentChildrenMap.entries()) { + const parentRelation = markerRelations.find(r => r.markerId === parentId); + if (!parentRelation || !parentRelation.node) continue; + + const parentNode = parentRelation.node; + const childrenProp = parentNode.props?.find((prop: any) => prop.type === 7 && prop.name === 'bind' && prop.arg?.content === 'children'); + + // 親ノードの開始位置を特定 + const parentNodeStart = parentNode.loc!.start.offset; + const endOfParentStartTag = parentNode.children && parentNode.children.length > 0 + ? code.lastIndexOf('>', parentNode.children[0].loc!.start.offset) + : code.indexOf('>', parentNodeStart); + + if (endOfParentStartTag === -1) continue; + + // 親タグのテキストを取得 + const parentTagText = code.substring(parentNodeStart, endOfParentStartTag + 1); + + if (childrenProp) { + // AST で :children 属性が検出された場合、それを更新 + try { + const childrenStart = code.indexOf('[', childrenProp.exp!.loc.start.offset); + const childrenEnd = code.indexOf(']', childrenProp.exp!.loc.start.offset); + if (childrenStart !== -1 && childrenEnd !== -1) { + const childrenArrayStr = code.slice(childrenStart, childrenEnd + 1); + let childrenArray = JSON5.parse(childrenArrayStr.replace(/'/g, '"')); + + // 新しいIDを追加(重複は除外) + const newIds = childIds.filter(id => !childrenArray.includes(id)); + if (newIds.length > 0) { + childrenArray = [...childrenArray, ...newIds]; + const updatedChildrenArrayStr = JSON5.stringify(childrenArray).replace(/"/g, "'"); + s.overwrite(childrenStart, childrenEnd + 1, updatedChildrenArrayStr); + logger.info(`Added ${newIds.length} child markerIds to existing :children in ${id}`); + } + } + } catch (e) { + logger.error('Error updating :children attribute:', e); + } + } else { + // AST では検出されなかった場合、タグテキストを調べる + const childrenRegex = /:children\s*=\s*["']\[(.*?)\]["']/; + const childrenMatch = parentTagText.match(childrenRegex); + + if (childrenMatch) { + // テキストから :children 属性値を解析して更新 + try { + const childrenContent = childrenMatch[1]; + const childrenArrayStr = `[${childrenContent}]`; + const childrenArray = JSON5.parse(childrenArrayStr.replace(/'/g, '"')); + + // 新しいIDを追加(重複は除外) + const newIds = childIds.filter(id => !childrenArray.includes(id)); + if (newIds.length > 0) { + childrenArray.push(...newIds); + + // :children="[...]" の位置を特定して上書き + const attrStart = parentTagText.indexOf(':children='); + if (attrStart > -1) { + const attrValueStart = parentTagText.indexOf('[', attrStart); + const attrValueEnd = parentTagText.indexOf(']', attrValueStart) + 1; + if (attrValueStart > -1 && attrValueEnd > -1) { + const absoluteStart = parentNodeStart + attrValueStart; + const absoluteEnd = parentNodeStart + attrValueEnd; + const updatedArrayStr = JSON5.stringify(childrenArray).replace(/"/g, "'"); + s.overwrite(absoluteStart, absoluteEnd, updatedArrayStr); + logger.info(`Updated existing :children in tag text for ${id}`); + } + } + } + } catch (e) { + logger.error('Error updating :children in tag text:', e); + } + } else { + // :children 属性がまだない場合、新規作成 + s.appendRight(endOfParentStartTag, ` :children="${JSON5.stringify(childIds).replace(/"/g, "'")}"`); + logger.info(`Created new :children attribute with ${childIds.length} markerIds in ${id}`); + } + } + } + } + + const transformedCode = s.toString(); // 変換後のコードを取得 + transformedCodeCache[normalizedId] = transformedCode; // 変換後のコードをキャッシュに保存 + + return { + code: transformedCode, // 変更後のコードを返す + map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要) + transformedCodeCache // キャッシュも返す + }; +} + + +// Rollup プラグインとして export +export default function pluginCreateSearchIndex(options: Options): Plugin { + let transformedCodeCache: Record = {}; // キャッシュオブジェクトをプラグインスコープで定義 + const isDevServer = process.env.NODE_ENV === 'development'; // 開発サーバーかどうか + + initLogger(options); // ロガーを初期化 + + return { + name: 'createSearchIndex', + enforce: 'pre', + + async buildStart() { + if (!isDevServer) { + return; + } + + const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => { + const matchedFiles = glob.sync(filePathPattern); + return [...acc, ...matchedFiles]; + }, []); + + for (const filePath of filePaths) { + const id = path.resolve(filePath); // 絶対パスに変換 + const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む + const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す + transformedCodeCache = newCache; // キャッシュを更新 + } + + await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行 + }, + + async transform(code, id) { + if (!id.endsWith('.vue')) { + return; + } + + // targetFilePaths にマッチするファイルのみ処理を行う + // glob パターンでマッチング + let isMatch = false; // isMatch の初期値を false に設定 + for (const pattern of options.targetFilePaths) { // パターンごとにマッチング確認 + const globbedFiles = glob.sync(pattern); + for (const globbedFile of globbedFiles) { + const normalizedGlobbedFile = path.resolve(globbedFile); // glob 結果を絶対パスに + const normalizedId = path.resolve(id); // id を絶対パスに + if (normalizedGlobbedFile === normalizedId) { // 絶対パス同士で比較 + isMatch = true; + break; // マッチしたらループを抜ける + } + } + if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける + } + + + if (!isMatch) { + return; + } + + const transformed = await processVueFile(code, id, options, transformedCodeCache); + transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新 + if (isDevServer) { + await analyzeVueProps({ ...options, transformedCodeCache }); // analyzeVueProps を呼び出す + } + return transformed; + }, + + async writeBundle() { + await analyzeVueProps({ ...options, transformedCodeCache }); // ビルド時にも analyzeVueProps を実行 + }, + }; +} + +// i18n参照を検出するためのヘルパー関数を追加 +function isI18nReference(text: string | null | undefined): boolean { + if (!text) return false; + // ドット記法(i18n.ts.something) + const dotPattern = /i18n\.ts\.\w+/; + // ブラケット記法(i18n.ts['something']) + const bracketPattern = /i18n\.ts\[['"][^'"]+['"]\]/; + return dotPattern.test(text) || bracketPattern.test(text); +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2bf7728d0a..5b40a8cb9f 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -51,6 +51,7 @@ "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", + "magic-string": "0.30.17", "matter-js": "0.20.0", "mfm-js": "0.24.0", "misskey-bubble-game": "workspace:*", diff --git a/packages/frontend/src/components/MkDisableSection.vue b/packages/frontend/src/components/MkDisableSection.vue new file mode 100644 index 0000000000..f596d5e3b5 --- /dev/null +++ b/packages/frontend/src/components/MkDisableSection.vue @@ -0,0 +1,41 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index e725d2a15d..c3fc1961eb 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -91,6 +91,14 @@ const buttonsRight = computed(() => { }); const reloadCount = ref(0); +function getSearchMarker(path: string) { + const hash = path.split('#')[1]; + if (hash == null) return null; + return hash; +} + +const searchMarkerId = ref(getSearchMarker(props.initialPath)); + windowRouter.addListener('push', ctx => { history.value.push({ path: ctx.path, key: ctx.key }); }); @@ -101,7 +109,8 @@ windowRouter.addListener('replace', ctx => { }); windowRouter.addListener('change', ctx => { - console.log('windowRouter: change', ctx.path); + if (_DEV_) console.log('windowRouter: change', ctx.path); + searchMarkerId.value = getSearchMarker(ctx.path); analytics.page({ path: ctx.path, title: ctx.path, @@ -111,6 +120,7 @@ windowRouter.addListener('change', ctx => { windowRouter.init(); provide('router', windowRouter); +provide('inAppSearchMarkerId', searchMarkerId); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 397aa68ed6..d8dec3aa2f 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -4,27 +4,60 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -58,10 +91,98 @@ export type SuperMenuDef = { diff --git a/packages/frontend/src/components/global/SearchKeyword.vue b/packages/frontend/src/components/global/SearchKeyword.vue new file mode 100644 index 0000000000..27a284faf0 --- /dev/null +++ b/packages/frontend/src/components/global/SearchKeyword.vue @@ -0,0 +1,14 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/SearchLabel.vue b/packages/frontend/src/components/global/SearchLabel.vue new file mode 100644 index 0000000000..27a284faf0 --- /dev/null +++ b/packages/frontend/src/components/global/SearchLabel.vue @@ -0,0 +1,14 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue new file mode 100644 index 0000000000..c5ec626cf4 --- /dev/null +++ b/packages/frontend/src/components/global/SearchMarker.vue @@ -0,0 +1,116 @@ + + + + + + + diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 0252bf0252..ebbad3e5b8 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { App } from 'vue'; - import Mfm from './global/MkMfm.js'; import MkA from './global/MkA.vue'; import MkAcct from './global/MkAcct.vue'; @@ -26,6 +24,11 @@ import MkSpacer from './global/MkSpacer.vue'; import MkFooterSpacer from './global/MkFooterSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; import MkLazy from './global/MkLazy.vue'; +import SearchMarker from './global/SearchMarker.vue'; +import SearchLabel from './global/SearchLabel.vue'; +import SearchKeyword from './global/SearchKeyword.vue'; + +import type { App } from 'vue'; export default function(app: App) { for (const [key, value] of Object.entries(components)) { @@ -55,6 +58,9 @@ export const components = { MkFooterSpacer: MkFooterSpacer, MkStickyContainer: MkStickyContainer, MkLazy: MkLazy, + SearchMarker: SearchMarker, + SearchLabel: SearchLabel, + SearchKeyword: SearchKeyword, }; declare module '@vue/runtime-core' { @@ -80,5 +86,8 @@ declare module '@vue/runtime-core' { MkFooterSpacer: typeof MkFooterSpacer; MkStickyContainer: typeof MkStickyContainer; MkLazy: typeof MkLazy; + SearchMarker: typeof SearchMarker; + SearchLabel: typeof SearchLabel; + SearchKeyword: typeof SearchKeyword; } } diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 776f59dda3..806599e801 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -4,74 +4,82 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue new file mode 100644 index 0000000000..465c2a38c2 --- /dev/null +++ b/packages/frontend/src/pages/settings/appearance.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index 9fca306f9f..79be2b9b1e 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -4,44 +4,46 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 5acbc50756..6b67a9a1a8 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -4,118 +4,143 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 41e475eade..2e453aeb8f 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -8,11 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only ref="buttonEl" v-ripple="canToggle" class="_button" - :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" + :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]" @click="toggleReaction()" @contextmenu.prevent.stop="menu" > - + {{ count }} @@ -30,11 +30,11 @@ import { useTooltip } from '@/scripts/use-tooltip.js'; import { $i } from '@/account.js'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; import { claimAchievement } from '@/scripts/achievements.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as sound from '@/scripts/sound.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; import { customEmojisMap } from '@/custom-emojis.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ reaction: string; @@ -90,7 +90,7 @@ async function toggleReaction() { } }); } else { - if (defaultStore.state.confirmOnReact) { + if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', text: i18n.tsx.reactAreYouSure({ emoji: props.reaction.replace('@.', '') }), @@ -135,7 +135,7 @@ async function menu(ev) { } function anime() { - if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return; + if (document.hidden || !prefer.s.animation || buttonEl.value == null) return; const rect = buttonEl.value.getBoundingClientRect(); const x = rect.left + 16; diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 63b202f9f3..bb60db8d34 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only - + - + diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index b76f6a6033..794af86e43 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
-
+
-
+
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 6aee91dfda..fedb2de8fe 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 1ea501c34c..95f6ba0bc6 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -8,36 +8,34 @@ SPDX-License-Identifier: AGPL-3.0-only -
- - - + diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 04445c913c..9a09250bff 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
@@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
+
{{ i18n.ts.postToGallery }}
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index c4aed8d6df..3f704e2b88 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
{{ instance.name || `(${i18n.ts.unknown})` }} @@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only manifest.json
-
+
@@ -116,14 +116,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
+
@@ -135,8 +135,9 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/components/global/PageWithAnimBg.vue b/packages/frontend/src/components/global/PageWithAnimBg.vue new file mode 100644 index 0000000000..a00b196a04 --- /dev/null +++ b/packages/frontend/src/components/global/PageWithAnimBg.vue @@ -0,0 +1,21 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue new file mode 100644 index 0000000000..e1cfd1be09 --- /dev/null +++ b/packages/frontend/src/components/global/PageWithHeader.vue @@ -0,0 +1,32 @@ + + + + + + + diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index c28c457e33..6c6903c3a4 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -25,6 +25,8 @@ import MkPageHeader from './global/MkPageHeader.vue'; import MkSpacer from './global/MkSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; import MkLazy from './global/MkLazy.vue'; +import PageWithHeader from './global/PageWithHeader.vue'; +import PageWithAnimBg from './global/PageWithAnimBg.vue'; import SearchMarker from './global/SearchMarker.vue'; import SearchLabel from './global/SearchLabel.vue'; import SearchKeyword from './global/SearchKeyword.vue'; @@ -60,6 +62,8 @@ export const components = { MkSpacer: MkSpacer, MkStickyContainer: MkStickyContainer, MkLazy: MkLazy, + PageWithHeader: PageWithHeader, + PageWithAnimBg: PageWithAnimBg, SearchMarker: SearchMarker, SearchLabel: SearchLabel, SearchKeyword: SearchKeyword, @@ -89,6 +93,8 @@ declare module '@vue/runtime-core' { MkSpacer: typeof MkSpacer; MkStickyContainer: typeof MkStickyContainer; MkLazy: typeof MkLazy; + PageWithHeader: typeof PageWithHeader; + PageWithAnimBg: typeof PageWithAnimBg; SearchMarker: typeof SearchMarker; SearchLabel: typeof SearchLabel; SearchKeyword: typeof SearchKeyword; diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 2d0135e6a6..481088fc30 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index cad5f2e109..b684d4b68b 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index b6ca104830..5a29f942bd 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 644b2d3d13..d2bf162ce5 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -52,12 +52,15 @@ import { miLocalStorage } from '@/local-storage.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import { prefer } from '@/preferences.js'; import { useRouter } from '@/router.js'; +import { useScrollPositionKeeper } from '@/use/use-scroll-position-keeper.js'; provide('shouldOmitHeaderTitle', true); const tlComponent = useTemplateRef('tlComponent'); const rootEl = useTemplateRef('rootEl'); +useScrollPositionKeeper(rootEl); + const router = useRouter(); router.useListener('same', () => { top(); -- cgit v1.2.3-freya From de19d9a4d429b4ce9d4713e647659fe6460bdd11 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:48:25 +0900 Subject: refactor(frontend): MkHorizontalSwipe -> MkSwiper --- .../frontend/src/components/MkHorizontalSwipe.vue | 238 --------------------- packages/frontend/src/components/MkSwiper.vue | 238 +++++++++++++++++++++ packages/frontend/src/pages/about.vue | 6 +- packages/frontend/src/pages/announcements.vue | 6 +- packages/frontend/src/pages/channel.vue | 6 +- packages/frontend/src/pages/channels.vue | 6 +- packages/frontend/src/pages/chat/home.vue | 6 +- packages/frontend/src/pages/drive.file.vue | 6 +- packages/frontend/src/pages/explore.vue | 6 +- packages/frontend/src/pages/flash/flash-index.vue | 6 +- packages/frontend/src/pages/follow-requests.vue | 6 +- packages/frontend/src/pages/gallery/index.vue | 6 +- packages/frontend/src/pages/instance-info.vue | 6 +- packages/frontend/src/pages/my-clips/index.vue | 6 +- packages/frontend/src/pages/notifications.vue | 2 +- packages/frontend/src/pages/pages.vue | 6 +- packages/frontend/src/pages/search.vue | 6 +- packages/frontend/src/pages/user/index.vue | 6 +- packages/frontend/src/utility/touch.ts | 2 +- 19 files changed, 285 insertions(+), 285 deletions(-) delete mode 100644 packages/frontend/src/components/MkHorizontalSwipe.vue create mode 100644 packages/frontend/src/components/MkSwiper.vue (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue deleted file mode 100644 index 1d0ffaea11..0000000000 --- a/packages/frontend/src/components/MkHorizontalSwipe.vue +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - diff --git a/packages/frontend/src/components/MkSwiper.vue b/packages/frontend/src/components/MkSwiper.vue new file mode 100644 index 0000000000..1d0ffaea11 --- /dev/null +++ b/packages/frontend/src/components/MkSwiper.vue @@ -0,0 +1,238 @@ + + + + + + diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index b4315a5cfa..ec286f109f 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -28,7 +28,7 @@ import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/utility/achievements.js'; import { definePage } from '@/page.js'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue')); const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue')); diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index 1902267a6a..7a04914dd7 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -50,7 +50,7 @@ import { ref, computed } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index a62e035198..e71d372722 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ref, defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; const props = defineProps<{ fileId: string; diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index 85b9fe4932..bcece47e35 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -25,7 +25,7 @@ import XFeatured from './explore.featured.vue'; import XUsers from './explore.users.vue'; import XRoles from './explore.roles.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 98ab587b55..4ef33cbe0f 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -43,7 +43,7 @@ import { computed, ref } from 'vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { useRouter } from '@/router.js'; diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 36643b1acb..d467d875fd 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -52,7 +52,7 @@ import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { infoImageUrl } from '@/instance.js'; import { $i } from '@/i.js'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; const paginationComponent = useTemplateRef('paginationComponent'); diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 4cf3fca83b..c6ce773ab0 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -50,7 +50,7 @@ import { watch, ref, computed } from 'vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; import { useRouter } from '@/router.js'; diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 66ddf627e4..fde462944c 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -153,7 +153,7 @@ import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination from '@/components/MkPagination.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; import { dateString } from '@/filters/date.js'; import MkTextarea from '@/components/MkTextarea.vue'; diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 1525bbef9b..5b9b3af90b 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -33,7 +33,7 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { clipsCache } from '@/cache.js'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; const pagination = { endpoint: 'clips/list' as const, diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 0a2bc02de5..61a1b2725c 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -24,7 +24,7 @@ import { computed, ref } from 'vue'; import { notificationTypes } from '@@/js/const.js'; import XNotifications from '@/components/MkNotifications.vue'; import MkNotes from '@/components/MkNotes.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index c99d7f1a0f..d412bad616 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -41,7 +41,7 @@ import { computed, ref } from 'vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { useRouter } from '@/router.js'; diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index e0cb2dcbab..814ddf3cb9 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -28,7 +28,7 @@ import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { notesSearchAvailable } from '@/utility/check-permissions.js'; import MkInfo from '@/components/MkInfo.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkSwiper from '@/components/MkSwiper.vue'; const props = withDefaults(defineProps<{ query?: string, diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 58f44d7591..83c7bf45bb 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue index 7ea0b5c97f..99f8df0780 100644 --- a/packages/frontend/src/components/global/PageWithHeader.vue +++ b/packages/frontend/src/components/global/PageWithHeader.vue @@ -20,6 +20,7 @@ import { useTemplateRef } from 'vue'; import { scrollInContainer } from '@@/js/scroll.js'; import type { PageHeaderItem } from '@/types/page-header.js'; import type { Tab } from './MkPageHeader.tabs.vue'; +import { useScrollPositionKeeper } from '@/use/use-scroll-position-keeper.js'; const props = withDefaults(defineProps<{ tabs?: Tab[]; @@ -35,6 +36,8 @@ const props = withDefaults(defineProps<{ const tab = defineModel('tab'); const rootEl = useTemplateRef('rootEl'); +useScrollPositionKeeper(rootEl); + defineExpose({ scrollToTop: () => { if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' }); diff --git a/packages/frontend/src/use/use-scroll-position-keeper.ts b/packages/frontend/src/use/use-scroll-position-keeper.ts index 00cc51a459..f7b3015cc9 100644 --- a/packages/frontend/src/use/use-scroll-position-keeper.ts +++ b/packages/frontend/src/use/use-scroll-position-keeper.ts @@ -4,19 +4,22 @@ */ import { throttle } from 'throttle-debounce'; -import { nextTick, onActivated, onUnmounted, watch } from 'vue'; +import { nextTick, onActivated, onDeactivated, onUnmounted, watch } from 'vue'; import type { Ref } from 'vue'; // note render skippingがオンだとズレるため、遷移直前にスクロール範囲に表示されているdata-scroll-anchor要素を特定して、復元時に当該要素までスクロールするようにする export function useScrollPositionKeeper(scrollContainerRef: Ref): void { let anchorId: string | null = null; + let ready = true; watch(scrollContainerRef, (el) => { if (!el) return; const onScroll = () => { if (!el) return; + if (!ready) return; + const scrollContainerRect = el.getBoundingClientRect(); const viewPosition = scrollContainerRect.height / 2; @@ -41,18 +44,32 @@ export function useScrollPositionKeeper(scrollContainerRef: Ref { + if (!anchorId) return; + const scrollContainer = scrollContainerRef.value; + if (!scrollContainer) return; + const scrollAnchorEl = scrollContainer.querySelector(`[data-scroll-anchor="${anchorId}"]`); + if (!scrollAnchorEl) return; + scrollAnchorEl.scrollIntoView({ + behavior: 'instant', + block: 'center', + inline: 'center', + }); + }; + + onDeactivated(() => { + ready = false; + }); + onActivated(() => { + restore(); nextTick(() => { - if (!anchorId) return; - const scrollContainer = scrollContainerRef.value; - if (!scrollContainer) return; - const scrollAnchorEl = scrollContainer.querySelector(`[data-scroll-anchor="${anchorId}"]`); - if (!scrollAnchorEl) return; - scrollAnchorEl.scrollIntoView({ - behavior: 'instant', - block: 'center', - inline: 'center', - }); + restore(); + window.setTimeout(() => { + restore(); + + ready = true; + }, 100); }); }); } -- cgit v1.2.3-freya From 17e7340933887fb4044bdee4798ed87c873f1dad Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:59:05 +0900 Subject: enhance(frontend): テーマでページヘッダーの色を変更できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend-shared/themes/_dark.json5 | 2 ++ packages/frontend-shared/themes/_light.json5 | 2 ++ packages/frontend/src/components/MkThemePreview.vue | 5 ++++- packages/frontend/src/components/global/MkPageHeader.vue | 11 +++++++++-- 4 files changed, 17 insertions(+), 3 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 index 924be27455..8ebaf20b64 100644 --- a/packages/frontend-shared/themes/_dark.json5 +++ b/packages/frontend-shared/themes/_dark.json5 @@ -33,6 +33,8 @@ navFg: '@fg', navActive: '@accent', navIndicator: '@indicator', + pageHeaderBg: '@bg', + pageHeaderFg: '@fg', link: '#44a4c1', hashtag: '#ff9156', mention: '@accent', diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5 index e3c62ff543..63ad95ff84 100644 --- a/packages/frontend-shared/themes/_light.json5 +++ b/packages/frontend-shared/themes/_light.json5 @@ -33,6 +33,8 @@ navFg: '@fg', navActive: '@accent', navIndicator: '@indicator', + pageHeaderBg: '@bg', + pageHeaderFg: '@fg', link: '#44a4c1', hashtag: '#ff9156', mention: '@accent', diff --git a/packages/frontend/src/components/MkThemePreview.vue b/packages/frontend/src/components/MkThemePreview.vue index 013ab9d6a4..cc4254a2f6 100644 --- a/packages/frontend/src/components/MkThemePreview.vue +++ b/packages/frontend/src/components/MkThemePreview.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -62,6 +62,7 @@ const themeVariables = ref<{ accent: string; accentedBg: string; navBg: string; + pageHeaderBg: string; success: string; warn: string; error: string; @@ -76,6 +77,7 @@ const themeVariables = ref<{ accent: 'var(--MI_THEME-accent)', accentedBg: 'var(--MI_THEME-accentedBg)', navBg: 'var(--MI_THEME-navBg)', + pageHeaderBg: 'var(--MI_THEME-pageHeaderBg)', success: 'var(--MI_THEME-success)', warn: 'var(--MI_THEME-warn)', error: 'var(--MI_THEME-error)', @@ -104,6 +106,7 @@ watch(() => props.theme, (theme) => { accent: compiled.accent ?? 'var(--MI_THEME-accent)', accentedBg: compiled.accentedBg ?? 'var(--MI_THEME-accentedBg)', navBg: compiled.navBg ?? 'var(--MI_THEME-navBg)', + pageHeaderBg: compiled.pageHeaderBg ?? 'var(--MI_THEME-pageHeaderBg)', success: compiled.success ?? 'var(--MI_THEME-success)', warn: compiled.warn ?? 'var(--MI_THEME-warn)', error: compiled.error ?? 'var(--MI_THEME-error)', diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 93f46a866a..dc0b52b141 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -124,11 +124,18 @@ onUnmounted(() => { diff --git a/packages/frontend/src/components/MkTl.vue b/packages/frontend/src/components/MkTl.vue new file mode 100644 index 0000000000..95cc4d2a2a --- /dev/null +++ b/packages/frontend/src/components/MkTl.vue @@ -0,0 +1,173 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue b/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue new file mode 100644 index 0000000000..5dd2887024 --- /dev/null +++ b/packages/frontend/src/pages/admin/federation-job-queue.chart.chart.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/federation-job-queue.chart.vue b/packages/frontend/src/pages/admin/federation-job-queue.chart.vue new file mode 100644 index 0000000000..4b10d682a5 --- /dev/null +++ b/packages/frontend/src/pages/admin/federation-job-queue.chart.vue @@ -0,0 +1,160 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/federation-job-queue.vue b/packages/frontend/src/pages/admin/federation-job-queue.vue new file mode 100644 index 0000000000..df976ba999 --- /dev/null +++ b/packages/frontend/src/pages/admin/federation-job-queue.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 8d03838a8f..d2246b7512 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -138,11 +138,16 @@ const menuDef = computed(() => [{ text: i18n.ts.federation, to: '/admin/federation', active: currentPage.value?.route.name === 'federation', + }, { + icon: 'ti ti-clock-play', + text: i18n.ts.federationJobs, + to: '/admin/federation-job-queue', + active: currentPage.value?.route.name === 'federationJobQueue', }, { icon: 'ti ti-clock-play', text: i18n.ts.jobQueue, - to: '/admin/queue', - active: currentPage.value?.route.name === 'queue', + to: '/admin/job-queue', + active: currentPage.value?.route.name === 'jobQueue', }, { icon: 'ti ti-cloud', text: i18n.ts.files, @@ -329,6 +334,8 @@ defineExpose({ diff --git a/packages/frontend/src/pages/admin/job-queue.vue b/packages/frontend/src/pages/admin/job-queue.vue new file mode 100644 index 0000000000..528c473c4f --- /dev/null +++ b/packages/frontend/src/pages/admin/job-queue.vue @@ -0,0 +1,370 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue deleted file mode 100644 index 5dd2887024..0000000000 --- a/packages/frontend/src/pages/admin/queue.chart.chart.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue deleted file mode 100644 index 1ba02d6e0e..0000000000 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue deleted file mode 100644 index ee56c258c9..0000000000 --- a/packages/frontend/src/pages/admin/queue.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts index d59b160b8b..a0a22b4338 100644 --- a/packages/frontend/src/router.definition.ts +++ b/packages/frontend/src/router.definition.ts @@ -392,9 +392,13 @@ export const ROUTE_DEF = [{ name: 'avatarDecorations', component: page(() => import('@/pages/avatar-decorations.vue')), }, { - path: '/queue', - name: 'queue', - component: page(() => import('@/pages/admin/queue.vue')), + path: '/federation-job-queue', + name: 'federationJobQueue', + component: page(() => import('@/pages/admin/federation-job-queue.vue')), + }, { + path: '/job-queue', + name: 'jobQueue', + component: page(() => import('@/pages/admin/job-queue.vue')), }, { path: '/files', name: 'files', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 5237d5bbf8..b43906109f 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -267,7 +267,22 @@ type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-dela type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json']; +type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; // @public (undocumented) type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json']; @@ -1531,7 +1546,12 @@ declare namespace entities { AdminQueueClearRequest, AdminQueueDeliverDelayedResponse, AdminQueueInboxDelayedResponse, - AdminQueuePromoteRequest, + AdminQueueJobsRequest, + AdminQueuePromoteJobsRequest, + AdminQueueQueueStatsRequest, + AdminQueueRemoveJobRequest, + AdminQueueRetryJobRequest, + AdminQueueShowJobRequest, AdminQueueStatsResponse, AdminRelaysAddRequest, AdminRelaysAddResponse, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index ae97084116..b607c93e1e 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -636,12 +636,78 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:queue* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:queue* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ - request( + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + request( endpoint: E, params: P, credential?: string | null, diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index d22395e866..6390314429 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -78,7 +78,12 @@ import type { AdminQueueClearRequest, AdminQueueDeliverDelayedResponse, AdminQueueInboxDelayedResponse, - AdminQueuePromoteRequest, + AdminQueueJobsRequest, + AdminQueuePromoteJobsRequest, + AdminQueueQueueStatsRequest, + AdminQueueRemoveJobRequest, + AdminQueueRetryJobRequest, + AdminQueueShowJobRequest, AdminQueueStatsResponse, AdminRelaysAddRequest, AdminRelaysAddResponse, @@ -694,7 +699,13 @@ export type Endpoints = { 'admin/queue/clear': { req: AdminQueueClearRequest; res: EmptyResponse }; 'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse }; 'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse }; - 'admin/queue/promote': { req: AdminQueuePromoteRequest; res: EmptyResponse }; + 'admin/queue/jobs': { req: AdminQueueJobsRequest; res: EmptyResponse }; + 'admin/queue/promote-jobs': { req: AdminQueuePromoteJobsRequest; res: EmptyResponse }; + 'admin/queue/queue-stats': { req: AdminQueueQueueStatsRequest; res: EmptyResponse }; + 'admin/queue/queues': { req: EmptyRequest; res: EmptyResponse }; + 'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse }; + 'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse }; + 'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: EmptyResponse }; 'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse }; 'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse }; 'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index a772c1559d..f814d7b3da 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -81,7 +81,12 @@ export type AdminPromoCreateRequest = operations['admin___promo___create']['requ export type AdminQueueClearRequest = operations['admin___queue___clear']['requestBody']['content']['application/json']; export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json']; export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json']; -export type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json']; +export type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json']; +export type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json']; +export type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json']; +export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json']; +export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; +export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json']; export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json']; export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 3c51bedb5c..bb7ba30f1b 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -531,14 +531,68 @@ export type paths = { */ post: operations['admin___queue___inbox-delayed']; }; - '/admin/queue/promote': { + '/admin/queue/jobs': { /** - * admin/queue/promote + * admin/queue/jobs + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + post: operations['admin___queue___jobs']; + }; + '/admin/queue/promote-jobs': { + /** + * admin/queue/promote-jobs * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ - post: operations['admin___queue___promote']; + post: operations['admin___queue___promote-jobs']; + }; + '/admin/queue/queue-stats': { + /** + * admin/queue/queue-stats + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + post: operations['admin___queue___queue-stats']; + }; + '/admin/queue/queues': { + /** + * admin/queue/queues + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + post: operations['admin___queue___queues']; + }; + '/admin/queue/remove-job': { + /** + * admin/queue/remove-job + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:queue* + */ + post: operations['admin___queue___remove-job']; + }; + '/admin/queue/retry-job': { + /** + * admin/queue/retry-job + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:queue* + */ + post: operations['admin___queue___retry-job']; + }; + '/admin/queue/show-job': { + /** + * admin/queue/show-job + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + post: operations['admin___queue___show-job']; }; '/admin/queue/stats': { /** @@ -8809,9 +8863,9 @@ export type operations = { content: { 'application/json': { /** @enum {string} */ - type: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; /** @enum {string} */ - state: '*' | 'wait' | 'delayed'; + state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed'; }; }; }; @@ -8945,17 +8999,326 @@ export type operations = { }; }; /** - * admin/queue/promote + * admin/queue/jobs + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + admin___queue___jobs: { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed')[]; + search?: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/promote-jobs * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ - admin___queue___promote: { + 'admin___queue___promote-jobs': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/queue-stats + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + 'admin___queue___queue-stats': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/queues + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + admin___queue___queues: { + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/remove-job + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:queue* + */ + 'admin___queue___remove-job': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + jobId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/retry-job + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:queue* + */ + 'admin___queue___retry-job': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + jobId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * admin/queue/show-job + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + 'admin___queue___show-job': { requestBody: { content: { 'application/json': { /** @enum {string} */ - type: 'deliver' | 'inbox'; + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + jobId: string; }; }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d142cde7b7..858060b731 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: '@twemoji/parser': specifier: 15.1.1 version: 15.1.1 + '@types/redis-info': + specifier: 3.0.3 + version: 3.0.3 accepts: specifier: 1.3.8 version: 1.3.8 @@ -368,6 +371,9 @@ importers: re2: specifier: 1.21.4 version: 1.21.4(patch_hash=018babd22b7ce951bcd10d6246f1e541a7ac7ba212f7fa8985e774ece67d08e1) + redis-info: + specifier: 3.1.0 + version: 3.1.0 redis-lock: specifier: 0.1.4 version: 0.1.4 @@ -4462,6 +4468,9 @@ packages: '@types/readdir-glob@1.1.1': resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} + '@types/redis-info@3.0.3': + resolution: {integrity: sha512-VIkNy6JbYI/RLdbPHdm9JQvv6RVld2uE2/6Hdid38Qdq+zvDli2FTpImI8pC5zwp8xS8qVqfzlfyAub8xZEd5g==} + '@types/rename@1.0.7': resolution: {integrity: sha512-E9qapfghUGfBMi3jNhsmCKPIp3f2zvNKpaX1BDGLGJNjzpgsZ/RTx7NaNksFjGoJ+r9NvWF1NSM5vVecnNjVmw==} @@ -9429,6 +9438,9 @@ packages: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} + redis-info@3.1.0: + resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} + redis-lock@0.1.4: resolution: {integrity: sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==} engines: {node: '>=0.6'} @@ -14937,6 +14949,8 @@ snapshots: dependencies: '@types/node': 22.14.0 + '@types/redis-info@3.0.3': {} + '@types/rename@1.0.7': {} '@types/resolve@1.20.3': {} @@ -21162,6 +21176,10 @@ snapshots: redis-errors@1.2.0: {} + redis-info@3.1.0: + dependencies: + lodash: 4.17.21 + redis-lock@0.1.4: {} redis-parser@3.0.0: -- cgit v1.2.3-freya From 5042a0aa8f4404edd11ed09208e23211049d2d24 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 27 Apr 2025 09:16:17 +0900 Subject: fix(frontend): タイムラインでノートが重複して表示されることがあるのを修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #15870 Close #15874 --- CHANGELOG.md | 1 + packages/frontend/src/components/MkNotes.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index d36cb8dd2b..7c3100b486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Fix: タイムラインのスクロール位置を記憶するように修正 - Fix: ノートの直後のノートを表示する機能で表示が逆順になっていた問題を修正 #15841 - Fix: アカウントの移行時にアンテナのフィルターのユーザが更新されない問題を修正 #15843 +- Fix: タイムラインでノートが重複して表示されることがあるのを修正 ### Server - Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index af29802ce8..f02216a5ca 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + -- cgit v1.2.3-freya From 6a69e4180b2506aa94983a7e2f1d6dafc2214b07 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 27 Apr 2025 10:46:55 +0900 Subject: fix(frontend): PageWithHeaderからPageHeaderに全Propsが伝わっていなかった問題を修正 (#15858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/global/MkPageHeader.vue | 27 +++++++++++++--------- .../src/components/global/PageWithHeader.vue | 21 ++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index dc0b52b141..542c3d8d12 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -40,19 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- + + @@ -52,7 +61,7 @@ defineExpose({ } -.body { +.body, .swiper { min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px))); } diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index d2bf162ce5..af28d3decd 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -4,36 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only --> - - diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 34cf598b84..ec6ea7c569 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -22,7 +22,6 @@ import MkLoading from './global/MkLoading.vue'; import MkError from './global/MkError.vue'; import MkAd from './global/MkAd.vue'; import MkPageHeader from './global/MkPageHeader.vue'; -import MkSpacer from './global/MkSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; import MkLazy from './global/MkLazy.vue'; import PageWithHeader from './global/PageWithHeader.vue'; @@ -60,7 +59,6 @@ export const components = { MkError: MkError, MkAd: MkAd, MkPageHeader: MkPageHeader, - MkSpacer: MkSpacer, MkStickyContainer: MkStickyContainer, MkLazy: MkLazy, PageWithHeader: PageWithHeader, @@ -92,7 +90,6 @@ declare module '@vue/runtime-core' { MkError: typeof MkError; MkAd: typeof MkAd; MkPageHeader: typeof MkPageHeader; - MkSpacer: typeof MkSpacer; MkStickyContainer: typeof MkStickyContainer; MkLazy: typeof MkLazy; PageWithHeader: typeof PageWithHeader; diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts index 58a2cce207..e2590da60b 100644 --- a/packages/frontend/src/di.ts +++ b/packages/frontend/src/di.ts @@ -17,5 +17,4 @@ export const DI = { mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>, inModal: Symbol() as InjectionKey, inAppSearchMarkerId: Symbol() as InjectionKey>, - forceSpacerMin: Symbol() as InjectionKey, }; diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 481088fc30..7605ceba30 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index b4ccd55452..0edf2db1eb 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -5,18 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue index 423e709da4..1560403b70 100644 --- a/packages/frontend/src/pages/achievements.vue +++ b/packages/frontend/src/pages/achievements.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 1e3e106842..8495642a8c 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 9d92ccda60..15cd219834 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue index 10925fa4ab..e81de8c9e7 100644 --- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ mode === 'create' ? i18n.ts._abuseReport._notificationRecipient.createRecipient : i18n.ts._abuseReport._notificationRecipient.modifyRecipient }}
- +
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
{{ i18n.ts.ok }} diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue index a569ab7c33..f5e77cbe4e 100644 --- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 2335cc3db7..3dc5c2ef7e 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index aa8ba2f7c3..c5baeda7b0 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index ea7f0cc73d..b2d7b4889a 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 2674879f90..19258216f6 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue index ae43507d66..9938d5cc4a 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.search }}
- +
- +
{{ i18n.ts.search }} diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index 566acdea31..e8e944df32 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> -- cgit v1.2.3-freya From e5fcb5b53f3719c0bafa1d6e39805b40c999cd2f Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:25:47 +0900 Subject: enhance(frontend): disable router view transition Fix #15723 --- .../frontend/src/components/global/RouterView.vue | 46 +--------------------- 1 file changed, 2 insertions(+), 44 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 78ac6900a3..27f7b18559 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->