diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2024-09-09 20:57:36 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-09-09 20:57:36 +0900 |
| commit | 2cbe1d1210a5745787f37069ecb59b8f6c03c224 (patch) | |
| tree | 9acb1e675d2ae85f7f1f0f34f6acdc4965bff3f9 /packages/frontend | |
| parent | refactor(misskey-js): warnを除去 (#14520) (diff) | |
| download | misskey-2cbe1d1210a5745787f37069ecb59b8f6c03c224.tar.gz misskey-2cbe1d1210a5745787f37069ecb59b8f6c03c224.tar.bz2 misskey-2cbe1d1210a5745787f37069ecb59b8f6c03c224.zip | |
feat(frontend): ノート・ユーザータイムライン埋め込み (#13929)
* fix
* navhookをbootに移動
* サーバーサイドのbootも分けるように
* 埋め込みページかどうかの判定は最初の一回だけに
* tooltipは出せるように
* fix design
* 埋め込み独自のtooltipを削除
* ロジックの分岐が多かったMkNoteDetailedを分離
* fix indent
* プレビュー用iframeにフォーカスが当たるのを修正
* popupの制御を出す側で行うように
* パラメータが逆になっていたのを修正
* Update MkEmbedCodeGenDialog.vue
* fix
* eliminate misskey-js lint warns
* fix
* add appropriate attributes to embed html
* enhance: サーバーサイドのembed系をさらに分離
* enhance: embed routerを分離(route定義をboot時に変更できるようにする改修を含む)
* type
* lint
* fix indent
* server-side styleを完全に分離
* Revert "refactor: 画面サイズのしきい値をconstにまとめる"
This reverts commit 05ca36f400889456981e89489ae0ae242fa09b67.
* fix
* revert all changes in base.pug
* embedドメインをまとめた
* embedドメインをまとめた
* prevent calling contextmenu in embed page by stopping at the caller
* fix import
* fix import
* improve directory structure
* fix import
* register timeline ui as a container
* wa-
* rename
* wa-
* Update EmMediaList.vue
* Update EmMediaList.vue
* Update EmMediaList.vue
* Update EmMediaImage.vue
* Update EmNote.vue
* revert mkmedialist changes
* 戻し漏れ
* wip
* tweak embed media ui
* revert original media components
* Update boot.embed.js
* rename
* wip
* Update MkNote.vue
* wip
* Update MkSubNoteContent.vue
* Update EmNote.vue
* Update packages/frontend/src/router/definition.ts
* Revert "Update packages/frontend/src/router/definition.ts"
This reverts commit 937ae44521cdb0f250796943b20142b65f8ed944.
* refactor EmMediaImage
* fix import
* remove unused imports
* Update router.ts
* wip
* Update boot.ts
* wip
* wip
* wip
* wip
* Update EmNote.vue
* Update EmNote.vue
* Create EmA.vue
* Create EmAvatar.vue
* Update EmAvatar.vue
* wip
* wip
* wip
* Create EmImgWithBlurhash.vue
* Update EmImgWithBlurhash.vue
* Create EmPagination.vue
* wip
* Update boot.ts
* wip
* wip
* wi@p
* wip
* wip
* wiop
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update boot.ts
* wip
* Update MkMisskeyFlavoredMarkdown.ts
* wip
* wip
* wip
* wip
* wip
* Update post-message.ts
* wip
* Update EmNoteDetailed.vue
* Update EmNoteDetailed.vue
* Create instance.ts
* Update EmNoteDetailed.vue
* wip
* Update EmNoteDetailed.vue
* wip
* wip
* wip
* Update pnpm-lock.yaml
* wip
* wip
* wp
* wip
* Update ClientServerService.ts
* wip
* Update boot.ts
* Update vite.config.local-dev.ts
* Update vite.config.ts
* Create index.html
* wa-
* wip
* Update boot.ts
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Create EmLink.vue
* Create EmMention.vue
* Update EmMfm.ts
* wip
* wip
* wip
* wip
* Update vite.config.ts
* Update boot.ts
* Update EmA.vue
* うぃp
* wip
* wip
* Create EmError.vue
* wip
* Update MkEmbedCodeGenDialog.vue
* Update EmNote.vue
* wip
* wip
* Update user-timeline.vue
* Update check-spdx-license-id.yml
* wip
* wip
* style(frontend-shared): lint fixes on build.js
* fix(frontend-shared): include `*.{js,json}` files in js-built
* wip
* use alias
* refactor
* refactor
* Update scroll.ts
* refactor
* refactor
* refactor
* wip
* wip
* wip
* wip
* Update roles.vue
* Update branding.vue
* wip
* wip
* wip
* Update page.vue
* wip
* fix import
* add missing css variables
* 絵文字をtwemojiに変更
クライアントデフォルトにあわせるため
* force empoll readonly
* fix compiler error
* fix broken imports
* tweak button style
* run api extractor
* fix storybook theme preloads
* fix storybook instance imports
* Update preview.ts
* Update preview.ts
* Update preview.ts
* Revert "Update preview.ts"
This reverts commit 12bab1c6fbd3baf753515df760ff19d027b85155.
* Revert "Update preview.ts"
This reverts commit 5c0ce01dbdf2194ffe94aba950f747a9968f29c4.
* Revert "Update preview.ts"
This reverts commit f4863524d7e5ca0f25470808849c24a72bea000a.
* Revert "fix storybook instance imports"
This reverts commit ed8eabb246edf731d31adffbe3c77c539e53ae9e.
* Revert "wip"
This reverts commit d3c1926519878155193a1654f49141e515d49683.
* Revert "Update page.vue"
This reverts commit 27c7900b0c1ae296b56075e8a9c22585d9cd744b.
* Revert "Update branding.vue"
This reverts commit c08ccb65ba66774c3e2b3dcfc6153004b5c0aa16.
* Revert "Update roles.vue"
This reverts commit 1488b670660cb1803d17d8f5c78f2d79e59fa52d.
* Revert "wip"
This reverts commit aab1c769814b08c257cad3025422a0eea3bfba4f.
* refactor: use common media proxy
* fix imports
* fix
* fix: MediaProxyの初期化を保証する(storybook対策?)
* enhance(frontend-embed): improve embedParams provide
* fix(backend): MK_DEV_PREFER=backendのときにembed viteが読み込めないのを修正
* fix
* embed-pageを共通化
* fix import
* fix import
* fix import
* const.jsを共通化
(たぶんrevertしすぎた)
* fix type error
* fix duplicated import
* fix lint
* fix
* コメントとして残す
* sharedとembedをlint対象にする
* lint
* attempt to fix eslint (frontend-shared)
* lint fixes
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Diffstat (limited to 'packages/frontend')
132 files changed, 912 insertions, 3654 deletions
diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index fb93d7be13..e5573f2ac3 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -30,7 +30,7 @@ const keys = [ 'd-u0', ] -await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { +await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { writeFile( new URL('./themes.ts', import.meta.url), `export default ${JSON.stringify( diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts index 0a7281898d..70afc356c1 100644 --- a/packages/frontend/@types/theme.d.ts +++ b/packages/frontend/@types/theme.d.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -declare module '@/themes/*.json5' { +declare module '@@/themes/*.json5' { import { Theme } from '@/scripts/theme.js'; const theme: Theme; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 1464be18a7..67be7f0598 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -55,6 +55,7 @@ "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", + "frontend-shared": "workspace:*", "photoswipe": "5.4.4", "punycode": "2.3.1", "rollup": "4.19.1", diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index d86ae18ffe..19d30f64ce 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { setupRouter } from '@/router/definition.js'; +import { setupRouter } from '@/router/main.js'; +import { createMainRouter } from '@/router/definition.js'; export async function common(createVue: () => App<Element>) { console.info(`Misskey v${version}`); @@ -239,7 +240,7 @@ export async function common(createVue: () => App<Element>) { const app = createVue(); - setupRouter(app); + setupRouter(app, createMainRouter); if (_DEV_) { app.config.performance = true; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 3e7c4f26f8..b31281dcf2 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -22,6 +22,7 @@ import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; +import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -62,6 +63,18 @@ export async function mainBoot() { } }); + stream.on('emojiAdded', emojiData => { + addCustomEmoji(emojiData.emoji); + }); + + stream.on('emojiUpdated', emojiData => { + updateCustomEmojis(emojiData.emojis); + }); + + stream.on('emojiDeleted', emojiData => { + removeCustomEmojis(emojiData.emojis); + }); + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { import('@/plugin.js').then(async ({ install }) => { // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 932c4ecb2e..f547991369 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import sanitizeHtml from 'sanitize-html'; +import { emojilist, getEmojiName } from '@@/js/emojilist.js'; import contains from '@/scripts/contains.js'; -import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js'; +import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; -import { emojilist, getEmojiName } from '@/scripts/emojilist.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { MFM_TAGS, MFM_PARAMS } from '@/const.js'; +import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js'; import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js'; const lib = emojilist.filter(x => x.category !== 'flags'); diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 00506fb735..9a0a9fba05 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onMounted, onUnmounted, ref } from 'vue'; import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import * as game from '@/scripts/clicker-game.js'; import number from '@/filters/number.js'; import { claimAchievement } from '@/scripts/achievements.js'; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index 1d4c0b6366..716dd92678 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.codeBlockRoot"> - <button :class="$style.codeBlockCopyButton" class="_button" @click="copy"> + <button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy"> <i class="ti ti-copy"></i> </button> <Suspense> @@ -32,12 +32,17 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ code: string; + forceShow?: boolean; + copyButton?: boolean; lang?: string; -}>(); +}>(), { + copyButton: true, + forceShow: false, +}); -const show = ref(!defaultStore.state.dataSaver.code); +const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code); const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue new file mode 100644 index 0000000000..51630c427c --- /dev/null +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -0,0 +1,412 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialogEl" + :width="1000" + :height="600" + :scroll="false" + :withOkButton="false" + @close="cancel()" + @closed="$emit('closed')" +> + <template #header>{{ i18n.ts._embedCodeGen.title }}</template> + + <div :class="$style.embedCodeGenRoot"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + > + <div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot"> + <div + :class="$style.embedCodeGenPreviewRoot" + > + <MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/> + <div :class="$style.embedCodeGenPreviewWrapper"> + <div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div> + <div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert> + <div + :class="$style.embedCodeGenPreviewResizer" + :style="{ transform: iframeStyle }" + > + <iframe + ref="iframeEl" + :src="embedPreviewUrl" + :class="$style.embedCodeGenPreviewIframe" + :style="{ height: `${iframeHeight}px` }" + @load="iframeOnLoad" + ></iframe> + </div> + </div> + </div> + </div> + <div :class="$style.embedCodeGenSettings" class="_gaps"> + <MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0"> + <template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template> + <template #suffix>px</template> + <template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template> + </MkInput> + <MkSelect v-model="colorMode"> + <template #label>{{ i18n.ts.theme }}</template> + <option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option> + <option value="light">{{ i18n.ts.light }}</option> + <option value="dark">{{ i18n.ts.dark }}</option> + </MkSelect> + <MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch> + <MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch> + <MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch> + <MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo> + <MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo> + <div class="_buttons"> + <MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton> + <MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + <div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot"> + <div :class="$style.embedCodeGenResultWrapper" class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div> + <div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div> + <div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div> + </div> + <div class="_gaps_s"> + <MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/> + <MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> + </div> + <MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton> + </div> + </div> + </Transition> + </div> +</MkModalWindow> +</template> + +<script setup lang="ts"> +import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue'; +import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; + +import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkButton from '@/components/MkButton.vue'; + +import MkCode from '@/components/MkCode.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import { url } from '@/config.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js'; +import { embedRouteWithScrollbar } from '@@/js/embed-page.js'; + +const emit = defineEmits<{ + (ev: 'ok'): void; + (ev: 'cancel'): void; + (ev: 'closed'): void; +}>(); + +const props = defineProps<{ + entity: EmbeddableEntity; + id: string; + params?: EmbedParams; +}>(); + +//#region Modalの制御 +const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); + +function cancel() { + emit('cancel'); + dialogEl.value?.close(); +} + +function close() { + dialogEl.value?.close(); +} + +const phase = ref<'input' | 'result'>('input'); +//#endregion + +//#region 埋め込みURL生成・カスタマイズ + +// 本URL生成用params +const paramsForUrl = computed<EmbedParams>(() => ({ + header: header.value, + maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined, + colorMode: colorMode.value === 'auto' ? undefined : colorMode.value, + rounded: rounded.value, + border: border.value, +})); + +// プレビュー用params(手動で更新を掛けるのでref) +const paramsForPreview = ref<EmbedParams>(props.params ?? {}); + +const embedPreviewUrl = computed(() => { + const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value)); + if (paramClass.has('maxHeight')) { + const maxHeight = parseInt(paramClass.get('maxHeight')!); + paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限 + } + return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`; +}); + +const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity)); +const header = ref(props.params?.header ?? true); +const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500); + +const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto'); +const rounded = ref(props.params?.rounded ?? true); +const border = ref(props.params?.border ?? true); + +function applyToPreview() { + const currentPreviewUrl = embedPreviewUrl.value; + + paramsForPreview.value = { + header: header.value, + maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined, + colorMode: colorMode.value === 'auto' ? undefined : colorMode.value, + rounded: rounded.value, + border: border.value, + }; + + nextTick(() => { + if (currentPreviewUrl === embedPreviewUrl.value) { + // URLが変わらなくてもリロード + iframeEl.value?.contentWindow?.location.reload(); + } + }); +} + +const result = ref(''); + +function generate() { + result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value); + phase.value = 'result'; +} + +function doCopy() { + copyToClipboard(result.value); + os.success(); +} +//#endregion + +//#region プレビューのリサイズ +const resizerRootEl = shallowRef<HTMLDivElement>(); +const iframeLoading = ref(true); +const iframeEl = shallowRef<HTMLIFrameElement>(); +const iframeHeight = ref(0); +const iframeScale = ref(1); +const iframeStyle = computed(() => { + return `translate(-50%, -50%) scale(${iframeScale.value})`; +}); +const resizeObserver = new ResizeObserver(() => { + calcScale(); +}); + +function iframeOnLoad() { + iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => { + iframeLoading.value = true; + nextTick(() => { + iframeHeight.value = 0; + iframeScale.value = 1; + }); + }); +} + +function windowEventHandler(event: MessageEvent) { + if (event.source !== iframeEl.value?.contentWindow) { + return; + } + if (event.data.type === 'misskey:embed:ready') { + iframeEl.value!.contentWindow?.postMessage({ + type: 'misskey:embedParent:registerIframeId', + payload: { + iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい + }, + }); + } + if (event.data.type === 'misskey:embed:changeHeight') { + iframeHeight.value = event.data.payload.height; + nextTick(() => { + calcScale(); + iframeLoading.value = false; // 初回の高さ変更まで待つ + }); + } +} + +function calcScale() { + if (!resizerRootEl.value) return; + const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ + const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ + const iframeWidth = 500; + const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしないので1を上限に + iframeScale.value = scale; +} + +onMounted(() => { + window.addEventListener('message', windowEventHandler); + if (!resizerRootEl.value) return; + resizeObserver.observe(resizerRootEl.value); +}); + +function reset() { + window.removeEventListener('message', windowEventHandler); + resizeObserver.disconnect(); + + // プレビューのリセット + iframeHeight.value = 0; + iframeScale.value = 1; + iframeLoading.value = true; + result.value = ''; + phase.value = 'input'; +} + +onDeactivated(() => { + reset(); +}); + +onUnmounted(() => { + reset(); +}); +//#endregion +</script> + +<style module> +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.embedCodeGenRoot { + container-type: inline-size; + height: 100%; +} + +.embedCodeGenInputRoot { + height: 100%; + display: grid; + grid-template-columns: 1fr 400px; +} + +.embedCodeGenPreviewRoot { + position: relative; + background-color: var(--bg); + cursor: not-allowed; +} + +.embedCodeGenPreviewWrapper { + display: flex; + flex-direction: column; + height: 100%; + pointer-events: none; + user-select: none; + -webkit-user-drag: none; +} + +.embedCodeGenPreviewTitle { + position: absolute; + z-index: 100; + top: 8px; + left: 8px; + padding: 6px 10px; + border-radius: 6px; + font-size: 85%; +} + +.embedCodeGenPreviewSpinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + user-select: none; + -webkit-user-drag: none; +} + +.embedCodeGenPreviewResizerRoot { + position: relative; + flex: 1 0; +} + +.embedCodeGenPreviewResizer { + position: absolute; + top: 50%; + left: 50%; +} + +.embedCodeGenPreviewIframe { + display: block; + border: none; + width: 500px; + color-scheme: light dark; +} + +.embedCodeGenSettings { + padding: 24px; + overflow-y: scroll; +} + +.embedCodeGenResultRoot { + box-sizing: border-box; + padding: 24px; + height: 100%; + max-width: 700px; + margin: 0 auto; + display: flex; + align-items: center; +} + +.embedCodeGenResultHeading { + text-align: center; + font-size: 1.2em; +} + +.embedCodeGenResultHeadingIcon { + margin: 0 auto; + background-color: var(--accentedBg); + color: var(--accent); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.embedCodeGenResultDescription { + text-align: center; + white-space: pre-wrap; +} + +.embedCodeGenResultWrapper, +.embedCodeGenResultCode { + width: 100%; +} + +.embedCodeGenResultButtons { + margin: 0 auto; +} + +@container (max-width: 800px) { + .embedCodeGenInputRoot { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } +} +</style> diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index c13164c296..fca7aa2f4e 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed, Ref } from 'vue'; -import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js'; +import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js'; import { i18n } from '@/i18n.js'; import { customEmojis } from '@/custom-emojis.js'; import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 4a3ed69f47..5ba175fc35 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -117,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, shallowRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; -import XSection from '@/components/MkEmojiPicker.section.vue'; import { emojilist, emojiCharByCategory, @@ -126,7 +125,8 @@ import { getEmojiName, CustomEmojiFolderTree, getUnicodeEmoji, -} from '@/scripts/emojilist.js'; +} from '@@/js/emojilist.js'; +import XSection from '@/components/MkEmojiPicker.section.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 8d301f16bd..eeecf052af 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only import DrawBlurhash from '@/workers/draw-blurhash?worker'; import TestWebGL2 from '@/workers/test-webgl2?worker'; import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js'; -import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js'; +import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => { // テスト環境で Web Worker インスタンスは作成できない diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index e695564f92..4c2fc1ba00 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js'; diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 2300802dcf..4a4a99be25 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -37,7 +37,7 @@ import XBanner from '@/components/MkMediaBanner.vue'; import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; import * as os from '@/os.js'; -import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js'; import { defaultStore } from '@/store.js'; import { focusParent } from '@/scripts/focus.js'; diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index f2f2bf47a8..1b6f6cef31 100644 --- a/packages/frontend/src/components/MkMiniChart.vue +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import tinycolor from 'tinycolor2'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const props = defineProps<{ src: number[]; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 4caafe54bf..2927a46977 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -627,7 +627,7 @@ function emitUpdReaction(emoji: string, delta: number) { // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?) //content-visibility: auto; - //contain-intrinsic-size: 0 128px; + //contain-intrinsic-size: 0 128px; &:focus-visible { outline: none; diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 71b38d99ed..47a9c79e45 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -35,7 +35,7 @@ import MkSwitch from './MkSwitch.vue'; import MkInfo from './MkInfo.vue'; import MkButton from './MkButton.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; import { i18n } from '@/i18n.js'; type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 389987338d..d67616e6b2 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -31,7 +31,7 @@ import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index bd86b01591..8049f88051 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -40,7 +40,7 @@ import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { openingWindowsCount } from '@/os.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { useRouterFactory } from '@/router/supplier.js'; import { mainRouter } from '@/router/main.js'; diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 62a85389ad..d30f915c55 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; +import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; -import { useDocumentVisibility } from '@/scripts/use-document-visibility.js'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 72bd8f4f6c..8e230cce4f 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { sum } from '@/scripts/array.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { host } from '@/config.js'; -import { useInterval } from '@/scripts/use-interval.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import { useInterval } from '@@/js/use-interval.js'; const props = defineProps<{ noteId: string; @@ -83,10 +83,10 @@ if (props.poll.expiresAt) { } const vote = async (id) => { - pleaseLogin(undefined, pleaseLoginContext.value); - if (props.readOnly || closed.value || isVoted.value) return; + pleaseLogin(undefined, pleaseLoginContext.value); + const { canceled } = await os.confirm({ type: 'question', text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }), @@ -145,7 +145,7 @@ const vote = async (id) => { .done { .choice { - cursor: default; + cursor: initial; } } </style> diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index e0d0b561be..4fb4c6fe56 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref, shallowRef } from 'vue'; import { i18n } from '@/i18n.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { isHorizontalSwipeSwiping } from '@/scripts/touch.js'; const SCROLL_STOP = 10; diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 60118fadd2..3dd02b261c 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; +import { getEmojiName } from '@@/js/emojilist.js'; import MkTooltip from './MkTooltip.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; -import { getEmojiName } from '@/scripts/emojilist.js'; defineProps<{ showing: boolean; diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 26223364ab..f42a0b3227 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject, onMounted, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { getUnicodeEmoji } from '@@/js/emojilist.js'; import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -34,7 +35,6 @@ 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 { getUnicodeEmoji } from '@/scripts/emojilist.js'; const props = defineProps<{ reaction: string; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 0eba8d6a9c..360d697d7c 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { MenuItem } from '@/types/menu.js'; diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 781145e1bc..dabbe97468 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -66,6 +66,7 @@ import { defineAsyncComponent, ref } from 'vue'; import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; +import { query, extractDomain } from '@@/js/url.js'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import MkButton from '@/components/MkButton.vue'; @@ -74,7 +75,6 @@ import MkInfo from '@/components/MkInfo.vue'; import { host as configHost } from '@/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { query, extractDomain } from '@/scripts/url.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index ee224dba49..35c07bc80c 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; +import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; import MkImgWithBlurhash from '../MkImgWithBlurhash.vue'; import MkA from './MkA.vue'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js'; import { acct, userPage } from '@/filters/user.js'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index fa780d4ad3..fc3745c009 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject } from 'vue'; -import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-base.js'; +import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js'; +import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js'; import { defaultStore } from '@/store.js'; -import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js'; import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 0d869892bd..ea1f3e2988 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -19,8 +19,13 @@ import MkSparkle from '@/components/MkSparkle.vue'; import MkA, { MkABehavior } from '@/components/global/MkA.vue'; import { host } from '@/config.js'; import { defaultStore } from '@/store.js'; -import { nyaize as doNyaize } from '@/scripts/nyaize.js'; -import { safeParseFloat } from '@/scripts/safe-parse.js'; + +function safeParseFloat(str: unknown): number | null { + if (typeof str !== 'string' || str === '') return null; + const num = parseFloat(str); + if (isNaN(num)) return null; + return num; +} const QUOTE_STYLE = ` display: block; @@ -86,7 +91,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven case 'text': { let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } if (!props.plain) { @@ -281,14 +286,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const child = token.children[0]; let text = child.type === 'text' ? child.props.text : ''; if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); } else { const rt = token.children.at(-1)!; let text = rt.type === 'text' ? rt.props.text : ''; if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); } @@ -400,7 +405,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'emojiCode': { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.author?.host == null) { return [h(MkCustomEmoji, { key: Math.random(), diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index f16d951679..f1a451808f 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue'; import tinycolor from 'tinycolor2'; import XTabs, { Tab } from './MkPageHeader.tabs.vue'; -import { scrollToTop } from '@/scripts/scroll.js'; +import { scrollToTop } from '@@/js/scroll.js'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index b12dc8cb31..3f37354908 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue'; -import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js'; const rootEl = shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>(); diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index d2ddd4aa85..8f4e3b853a 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -30,10 +30,17 @@ import { toUnicode as decodePunycode } from 'punycode/'; import { url as local } from '@/config.js'; import * as os from '@/os.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; -import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { MkABehavior } from '@/components/global/MkA.vue'; +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} + const props = withDefaults(defineProps<{ url: string; rel?: string; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts deleted file mode 100644 index e135bc69a0..0000000000 --- a/packages/frontend/src/const.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// ブラウザで直接表示することを許可するファイルの種類のリスト -// ここに含まれないものは application/octet-stream としてレスポンスされる -// SVGはXSSを生むので許可しない -export const FILE_TYPE_BROWSERSAFE = [ - // Images - 'image/png', - 'image/gif', - 'image/jpeg', - 'image/webp', - 'image/avif', - 'image/apng', - 'image/bmp', - 'image/tiff', - 'image/x-icon', - - // OggS - 'audio/opus', - 'video/ogg', - 'audio/ogg', - 'application/ogg', - - // ISO/IEC base media file format - 'video/quicktime', - 'video/mp4', - 'audio/mp4', - 'video/x-m4v', - 'audio/x-m4a', - 'video/3gpp', - 'video/3gpp2', - - 'video/mpeg', - 'audio/mpeg', - - 'video/webm', - 'audio/webm', - - 'audio/aac', - - // see https://github.com/misskey-dev/misskey/pull/10686 - 'audio/flac', - 'audio/wav', - // backward compatibility - 'audio/x-flac', - 'audio/vnd.wave', -]; -/* -https://github.com/sindresorhus/file-type/blob/main/supported.js -https://github.com/sindresorhus/file-type/blob/main/core.js -https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers -*/ - -export const notificationTypes = [ - 'note', - 'follow', - 'mention', - 'reply', - 'renote', - 'quote', - 'reaction', - 'pollEnded', - 'receiveFollowRequest', - 'followRequestAccepted', - 'roleAssigned', - 'achievementEarned', - 'app', -] as const; -export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; - -export const ROLE_POLICIES = [ - 'gtlAvailable', - 'ltlAvailable', - 'canPublicNote', - 'mentionLimit', - 'canInvite', - 'inviteLimit', - 'inviteLimitCycle', - 'inviteExpirationTime', - 'canManageCustomEmojis', - 'canManageAvatarDecorations', - 'canSearchNotes', - 'canUseTranslator', - 'canHideAds', - 'driveCapacityMb', - 'alwaysMarkNsfw', - 'canUpdateBioMedia', - 'pinLimit', - 'antennaLimit', - 'wordMuteLimit', - 'webhookLimit', - 'clipLimit', - 'noteEachClipsLimit', - 'userListLimit', - 'userEachUserListsLimit', - 'rateLimitFactor', - 'avatarDecorationLimit', -] as const; - -// なんか動かない -//export const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP'); -//export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM'); -export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP'; -export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM'; - -export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg'; -export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg'; -export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg'; - -export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; -export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = { - tada: ['speed=', 'delay='], - jelly: ['speed=', 'delay='], - twitch: ['speed=', 'delay='], - shake: ['speed=', 'delay='], - spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'], - jump: ['speed=', 'delay='], - bounce: ['speed=', 'delay='], - flip: ['h', 'v'], - x2: [], - x3: [], - x4: [], - scale: ['x=', 'y='], - position: ['x=', 'y='], - fg: ['color='], - bg: ['color='], - border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], - font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'], - blur: [], - rainbow: ['speed=', 'delay='], - rotate: ['deg='], - ruby: [], - unixtime: [], -}; diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 9da3582e1a..0d03282cee 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -6,7 +6,6 @@ import { shallowRef, computed, markRaw, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useStream } from '@/stream.js'; import { get, set } from '@/scripts/idb-proxy.js'; const storageCache = await get('emojis'); @@ -29,23 +28,20 @@ watch(customEmojis, emojis => { } }, { immediate: true }); -// TODO: ここら辺副作用なのでいい感じにする -const stream = useStream(); - -stream.on('emojiAdded', emojiData => { - customEmojis.value = [emojiData.emoji, ...customEmojis.value]; +export function addCustomEmoji(emoji: Misskey.entities.EmojiSimple) { + customEmojis.value = [emoji, ...customEmojis.value]; set('emojis', customEmojis.value); -}); +} -stream.on('emojiUpdated', emojiData => { - customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item); +export function updateCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) { + customEmojis.value = customEmojis.value.map(item => emojis.find(search => search.name === item.name) ?? item); set('emojis', customEmojis.value); -}); +} -stream.on('emojiDeleted', emojiData => { - customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name)); +export function removeCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) { + customEmojis.value = customEmojis.value.filter(item => !emojis.some(search => search.name === item.name)); set('emojis', customEmojis.value); -}); +} export async function fetchCustomEmojis(force = false) { const now = Date.now(); diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts index f200f242ed..615dd99fa8 100644 --- a/packages/frontend/src/directives/follow-append.ts +++ b/packages/frontend/src/directives/follow-append.ts @@ -4,7 +4,7 @@ */ import { Directive } from 'vue'; -import { getScrollContainer, getScrollPosition } from '@/scripts/scroll.js'; +import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js'; export default { mounted(src, binding, vn) { diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json deleted file mode 100644 index 75d5c34d71..0000000000 --- a/packages/frontend/src/emojilist.json +++ /dev/null @@ -1,1805 +0,0 @@ -[ - ["😀", "grinning", 0], - ["😬", "grimacing", 0], - ["😁", "grin", 0], - ["😂", "joy", 0], - ["🤣", "rofl", 0], - ["🥳", "partying", 0], - ["😃", "smiley", 0], - ["😄", "smile", 0], - ["😅", "sweat_smile", 0], - ["🥲", "smiling_face_with_tear", 0], - ["😆", "laughing", 0], - ["😇", "innocent", 0], - ["😉", "wink", 0], - ["😊", "blush", 0], - ["🙂", "slightly_smiling_face", 0], - ["🙃", "upside_down_face", 0], - ["☺️", "relaxed", 0], - ["😋", "yum", 0], - ["😌", "relieved", 0], - ["😍", "heart_eyes", 0], - ["🥰", "smiling_face_with_three_hearts", 0], - ["😘", "kissing_heart", 0], - ["😗", "kissing", 0], - ["😙", "kissing_smiling_eyes", 0], - ["😚", "kissing_closed_eyes", 0], - ["😜", "stuck_out_tongue_winking_eye", 0], - ["🤪", "zany", 0], - ["🤨", "raised_eyebrow", 0], - ["🧐", "monocle", 0], - ["😝", "stuck_out_tongue_closed_eyes", 0], - ["😛", "stuck_out_tongue", 0], - ["🤑", "money_mouth_face", 0], - ["🤓", "nerd_face", 0], - ["🥸", "disguised_face", 0], - ["😎", "sunglasses", 0], - ["🤩", "star_struck", 0], - ["🤡", "clown_face", 0], - ["🤠", "cowboy_hat_face", 0], - ["🤗", "hugs", 0], - ["😏", "smirk", 0], - ["😶", "no_mouth", 0], - ["😐", "neutral_face", 0], - ["😑", "expressionless", 0], - ["😒", "unamused", 0], - ["🙄", "roll_eyes", 0], - ["🤔", "thinking", 0], - ["🤥", "lying_face", 0], - ["🤭", "hand_over_mouth", 0], - ["🤫", "shushing", 0], - ["🤬", "symbols_over_mouth", 0], - ["🤯", "exploding_head", 0], - ["😳", "flushed", 0], - ["😞", "disappointed", 0], - ["😟", "worried", 0], - ["😠", "angry", 0], - ["😡", "rage", 0], - ["😔", "pensive", 0], - ["😕", "confused", 0], - ["🙁", "slightly_frowning_face", 0], - ["☹", "frowning_face", 0], - ["😣", "persevere", 0], - ["😖", "confounded", 0], - ["😫", "tired_face", 0], - ["😩", "weary", 0], - ["🥺", "pleading", 0], - ["😤", "triumph", 0], - ["😮", "open_mouth", 0], - ["😱", "scream", 0], - ["😨", "fearful", 0], - ["😰", "cold_sweat", 0], - ["😯", "hushed", 0], - ["😦", "frowning", 0], - ["😧", "anguished", 0], - ["😢", "cry", 0], - ["😥", "disappointed_relieved", 0], - ["🤤", "drooling_face", 0], - ["😪", "sleepy", 0], - ["😓", "sweat", 0], - ["🥵", "hot", 0], - ["🥶", "cold", 0], - ["😭", "sob", 0], - ["😵", "dizzy_face", 0], - ["😲", "astonished", 0], - ["🤐", "zipper_mouth_face", 0], - ["🤢", "nauseated_face", 0], - ["🤧", "sneezing_face", 0], - ["🤮", "vomiting", 0], - ["😷", "mask", 0], - ["🤒", "face_with_thermometer", 0], - ["🤕", "face_with_head_bandage", 0], - ["🥴", "woozy", 0], - ["🥱", "yawning", 0], - ["😴", "sleeping", 0], - ["💤", "zzz", 0], - ["😶🌫️", "face_in_clouds", 0], - ["😮💨", "face_exhaling", 0], - ["😵💫", "face_with_spiral_eyes", 0], - ["🫠", "melting_face", 0], - ["🫢", "face_with_open_eyes_and_hand_over_mouth", 0], - ["🫣", "face_with_peeking_eye", 0], - ["🫡", "saluting_face", 0], - ["🫥", "dotted_line_face", 0], - ["🫤", "face_with_diagonal_mouth", 0], - ["🥹", "face_holding_back_tears", 0], - ["🫨", "shaking_face", 0], - ["💩", "poop", 0], - ["😈", "smiling_imp", 0], - ["👿", "imp", 0], - ["👹", "japanese_ogre", 0], - ["👺", "japanese_goblin", 0], - ["💀", "skull", 0], - ["👻", "ghost", 0], - ["👽", "alien", 0], - ["🤖", "robot", 0], - ["😺", "smiley_cat", 0], - ["😸", "smile_cat", 0], - ["😹", "joy_cat", 0], - ["😻", "heart_eyes_cat", 0], - ["😼", "smirk_cat", 0], - ["😽", "kissing_cat", 0], - ["🙀", "scream_cat", 0], - ["😿", "crying_cat_face", 0], - ["😾", "pouting_cat", 0], - ["🤲", "palms_up", 1], - ["🙌", "raised_hands", 1], - ["👏", "clap", 1], - ["👋", "wave", 1], - ["🤙", "call_me_hand", 1], - ["👍", "+1", 1], - ["👎", "-1", 1], - ["👊", "facepunch", 1], - ["✊", "fist", 1], - ["🤛", "fist_left", 1], - ["🤜", "fist_right", 1], - ["🫷", "leftwards_pushing_hand", 1], - ["🫸", "rightwards_pushing_hand", 1], - ["✌", "v", 1], - ["👌", "ok_hand", 1], - ["✋", "raised_hand", 1], - ["🤚", "raised_back_of_hand", 1], - ["👐", "open_hands", 1], - ["💪", "muscle", 1], - ["🦾", "mechanical_arm", 1], - ["🙏", "pray", 1], - ["🦶", "foot", 1], - ["🦵", "leg", 1], - ["🦿", "mechanical_leg", 1], - ["🤝", "handshake", 1], - ["☝", "point_up", 1], - ["👆", "point_up_2", 1], - ["👇", "point_down", 1], - ["👈", "point_left", 1], - ["👉", "point_right", 1], - ["🖕", "fu", 1], - ["🖐", "raised_hand_with_fingers_splayed", 1], - ["🤟", "love_you", 1], - ["🤘", "metal", 1], - ["🤞", "crossed_fingers", 1], - ["🖖", "vulcan_salute", 1], - ["✍", "writing_hand", 1], - ["🫰", "hand_with_index_finger_and_thumb_crossed", 1], - ["🫱", "rightwards_hand", 1], - ["🫲", "leftwards_hand", 1], - ["🫳", "palm_down_hand", 1], - ["🫴", "palm_up_hand", 1], - ["🫵", "index_pointing_at_the_viewer", 1], - ["🫶", "heart_hands", 1], - ["🤏", "pinching_hand", 1], - ["🤌", "pinched_fingers", 1], - ["🤳", "selfie", 1], - ["💅", "nail_care", 1], - ["👄", "lips", 1], - ["🫦", "biting_lip", 1], - ["🦷", "tooth", 1], - ["👅", "tongue", 1], - ["👂", "ear", 1], - ["🦻", "ear_with_hearing_aid", 1], - ["👃", "nose", 1], - ["👁", "eye", 1], - ["👀", "eyes", 1], - ["🧠", "brain", 1], - ["🫀", "anatomical_heart", 1], - ["🫁", "lungs", 1], - ["👤", "bust_in_silhouette", 1], - ["👥", "busts_in_silhouette", 1], - ["🗣", "speaking_head", 1], - ["👶", "baby", 1], - ["🧒", "child", 1], - ["👦", "boy", 1], - ["👧", "girl", 1], - ["🧑", "adult", 1], - ["👨", "man", 1], - ["👩", "woman", 1], - ["🧑🦱", "curly_hair", 1], - ["👩🦱", "curly_hair_woman", 1], - ["👨🦱", "curly_hair_man", 1], - ["🧑🦰", "red_hair", 1], - ["👩🦰", "red_hair_woman", 1], - ["👨🦰", "red_hair_man", 1], - ["👱♀️", "blonde_woman", 1], - ["👱", "blonde_man", 1], - ["🧑🦳", "white_hair", 1], - ["👩🦳", "white_hair_woman", 1], - ["👨🦳", "white_hair_man", 1], - ["🧑🦲", "bald", 1], - ["👩🦲", "bald_woman", 1], - ["👨🦲", "bald_man", 1], - ["🧔", "bearded_person", 1], - ["🧓", "older_adult", 1], - ["👴", "older_man", 1], - ["👵", "older_woman", 1], - ["👲", "man_with_gua_pi_mao", 1], - ["🧕", "woman_with_headscarf", 1], - ["👳♀️", "woman_with_turban", 1], - ["👳", "man_with_turban", 1], - ["👮♀️", "policewoman", 1], - ["👮", "policeman", 1], - ["👷♀️", "construction_worker_woman", 1], - ["👷", "construction_worker_man", 1], - ["💂♀️", "guardswoman", 1], - ["💂", "guardsman", 1], - ["🕵️♀️", "female_detective", 1], - ["🕵", "male_detective", 1], - ["🧑⚕️", "health_worker", 1], - ["👩⚕️", "woman_health_worker", 1], - ["👨⚕️", "man_health_worker", 1], - ["🧑🌾", "farmer", 1], - ["👩🌾", "woman_farmer", 1], - ["👨🌾", "man_farmer", 1], - ["🧑🍳", "cook", 1], - ["👩🍳", "woman_cook", 1], - ["👨🍳", "man_cook", 1], - ["🧑🎓", "student", 1], - ["👩🎓", "woman_student", 1], - ["👨🎓", "man_student", 1], - ["🧑🎤", "singer", 1], - ["👩🎤", "woman_singer", 1], - ["👨🎤", "man_singer", 1], - ["🧑🏫", "teacher", 1], - ["👩🏫", "woman_teacher", 1], - ["👨🏫", "man_teacher", 1], - ["🧑🏭", "factory_worker", 1], - ["👩🏭", "woman_factory_worker", 1], - ["👨🏭", "man_factory_worker", 1], - ["🧑💻", "technologist", 1], - ["👩💻", "woman_technologist", 1], - ["👨💻", "man_technologist", 1], - ["🧑💼", "office_worker", 1], - ["👩💼", "woman_office_worker", 1], - ["👨💼", "man_office_worker", 1], - ["🧑🔧", "mechanic", 1], - ["👩🔧", "woman_mechanic", 1], - ["👨🔧", "man_mechanic", 1], - ["🧑🔬", "scientist", 1], - ["👩🔬", "woman_scientist", 1], - ["👨🔬", "man_scientist", 1], - ["🧑🎨", "artist", 1], - ["👩🎨", "woman_artist", 1], - ["👨🎨", "man_artist", 1], - ["🧑🚒", "firefighter", 1], - ["👩🚒", "woman_firefighter", 1], - ["👨🚒", "man_firefighter", 1], - ["🧑✈️", "pilot", 1], - ["👩✈️", "woman_pilot", 1], - ["👨✈️", "man_pilot", 1], - ["🧑🚀", "astronaut", 1], - ["👩🚀", "woman_astronaut", 1], - ["👨🚀", "man_astronaut", 1], - ["🧑⚖️", "judge", 1], - ["👩⚖️", "woman_judge", 1], - ["👨⚖️", "man_judge", 1], - ["🦸♀️", "woman_superhero", 1], - ["🦸♂️", "man_superhero", 1], - ["🦹♀️", "woman_supervillain", 1], - ["🦹♂️", "man_supervillain", 1], - ["🤶", "mrs_claus", 1], - ["🧑🎄", "mx_claus", 1], - ["🎅", "santa", 1], - ["🥷", "ninja", 1], - ["🧙♀️", "sorceress", 1], - ["🧙♂️", "wizard", 1], - ["🧝♀️", "woman_elf", 1], - ["🧝♂️", "man_elf", 1], - ["🧛♀️", "woman_vampire", 1], - ["🧛♂️", "man_vampire", 1], - ["🧟♀️", "woman_zombie", 1], - ["🧟♂️", "man_zombie", 1], - ["🧞♀️", "woman_genie", 1], - ["🧞♂️", "man_genie", 1], - ["🧜♀️", "mermaid", 1], - ["🧜♂️", "merman", 1], - ["🧚♀️", "woman_fairy", 1], - ["🧚♂️", "man_fairy", 1], - ["👼", "angel", 1], - ["🧌", "troll", 1], - ["🤰", "pregnant_woman", 1], - ["🫃", "pregnant_man", 1], - ["🫄", "pregnant_person", 1], - ["🫅", "person_with_crown", 1], - ["🤱", "breastfeeding", 1], - ["👩🍼", "woman_feeding_baby", 1], - ["👨🍼", "man_feeding_baby", 1], - ["🧑🍼", "person_feeding_baby", 1], - ["👸", "princess", 1], - ["🤴", "prince", 1], - ["👰", "person_with_veil", 1], - ["👰", "bride_with_veil", 1], - ["🤵", "person_in_tuxedo", 1], - ["🤵", "man_in_tuxedo", 1], - ["🏃♀️", "running_woman", 1], - ["🏃", "running_man", 1], - ["🚶♀️", "walking_woman", 1], - ["🚶", "walking_man", 1], - ["💃", "dancer", 1], - ["🕺", "man_dancing", 1], - ["👯", "dancing_women", 1], - ["👯♂️", "dancing_men", 1], - ["👫", "couple", 1], - ["🧑🤝🧑", "people_holding_hands", 1], - ["👬", "two_men_holding_hands", 1], - ["👭", "two_women_holding_hands", 1], - ["🫂", "people_hugging", 1], - ["🙇♀️", "bowing_woman", 1], - ["🙇", "bowing_man", 1], - ["🤦♂️", "man_facepalming", 1], - ["🤦♀️", "woman_facepalming", 1], - ["🤷", "woman_shrugging", 1], - ["🤷♂️", "man_shrugging", 1], - ["💁", "tipping_hand_woman", 1], - ["💁♂️", "tipping_hand_man", 1], - ["🙅", "no_good_woman", 1], - ["🙅♂️", "no_good_man", 1], - ["🙆", "ok_woman", 1], - ["🙆♂️", "ok_man", 1], - ["🙋", "raising_hand_woman", 1], - ["🙋♂️", "raising_hand_man", 1], - ["🙎", "pouting_woman", 1], - ["🙎♂️", "pouting_man", 1], - ["🙍", "frowning_woman", 1], - ["🙍♂️", "frowning_man", 1], - ["💇", "haircut_woman", 1], - ["💇♂️", "haircut_man", 1], - ["💆", "massage_woman", 1], - ["💆♂️", "massage_man", 1], - ["🧖♀️", "woman_in_steamy_room", 1], - ["🧖♂️", "man_in_steamy_room", 1], - ["🧏♀️", "woman_deaf", 1], - ["🧏♂️", "man_deaf", 1], - ["🧍♀️", "woman_standing", 1], - ["🧍♂️", "man_standing", 1], - ["🧎♀️", "woman_kneeling", 1], - ["🧎♂️", "man_kneeling", 1], - ["🧑🦯", "person_with_probing_cane", 1], - ["👩🦯", "woman_with_probing_cane", 1], - ["👨🦯", "man_with_probing_cane", 1], - ["🧑🦼", "person_in_motorized_wheelchair", 1], - ["👩🦼", "woman_in_motorized_wheelchair", 1], - ["👨🦼", "man_in_motorized_wheelchair", 1], - ["🧑🦽", "person_in_manual_wheelchair", 1], - ["👩🦽", "woman_in_manual_wheelchair", 1], - ["👨🦽", "man_in_manual_wheelchair", 1], - ["💑", "couple_with_heart_woman_man", 1], - ["👩❤️👩", "couple_with_heart_woman_woman", 1], - ["👨❤️👨", "couple_with_heart_man_man", 1], - ["💏", "couplekiss_man_woman", 1], - ["👩❤️💋👩", "couplekiss_woman_woman", 1], - ["👨❤️💋👨", "couplekiss_man_man", 1], - ["👪", "family_man_woman_boy", 1], - ["👨👩👧", "family_man_woman_girl", 1], - ["👨👩👧👦", "family_man_woman_girl_boy", 1], - ["👨👩👦👦", "family_man_woman_boy_boy", 1], - ["👨👩👧👧", "family_man_woman_girl_girl", 1], - ["👩👩👦", "family_woman_woman_boy", 1], - ["👩👩👧", "family_woman_woman_girl", 1], - ["👩👩👧👦", "family_woman_woman_girl_boy", 1], - ["👩👩👦👦", "family_woman_woman_boy_boy", 1], - ["👩👩👧👧", "family_woman_woman_girl_girl", 1], - ["👨👨👦", "family_man_man_boy", 1], - ["👨👨👧", "family_man_man_girl", 1], - ["👨👨👧👦", "family_man_man_girl_boy", 1], - ["👨👨👦👦", "family_man_man_boy_boy", 1], - ["👨👨👧👧", "family_man_man_girl_girl", 1], - ["👩👦", "family_woman_boy", 1], - ["👩👧", "family_woman_girl", 1], - ["👩👧👦", "family_woman_girl_boy", 1], - ["👩👦👦", "family_woman_boy_boy", 1], - ["👩👧👧", "family_woman_girl_girl", 1], - ["👨👦", "family_man_boy", 1], - ["👨👧", "family_man_girl", 1], - ["👨👧👦", "family_man_girl_boy", 1], - ["👨👦👦", "family_man_boy_boy", 1], - ["👨👧👧", "family_man_girl_girl", 1], - ["🧶", "yarn", 1], - ["🧵", "thread", 1], - ["🧥", "coat", 1], - ["🥼", "labcoat", 1], - ["👚", "womans_clothes", 1], - ["👕", "tshirt", 1], - ["👖", "jeans", 1], - ["👔", "necktie", 1], - ["👗", "dress", 1], - ["👙", "bikini", 1], - ["🩱", "one_piece_swimsuit", 1], - ["👘", "kimono", 1], - ["🥻", "sari", 1], - ["🩲", "briefs", 1], - ["🩳", "shorts", 1], - ["💄", "lipstick", 1], - ["💋", "kiss", 1], - ["👣", "footprints", 1], - ["🥿", "flat_shoe", 1], - ["👠", "high_heel", 1], - ["👡", "sandal", 1], - ["👢", "boot", 1], - ["👞", "mans_shoe", 1], - ["👟", "athletic_shoe", 1], - ["🩴", "thong_sandal", 1], - ["🩰", "ballet_shoes", 1], - ["🧦", "socks", 1], - ["🧤", "gloves", 1], - ["🧣", "scarf", 1], - ["👒", "womans_hat", 1], - ["🎩", "tophat", 1], - ["🧢", "billed_hat", 1], - ["⛑", "rescue_worker_helmet", 1], - ["🪖", "military_helmet", 1], - ["🎓", "mortar_board", 1], - ["👑", "crown", 1], - ["🎒", "school_satchel", 1], - ["🧳", "luggage", 1], - ["👝", "pouch", 1], - ["👛", "purse", 1], - ["👜", "handbag", 1], - ["💼", "briefcase", 1], - ["👓", "eyeglasses", 1], - ["🕶", "dark_sunglasses", 1], - ["🥽", "goggles", 1], - ["💍", "ring", 1], - ["🌂", "closed_umbrella", 1], - ["🐶", "dog", 2], - ["🐱", "cat", 2], - ["🐈⬛", "black_cat", 2], - ["🐭", "mouse", 2], - ["🐹", "hamster", 2], - ["🐰", "rabbit", 2], - ["🦊", "fox_face", 2], - ["🐻", "bear", 2], - ["🐼", "panda_face", 2], - ["🐨", "koala", 2], - ["🐯", "tiger", 2], - ["🦁", "lion", 2], - ["🐮", "cow", 2], - ["🐷", "pig", 2], - ["🐽", "pig_nose", 2], - ["🐸", "frog", 2], - ["🦑", "squid", 2], - ["🐙", "octopus", 2], - ["🪼", "jellyfish", 2], - ["🦐", "shrimp", 2], - ["🐵", "monkey_face", 2], - ["🦍", "gorilla", 2], - ["🙈", "see_no_evil", 2], - ["🙉", "hear_no_evil", 2], - ["🙊", "speak_no_evil", 2], - ["🐒", "monkey", 2], - ["🐔", "chicken", 2], - ["🐧", "penguin", 2], - ["🐦", "bird", 2], - ["🐤", "baby_chick", 2], - ["🐣", "hatching_chick", 2], - ["🐥", "hatched_chick", 2], - ["🪿", "goose", 2], - ["🦆", "duck", 2], - ["🐦⬛", "black_bird", 2], - ["🦅", "eagle", 2], - ["🦉", "owl", 2], - ["🦇", "bat", 2], - ["🐺", "wolf", 2], - ["🐗", "boar", 2], - ["🐴", "horse", 2], - ["🦄", "unicorn", 2], - ["🫎", "moose", 2], - ["🐝", "honeybee", 2], - ["🐛", "bug", 2], - ["🦋", "butterfly", 2], - ["🐌", "snail", 2], - ["🐞", "lady_beetle", 2], - ["🐜", "ant", 2], - ["🦗", "grasshopper", 2], - ["🕷", "spider", 2], - ["🪲", "beetle", 2], - ["🪳", "cockroach", 2], - ["🪰", "fly", 2], - ["🪱", "worm", 2], - ["🦂", "scorpion", 2], - ["🦀", "crab", 2], - ["🐍", "snake", 2], - ["🦎", "lizard", 2], - ["🦖", "t-rex", 2], - ["🦕", "sauropod", 2], - ["🐢", "turtle", 2], - ["🐠", "tropical_fish", 2], - ["🐟", "fish", 2], - ["🐡", "blowfish", 2], - ["🐬", "dolphin", 2], - ["🦈", "shark", 2], - ["🐳", "whale", 2], - ["🐋", "whale2", 2], - ["🐊", "crocodile", 2], - ["🐆", "leopard", 2], - ["🦓", "zebra", 2], - ["🐅", "tiger2", 2], - ["🐃", "water_buffalo", 2], - ["🐂", "ox", 2], - ["🐄", "cow2", 2], - ["🦌", "deer", 2], - ["🐪", "dromedary_camel", 2], - ["🐫", "camel", 2], - ["🦒", "giraffe", 2], - ["🐘", "elephant", 2], - ["🦏", "rhinoceros", 2], - ["🐐", "goat", 2], - ["🐏", "ram", 2], - ["🐑", "sheep", 2], - ["🫏", "donkey", 2], - ["🐎", "racehorse", 2], - ["🐖", "pig2", 2], - ["🐀", "rat", 2], - ["🐁", "mouse2", 2], - ["🐓", "rooster", 2], - ["🦃", "turkey", 2], - ["🕊", "dove", 2], - ["🐕", "dog2", 2], - ["🐩", "poodle", 2], - ["🐈", "cat2", 2], - ["🐇", "rabbit2", 2], - ["🐿", "chipmunk", 2], - ["🦔", "hedgehog", 2], - ["🦝", "raccoon", 2], - ["🦙", "llama", 2], - ["🦛", "hippopotamus", 2], - ["🦘", "kangaroo", 2], - ["🦡", "badger", 2], - ["🦢", "swan", 2], - ["🦚", "peacock", 2], - ["🦜", "parrot", 2], - ["🦞", "lobster", 2], - ["🦠", "microbe", 2], - ["🦟", "mosquito", 2], - ["🦬", "bison", 2], - ["🦣", "mammoth", 2], - ["🦫", "beaver", 2], - ["🐻❄️", "polar_bear", 2], - ["🦤", "dodo", 2], - ["🪶", "feather", 2], - ["🪽", "wing", 2], - ["🦭", "seal", 2], - ["🐾", "paw_prints", 2], - ["🐉", "dragon", 2], - ["🐲", "dragon_face", 2], - ["🦧", "orangutan", 2], - ["🦮", "guide_dog", 2], - ["🐕🦺", "service_dog", 2], - ["🦥", "sloth", 2], - ["🦦", "otter", 2], - ["🦨", "skunk", 2], - ["🦩", "flamingo", 2], - ["🌵", "cactus", 2], - ["🎄", "christmas_tree", 2], - ["🌲", "evergreen_tree", 2], - ["🌳", "deciduous_tree", 2], - ["🌴", "palm_tree", 2], - ["🌱", "seedling", 2], - ["🌿", "herb", 2], - ["☘", "shamrock", 2], - ["🍀", "four_leaf_clover", 2], - ["🎍", "bamboo", 2], - ["🎋", "tanabata_tree", 2], - ["🍃", "leaves", 2], - ["🍂", "fallen_leaf", 2], - ["🍁", "maple_leaf", 2], - ["🌾", "ear_of_rice", 2], - ["🌺", "hibiscus", 2], - ["🌻", "sunflower", 2], - ["🌹", "rose", 2], - ["🥀", "wilted_flower", 2], - ["🪻", "hyacinth", 2], - ["🌷", "tulip", 2], - ["🌼", "blossom", 2], - ["🌸", "cherry_blossom", 2], - ["💐", "bouquet", 2], - ["🍄", "mushroom", 2], - ["🪴", "potted_plant", 2], - ["🌰", "chestnut", 2], - ["🎃", "jack_o_lantern", 2], - ["🐚", "shell", 2], - ["🕸", "spider_web", 2], - ["🌎", "earth_americas", 2], - ["🌍", "earth_africa", 2], - ["🌏", "earth_asia", 2], - ["🪐", "ringed_planet", 2], - ["🌕", "full_moon", 2], - ["🌖", "waning_gibbous_moon", 2], - ["🌗", "last_quarter_moon", 2], - ["🌘", "waning_crescent_moon", 2], - ["🌑", "new_moon", 2], - ["🌒", "waxing_crescent_moon", 2], - ["🌓", "first_quarter_moon", 2], - ["🌔", "waxing_gibbous_moon", 2], - ["🌚", "new_moon_with_face", 2], - ["🌝", "full_moon_with_face", 2], - ["🌛", "first_quarter_moon_with_face", 2], - ["🌜", "last_quarter_moon_with_face", 2], - ["🌞", "sun_with_face", 2], - ["🌙", "crescent_moon", 2], - ["⭐", "star", 2], - ["🌟", "star2", 2], - ["💫", "dizzy", 2], - ["✨", "sparkles", 2], - ["☄", "comet", 2], - ["☀️", "sunny", 2], - ["🌤", "sun_behind_small_cloud", 2], - ["⛅", "partly_sunny", 2], - ["🌥", "sun_behind_large_cloud", 2], - ["🌦", "sun_behind_rain_cloud", 2], - ["☁️", "cloud", 2], - ["🌧", "cloud_with_rain", 2], - ["⛈", "cloud_with_lightning_and_rain", 2], - ["🌩", "cloud_with_lightning", 2], - ["⚡", "zap", 2], - ["🔥", "fire", 2], - ["💥", "boom", 2], - ["❄️", "snowflake", 2], - ["🌨", "cloud_with_snow", 2], - ["⛄", "snowman", 2], - ["☃", "snowman_with_snow", 2], - ["🌬", "wind_face", 2], - ["💨", "dash", 2], - ["🌪", "tornado", 2], - ["🌫", "fog", 2], - ["☂", "open_umbrella", 2], - ["☔", "umbrella", 2], - ["💧", "droplet", 2], - ["💦", "sweat_drops", 2], - ["🌊", "ocean", 2], - ["🪷", "lotus", 2], - ["🪸", "coral", 2], - ["🪹", "empty_nest", 2], - ["🪺", "nest_with_eggs", 2], - ["🍏", "green_apple", 3], - ["🍎", "apple", 3], - ["🍐", "pear", 3], - ["🍊", "tangerine", 3], - ["🍋", "lemon", 3], - ["🍌", "banana", 3], - ["🍉", "watermelon", 3], - ["🍇", "grapes", 3], - ["🍓", "strawberry", 3], - ["🍈", "melon", 3], - ["🍒", "cherries", 3], - ["🍑", "peach", 3], - ["🍍", "pineapple", 3], - ["🥥", "coconut", 3], - ["🥝", "kiwi_fruit", 3], - ["🥭", "mango", 3], - ["🥑", "avocado", 3], - ["🫛", "pea_pod", 3], - ["🥦", "broccoli", 3], - ["🍅", "tomato", 3], - ["🍆", "eggplant", 3], - ["🥒", "cucumber", 3], - ["🫐", "blueberries", 3], - ["🫒", "olive", 3], - ["🫑", "bell_pepper", 3], - ["🥕", "carrot", 3], - ["🌶", "hot_pepper", 3], - ["🥔", "potato", 3], - ["🌽", "corn", 3], - ["🥬", "leafy_greens", 3], - ["🍠", "sweet_potato", 3], - ["🫚", "ginger_root", 3], - ["🥜", "peanuts", 3], - ["🧄", "garlic", 3], - ["🧅", "onion", 3], - ["🍯", "honey_pot", 3], - ["🥐", "croissant", 3], - ["🍞", "bread", 3], - ["🥖", "baguette_bread", 3], - ["🥯", "bagel", 3], - ["🥨", "pretzel", 3], - ["🧀", "cheese", 3], - ["🥚", "egg", 3], - ["🥓", "bacon", 3], - ["🥩", "steak", 3], - ["🥞", "pancakes", 3], - ["🍗", "poultry_leg", 3], - ["🍖", "meat_on_bone", 3], - ["🦴", "bone", 3], - ["🍤", "fried_shrimp", 3], - ["🍳", "fried_egg", 3], - ["🍔", "hamburger", 3], - ["🍟", "fries", 3], - ["🥙", "stuffed_flatbread", 3], - ["🌭", "hotdog", 3], - ["🍕", "pizza", 3], - ["🥪", "sandwich", 3], - ["🥫", "canned_food", 3], - ["🍝", "spaghetti", 3], - ["🌮", "taco", 3], - ["🌯", "burrito", 3], - ["🥗", "green_salad", 3], - ["🥘", "shallow_pan_of_food", 3], - ["🍜", "ramen", 3], - ["🍲", "stew", 3], - ["🍥", "fish_cake", 3], - ["🥠", "fortune_cookie", 3], - ["🍣", "sushi", 3], - ["🍱", "bento", 3], - ["🍛", "curry", 3], - ["🍙", "rice_ball", 3], - ["🍚", "rice", 3], - ["🍘", "rice_cracker", 3], - ["🍢", "oden", 3], - ["🍡", "dango", 3], - ["🍧", "shaved_ice", 3], - ["🍨", "ice_cream", 3], - ["🍦", "icecream", 3], - ["🥧", "pie", 3], - ["🍰", "cake", 3], - ["🧁", "cupcake", 3], - ["🥮", "moon_cake", 3], - ["🎂", "birthday", 3], - ["🍮", "custard", 3], - ["🍬", "candy", 3], - ["🍭", "lollipop", 3], - ["🍫", "chocolate_bar", 3], - ["🍿", "popcorn", 3], - ["🥟", "dumpling", 3], - ["🍩", "doughnut", 3], - ["🍪", "cookie", 3], - ["🧇", "waffle", 3], - ["🧆", "falafel", 3], - ["🧈", "butter", 3], - ["🦪", "oyster", 3], - ["🫓", "flatbread", 3], - ["🫔", "tamale", 3], - ["🫕", "fondue", 3], - ["🥛", "milk_glass", 3], - ["🍺", "beer", 3], - ["🍻", "beers", 3], - ["🥂", "clinking_glasses", 3], - ["🍷", "wine_glass", 3], - ["🥃", "tumbler_glass", 3], - ["🍸", "cocktail", 3], - ["🍹", "tropical_drink", 3], - ["🍾", "champagne", 3], - ["🍶", "sake", 3], - ["🍵", "tea", 3], - ["🥤", "cup_with_straw", 3], - ["☕", "coffee", 3], - ["🫖", "teapot", 3], - ["🧋", "bubble_tea", 3], - ["🍼", "baby_bottle", 3], - ["🧃", "beverage_box", 3], - ["🧉", "mate", 3], - ["🧊", "ice_cube", 3], - ["🧂", "salt", 3], - ["🥄", "spoon", 3], - ["🍴", "fork_and_knife", 3], - ["🍽", "plate_with_cutlery", 3], - ["🥣", "bowl_with_spoon", 3], - ["🥡", "takeout_box", 3], - ["🥢", "chopsticks", 3], - ["🫗", "pouring_liquid", 3], - ["🫘", "beans", 3], - ["🫙", "jar", 3], - ["⚽", "soccer", 4], - ["🏀", "basketball", 4], - ["🏈", "football", 4], - ["⚾", "baseball", 4], - ["🥎", "softball", 4], - ["🎾", "tennis", 4], - ["🏐", "volleyball", 4], - ["🏉", "rugby_football", 4], - ["🥏", "flying_disc", 4], - ["🎱", "8ball", 4], - ["⛳", "golf", 4], - ["🏌️♀️", "golfing_woman", 4], - ["🏌", "golfing_man", 4], - ["🏓", "ping_pong", 4], - ["🏸", "badminton", 4], - ["🥅", "goal_net", 4], - ["🏒", "ice_hockey", 4], - ["🏑", "field_hockey", 4], - ["🥍", "lacrosse", 4], - ["🏏", "cricket", 4], - ["🎿", "ski", 4], - ["⛷", "skier", 4], - ["🏂", "snowboarder", 4], - ["🤺", "person_fencing", 4], - ["🤼♀️", "women_wrestling", 4], - ["🤼♂️", "men_wrestling", 4], - ["🤸♀️", "woman_cartwheeling", 4], - ["🤸♂️", "man_cartwheeling", 4], - ["🤾♀️", "woman_playing_handball", 4], - ["🤾♂️", "man_playing_handball", 4], - ["⛸", "ice_skate", 4], - ["🥌", "curling_stone", 4], - ["🛹", "skateboard", 4], - ["🛷", "sled", 4], - ["🏹", "bow_and_arrow", 4], - ["🎣", "fishing_pole_and_fish", 4], - ["🥊", "boxing_glove", 4], - ["🥋", "martial_arts_uniform", 4], - ["🚣♀️", "rowing_woman", 4], - ["🚣", "rowing_man", 4], - ["🧗♀️", "climbing_woman", 4], - ["🧗♂️", "climbing_man", 4], - ["🏊♀️", "swimming_woman", 4], - ["🏊", "swimming_man", 4], - ["🤽♀️", "woman_playing_water_polo", 4], - ["🤽♂️", "man_playing_water_polo", 4], - ["🧘♀️", "woman_in_lotus_position", 4], - ["🧘♂️", "man_in_lotus_position", 4], - ["🏄♀️", "surfing_woman", 4], - ["🏄", "surfing_man", 4], - ["🛀", "bath", 4], - ["⛹️♀️", "basketball_woman", 4], - ["⛹", "basketball_man", 4], - ["🏋️♀️", "weight_lifting_woman", 4], - ["🏋", "weight_lifting_man", 4], - ["🚴♀️", "biking_woman", 4], - ["🚴", "biking_man", 4], - ["🚵♀️", "mountain_biking_woman", 4], - ["🚵", "mountain_biking_man", 4], - ["🏇", "horse_racing", 4], - ["🤿", "diving_mask", 4], - ["🪀", "yo_yo", 4], - ["🪁", "kite", 4], - ["🦺", "safety_vest", 4], - ["🪡", "sewing_needle", 4], - ["🪢", "knot", 4], - ["🕴", "business_suit_levitating", 4], - ["🏆", "trophy", 4], - ["🎽", "running_shirt_with_sash", 4], - ["🏅", "medal_sports", 4], - ["🎖", "medal_military", 4], - ["🥇", "1st_place_medal", 4], - ["🥈", "2nd_place_medal", 4], - ["🥉", "3rd_place_medal", 4], - ["🎗", "reminder_ribbon", 4], - ["🏵", "rosette", 4], - ["🎫", "ticket", 4], - ["🎟", "tickets", 4], - ["🎭", "performing_arts", 4], - ["🎨", "art", 4], - ["🎪", "circus_tent", 4], - ["🤹♀️", "woman_juggling", 4], - ["🤹♂️", "man_juggling", 4], - ["🎤", "microphone", 4], - ["🎧", "headphones", 4], - ["🎼", "musical_score", 4], - ["🎹", "musical_keyboard", 4], - ["🪇", "maracas", 4], - ["🥁", "drum", 4], - ["🎷", "saxophone", 4], - ["🎺", "trumpet", 4], - ["🪈", "flute", 4], - ["🎸", "guitar", 4], - ["🎻", "violin", 4], - ["🪕", "banjo", 4], - ["🪗", "accordion", 4], - ["🪘", "long_drum", 4], - ["🎬", "clapper", 4], - ["🎮", "video_game", 4], - ["👾", "space_invader", 4], - ["🎯", "dart", 4], - ["🎲", "game_die", 4], - ["♟️", "chess_pawn", 4], - ["🎰", "slot_machine", 4], - ["🧩", "jigsaw", 4], - ["🎳", "bowling", 4], - ["🪄", "magic_wand", 4], - ["🪅", "pinata", 4], - ["🪆", "nesting_dolls", 4], - ["🪬", "hamsa", 4], - ["🪩", "mirror_ball", 4], - ["🚗", "red_car", 5], - ["🚕", "taxi", 5], - ["🚙", "blue_car", 5], - ["🚌", "bus", 5], - ["🚎", "trolleybus", 5], - ["🏎", "racing_car", 5], - ["🚓", "police_car", 5], - ["🚑", "ambulance", 5], - ["🚒", "fire_engine", 5], - ["🚐", "minibus", 5], - ["🚚", "truck", 5], - ["🚛", "articulated_lorry", 5], - ["🚜", "tractor", 5], - ["🛴", "kick_scooter", 5], - ["🏍", "motorcycle", 5], - ["🚲", "bike", 5], - ["🛵", "motor_scooter", 5], - ["🦽", "manual_wheelchair", 5], - ["🦼", "motorized_wheelchair", 5], - ["🛺", "auto_rickshaw", 5], - ["🪂", "parachute", 5], - ["🚨", "rotating_light", 5], - ["🚔", "oncoming_police_car", 5], - ["🚍", "oncoming_bus", 5], - ["🚘", "oncoming_automobile", 5], - ["🚖", "oncoming_taxi", 5], - ["🚡", "aerial_tramway", 5], - ["🚠", "mountain_cableway", 5], - ["🚟", "suspension_railway", 5], - ["🚃", "railway_car", 5], - ["🚋", "train", 5], - ["🚝", "monorail", 5], - ["🚄", "bullettrain_side", 5], - ["🚅", "bullettrain_front", 5], - ["🚈", "light_rail", 5], - ["🚞", "mountain_railway", 5], - ["🚂", "steam_locomotive", 5], - ["🚆", "train2", 5], - ["🚇", "metro", 5], - ["🚊", "tram", 5], - ["🚉", "station", 5], - ["🛸", "flying_saucer", 5], - ["🚁", "helicopter", 5], - ["🛩", "small_airplane", 5], - ["✈️", "airplane", 5], - ["🛫", "flight_departure", 5], - ["🛬", "flight_arrival", 5], - ["⛵", "sailboat", 5], - ["🛥", "motor_boat", 5], - ["🚤", "speedboat", 5], - ["⛴", "ferry", 5], - ["🛳", "passenger_ship", 5], - ["🚀", "rocket", 5], - ["🛰", "artificial_satellite", 5], - ["🛻", "pickup_truck", 5], - ["🛼", "roller_skate", 5], - ["💺", "seat", 5], - ["🛶", "canoe", 5], - ["⚓", "anchor", 5], - ["🚧", "construction", 5], - ["⛽", "fuelpump", 5], - ["🚏", "busstop", 5], - ["🚦", "vertical_traffic_light", 5], - ["🚥", "traffic_light", 5], - ["🏁", "checkered_flag", 5], - ["🚢", "ship", 5], - ["🎡", "ferris_wheel", 5], - ["🎢", "roller_coaster", 5], - ["🎠", "carousel_horse", 5], - ["🏗", "building_construction", 5], - ["🌁", "foggy", 5], - ["🏭", "factory", 5], - ["⛲", "fountain", 5], - ["🎑", "rice_scene", 5], - ["⛰", "mountain", 5], - ["🏔", "mountain_snow", 5], - ["🗻", "mount_fuji", 5], - ["🌋", "volcano", 5], - ["🗾", "japan", 5], - ["🏕", "camping", 5], - ["⛺", "tent", 5], - ["🏞", "national_park", 5], - ["🛣", "motorway", 5], - ["🛤", "railway_track", 5], - ["🌅", "sunrise", 5], - ["🌄", "sunrise_over_mountains", 5], - ["🏜", "desert", 5], - ["🏖", "beach_umbrella", 5], - ["🏝", "desert_island", 5], - ["🌇", "city_sunrise", 5], - ["🌆", "city_sunset", 5], - ["🏙", "cityscape", 5], - ["🌃", "night_with_stars", 5], - ["🌉", "bridge_at_night", 5], - ["🌌", "milky_way", 5], - ["🌠", "stars", 5], - ["🎇", "sparkler", 5], - ["🎆", "fireworks", 5], - ["🌈", "rainbow", 5], - ["🏘", "houses", 5], - ["🏰", "european_castle", 5], - ["🏯", "japanese_castle", 5], - ["🗼", "tokyo_tower", 5], - ["", "shibuya_109", 5], - ["🏟", "stadium", 5], - ["🗽", "statue_of_liberty", 5], - ["🏠", "house", 5], - ["🏡", "house_with_garden", 5], - ["🏚", "derelict_house", 5], - ["🏢", "office", 5], - ["🏬", "department_store", 5], - ["🏣", "post_office", 5], - ["🏤", "european_post_office", 5], - ["🏥", "hospital", 5], - ["🏦", "bank", 5], - ["🏨", "hotel", 5], - ["🏪", "convenience_store", 5], - ["🏫", "school", 5], - ["🏩", "love_hotel", 5], - ["💒", "wedding", 5], - ["🏛", "classical_building", 5], - ["⛪", "church", 5], - ["🕌", "mosque", 5], - ["🕍", "synagogue", 5], - ["🕋", "kaaba", 5], - ["⛩", "shinto_shrine", 5], - ["🛕", "hindu_temple", 5], - ["🪨", "rock", 5], - ["🪵", "wood", 5], - ["🛖", "hut", 5], - ["🛝", "playground_slide", 5], - ["🛞", "wheel", 5], - ["🛟", "ring_buoy", 5], - ["⌚", "watch", 6], - ["📱", "iphone", 6], - ["📲", "calling", 6], - ["💻", "computer", 6], - ["⌨", "keyboard", 6], - ["🖥", "desktop_computer", 6], - ["🖨", "printer", 6], - ["🖱", "computer_mouse", 6], - ["🖲", "trackball", 6], - ["🕹", "joystick", 6], - ["🗜", "clamp", 6], - ["💽", "minidisc", 6], - ["💾", "floppy_disk", 6], - ["💿", "cd", 6], - ["📀", "dvd", 6], - ["📼", "vhs", 6], - ["📷", "camera", 6], - ["📸", "camera_flash", 6], - ["📹", "video_camera", 6], - ["🎥", "movie_camera", 6], - ["📽", "film_projector", 6], - ["🎞", "film_strip", 6], - ["📞", "telephone_receiver", 6], - ["☎️", "phone", 6], - ["📟", "pager", 6], - ["📠", "fax", 6], - ["📺", "tv", 6], - ["📻", "radio", 6], - ["🎙", "studio_microphone", 6], - ["🎚", "level_slider", 6], - ["🎛", "control_knobs", 6], - ["🧭", "compass", 6], - ["⏱", "stopwatch", 6], - ["⏲", "timer_clock", 6], - ["⏰", "alarm_clock", 6], - ["🕰", "mantelpiece_clock", 6], - ["⏳", "hourglass_flowing_sand", 6], - ["⌛", "hourglass", 6], - ["📡", "satellite", 6], - ["🔋", "battery", 6], - ["🪫", "low_battery", 6], - ["🔌", "electric_plug", 6], - ["💡", "bulb", 6], - ["🔦", "flashlight", 6], - ["🕯", "candle", 6], - ["🧯", "fire_extinguisher", 6], - ["🗑", "wastebasket", 6], - ["🛢", "oil_drum", 6], - ["💸", "money_with_wings", 6], - ["💵", "dollar", 6], - ["💴", "yen", 6], - ["💶", "euro", 6], - ["💷", "pound", 6], - ["💰", "moneybag", 6], - ["🪙", "coin", 6], - ["💳", "credit_card", 6], - ["🪪", "identification_card", 6], - ["💎", "gem", 6], - ["⚖", "balance_scale", 6], - ["🧰", "toolbox", 6], - ["🔧", "wrench", 6], - ["🔨", "hammer", 6], - ["⚒", "hammer_and_pick", 6], - ["🛠", "hammer_and_wrench", 6], - ["⛏", "pick", 6], - ["🪓", "axe", 6], - ["🦯", "probing_cane", 6], - ["🔩", "nut_and_bolt", 6], - ["⚙", "gear", 6], - ["🪃", "boomerang", 6], - ["🪚", "carpentry_saw", 6], - ["🪛", "screwdriver", 6], - ["🪝", "hook", 6], - ["🪜", "ladder", 6], - ["🧱", "brick", 6], - ["⛓", "chains", 6], - ["🧲", "magnet", 6], - ["🔫", "gun", 6], - ["💣", "bomb", 6], - ["🧨", "firecracker", 6], - ["🔪", "hocho", 6], - ["🗡", "dagger", 6], - ["⚔", "crossed_swords", 6], - ["🛡", "shield", 6], - ["🚬", "smoking", 6], - ["☠", "skull_and_crossbones", 6], - ["⚰", "coffin", 6], - ["⚱", "funeral_urn", 6], - ["🏺", "amphora", 6], - ["🔮", "crystal_ball", 6], - ["📿", "prayer_beads", 6], - ["🧿", "nazar_amulet", 6], - ["💈", "barber", 6], - ["⚗", "alembic", 6], - ["🔭", "telescope", 6], - ["🔬", "microscope", 6], - ["🕳", "hole", 6], - ["💊", "pill", 6], - ["💉", "syringe", 6], - ["🩸", "drop_of_blood", 6], - ["🩹", "adhesive_bandage", 6], - ["🩺", "stethoscope", 6], - ["🪒", "razor", 6], - ["🪮", "hair_pick", 6], - ["🩻", "xray", 6], - ["🩼", "crutch", 6], - ["🧬", "dna", 6], - ["🧫", "petri_dish", 6], - ["🧪", "test_tube", 6], - ["🌡", "thermometer", 6], - ["🧹", "broom", 6], - ["🧺", "basket", 6], - ["🧻", "toilet_paper", 6], - ["🏷", "label", 6], - ["🔖", "bookmark", 6], - ["🚽", "toilet", 6], - ["🚿", "shower", 6], - ["🛁", "bathtub", 6], - ["🧼", "soap", 6], - ["🧽", "sponge", 6], - ["🧴", "lotion_bottle", 6], - ["🔑", "key", 6], - ["🗝", "old_key", 6], - ["🛋", "couch_and_lamp", 6], - ["🪔", "diya_Lamp", 6], - ["🛌", "sleeping_bed", 6], - ["🛏", "bed", 6], - ["🚪", "door", 6], - ["🪑", "chair", 6], - ["🛎", "bellhop_bell", 6], - ["🧸", "teddy_bear", 6], - ["🖼", "framed_picture", 6], - ["🗺", "world_map", 6], - ["🛗", "elevator", 6], - ["🪞", "mirror", 6], - ["🪟", "window", 6], - ["🪠", "plunger", 6], - ["🪤", "mouse_trap", 6], - ["🪣", "bucket", 6], - ["🪥", "toothbrush", 6], - ["🫧", "bubbles", 6], - ["⛱", "parasol_on_ground", 6], - ["🗿", "moyai", 6], - ["🛍", "shopping", 6], - ["🛒", "shopping_cart", 6], - ["🎈", "balloon", 6], - ["🎏", "flags", 6], - ["🎀", "ribbon", 6], - ["🎁", "gift", 6], - ["🎊", "confetti_ball", 6], - ["🎉", "tada", 6], - ["🎎", "dolls", 6], - ["🪭", "folding_hand_fan", 6], - ["🎐", "wind_chime", 6], - ["🎌", "crossed_flags", 6], - ["🏮", "izakaya_lantern", 6], - ["🧧", "red_envelope", 6], - ["✉️", "email", 6], - ["📩", "envelope_with_arrow", 6], - ["📨", "incoming_envelope", 6], - ["📧", "e-mail", 6], - ["💌", "love_letter", 6], - ["📮", "postbox", 6], - ["📪", "mailbox_closed", 6], - ["📫", "mailbox", 6], - ["📬", "mailbox_with_mail", 6], - ["📭", "mailbox_with_no_mail", 6], - ["📦", "package", 6], - ["📯", "postal_horn", 6], - ["📥", "inbox_tray", 6], - ["📤", "outbox_tray", 6], - ["📜", "scroll", 6], - ["📃", "page_with_curl", 6], - ["📑", "bookmark_tabs", 6], - ["🧾", "receipt", 6], - ["📊", "bar_chart", 6], - ["📈", "chart_with_upwards_trend", 6], - ["📉", "chart_with_downwards_trend", 6], - ["📄", "page_facing_up", 6], - ["📅", "date", 6], - ["📆", "calendar", 6], - ["🗓", "spiral_calendar", 6], - ["📇", "card_index", 6], - ["🗃", "card_file_box", 6], - ["🗳", "ballot_box", 6], - ["🗄", "file_cabinet", 6], - ["📋", "clipboard", 6], - ["🗒", "spiral_notepad", 6], - ["📁", "file_folder", 6], - ["📂", "open_file_folder", 6], - ["🗂", "card_index_dividers", 6], - ["🗞", "newspaper_roll", 6], - ["📰", "newspaper", 6], - ["📓", "notebook", 6], - ["📕", "closed_book", 6], - ["📗", "green_book", 6], - ["📘", "blue_book", 6], - ["📙", "orange_book", 6], - ["📔", "notebook_with_decorative_cover", 6], - ["📒", "ledger", 6], - ["📚", "books", 6], - ["📖", "open_book", 6], - ["🧷", "safety_pin", 6], - ["🔗", "link", 6], - ["📎", "paperclip", 6], - ["🖇", "paperclips", 6], - ["✂️", "scissors", 6], - ["📐", "triangular_ruler", 6], - ["📏", "straight_ruler", 6], - ["🧮", "abacus", 6], - ["📌", "pushpin", 6], - ["📍", "round_pushpin", 6], - ["🚩", "triangular_flag_on_post", 6], - ["🏳", "white_flag", 6], - ["🏴", "black_flag", 6], - ["🏳️🌈", "rainbow_flag", 6], - ["🏳️⚧️", "transgender_flag", 6], - ["🔐", "closed_lock_with_key", 6], - ["🔒", "lock", 6], - ["🔓", "unlock", 6], - ["🔏", "lock_with_ink_pen", 6], - ["🖊", "pen", 6], - ["🖋", "fountain_pen", 6], - ["✒️", "black_nib", 6], - ["📝", "memo", 6], - ["✏️", "pencil2", 6], - ["🖍", "crayon", 6], - ["🖌", "paintbrush", 6], - ["🔍", "mag", 6], - ["🔎", "mag_right", 6], - ["🪦", "headstone", 6], - ["🪧", "placard", 6], - ["💯", "100", 7], - ["🔢", "1234", 7], - ["🩷", "pink_heart", 7], - ["❤️", "heart", 7], - ["🧡", "orange_heart", 7], - ["💛", "yellow_heart", 7], - ["💚", "green_heart", 7], - ["🩵", "light_blue_heart", 7], - ["💙", "blue_heart", 7], - ["💜", "purple_heart", 7], - ["🤎", "brown_heart", 7], - ["🖤", "black_heart", 7], - ["🩶", "grey_heart", 7], - ["🤍", "white_heart", 7], - ["💔", "broken_heart", 7], - ["❣", "heavy_heart_exclamation", 7], - ["💕", "two_hearts", 7], - ["💞", "revolving_hearts", 7], - ["💓", "heartbeat", 7], - ["💗", "heartpulse", 7], - ["💖", "sparkling_heart", 7], - ["💘", "cupid", 7], - ["💝", "gift_heart", 7], - ["💟", "heart_decoration", 7], - ["❤️🔥", "heart_on_fire", 7], - ["❤️🩹", "mending_heart", 7], - ["☮", "peace_symbol", 7], - ["✝", "latin_cross", 7], - ["☪", "star_and_crescent", 7], - ["🕉", "om", 7], - ["☸", "wheel_of_dharma", 7], - ["🪯", "khanda", 7], - ["✡", "star_of_david", 7], - ["🔯", "six_pointed_star", 7], - ["🕎", "menorah", 7], - ["☯", "yin_yang", 7], - ["☦", "orthodox_cross", 7], - ["🛐", "place_of_worship", 7], - ["⛎", "ophiuchus", 7], - ["♈", "aries", 7], - ["♉", "taurus", 7], - ["♊", "gemini", 7], - ["♋", "cancer", 7], - ["♌", "leo", 7], - ["♍", "virgo", 7], - ["♎", "libra", 7], - ["♏", "scorpius", 7], - ["♐", "sagittarius", 7], - ["♑", "capricorn", 7], - ["♒", "aquarius", 7], - ["♓", "pisces", 7], - ["🆔", "id", 7], - ["⚛", "atom_symbol", 7], - ["⚧️", "transgender_symbol", 7], - ["🈳", "u7a7a", 7], - ["🈹", "u5272", 7], - ["☢", "radioactive", 7], - ["☣", "biohazard", 7], - ["📴", "mobile_phone_off", 7], - ["📳", "vibration_mode", 7], - ["🈶", "u6709", 7], - ["🈚", "u7121", 7], - ["🈸", "u7533", 7], - ["🈺", "u55b6", 7], - ["🈷️", "u6708", 7], - ["✴️", "eight_pointed_black_star", 7], - ["🆚", "vs", 7], - ["🉑", "accept", 7], - ["💮", "white_flower", 7], - ["🉐", "ideograph_advantage", 7], - ["㊙️", "secret", 7], - ["㊗️", "congratulations", 7], - ["🈴", "u5408", 7], - ["🈵", "u6e80", 7], - ["🈲", "u7981", 7], - ["🅰️", "a", 7], - ["🅱️", "b", 7], - ["🆎", "ab", 7], - ["🆑", "cl", 7], - ["🅾️", "o2", 7], - ["🆘", "sos", 7], - ["⛔", "no_entry", 7], - ["📛", "name_badge", 7], - ["🚫", "no_entry_sign", 7], - ["❌", "x", 7], - ["⭕", "o", 7], - ["🛑", "stop_sign", 7], - ["💢", "anger", 7], - ["♨️", "hotsprings", 7], - ["🚷", "no_pedestrians", 7], - ["🚯", "do_not_litter", 7], - ["🚳", "no_bicycles", 7], - ["🚱", "non-potable_water", 7], - ["🔞", "underage", 7], - ["📵", "no_mobile_phones", 7], - ["❗", "exclamation", 7], - ["❕", "grey_exclamation", 7], - ["❓", "question", 7], - ["❔", "grey_question", 7], - ["‼️", "bangbang", 7], - ["⁉️", "interrobang", 7], - ["🔅", "low_brightness", 7], - ["🔆", "high_brightness", 7], - ["🔱", "trident", 7], - ["⚜", "fleur_de_lis", 7], - ["〽️", "part_alternation_mark", 7], - ["⚠️", "warning", 7], - ["🚸", "children_crossing", 7], - ["🔰", "beginner", 7], - ["♻️", "recycle", 7], - ["🈯", "u6307", 7], - ["💹", "chart", 7], - ["❇️", "sparkle", 7], - ["✳️", "eight_spoked_asterisk", 7], - ["❎", "negative_squared_cross_mark", 7], - ["✅", "white_check_mark", 7], - ["💠", "diamond_shape_with_a_dot_inside", 7], - ["🌀", "cyclone", 7], - ["➿", "loop", 7], - ["🌐", "globe_with_meridians", 7], - ["Ⓜ️", "m", 7], - ["🏧", "atm", 7], - ["🈂️", "sa", 7], - ["🛂", "passport_control", 7], - ["🛃", "customs", 7], - ["🛄", "baggage_claim", 7], - ["🛅", "left_luggage", 7], - ["🛜", "wireless", 7], - ["♿", "wheelchair", 7], - ["🚭", "no_smoking", 7], - ["🚾", "wc", 7], - ["🅿️", "parking", 7], - ["🚰", "potable_water", 7], - ["🚹", "mens", 7], - ["🚺", "womens", 7], - ["🚼", "baby_symbol", 7], - ["🚻", "restroom", 7], - ["🚮", "put_litter_in_its_place", 7], - ["🎦", "cinema", 7], - ["📶", "signal_strength", 7], - ["🈁", "koko", 7], - ["🆖", "ng", 7], - ["🆗", "ok", 7], - ["🆙", "up", 7], - ["🆒", "cool", 7], - ["🆕", "new", 7], - ["🆓", "free", 7], - ["0️⃣", "zero", 7], - ["1️⃣", "one", 7], - ["2️⃣", "two", 7], - ["3️⃣", "three", 7], - ["4️⃣", "four", 7], - ["5️⃣", "five", 7], - ["6️⃣", "six", 7], - ["7️⃣", "seven", 7], - ["8️⃣", "eight", 7], - ["9️⃣", "nine", 7], - ["🔟", "keycap_ten", 7], - ["*⃣", "asterisk", 7], - ["⏏️", "eject_button", 7], - ["▶️", "arrow_forward", 7], - ["⏸", "pause_button", 7], - ["⏭", "next_track_button", 7], - ["⏹", "stop_button", 7], - ["⏺", "record_button", 7], - ["⏯", "play_or_pause_button", 7], - ["⏮", "previous_track_button", 7], - ["⏩", "fast_forward", 7], - ["⏪", "rewind", 7], - ["🔀", "twisted_rightwards_arrows", 7], - ["🔁", "repeat", 7], - ["🔂", "repeat_one", 7], - ["◀️", "arrow_backward", 7], - ["🔼", "arrow_up_small", 7], - ["🔽", "arrow_down_small", 7], - ["⏫", "arrow_double_up", 7], - ["⏬", "arrow_double_down", 7], - ["➡️", "arrow_right", 7], - ["⬅️", "arrow_left", 7], - ["⬆️", "arrow_up", 7], - ["⬇️", "arrow_down", 7], - ["↗️", "arrow_upper_right", 7], - ["↘️", "arrow_lower_right", 7], - ["↙️", "arrow_lower_left", 7], - ["↖️", "arrow_upper_left", 7], - ["↕️", "arrow_up_down", 7], - ["↔️", "left_right_arrow", 7], - ["🔄", "arrows_counterclockwise", 7], - ["↪️", "arrow_right_hook", 7], - ["↩️", "leftwards_arrow_with_hook", 7], - ["⤴️", "arrow_heading_up", 7], - ["⤵️", "arrow_heading_down", 7], - ["#️⃣", "hash", 7], - ["ℹ️", "information_source", 7], - ["🔤", "abc", 7], - ["🔡", "abcd", 7], - ["🔠", "capital_abcd", 7], - ["🔣", "symbols", 7], - ["🎵", "musical_note", 7], - ["🎶", "notes", 7], - ["〰️", "wavy_dash", 7], - ["➰", "curly_loop", 7], - ["✔️", "heavy_check_mark", 7], - ["🔃", "arrows_clockwise", 7], - ["➕", "heavy_plus_sign", 7], - ["➖", "heavy_minus_sign", 7], - ["➗", "heavy_division_sign", 7], - ["✖️", "heavy_multiplication_x", 7], - ["🟰", "heavy_equals_sign", 7], - ["♾", "infinity", 7], - ["💲", "heavy_dollar_sign", 7], - ["💱", "currency_exchange", 7], - ["©️", "copyright", 7], - ["®️", "registered", 7], - ["™️", "tm", 7], - ["🔚", "end", 7], - ["🔙", "back", 7], - ["🔛", "on", 7], - ["🔝", "top", 7], - ["🔜", "soon", 7], - ["☑️", "ballot_box_with_check", 7], - ["🔘", "radio_button", 7], - ["⚫", "black_circle", 7], - ["⚪", "white_circle", 7], - ["🔴", "red_circle", 7], - ["🟠", "orange_circle", 7], - ["🟡", "yellow_circle", 7], - ["🟢", "green_circle", 7], - ["🔵", "large_blue_circle", 7], - ["🟣", "purple_circle", 7], - ["🟤", "brown_circle", 7], - ["🔸", "small_orange_diamond", 7], - ["🔹", "small_blue_diamond", 7], - ["🔶", "large_orange_diamond", 7], - ["🔷", "large_blue_diamond", 7], - ["🔺", "small_red_triangle", 7], - ["▪️", "black_small_square", 7], - ["▫️", "white_small_square", 7], - ["⬛", "black_large_square", 7], - ["⬜", "white_large_square", 7], - ["🟥", "red_square", 7], - ["🟧", "orange_square", 7], - ["🟨", "yellow_square", 7], - ["🟩", "green_square", 7], - ["🟦", "blue_square", 7], - ["🟪", "purple_square", 7], - ["🟫", "brown_square", 7], - ["🔻", "small_red_triangle_down", 7], - ["◼️", "black_medium_square", 7], - ["◻️", "white_medium_square", 7], - ["◾", "black_medium_small_square", 7], - ["◽", "white_medium_small_square", 7], - ["🔲", "black_square_button", 7], - ["🔳", "white_square_button", 7], - ["🔈", "speaker", 7], - ["🔉", "sound", 7], - ["🔊", "loud_sound", 7], - ["🔇", "mute", 7], - ["📣", "mega", 7], - ["📢", "loudspeaker", 7], - ["🔔", "bell", 7], - ["🔕", "no_bell", 7], - ["🃏", "black_joker", 7], - ["🀄", "mahjong", 7], - ["♠️", "spades", 7], - ["♣️", "clubs", 7], - ["♥️", "hearts", 7], - ["♦️", "diamonds", 7], - ["🎴", "flower_playing_cards", 7], - ["💭", "thought_balloon", 7], - ["🗯", "right_anger_bubble", 7], - ["💬", "speech_balloon", 7], - ["🗨", "left_speech_bubble", 7], - ["🕐", "clock1", 7], - ["🕑", "clock2", 7], - ["🕒", "clock3", 7], - ["🕓", "clock4", 7], - ["🕔", "clock5", 7], - ["🕕", "clock6", 7], - ["🕖", "clock7", 7], - ["🕗", "clock8", 7], - ["🕘", "clock9", 7], - ["🕙", "clock10", 7], - ["🕚", "clock11", 7], - ["🕛", "clock12", 7], - ["🕜", "clock130", 7], - ["🕝", "clock230", 7], - ["🕞", "clock330", 7], - ["🕟", "clock430", 7], - ["🕠", "clock530", 7], - ["🕡", "clock630", 7], - ["🕢", "clock730", 7], - ["🕣", "clock830", 7], - ["🕤", "clock930", 7], - ["🕥", "clock1030", 7], - ["🕦", "clock1130", 7], - ["🕧", "clock1230", 7], - ["🇦🇫", "afghanistan", 8], - ["🇦🇽", "aland_islands", 8], - ["🇦🇱", "albania", 8], - ["🇩🇿", "algeria", 8], - ["🇦🇸", "american_samoa", 8], - ["🇦🇩", "andorra", 8], - ["🇦🇴", "angola", 8], - ["🇦🇮", "anguilla", 8], - ["🇦🇶", "antarctica", 8], - ["🇦🇬", "antigua_barbuda", 8], - ["🇦🇷", "argentina", 8], - ["🇦🇲", "armenia", 8], - ["🇦🇼", "aruba", 8], - ["🇦🇨", "ascension_island", 8], - ["🇦🇺", "australia", 8], - ["🇦🇹", "austria", 8], - ["🇦🇿", "azerbaijan", 8], - ["🇧🇸", "bahamas", 8], - ["🇧🇭", "bahrain", 8], - ["🇧🇩", "bangladesh", 8], - ["🇧🇧", "barbados", 8], - ["🇧🇾", "belarus", 8], - ["🇧🇪", "belgium", 8], - ["🇧🇿", "belize", 8], - ["🇧🇯", "benin", 8], - ["🇧🇲", "bermuda", 8], - ["🇧🇹", "bhutan", 8], - ["🇧🇴", "bolivia", 8], - ["🇧🇶", "caribbean_netherlands", 8], - ["🇧🇦", "bosnia_herzegovina", 8], - ["🇧🇼", "botswana", 8], - ["🇧🇷", "brazil", 8], - ["🇮🇴", "british_indian_ocean_territory", 8], - ["🇻🇬", "british_virgin_islands", 8], - ["🇧🇳", "brunei", 8], - ["🇧🇬", "bulgaria", 8], - ["🇧🇫", "burkina_faso", 8], - ["🇧🇮", "burundi", 8], - ["🇨🇻", "cape_verde", 8], - ["🇰🇭", "cambodia", 8], - ["🇨🇲", "cameroon", 8], - ["🇨🇦", "canada", 8], - ["🇮🇨", "canary_islands", 8], - ["🇰🇾", "cayman_islands", 8], - ["🇨🇫", "central_african_republic", 8], - ["🇹🇩", "chad", 8], - ["🇨🇱", "chile", 8], - ["🇨🇳", "cn", 8], - ["🇨🇽", "christmas_island", 8], - ["🇨🇨", "cocos_islands", 8], - ["🇨🇴", "colombia", 8], - ["🇰🇲", "comoros", 8], - ["🇨🇬", "congo_brazzaville", 8], - ["🇨🇩", "congo_kinshasa", 8], - ["🇨🇰", "cook_islands", 8], - ["🇨🇷", "costa_rica", 8], - ["🇭🇷", "croatia", 8], - ["🇨🇺", "cuba", 8], - ["🇨🇼", "curacao", 8], - ["🇨🇾", "cyprus", 8], - ["🇨🇿", "czech_republic", 8], - ["🇩🇰", "denmark", 8], - ["🇩🇯", "djibouti", 8], - ["🇩🇲", "dominica", 8], - ["🇩🇴", "dominican_republic", 8], - ["🇪🇨", "ecuador", 8], - ["🇪🇬", "egypt", 8], - ["🇸🇻", "el_salvador", 8], - ["🇬🇶", "equatorial_guinea", 8], - ["🇪🇷", "eritrea", 8], - ["🇪🇪", "estonia", 8], - ["🇪🇹", "ethiopia", 8], - ["🇪🇺", "eu", 8], - ["🇫🇰", "falkland_islands", 8], - ["🇫🇴", "faroe_islands", 8], - ["🇫🇯", "fiji", 8], - ["🇫🇮", "finland", 8], - ["🇫🇷", "fr", 8], - ["🇬🇫", "french_guiana", 8], - ["🇵🇫", "french_polynesia", 8], - ["🇹🇫", "french_southern_territories", 8], - ["🇬🇦", "gabon", 8], - ["🇬🇲", "gambia", 8], - ["🇬🇪", "georgia", 8], - ["🇩🇪", "de", 8], - ["🇬🇭", "ghana", 8], - ["🇬🇮", "gibraltar", 8], - ["🇬🇷", "greece", 8], - ["🇬🇱", "greenland", 8], - ["🇬🇩", "grenada", 8], - ["🇬🇵", "guadeloupe", 8], - ["🇬🇺", "guam", 8], - ["🇬🇹", "guatemala", 8], - ["🇬🇬", "guernsey", 8], - ["🇬🇳", "guinea", 8], - ["🇬🇼", "guinea_bissau", 8], - ["🇬🇾", "guyana", 8], - ["🇭🇹", "haiti", 8], - ["🇭🇳", "honduras", 8], - ["🇭🇰", "hong_kong", 8], - ["🇭🇺", "hungary", 8], - ["🇮🇸", "iceland", 8], - ["🇮🇳", "india", 8], - ["🇮🇩", "indonesia", 8], - ["🇮🇷", "iran", 8], - ["🇮🇶", "iraq", 8], - ["🇮🇪", "ireland", 8], - ["🇮🇲", "isle_of_man", 8], - ["🇮🇱", "israel", 8], - ["🇮🇹", "it", 8], - ["🇨🇮", "cote_divoire", 8], - ["🇯🇲", "jamaica", 8], - ["🇯🇵", "jp", 8], - ["🇯🇪", "jersey", 8], - ["🇯🇴", "jordan", 8], - ["🇰🇿", "kazakhstan", 8], - ["🇰🇪", "kenya", 8], - ["🇰🇮", "kiribati", 8], - ["🇽🇰", "kosovo", 8], - ["🇰🇼", "kuwait", 8], - ["🇰🇬", "kyrgyzstan", 8], - ["🇱🇦", "laos", 8], - ["🇱🇻", "latvia", 8], - ["🇱🇧", "lebanon", 8], - ["🇱🇸", "lesotho", 8], - ["🇱🇷", "liberia", 8], - ["🇱🇾", "libya", 8], - ["🇱🇮", "liechtenstein", 8], - ["🇱🇹", "lithuania", 8], - ["🇱🇺", "luxembourg", 8], - ["🇲🇴", "macau", 8], - ["🇲🇰", "macedonia", 8], - ["🇲🇬", "madagascar", 8], - ["🇲🇼", "malawi", 8], - ["🇲🇾", "malaysia", 8], - ["🇲🇻", "maldives", 8], - ["🇲🇱", "mali", 8], - ["🇲🇹", "malta", 8], - ["🇲🇭", "marshall_islands", 8], - ["🇲🇶", "martinique", 8], - ["🇲🇷", "mauritania", 8], - ["🇲🇺", "mauritius", 8], - ["🇾🇹", "mayotte", 8], - ["🇲🇽", "mexico", 8], - ["🇫🇲", "micronesia", 8], - ["🇲🇩", "moldova", 8], - ["🇲🇨", "monaco", 8], - ["🇲🇳", "mongolia", 8], - ["🇲🇪", "montenegro", 8], - ["🇲🇸", "montserrat", 8], - ["🇲🇦", "morocco", 8], - ["🇲🇿", "mozambique", 8], - ["🇲🇲", "myanmar", 8], - ["🇳🇦", "namibia", 8], - ["🇳🇷", "nauru", 8], - ["🇳🇵", "nepal", 8], - ["🇳🇱", "netherlands", 8], - ["🇳🇨", "new_caledonia", 8], - ["🇳🇿", "new_zealand", 8], - ["🇳🇮", "nicaragua", 8], - ["🇳🇪", "niger", 8], - ["🇳🇬", "nigeria", 8], - ["🇳🇺", "niue", 8], - ["🇳🇫", "norfolk_island", 8], - ["🇲🇵", "northern_mariana_islands", 8], - ["🇰🇵", "north_korea", 8], - ["🇳🇴", "norway", 8], - ["🇴🇲", "oman", 8], - ["🇵🇰", "pakistan", 8], - ["🇵🇼", "palau", 8], - ["🇵🇸", "palestinian_territories", 8], - ["🇵🇦", "panama", 8], - ["🇵🇬", "papua_new_guinea", 8], - ["🇵🇾", "paraguay", 8], - ["🇵🇪", "peru", 8], - ["🇵🇭", "philippines", 8], - ["🇵🇳", "pitcairn_islands", 8], - ["🇵🇱", "poland", 8], - ["🇵🇹", "portugal", 8], - ["🇵🇷", "puerto_rico", 8], - ["🇶🇦", "qatar", 8], - ["🇷🇪", "reunion", 8], - ["🇷🇴", "romania", 8], - ["🇷🇺", "ru", 8], - ["🇷🇼", "rwanda", 8], - ["🇧🇱", "st_barthelemy", 8], - ["🇸🇭", "st_helena", 8], - ["🇰🇳", "st_kitts_nevis", 8], - ["🇱🇨", "st_lucia", 8], - ["🇵🇲", "st_pierre_miquelon", 8], - ["🇻🇨", "st_vincent_grenadines", 8], - ["🇼🇸", "samoa", 8], - ["🇸🇲", "san_marino", 8], - ["🇸🇹", "sao_tome_principe", 8], - ["🇸🇦", "saudi_arabia", 8], - ["🇸🇳", "senegal", 8], - ["🇷🇸", "serbia", 8], - ["🇸🇨", "seychelles", 8], - ["🇸🇱", "sierra_leone", 8], - ["🇸🇬", "singapore", 8], - ["🇸🇽", "sint_maarten", 8], - ["🇸🇰", "slovakia", 8], - ["🇸🇮", "slovenia", 8], - ["🇸🇧", "solomon_islands", 8], - ["🇸🇴", "somalia", 8], - ["🇿🇦", "south_africa", 8], - ["🇬🇸", "south_georgia_south_sandwich_islands", 8], - ["🇰🇷", "kr", 8], - ["🇸🇸", "south_sudan", 8], - ["🇪🇸", "es", 8], - ["🇱🇰", "sri_lanka", 8], - ["🇸🇩", "sudan", 8], - ["🇸🇷", "suriname", 8], - ["🇸🇿", "swaziland", 8], - ["🇸🇪", "sweden", 8], - ["🇨🇭", "switzerland", 8], - ["🇸🇾", "syria", 8], - ["🇹🇼", "taiwan", 8], - ["🇹🇯", "tajikistan", 8], - ["🇹🇿", "tanzania", 8], - ["🇹🇭", "thailand", 8], - ["🇹🇱", "timor_leste", 8], - ["🇹🇬", "togo", 8], - ["🇹🇰", "tokelau", 8], - ["🇹🇴", "tonga", 8], - ["🇹🇹", "trinidad_tobago", 8], - ["🇹🇦", "tristan_da_cunha", 8], - ["🇹🇳", "tunisia", 8], - ["🇹🇷", "tr", 8], - ["🇹🇲", "turkmenistan", 8], - ["🇹🇨", "turks_caicos_islands", 8], - ["🇹🇻", "tuvalu", 8], - ["🇺🇬", "uganda", 8], - ["🇺🇦", "ukraine", 8], - ["🇦🇪", "united_arab_emirates", 8], - ["🇬🇧", "uk", 8], - ["🏴", "england", 8], - ["🏴", "scotland", 8], - ["🏴", "wales", 8], - ["🇺🇸", "us", 8], - ["🇻🇮", "us_virgin_islands", 8], - ["🇺🇾", "uruguay", 8], - ["🇺🇿", "uzbekistan", 8], - ["🇻🇺", "vanuatu", 8], - ["🇻🇦", "vatican_city", 8], - ["🇻🇪", "venezuela", 8], - ["🇻🇳", "vietnam", 8], - ["🇼🇫", "wallis_futuna", 8], - ["🇪🇭", "western_sahara", 8], - ["🇾🇪", "yemen", 8], - ["🇿🇲", "zambia", 8], - ["🇿🇼", "zimbabwe", 8], - ["🇺🇳", "united_nations", 8], - ["🏴☠️", "pirate_flag", 8] -] diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts index 10d6adbcd0..17e787f9fc 100644 --- a/packages/frontend/src/i18n.ts +++ b/packages/frontend/src/i18n.ts @@ -4,11 +4,11 @@ */ import { markRaw } from 'vue'; +import { I18n } from '@@/js/i18n.js'; import type { Locale } from '../../../locales/index.js'; import { locale } from '@/config.js'; -import { I18n } from '@/scripts/i18n.js'; -export const i18n = markRaw(new I18n<Locale>(locale)); +export const i18n = markRaw(new I18n<Locale>(locale, _DEV_)); export function updateI18n(newLocale: Locale) { i18n.locale = newLocale; diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index 6847321d6c..71cb42b30c 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -7,7 +7,7 @@ import { computed, reactive } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { miLocalStorage } from '@/local-storage.js'; -import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js'; +import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js'; // TODO: 他のタブと永続化されたstateを同期 diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 8029bca68d..5b8ba77e01 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -type Keys = +export type Keys = 'v' | 'lastVersion' | 'instance' | @@ -38,12 +38,22 @@ type Keys = `aiscript:${string}` | 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) 'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~); - `channelLastReadedAt:${string}` + `channelLastReadedAt:${string}` | + `idbfallback::${string}` + +// セッション毎に廃棄されるLocalStorage代替(セーフモードなどで使用できそう) +//const safeSessionStorage = new Map<Keys, string>(); export const miLocalStorage = { - getItem: (key: Keys): string | null => window.localStorage.getItem(key), - setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value), - removeItem: (key: Keys): void => window.localStorage.removeItem(key), + getItem: (key: Keys): string | null => { + return window.localStorage.getItem(key); + }, + setItem: (key: Keys, value: string): void => { + window.localStorage.setItem(key, value); + }, + removeItem: (key: Keys): void => { + window.localStorage.removeItem(key); + }, getItemAsJson: (key: Keys): any | undefined => { const item = miLocalStorage.getItem(key); if (item === null) { @@ -51,5 +61,7 @@ export const miLocalStorage = { } return JSON.parse(item); }, - setItemAsJson: (key: Keys, value: any): void => window.localStorage.setItem(key, JSON.stringify(value)), + setItemAsJson: (key: Keys, value: any): void => { + miLocalStorage.setItem(key, JSON.stringify(value)); + }, }; diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts index 6a8ea09ed6..25f853453a 100644 --- a/packages/frontend/src/nirax.ts +++ b/packages/frontend/src/nirax.ts @@ -7,7 +7,14 @@ import { Component, onMounted, shallowRef, ShallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; -import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; + +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} interface RouteDefBase { path: string; diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue index b88f078598..d22e078c2a 100644 --- a/packages/frontend/src/pages/admin/_header_.vue +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue'; import tinycolor from 'tinycolor2'; import { popupMenu } from '@/os.js'; -import { scrollToTop } from '@/scripts/scroll.js'; +import { scrollToTop } from '@@/js/scroll.js'; import MkButton from '@/components/MkButton.vue'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue index a09db2a6d5..292e2e1dbc 100644 --- a/packages/frontend/src/pages/admin/overview.instances.vue +++ b/packages/frontend/src/pages/admin/overview.instances.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index a7dd4c0a48..8c9d7a8197 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 3e948abdf1..b0137abb3f 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -608,7 +608,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import { i18n } from '@/i18n.js'; -import { ROLE_POLICIES } from '@/const.js'; +import { ROLE_POLICIES } from '@@/js/const.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 6fb950494b..7e29f6e0d8 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -253,7 +253,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance, fetchInstance } from '@/instance.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { ROLE_POLICIES } from '@/const.js'; +import { ROLE_POLICIES } from '@@/js/const.js'; import { useRouter } from '@/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index ea64e457e3..22c5231dd9 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index fb984de368..aad6acb4b5 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -44,6 +44,7 @@ import MkButton from '@/components/MkButton.vue'; import { clipsCache } from '@/cache.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; const props = defineProps<{ clipId: string, @@ -127,21 +128,33 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ clipsCache.delete(); }, }, ...(clip.value.isPublic ? [{ - icon: 'ti ti-link', - text: i18n.ts.copyUrl, - handler: async (): Promise<void> => { - copyToClipboard(`${url}/clips/${clip.value.id}`); - os.success(); - }, -}] : []), ...(clip.value.isPublic && isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, - handler: async (): Promise<void> => { - navigator.share({ - title: clip.value.name, - text: clip.value.description, - url: `${url}/clips/${clip.value.id}`, - }); + handler: (ev: MouseEvent): void => { + os.popupMenu([{ + icon: 'ti ti-link', + text: i18n.ts.copyUrl, + action: () => { + copyToClipboard(`${url}/clips/${clip.value!.id}`); + os.success(); + }, + }, { + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + action: () => { + genEmbedCode('clips', clip.value!.id); + }, + }, ...(isSupportShare() ? [{ + icon: 'ti ti-share', + text: i18n.ts.share, + action: async () => { + navigator.share({ + title: clip.value!.name, + text: clip.value!.description ?? '', + url: `${url}/clips/${clip.value!.id}`, + }); + }, + }] : [])], ev.currentTarget ?? ev.target); }, }] : []), { icon: 'ti ti-trash', diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 3026d00a2c..ffedaf27bf 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -99,12 +99,12 @@ const file = ref<Misskey.entities.DriveFile>(); const folderHierarchy = computed(() => { if (!file.value) return [i18n.ts.drive]; const folderNames = [i18n.ts.drive]; - + function get(folder: Misskey.entities.DriveFolder) { if (folder.parent) get(folder.parent); folderNames.push(folder.name); } - + if (file.value.folder) get(file.value.folder); return folderNames; }); diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index 0f0b7e1ea8..b5e4902126 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -205,7 +205,7 @@ import { claimAchievement } from '@/scripts/achievements.js'; import { defaultStore } from '@/store.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { apiUrl } from '@/config.js'; import { $i } from '@/account.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 28f5838296..bd93fc8369 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -30,7 +30,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; const tab = ref('all'); const includeTypes = ref<string[] | null>(null); diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 7d9cefa5c9..578fd65ba1 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -149,7 +149,7 @@ import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { deepClone } from '@/scripts/clone.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { signinRequired } from '@/account.js'; import { url } from '@/config.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 97a793753d..a25595e884 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -22,7 +22,7 @@ import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; import { url } from '@/config.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const $i = signinRequired(); diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 51a03e4418..d823861b4a 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -117,7 +117,7 @@ import { $i } from '@/account.js'; import MkPagination from '@/components/MkPagination.vue'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 70db6a5109..cce671a7cb 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -69,7 +69,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; const $i = signinRequired(); diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 9b77392872..1b3e1ecaee 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -28,6 +28,7 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; const props = defineProps<{ tag: string; @@ -51,7 +52,19 @@ async function post() { notes.value?.pagingComponent?.reload(); } -const headerActions = computed(() => []); +const headerActions = computed(() => [{ + icon: 'ti ti-dots', + label: i18n.ts.more, + handler: (ev: MouseEvent) => { + os.popupMenu([{ + text: i18n.ts.genEmbedCode, + icon: 'ti ti-code', + action: () => { + genEmbedCode('tags', props.tag); + }, + }], ev.currentTarget ?? ev.target); + } +}]); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 50c3beeabc..fe7896b7d9 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -79,6 +79,8 @@ import tinycolor from 'tinycolor2'; import { v4 as uuid } from 'uuid'; import JSON5 from 'json5'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import MkButton from '@/components/MkButton.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -86,8 +88,6 @@ import MkFolder from '@/components/MkFolder.vue'; import { $i } from '@/account.js'; import { Theme, applyTheme } from '@/scripts/theme.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; import { host } from '@/config.js'; import * as os from '@/os.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index d5943e8fbc..cc1ed3d01f 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -40,7 +40,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; @@ -54,7 +54,7 @@ import { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js'; - + provide('shouldOmitHeaderTitle', true); const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>(); diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index de6737f37d..31a3f1b060 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 3039ec7499..8e0292c7fe 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -161,7 +161,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkOmit from '@/components/MkOmit.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkButton from '@/components/MkButton.vue'; -import { getScrollPosition } from '@/scripts/scroll.js'; +import { getScrollPosition } from '@@/js/scroll.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index db326f9e6c..732d483615 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -24,7 +24,7 @@ import * as Misskey from 'misskey-js'; import { onUpdated, ref, shallowRef } from 'vue'; import XNote from '@/pages/welcome.timeline.note.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; const notes = ref<Misskey.entities.Note[]>([]); const isScrolling = ref(false); diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 995a2055b8..8a29fd677e 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue'; -import type { RouteDef } from '@/nirax.js'; -import { IRouter, Router } from '@/nirax.js'; +import { AsyncComponentLoader, defineAsyncComponent } from 'vue'; +import type { IRouter, RouteDef } from '@/nirax.js'; +import { Router } from '@/nirax.js'; import { $i, iAmModerator } from '@/account.js'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; -import { setMainRouter } from '@/router/main.js'; -const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ +export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ loader: loader, loadingComponent: MkLoading, errorComponent: MkError, @@ -240,7 +239,7 @@ const routes: RouteDef[] = [{ origin: 'origin', }, }, { - // Legacy Compatibility + // Legacy Compatibility path: '/authorize-follow', redirect: '/lookup', loginRequired: true, @@ -597,32 +596,6 @@ const routes: RouteDef[] = [{ component: page(() => import('@/pages/not-found.vue')), }]; -function createRouterImpl(path: string): IRouter { +export function createMainRouter(path: string): IRouter { return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue'))); } - -/** - * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 - * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) - */ -export function setupRouter(app: App) { - app.provide('routerFactory', createRouterImpl); - - const mainRouter = createRouterImpl(location.pathname + location.search + location.hash); - - window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); - }); - - mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.addListener('replace', ctx => { - window.history.replaceState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.init(); - - setMainRouter(mainRouter); -} diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts index 7a3fde131e..6ee967e6f4 100644 --- a/packages/frontend/src/router/main.ts +++ b/packages/frontend/src/router/main.ts @@ -3,10 +3,37 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ShallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; +import type { App, ShallowRef } from 'vue'; + +/** + * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 + * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) + */ +export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)): void { + app.provide('routerFactory', routerFactory); + + const mainRouter = routerFactory(location.pathname + location.search + location.hash); + + window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); + }); + + mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.addListener('replace', ctx => { + window.history.replaceState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.init(); + + setMainRouter(mainRouter); +} + function getMainRouter(): IRouter { const router = mainRouterHolder; if (!router) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 98a0c61752..417ba08c3f 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -4,13 +4,13 @@ */ import { utils, values } from '@syuilo/aiscript'; +import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; import { url, lang } from '@/config.js'; -import { nyaize } from '@/scripts/nyaize.js'; export function aiScriptReadline(q: string): Promise<string> { return new Promise(ok => { @@ -87,7 +87,7 @@ export function createAiScriptEnv(opts) { }), 'Mk:nyaize': values.FN_NATIVE(([text]) => { utils.assertString(text); - return values.STR(nyaize(text.value)); + return values.STR(Misskey.nyaize(text.value)); }), }; } diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts index 8fc857f84f..c3c3f419a9 100644 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { UnicodeEmojiDef } from './emojilist.js'; +import { UnicodeEmojiDef } from '@@/js/emojilist.js'; export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean { if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする; diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index e94027d302..b0ffac93d7 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -7,13 +7,13 @@ import { getHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { unique } from './array.js'; import { deepClone } from './clone.js'; import { deepMerge } from './merge.js'; import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; import { ColdDeviceStorage } from '@/store.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; let _highlighter: HighlighterCore | null = null; diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts deleted file mode 100644 index a01540a3e4..0000000000 --- a/packages/frontend/src/scripts/emoji-base.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const twemojiSvgBase = '/twemoji'; -const fluentEmojiPngBase = '/fluent-emoji'; - -export function char2twemojiFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.join('-'); - return `${twemojiSvgBase}/${fileName}.svg`; -} - -export function char2fluentEmojiFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 - if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); - return `${fluentEmojiPngBase}/${fileName}.png`; -} diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts deleted file mode 100644 index 6565feba97..0000000000 --- a/packages/frontend/src/scripts/emojilist.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const; - -export type UnicodeEmojiDef = { - name: string; - char: string; - category: typeof unicodeEmojiCategories[number]; -} - -// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; - -export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ - name: x[1] as string, - char: x[0] as string, - category: unicodeEmojiCategories[x[2]], -})); - -const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>( - emojilist.map(x => [x.char, x]), -); - -const _indexByChar = new Map<string, number>(); -const _charGroupByCategory = new Map<string, string[]>(); -for (let i = 0; i < emojilist.length; i++) { - const emo = emojilist[i]; - _indexByChar.set(emo.char, i); - - if (_charGroupByCategory.has(emo.category)) { - _charGroupByCategory.get(emo.category)?.push(emo.char); - } else { - _charGroupByCategory.set(emo.category, [emo.char]); - } -} - -export const emojiCharByCategory = _charGroupByCategory; - -export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string { - // Colorize it because emojilist.json assumes that - return unicodeEmojisMap.get(colorizeEmoji(char)) - // カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする - ?? unicodeEmojisMap.get(char) - // それでも見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する) - ?? char; -} - -export function getEmojiName(char: string): string { - // Colorize it because emojilist.json assumes that - const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char); - if (idx === undefined) { - // 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い - return char; - } else { - return emojilist[idx].name; - } -} - -/** - * テキストスタイル絵文字(U+260Eなどの1文字で表現される絵文字)をカラースタイル絵文字に変換します(VS16:U+FE0Fを付与)。 - */ -export function colorizeEmoji(char: string) { - return char.length === 1 ? `${char}\uFE0F` : char; -} - -export interface CustomEmojiFolderTree { - value: string; - category: string; - children: CustomEmojiFolderTree[]; -} diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts deleted file mode 100644 index 992f6e9a16..0000000000 --- a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function extractAvgColorFromBlurhash(hash: string) { - return typeof hash === 'string' - ? '#' + [...hash.slice(2, 6)] - .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) - .reduce((a, c) => a * 83 + c, 0) - .toString(16) - .padStart(6, '0') - : undefined; -} diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index eb2da5ad86..81278b17ea 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js'; +import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js'; import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement; diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts new file mode 100644 index 0000000000..007cd6561b --- /dev/null +++ b/packages/frontend/src/scripts/get-embed-code.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { defineAsyncComponent } from 'vue'; +import { v4 as uuid } from 'uuid'; +import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js'; +import { url } from '@/config.js'; +import * as os from '@/os.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js'; + +const MOBILE_THRESHOLD = 500; + +/** + * パラメータを正規化する(埋め込みコード作成用) + * @param params パラメータ + * @returns 正規化されたパラメータ + */ +export function normalizeEmbedParams(params: EmbedParams): Record<string, string> { + // paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す + const normalizedParams: Record<string, string> = {}; + for (const key in params) { + // デフォルトの値と同じならparamsに含めない + if (params[key] == null || params[key] === defaultEmbedParams[key]) { + continue; + } + switch (typeof params[key]) { + case 'number': + normalizedParams[key] = params[key].toString(); + break; + case 'boolean': + normalizedParams[key] = params[key] ? 'true' : 'false'; + break; + default: + normalizedParams[key] = params[key]; + break; + } + } + return normalizedParams; +} + +/** + * 埋め込みコードを生成(iframe IDの発番もやる) + */ +export function getEmbedCode(path: string, params?: EmbedParams): string { + const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく + + let paramString = ''; + if (params) { + const searchParams = new URLSearchParams(normalizeEmbedParams(params)); + paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString(); + } + + const iframeCode = [ + `<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`, + `<script defer src="${url}/embed.js"></script>`, + ]; + return iframeCode.join('\n'); +} + +/** + * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) + * + * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください + */ +export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { + const _params = { ...params }; + + if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { + _params.maxHeight = 700; + } + + // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー + if (window.innerWidth < MOBILE_THRESHOLD) { + copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); + os.success(); + } else { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { + entity, + id, + params: _params, + }, { + closed: () => dispose(), + }); + } +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b5d7350a41..e0ccea813d 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -21,6 +21,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -156,6 +157,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): }; } +function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { + if (note.url != null || note.uri != null) return undefined; + if (['specified', 'followers'].includes(note.visibility)) return undefined; + + return { + icon: 'ti ti-code', + text, + action: (): void => { + genEmbedCode('notes', note.id); + }, + }; +} + export function getNoteMenu(props: { note: Misskey.entities.Note; translation: Ref<Misskey.entities.NotesTranslateResponse | null>; @@ -310,7 +324,7 @@ export function getNoteMenu(props: { action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined, + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode), ...(isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, @@ -443,14 +457,14 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? { + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink), + (appearNote.url || appearNote.uri) ? { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined] + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)] .filter(x => x !== undefined); } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 33f16a68aa..035abc7bd0 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -17,6 +17,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; import { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { @@ -179,7 +180,17 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if (user.url == null) return; window.open(user.url, '_blank', 'noopener'); }, - }] : []), { + }] : [{ + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + type: 'parent' as const, + children: [{ + text: i18n.ts.noteOfThisUser, + action: () => { + genEmbedCode('user-timeline', user.id); + }, + }], // TODO: ユーザーカードの埋め込みなど + }]), { icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts deleted file mode 100644 index b258a2a678..0000000000 --- a/packages/frontend/src/scripts/i18n.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; - -type FlattenKeys<T extends ILocale, TPrediction> = keyof { - [K in keyof T as T[K] extends ILocale - ? FlattenKeys<T[K], TPrediction> extends infer C extends string - ? `${K & string}.${C}` - : never - : T[K] extends TPrediction - ? K - : never]: T[K]; -}; - -type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}` - // @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。 - ? ParametersOf<T[K], C> - : TKey extends keyof T - ? T[TKey] extends ParameterizedString<infer P> - ? P - : never - : never; - -type Tsx<T extends ILocale> = { - readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P> - ? (arg: { readonly [_ in P]: string | number }) => string - // @ts-expect-error -- 証明省略 - : Tsx<T[K]>; -}; - -export class I18n<T extends ILocale> { - private tsxCache?: Tsx<T>; - - constructor(public locale: T) { - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - public get ts(): T { - if (_DEV_) { - class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); - } - - if (typeof value === 'string') { - const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter); - - if (parameters.length) { - console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`); - } - - return value; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return new Proxy(this.locale, new Handler()); - } - - return this.locale; - } - - public get tsx(): Tsx<T> { - if (_DEV_) { - if (this.tsxCache) { - return this.tsxCache; - } - - class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); - } - - if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - console.error(`Unexpected locale key: ${String(p)}`); - - return () => value; - } - - return (arg) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - if (!Object.hasOwn(arg, expressions[i])) { - console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`); - } - - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>; - } - - if (this.tsxCache) { - return this.tsxCache; - } - - function build(target: ILocale): Tsx<T> { - const result = {} as Tsx<T>; - - for (const k in target) { - if (!Object.hasOwn(target, k)) { - continue; - } - - const value = target[k as keyof typeof target]; - - if (typeof value === 'object') { - result[k] = build(value as ILocale); - } else if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - continue; - } - - result[k] = (arg) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - } - return result; - } - - return this.tsxCache = build(this.locale); - } - - /** - * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t<TKey extends FlattenKeys<T, string>>(key: TKey): string; - /** - * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string; - public t(key: string, args?: { readonly [_: string]: string | number }) { - let str: string | ParameterizedString | ILocale = this.locale; - - for (const k of key.split('.')) { - str = str[k]; - - if (_DEV_) { - if (typeof str === 'undefined') { - console.error(`Unexpected locale key: ${key}`); - return key; - } - } - } - - if (args) { - if (_DEV_) { - const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); - - if (missing.length) { - console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`); - } - } - - for (const [k, v] of Object.entries(args)) { - const search = `{${k}}`; - - if (_DEV_) { - if (!(str as string).includes(search)) { - console.error(`Unexpected locale parameter: ${k} at ${key}`); - } - } - - str = (str as string).replace(search, v.toString()); - } - } - - return str; - } -} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 6b511f2a5f..20f51660c7 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,10 +10,11 @@ import { set as iset, del as idel, } from 'idb-keyval'; +import { miLocalStorage } from '@/local-storage.js'; -const fallbackName = (key: string) => `idbfallback::${key}`; +const PREFIX = 'idbfallback::'; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと @@ -38,15 +39,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return JSON.parse(window.localStorage.getItem(fallbackName(key))); + return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); + return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); } export async function del(key: string) { if (idbAvailable) return idel(key); - return window.localStorage.removeItem(fallbackName(key)); + return miLocalStorage.removeItem(`${PREFIX}${key}`); } diff --git a/packages/frontend/src/scripts/is-link.ts b/packages/frontend/src/scripts/is-link.ts new file mode 100644 index 0000000000..946f86400e --- /dev/null +++ b/packages/frontend/src/scripts/is-link.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isLink(el: HTMLElement) { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; +} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 099a22163a..68a5a1dcf8 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -3,51 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { query } from '@/scripts/url.js'; +import { MediaProxy } from '@@/js/media-proxy.js'; import { url } from '@/config.js'; import { instance } from '@/instance.js'; -export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { - const localProxy = `${url}/proxy`; +let _mediaProxy: MediaProxy | null = null; - if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { - // もう既にproxyっぽそうだったらurlを取り出す - imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; +export function getProxiedImageUrl(...args: Parameters<MediaProxy['getProxiedImageUrl']>): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${mustOrigin ? localProxy : instance.mediaProxy}/${ - type === 'preview' ? 'preview.webp' - : 'image.webp' - }?${query({ - url: imageUrl, - ...(!noFallback ? { 'fallback': '1' } : {}), - ...(type ? { [type]: '1' } : {}), - ...(mustOrigin ? { origin: '1' } : {}), - })}`; + return _mediaProxy.getProxiedImageUrl(...args); } -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} - -export function getStaticImageUrl(baseUrl: string): string { - const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); - - if (u.href.startsWith(`${url}/emoji/`)) { - // もう既にemojiっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; +export function getProxiedImageUrlNullable(...args: Parameters<MediaProxy['getProxiedImageUrlNullable']>): string | null { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - if (u.href.startsWith(instance.mediaProxy + '/')) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; + return _mediaProxy.getProxiedImageUrlNullable(...args); +} + +export function getStaticImageUrl(...args: Parameters<MediaProxy['getStaticImageUrl']>): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${instance.mediaProxy}/static.webp?${query({ - url: u.href, - static: '1', - })}`; + return _mediaProxy.getStaticImageUrl(...args); } diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 9938e534c1..bf59fe98a0 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -6,7 +6,7 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@/const.js'; +import { MFM_TAGS } from '@@/js/const.js'; import type { MenuItem } from '@/types/menu.js'; /** diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts deleted file mode 100644 index abc8ada461..0000000000 --- a/packages/frontend/src/scripts/nyaize.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const enRegex1 = /(?<=n)a/gi; -const enRegex2 = /(?<=morn)ing/gi; -const enRegex3 = /(?<=every)one/gi; -const koRegex1 = /[나-낳]/g; -const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; -const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; - -export function nyaize(text: string): string { - return text - // ja-JP - .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') - // en-US - .replace(enRegex1, x => x === 'A' ? 'YA' : 'ya') - .replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan') - .replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan') - // ko-KR - .replace(koRegex1, match => String.fromCharCode( - match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), - )) - .replace(koRegex2, '다냥') - .replace(koRegex3, '냥'); -} diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 1caa2dfc21..ed49611b4f 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from './url.js'; +import { appendQuery } from '@@/js/url.js'; import * as config from '@/config.js'; export function popout(path: string, w?: HTMLElement) { diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 31a9ac1ad9..11b6f52ddd 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -18,7 +18,7 @@ export type MiPostMessageEvent = { * 親フレームにイベントを送信 */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.postMessage({ + window.parent.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts deleted file mode 100644 index 6bfcef6c36..0000000000 --- a/packages/frontend/src/scripts/safe-parse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeParseFloat(str: unknown): number | null { - if (typeof str !== 'string' || str === '') return null; - const num = parseFloat(str); - if (isNaN(num)) return null; - return num; -} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 0edf4e9eba..0000000000 --- a/packages/frontend/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts deleted file mode 100644 index f0274034b5..0000000000 --- a/packages/frontend/src/scripts/scroll.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -type ScrollBehavior = 'auto' | 'smooth' | 'instant'; - -export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { - if (el == null || el.tagName === 'HTML') return null; - const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y'); - if (overflow === 'scroll' || overflow === 'auto') { - return el; - } else { - return getScrollContainer(el.parentElement); - } -} - -export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top = 0) { - if (!el.parentElement) return top; - const data = el.dataset.stickyContainerHeaderHeight; - const newTop = data ? Number(data) + top : top; - if (el === container) return newTop; - return getStickyTop(el.parentElement, container, newTop); -} - -export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) { - if (!el.parentElement) return bottom; - const data = el.dataset.stickyContainerFooterHeight; - const newBottom = data ? Number(data) + bottom : bottom; - if (el === container) return newBottom; - return getStickyBottom(el.parentElement, container, newBottom); -} - -export function getScrollPosition(el: HTMLElement | null): number { - const container = getScrollContainer(el); - return container == null ? window.scrollY : container.scrollTop; -} - -export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { - // とりあえず評価してみる - if (el.isConnected && isTopVisible(el)) { - cb(); - if (once) return null; - } - - const container = getScrollContainer(el) ?? window; - - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isTopVisible(el, tolerance)) { - cb(); - if (once) removeListener(); - } - }; - - function removeListener() { container.removeEventListener('scroll', onScroll); } - - container.addEventListener('scroll', onScroll, { passive: true }); - return removeListener; -} - -export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { - const container = getScrollContainer(el); - - // とりあえず評価してみる - if (el.isConnected && isBottomVisible(el, tolerance, container)) { - cb(); - if (once) return null; - } - - const containerOrWindow = container ?? window; - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isBottomVisible(el, 1, container)) { - cb(); - if (once) removeListener(); - } - }; - - function removeListener() { - containerOrWindow.removeEventListener('scroll', onScroll); - } - - containerOrWindow.addEventListener('scroll', onScroll, { passive: true }); - return removeListener; -} - -export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { - const container = getScrollContainer(el); - if (container == null) { - window.scroll(options); - } else { - container.scroll(options); - } -} - -/** - * Scroll to Top - * @param el Scroll container element - * @param options Scroll options - */ -export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 0, ...options }); -} - -/** - * Scroll to Bottom - * @param el Content element - * @param options Scroll options - * @param container Scroll container element - */ -export function scrollToBottom( - el: HTMLElement, - options: ScrollToOptions = {}, - container = getScrollContainer(el), -) { - if (container) { - container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options }); - } else { - window.scroll({ - top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0, - ...options, - }); - } -} - -export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { - const scrollTop = getScrollPosition(el); - return scrollTop <= tolerance; -} - -export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { - if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; - return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; -} - -// https://ja.javascript.info/size-and-scroll-window#ref-932 -export function getBodyScrollHeight() { - return Math.max( - document.body.scrollHeight, document.documentElement.scrollHeight, - document.body.offsetHeight, document.documentElement.offsetHeight, - document.body.clientHeight, document.documentElement.clientHeight, - ); -} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts new file mode 100644 index 0000000000..cb0e607fcb --- /dev/null +++ b/packages/frontend/src/scripts/stream-mock.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Misskey from 'misskey-js'; +import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; + +type AnyOf<T extends Record<any, any>> = T[keyof T]; +type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never; + +/** + * Websocket無効化時に使うStreamのモック(なにもしない) + */ +export class StreamMock extends EventEmitter<StreamEvents> implements IStream { + public readonly state = 'initializing'; + + constructor(...args: ConstructorParameters<typeof Misskey.Stream>) { + super(); + // do nothing + } + + public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> { + return new ChannelConnectionMock(this, channel, name); + } + + public removeSharedConnection(connection: any): void { + // do nothing + } + + public removeSharedConnectionPool(pool: any): void { + // do nothing + } + + public disconnectToChannel(): void { + // do nothing + } + + public send(typeOrPayload: string): void + public send(typeOrPayload: string, payload: any): void + public send(typeOrPayload: Record<string, any> | any[]): void + public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void { + // do nothing + } + + public ping(): void { + // do nothing + } + + public heartbeat(): void { + // do nothing + } + + public close(): void { + // do nothing + } +} + +class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> { + public id = ''; + public name?: string; // for debug + public inCount = 0; // for debug + public outCount = 0; // for debug + public channel: string; + + constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) { + super(); + + this.channel = args[0]; + this.name = args[1]; + } + + public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void { + // do nothing + } + + public dispose(): void { + // do nothing + } +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index c7f8b3d596..9b9f1f030c 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,11 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts deleted file mode 100644 index 5a8265af9e..0000000000 --- a/packages/frontend/src/scripts/url.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* objを検査して - * 1. 配列に何も入っていない時はクエリを付けない - * 2. プロパティがundefinedの時はクエリを付けない - * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ -export function query(obj: Record<string, any>): string { - const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); - - return Object.entries(params) - .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) - .join('&'); -} - -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; -} - -export function extractDomain(url: string) { - const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im); - return match ? match[1] : null; -} diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts deleted file mode 100644 index a8f4d5e03a..0000000000 --- a/packages/frontend/src/scripts/use-document-visibility.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onMounted, onUnmounted, ref, Ref } from 'vue'; - -export function useDocumentVisibility(): Ref<DocumentVisibilityState> { - const visibility = ref(document.visibilityState); - - const onChange = (): void => { - visibility.value = document.visibilityState; - }; - - onMounted(() => { - document.addEventListener('visibilitychange', onChange); - }); - - onUnmounted(() => { - document.removeEventListener('visibilitychange', onChange); - }); - - return visibility; -} diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts deleted file mode 100644 index b50e78c3cc..0000000000 --- a/packages/frontend/src/scripts/use-interval.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'; - -export function useInterval(fn: () => void, interval: number, options: { - immediate: boolean; - afterMounted: boolean; -}): (() => void) | undefined { - if (Number.isNaN(interval)) return; - - let intervalId: number | null = null; - - if (options.afterMounted) { - onMounted(() => { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - } else { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - } - - const clear = () => { - if (intervalId) window.clearInterval(intervalId); - intervalId = null; - }; - - onActivated(() => { - if (intervalId) return; - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - - onDeactivated(() => { - clear(); - }); - - onUnmounted(() => { - clear(); - }); - - return clear; -} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 437314074a..0bf499bb4d 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -458,10 +458,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - contextMenu: { + contextMenu: { where: 'device', default: 'app' as 'app' | 'appWithShift' | 'native', - }, + }, sound_masterVolume: { where: 'device', @@ -520,8 +520,8 @@ interface Watcher { /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ -import lightTheme from '@/themes/l-light.json5'; -import darkTheme from '@/themes/d-green-lime.json5'; +import lightTheme from '@@/themes/l-light.json5'; +import darkTheme from '@@/themes/d-green-lime.json5'; export class ColdDeviceStorage { public static default = { @@ -558,7 +558,7 @@ export class ColdDeviceStorage { public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼び出し側のバグ等で undefined が来ることがある // undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index 0d5bd78b09..9d7edce890 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -7,17 +7,20 @@ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; import { wsOrigin } from '@/config.js'; +// TODO: No WebsocketモードでStreamMockが使えそう +//import { StreamMock } from '@/scripts/stream-mock.js'; // heart beat interval in ms const HEART_BEAT_INTERVAL = 1000 * 60; -let stream: Misskey.Stream | null = null; -let timeoutHeartBeat: ReturnType<typeof setTimeout> | null = null; +let stream: Misskey.IStream | null = null; +let timeoutHeartBeat: number | null = null; let lastHeartbeatCall = 0; -export function useStream(): Misskey.Stream { +export function useStream(): Misskey.IStream { if (stream) return stream; + // TODO: No Websocketモードもここで判定 stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 deleted file mode 100644 index 17fb98e4ee..0000000000 --- a/packages/frontend/src/themes/_dark.json5 +++ /dev/null @@ -1,93 +0,0 @@ -// ダークテーマのベーステーマ -// このテーマが直接使われることは無い -{ - id: 'dark', - - name: 'Dark', - author: 'syuilo', - desc: 'Default dark theme', - kind: 'dark', - - props: { - accent: '#86b300', - accentDarken: ':darken<10<@accent', - accentLighten: ':lighten<10<@accent', - accentedBg: ':alpha<0.15<@accent', - focus: ':alpha<0.3<@accent', - bg: '#000', - acrylicBg: ':alpha<0.5<@bg', - fg: '#dadada', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgHighlighted: ':lighten<3<@fg', - fgOnAccent: '#fff', - fgOnWhite: '#333', - divider: 'rgba(255, 255, 255, 0.1)', - indicator: '@accent', - panel: ':lighten<3<@bg', - panelHighlight: ':lighten<3<@panel', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - panelBorder: '" solid 1px var(--divider)', - acrylicPanel: ':alpha<0.5<@panel', - windowHeader: ':alpha<0.85<@panel', - popup: ':lighten<3<@panel', - shadow: 'rgba(0, 0, 0, 0.3)', - header: ':alpha<0.7<@panel', - navBg: '@panel', - navFg: '@fg', - navHoverFg: ':lighten<17<@fg', - navActive: '@accent', - navIndicator: '@indicator', - link: '#44a4c1', - hashtag: '#ff9156', - mention: '@accent', - mentionMe: '@mention', - renote: '#229e82', - modalBg: 'rgba(0, 0, 0, 0.5)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - dateLabelFg: '@fg', - infoBg: '#253142', - infoFg: '#fff', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - switchBg: 'rgba(255, 255, 255, 0.15)', - buttonBg: 'rgba(255, 255, 255, 0.05)', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - switchOffBg: 'rgba(255, 255, 255, 0.1)', - switchOffFg: ':alpha<0.8<@fg', - switchOnBg: '@accentedBg', - switchOnFg: '@accent', - inputBorder: 'rgba(255, 255, 255, 0.1)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - badge: '#31b1ce', - messageBg: '@bg', - success: '#86b300', - error: '#ec4137', - warn: '#ecb637', - codeString: '#ffb675', - codeNumber: '#cfff9e', - codeBoolean: '#c59eff', - deckBg: '#000', - htmlThemeColor: '@bg', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - }, - - codeHighlighter: { - base: 'one-dark-pro', - }, -} diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 deleted file mode 100644 index ca6c059e16..0000000000 --- a/packages/frontend/src/themes/_light.json5 +++ /dev/null @@ -1,93 +0,0 @@ -// ライトテーマのベーステーマ -// このテーマが直接使われることは無い -{ - id: 'light', - - name: 'Light', - author: 'syuilo', - desc: 'Default light theme', - kind: 'light', - - props: { - accent: '#86b300', - accentDarken: ':darken<10<@accent', - accentLighten: ':lighten<10<@accent', - accentedBg: ':alpha<0.15<@accent', - focus: ':alpha<0.3<@accent', - bg: '#fff', - acrylicBg: ':alpha<0.5<@bg', - fg: '#5f5f5f', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgHighlighted: ':darken<3<@fg', - fgOnAccent: '#fff', - fgOnWhite: '#333', - divider: 'rgba(0, 0, 0, 0.1)', - indicator: '@accent', - panel: ':lighten<3<@bg', - panelHighlight: ':darken<3<@panel', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - panelBorder: '" solid 1px var(--divider)', - acrylicPanel: ':alpha<0.5<@panel', - windowHeader: ':alpha<0.85<@panel', - popup: ':lighten<3<@panel', - shadow: 'rgba(0, 0, 0, 0.1)', - header: ':alpha<0.7<@panel', - navBg: '@panel', - navFg: '@fg', - navHoverFg: ':darken<17<@fg', - navActive: '@accent', - navIndicator: '@indicator', - link: '#44a4c1', - hashtag: '#ff9156', - mention: '@accent', - mentionMe: '@mention', - renote: '#229e82', - modalBg: 'rgba(0, 0, 0, 0.3)', - scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - dateLabelFg: '@fg', - infoBg: '#e5f5ff', - infoFg: '#72818a', - infoWarnBg: '#fff0db', - infoWarnFg: '#8f6e31', - switchBg: 'rgba(0, 0, 0, 0.15)', - buttonBg: 'rgba(0, 0, 0, 0.05)', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - switchOffBg: 'rgba(0, 0, 0, 0.1)', - switchOffFg: '@panel', - switchOnBg: '@accent', - switchOnFg: '@fgOnAccent', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', - wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', - badge: '#31b1ce', - messageBg: '@bg', - success: '#86b300', - error: '#ec4137', - warn: '#ecb637', - codeString: '#b98710', - codeNumber: '#0fbbbb', - codeBoolean: '#62b70c', - deckBg: ':darken<3<@bg', - htmlThemeColor: '@bg', - X3: 'rgba(0, 0, 0, 0.05)', - X4: 'rgba(0, 0, 0, 0.1)', - X5: 'rgba(0, 0, 0, 0.05)', - X6: 'rgba(0, 0, 0, 0.25)', - X7: 'rgba(0, 0, 0, 0.05)', - X11: 'rgba(0, 0, 0, 0.1)', - X12: 'rgba(0, 0, 0, 0.1)', - X13: 'rgba(0, 0, 0, 0.15)', - }, - - codeHighlighter: { - base: 'catppuccin-latte', - }, -} diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend/src/themes/d-astro.json5 deleted file mode 100644 index 1cbb4e519d..0000000000 --- a/packages/frontend/src/themes/d-astro.json5 +++ /dev/null @@ -1,69 +0,0 @@ -{ - id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea', - base: 'dark', - name: 'Mi Astro Dark', - author: 'syuilo', - props: { - bg: '#232125', - fg: '#efdab9', - link: '#78b0a0', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: '#2a272b', - accent: '#81c08b', - header: ':alpha<0.7<@bg', - infoBg: '#253142', - infoFg: '#fff', - renote: '#659CC8', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: '#ff9156', - mention: '#ffd152', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '#fb5d38', - messageBg: '@bg', - navActive: '@accent', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@accent', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<-20<@accent', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - htmlThemeColor: '@bg', - fgOnWhite: '@accent', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - }, -} diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend/src/themes/d-botanical.json5 deleted file mode 100644 index 62208d2378..0000000000 --- a/packages/frontend/src/themes/d-botanical.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: '504debaf-4912-6a4c-5059-1db08a76b737', - - name: 'Mi Botanical Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(148, 179, 0)', - bg: 'rgb(37, 38, 36)', - fg: 'rgb(216, 212, 199)', - fgHighlighted: '#fff', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.14)', - panel: 'rgb(47, 47, 44)', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - header: ':alpha<0.7<@panel', - navBg: '#363636', - renote: '@accent', - mention: 'rgb(212, 153, 76)', - mentionMe: 'rgb(212, 210, 76)', - hashtag: '#5bcbb0', - link: '@accent', - }, -} diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend/src/themes/d-cherry.json5 deleted file mode 100644 index f9638124c2..0000000000 --- a/packages/frontend/src/themes/d-cherry.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: '679b3b87-a4e9-4789-8696-b56c15cc33b0', - - name: 'Mi Cherry Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(255, 89, 117)', - bg: 'rgb(28, 28, 37)', - fg: 'rgb(236, 239, 244)', - fgOnWhite: '@accent', - panel: 'rgb(35, 35, 47)', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - divider: 'rgb(63, 63, 80)', - }, -} diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend/src/themes/d-dark.json5 deleted file mode 100644 index ae4f7d53f5..0000000000 --- a/packages/frontend/src/themes/d-dark.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: '8050783a-7f63-445a-b270-36d0f6ba1677', - - name: 'Mi Dark', - author: 'syuilo', - desc: 'Default light theme', - - base: 'dark', - - props: { - bg: '#232323', - fg: 'rgb(199, 209, 216)', - fgHighlighted: '#fff', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.14)', - panel: '#2d2d2d', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - header: ':alpha<0.7<@panel', - navBg: '#363636', - renote: '@accent', - mention: '#da6d35', - mentionMe: '#d44c4c', - hashtag: '#4cb8d4', - link: '@accent', - }, -} diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend/src/themes/d-future.json5 deleted file mode 100644 index f2c1f3eb86..0000000000 --- a/packages/frontend/src/themes/d-future.json5 +++ /dev/null @@ -1,27 +0,0 @@ -{ - id: '32a637ef-b47a-4775-bb7b-bacbb823f865', - - name: 'Mi Future Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#63e2b7', - bg: '#101014', - fg: '#D5D5D6', - fgHighlighted: '#fff', - fgOnAccent: '#000', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.1)', - panel: '#18181c', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - renote: '@accent', - mention: '#f2c97d', - mentionMe: '@accent', - hashtag: '#70c0e8', - link: '#e88080', - buttonGradateA: '@accent', - buttonGradateB: ':saturate<30<:hue<30<@accent', - }, -} diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend/src/themes/d-green-lime.json5 deleted file mode 100644 index ca4e688fdb..0000000000 --- a/packages/frontend/src/themes/d-green-lime.json5 +++ /dev/null @@ -1,24 +0,0 @@ -{ - id: '02816013-8107-440f-877e-865083ffe194', - - name: 'Mi Green+Lime Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#b4e900', - bg: '#0C1210', - fg: '#dee7e4', - fgHighlighted: '#fff', - fgOnAccent: '#192320', - fgOnWhite: '@accent', - divider: '#e7fffb24', - panel: '#192320', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - popup: '#293330', - renote: '@accent', - mentionMe: '#ffaa00', - link: '#24d7ce', - }, -} diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend/src/themes/d-green-orange.json5 deleted file mode 100644 index c2539816e2..0000000000 --- a/packages/frontend/src/themes/d-green-orange.json5 +++ /dev/null @@ -1,24 +0,0 @@ -{ - id: 'dc489603-27b5-424a-9b25-1ff6aec9824a', - - name: 'Mi Green+Orange Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#e97f00', - bg: '#0C1210', - fg: '#dee7e4', - fgHighlighted: '#fff', - fgOnAccent: '#192320', - fgOnWhite: '@accent', - divider: '#e7fffb24', - panel: '#192320', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - popup: '#293330', - renote: '@accent', - mentionMe: '#b4e900', - link: '#24d7ce', - }, -} diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend/src/themes/d-ice.json5 deleted file mode 100644 index b4abc0cacb..0000000000 --- a/packages/frontend/src/themes/d-ice.json5 +++ /dev/null @@ -1,14 +0,0 @@ -{ - id: '66e7e5a9-cd43-42cd-837d-12f47841fa34', - - name: 'Mi Ice Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#47BFE8', - fgOnWhite: '@accent', - bg: '#212526', - }, -} diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend/src/themes/d-persimmon.json5 deleted file mode 100644 index 0ab6523dd7..0000000000 --- a/packages/frontend/src/themes/d-persimmon.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', - - name: 'Mi Persimmon Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(206, 102, 65)', - bg: 'rgb(31, 33, 31)', - fg: '#cdd8c7', - fgHighlighted: '#fff', - fgOnWhite: '@accent', - divider: 'rgba(255, 255, 255, 0.14)', - panel: 'rgb(41, 43, 41)', - infoFg: '@fg', - infoBg: '#333c3b', - navBg: '#141714', - renote: '@accent', - mention: '@accent', - mentionMe: '#de6161', - hashtag: '#68bad0', - link: '#a1c758', - }, -} diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend/src/themes/d-u0.json5 deleted file mode 100644 index c8a31bb1a7..0000000000 --- a/packages/frontend/src/themes/d-u0.json5 +++ /dev/null @@ -1,83 +0,0 @@ -{ - id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb', - base: 'dark', - name: 'Mi U0 Dark', - props: { - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - bg: '#172426', - fg: '#dadada', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - link: '@accent', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: '#00a497', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: '#e6b422', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - codeNumber: '#cfff9e', - codeString: '#ffb675', - fgOnAccent: '#fff', - fgOnWhite: '@accent', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - codeBoolean: '#c59eff', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - deckBg: '#142022', - }, -} diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend/src/themes/l-apricot.json5 deleted file mode 100644 index fe1f9f8927..0000000000 --- a/packages/frontend/src/themes/l-apricot.json5 +++ /dev/null @@ -1,23 +0,0 @@ -{ - id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', - - name: 'Mi Apricot Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: 'rgb(234, 154, 82)', - bg: '#e6e5e2', - fg: 'rgb(149, 143, 139)', - fgOnWhite: '@accent', - panel: '#EEECE8', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - infoBg: 'rgb(226, 235, 241)', - }, -} diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend/src/themes/l-botanical.json5 deleted file mode 100644 index 17e9ca246f..0000000000 --- a/packages/frontend/src/themes/l-botanical.json5 +++ /dev/null @@ -1,30 +0,0 @@ -{ - id: '1100673c-f902-4ccd-93aa-7cb88be56178', - - name: 'Mi Botanical Light', - author: 'ThinaticSystem', - - base: 'light', - - props: { - accent: '#77b58c', - bg: 'e2deda', - fg: '#3d3d3d', - fgHighlighted: '#6bc9a0', - fgOnWhite: '@accent', - divider: '#cfcfcf', - panel: '@X14', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - header: ':alpha<0.7<@panel', - navBg: '@X14', - renote: '#229e92', - mention: '#da6d35', - mentionMe: '#d44c4c', - hashtag: '#4cb8d4', - link: '@accent', - buttonGradateB: ':hue<-70<@accent', - success: '#86b300', - X14: '#ebe7e5' - }, -} diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend/src/themes/l-cherry.json5 deleted file mode 100644 index 1189a28fe6..0000000000 --- a/packages/frontend/src/themes/l-cherry.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - id: 'ac168876-f737-4074-a3fc-a370c732ef48', - - name: 'Mi Cherry Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: 'rgb(219, 96, 114)', - bg: 'rgb(254, 248, 249)', - fg: 'rgb(152, 13, 26)', - fgOnWhite: '@accent', - panel: 'rgb(255, 255, 255)', - renote: '@accent', - link: 'rgb(156, 187, 5)', - mention: '@accent', - hashtag: '@accent', - divider: 'rgba(134, 51, 51, 0.1)', - inputBorderHover: 'rgb(238, 221, 222)', - }, -} diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend/src/themes/l-coffee.json5 deleted file mode 100644 index b64cc73583..0000000000 --- a/packages/frontend/src/themes/l-coffee.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab', - - name: 'Mi Coffee Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#9f8989', - bg: '#f5f3f3', - fg: '#7f6666', - fgOnWhite: '@accent', - panel: '#fff', - divider: 'rgba(87, 68, 68, 0.1)', - renote: 'rgb(160, 172, 125)', - link: 'rgb(137, 151, 159)', - mention: '@accent', - mentionMe: 'rgb(170, 149, 98)', - hashtag: '@accent', - }, -} diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend/src/themes/l-light.json5 deleted file mode 100644 index 63c2e6d278..0000000000 --- a/packages/frontend/src/themes/l-light.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: '4eea646f-7afa-4645-83e9-83af0333cd37', - - name: 'Mi Light', - author: 'syuilo', - desc: 'Default light theme', - - base: 'light', - - props: { - bg: '#f9f9f9', - fg: '#676767', - fgOnWhite: '@accent', - divider: '#e8e8e8', - header: ':alpha<0.7<@panel', - navBg: '#fff', - panel: '#fff', - panelHeaderDivider: '@divider', - mentionMe: 'rgb(0, 179, 70)', - }, -} diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend/src/themes/l-rainy.json5 deleted file mode 100644 index e7d1d5af00..0000000000 --- a/packages/frontend/src/themes/l-rainy.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', - - name: 'Mi Rainy Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#5db0da', - bg: 'rgb(246 248 249)', - fg: '#636b71', - fgOnWhite: '@accent', - panel: '#fff', - divider: 'rgb(230 233 234)', - panelHeaderDivider: '@divider', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - }, -} diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend/src/themes/l-sushi.json5 deleted file mode 100644 index e787d63734..0000000000 --- a/packages/frontend/src/themes/l-sushi.json5 +++ /dev/null @@ -1,19 +0,0 @@ -{ - id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c', - - name: 'Mi Sushi Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#e36749', - bg: '#f0eee9', - fg: '#5f5f5f', - fgOnWhite: '@accent', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '#229e82', - }, -} diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend/src/themes/l-u0.json5 deleted file mode 100644 index 0b952b003a..0000000000 --- a/packages/frontend/src/themes/l-u0.json5 +++ /dev/null @@ -1,82 +0,0 @@ -{ - id: 'e2c940b5-6e9a-4c03-b738-261c720c426d', - base: 'light', - name: 'Mi U0 Light', - props: { - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - bg: '#e7e7eb', - fg: '#5f5f5f', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - link: '@accent', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: '#478384', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: '#4646461a', - hashtag: '#1f3134', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: '#0000000d', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - codeNumber: '#cfff9e', - codeString: '#ffb675', - fgOnAccent: '#fff', - fgOnWhite: '@accent', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - codeBoolean: '#c59eff', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: '#0000001a', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: '#74747433', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - }, -} diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend/src/themes/l-vivid.json5 deleted file mode 100644 index 3da2ca28fb..0000000000 --- a/packages/frontend/src/themes/l-vivid.json5 +++ /dev/null @@ -1,72 +0,0 @@ -{ - id: '6128c2a9-5c54-43fe-a47d-17942356470b', - - name: 'Mi Vivid Light', - author: 'syuilo', - - base: 'light', - - props: { - bg: '#fafafa', - fg: '#444', - link: '#ff9400', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: '#fff', - accent: '#008cff', - header: ':alpha<0.7<@panel', - infoBg: '#e5f5ff', - infoFg: '#72818a', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.1)', - divider: 'rgba(0, 0, 0, 0.08)', - hashtag: '#92d400', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.3)', - success: '#86b300', - buttonBg: 'rgba(0, 0, 0, 0.05)', - acrylicBg: ':alpha<0.5<@bg', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - infoWarnBg: '#fff0db', - infoWarnFg: '#8f6e31', - navHoverFg: ':darken<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@accent', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':darken<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgOnWhite: '@accent', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - htmlThemeColor: '@bg', - panelHighlight: ':darken<3<@panel', - listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: '@divider', - scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - X3: 'rgba(0, 0, 0, 0.05)', - X4: 'rgba(0, 0, 0, 0.1)', - X5: 'rgba(0, 0, 0, 0.05)', - X6: 'rgba(0, 0, 0, 0.25)', - X7: 'rgba(0, 0, 0, 0.05)', - X11: 'rgba(0, 0, 0, 0.1)', - X12: 'rgba(0, 0, 0, 0.1)', - X13: 'rgba(0, 0, 0, 0.15)', - }, -} diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index 8dad666623..e234bb3a33 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -35,7 +35,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index 6e1d06eec1..550fc39b00 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { shuffle } from '@/scripts/shuffle.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 67f8b109c4..078b595dca 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -35,7 +35,7 @@ import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 79c9671917..e7ecf7fd20 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -26,7 +26,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { mainRouter } from '@/router/main.js'; defineProps<{ diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 073acbd4db..00a6811fc9 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -108,7 +108,7 @@ import { $i } from '@/account.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; -import { CURRENT_STICKY_BOTTOM } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; import { useScrollPositionManager } from '@/nirax.js'; import { mainRouter } from '@/router/main.js'; diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 49fd103d37..bcfaaf00ab 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import { $i } from '@/account.js'; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 6ece33eff3..412d527819 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -42,7 +42,7 @@ import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const name = 'calendar'; diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index ed907de9b8..c10416e4fb 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -32,7 +32,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index 76ccdb3971..d090372b9a 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -26,7 +26,7 @@ import MkContainer from '@/components/MkContainer.vue'; import MkTagCloud from '@/components/MkTagCloud.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const name = 'instanceCloud'; diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 5c89a06c62..d56ee96ac1 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -18,7 +18,7 @@ import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import number from '@/filters/number.js'; diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index e5758662cc..13f5a4802a 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -30,7 +30,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { url as base } from '@/config.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { infoImageUrl } from '@/instance.js'; const name = 'rss'; diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 16306ef5ba..51f1cac97f 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -35,7 +35,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { shuffle } from '@/scripts/shuffle.js'; import { url as base } from '@/config.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const name = 'rssTicker'; diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index b8efd3bda9..3fea1d7053 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -23,7 +23,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; const name = 'slideshow'; diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 4299181a27..a41db513e8 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index d9f4dc49ea..72391d622e 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts index 9a2989b373..cf686efd0d 100644 --- a/packages/frontend/test/emoji.test.ts +++ b/packages/frontend/test/emoji.test.ts @@ -6,7 +6,7 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import { defaultStoreState } from './init.js'; -import { getEmojiName } from '@/scripts/emojilist.js'; +import { getEmojiName } from '@@/js/emojilist.js'; import { components } from '@/components/index.js'; import { directives } from '@/directives/index.js'; import MkEmoji from '@/components/global/MkEmoji.vue'; diff --git a/packages/frontend/test/i18n.test.ts b/packages/frontend/test/i18n.test.ts index e1cab1f15f..9d6cf855f3 100644 --- a/packages/frontend/test/i18n.test.ts +++ b/packages/frontend/test/i18n.test.ts @@ -4,9 +4,11 @@ */ import { describe, expect, it } from 'vitest'; -import { I18n } from '@/scripts/i18n.js'; +import { I18n } from '../../frontend-shared/js/i18n.js'; // @@で参照できなかったので import { ParameterizedString } from '../../../locales/index.js'; +// TODO: このテストはfrontend-sharedに移動する + describe('i18n', () => { it('t', () => { const i18n = new I18n({ diff --git a/packages/frontend/test/scroll.test.ts b/packages/frontend/test/scroll.test.ts index a0b56b7221..32a5a1c558 100644 --- a/packages/frontend/test/scroll.test.ts +++ b/packages/frontend/test/scroll.test.ts @@ -5,7 +5,7 @@ import { describe, test, assert, afterEach } from 'vitest'; import { Window } from 'happy-dom'; -import { onScrollBottom, onScrollTop } from '@/scripts/scroll.js'; +import { onScrollBottom, onScrollTop } from '@@/js/scroll.js'; describe('Scroll', () => { describe('onScrollTop', () => { diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index fe4d202894..b88773b598 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -23,7 +23,8 @@ "useDefineForClassFields": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@@/*": ["../frontend-shared/*"] }, "typeRoots": [ "./@types", diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts index 887ab7927e..922fb45995 100644 --- a/packages/frontend/vite.config.local-dev.ts +++ b/packages/frontend/vite.config.local-dev.ts @@ -15,6 +15,7 @@ const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')) const httpUrl = `http://localhost:${port}/`; const websocketUrl = `ws://localhost:${port}/`; +const embedUrl = `http://localhost:5174/`; // activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す function varyHandler(req: IncomingMessage) { @@ -50,6 +51,12 @@ const devConfig: UserConfig = { ws: true, }, '/favicon.ico': httpUrl, + '/robots.txt': httpUrl, + '/embed.js': httpUrl, + '/embed': { + target: embedUrl, + ws: true, + }, '/identicon': { target: httpUrl, rewrite(path) { diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 6decbc0ef7..e982df8ffd 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -65,6 +65,9 @@ export function getConfig(): UserConfig { server: { port: 5173, + headers: { // なんか効かない + 'X-Frame-Options': 'DENY', + }, }, plugins: [ @@ -87,6 +90,7 @@ export function getConfig(): UserConfig { extensions, alias: { '@/': __dirname + '/src/', + '@@/': __dirname + '/../frontend-shared/', '/client-assets/': __dirname + '/assets/', '/static-assets/': __dirname + '/../backend/assets/', '/fluent-emojis/': __dirname + '/../../fluent-emojis/dist/', @@ -151,7 +155,7 @@ export function getConfig(): UserConfig { }, }, cssCodeSplit: true, - outDir: __dirname + '/../../built/_vite_', + outDir: __dirname + '/../../built/_frontend_vite_', assetsDir: '.', emptyOutDir: false, sourcemap: process.env.NODE_ENV === 'development', |