diff options
Diffstat (limited to 'packages/frontend/src/components')
184 files changed, 2472 insertions, 1440 deletions
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index e48b6ef781..c7252e7c98 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template> <template #suffix>#{{ report.targetUserId.toUpperCase() }}</template> - <div style="container-type: inline-size;"> + <div style="height: 300px; --MI-stickyTop: 0; --MI-stickyBottom: 0;"> <RouterView :router="targetRouter"/> </div> </MkFolder> @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template> <template #suffix>#{{ report.reporterId.toUpperCase() }}</template> - <div style="container-type: inline-size;"> + <div style="height: 300px; --MI-stickyTop: 0; --MI-stickyBottom: 0;"> <RouterView :router="reporterRouter"/> </div> </MkFolder> @@ -88,9 +88,9 @@ import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; import MkFolder from '@/components/MkFolder.vue'; import RouterView from '@/components/global/RouterView.vue'; -import { useRouterFactory } from '@/router/supplier'; import MkTextarea from '@/components/MkTextarea.vue'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; +import { createRouter } from '@/router.js'; const props = defineProps<{ report: Misskey.entities.AdminAbuseUserReportsResponse[number]; @@ -100,10 +100,9 @@ const emit = defineEmits<{ (ev: 'resolved', reportId: string): void; }>(); -const routerFactory = useRouterFactory(); -const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`); +const targetRouter = createRouter(`/admin/user/${props.report.targetUserId}`); targetRouter.init(); -const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`); +const reporterRouter = createRouter(`/admin/user/${props.report.reporterId}`); reporterRouter.init(); const moderationNote = ref(props.report.moderationNote ?? ''); @@ -135,7 +134,7 @@ function forward() { function showMenu(ev: MouseEvent) { os.popupMenu([{ - icon: 'ti ti-id', + icon: 'ti ti-hash', text: 'Copy ID', action: () => { copyToClipboard(props.report.id); diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index a634a748e9..dbac5e9dd7 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { ref, shallowRef } from 'vue'; +import { ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkWindow from '@/components/MkWindow.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -47,7 +47,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const uiWindow = shallowRef<InstanceType<typeof MkWindow>>(); +const uiWindow = useTemplateRef('uiWindow'); const comment = ref(props.initialComment ?? ''); function send() { diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 0839955d9d..cb8032c019 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js'; import MkMention from './MkMention.vue'; import { i18n } from '@/i18n.js'; import { host as localHost } from '@@/js/config.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; const user = ref<Misskey.entities.UserLite>(); diff --git a/packages/frontend/src/components/MkAchievements.stories.impl.ts b/packages/frontend/src/components/MkAchievements.stories.impl.ts index bbd3f69d7c..d838997616 100644 --- a/packages/frontend/src/components/MkAchievements.stories.impl.ts +++ b/packages/frontend/src/components/MkAchievements.stories.impl.ts @@ -9,7 +9,7 @@ import { HttpResponse, http } from 'msw'; import { userDetailed } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkAchievements from './MkAchievements.vue'; -import { ACHIEVEMENT_TYPES } from '@/scripts/achievements.js'; +import { ACHIEVEMENT_TYPES } from '@/utility/achievements.js'; export const Empty = { render(args) { return { diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index c8134416b5..70766634ce 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -55,9 +55,9 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { onMounted, ref, computed } from 'vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js'; +import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utility/achievements.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.User; diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index c8fa6246e0..eac1ea9534 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onMounted, onBeforeUnmount, ref } from 'vue'; import tinycolor from 'tinycolor2'; import { globalEvents } from '@/events.js'; -import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js'; +import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js'; // https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles const angleDiff = (a: number, b: number) => { @@ -192,7 +192,7 @@ function tick() { tick(); function calcColors() { - const computedStyle = getComputedStyle(document.documentElement); + const computedStyle = getComputedStyle(window.document.documentElement); const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark(); const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue index 4bf6125af5..e57fbcdee3 100644 --- a/packages/frontend/src/components/MkAnimBg.vue +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -4,14 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas> +<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas> </template> <script lang="ts" setup> -import { onMounted, onUnmounted, shallowRef } from 'vue'; +import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import isChromatic from 'chromatic/isChromatic'; -const canvasEl = shallowRef<HTMLCanvasElement>(); +const canvasEl = useTemplateRef('canvasEl'); const props = withDefaults(defineProps<{ scale?: number; diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue index 3045a47585..6e5b29654b 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.vue +++ b/packages/frontend/src/components/MkAnnouncementDialog.vue @@ -22,22 +22,23 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, shallowRef } from 'vue'; +import { onMounted, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { $i, updateAccountPartial } from '@/account.js'; +import { $i } from '@/i.js'; +import { updateCurrentAccountPartial } from '@/accounts.js'; const props = withDefaults(defineProps<{ announcement: Misskey.entities.Announcement; }>(), { }); -const rootEl = shallowRef<HTMLDivElement>(); -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const rootEl = useTemplateRef('rootEl'); +const modal = useTemplateRef('modal'); async function ok() { if (props.announcement.needConfirmationToRead) { @@ -51,7 +52,7 @@ async function ok() { modal.value?.close(); misskeyApi('i/read-announcement', { announcementId: props.announcement.id }); - updateAccountPartial({ + updateCurrentAccountPartial({ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id), }); } diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue index e622d57f1e..59099d54bd 100644 --- a/packages/frontend/src/components/MkAntennaEditor.vue +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch> <MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch> <MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch> + <MkSwitch v-model="excludeNotesInSensitiveChannel">{{ i18n.ts.excludeNotesInSensitiveChannel }}</MkSwitch> </div> <div :class="$style.actions"> <div class="_buttons"> @@ -53,16 +54,16 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import type { DeepPartial } from '@/utility/merge.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { deepMerge } from '@/scripts/merge.js'; -import type { DeepPartial } from '@/scripts/merge.js'; +import { deepMerge } from '@/utility/merge.js'; type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & { id?: string; @@ -86,6 +87,7 @@ const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, { caseSensitive: false, localOnly: false, withFile: false, + excludeNotesInSensitiveChannel: false, isActive: true, hasUnreadNote: false, notify: false, @@ -108,6 +110,7 @@ const localOnly = ref<boolean>(initialAntenna.localOnly); const excludeBots = ref<boolean>(initialAntenna.excludeBots); const withReplies = ref<boolean>(initialAntenna.withReplies); const withFile = ref<boolean>(initialAntenna.withFile); +const excludeNotesInSensitiveChannel = ref<boolean>(initialAntenna.excludeNotesInSensitiveChannel); const userLists = ref<Misskey.entities.UserList[] | null>(null); watch(() => src.value, async () => { @@ -124,6 +127,7 @@ async function saveAntenna() { excludeBots: excludeBots.value, withReplies: withReplies.value, withFile: withFile.value, + excludeNotesInSensitiveChannel: excludeNotesInSensitiveChannel.value, caseSensitive: caseSensitive.value, localOnly: localOnly.value, users: users.value.trim().split('\n').map(x => x.trim()), diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.vue b/packages/frontend/src/components/MkAntennaEditorDialog.vue index 6d815d29f3..0ebf5abf4c 100644 --- a/packages/frontend/src/components/MkAntennaEditorDialog.vue +++ b/packages/frontend/src/components/MkAntennaEditorDialog.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { shallowRef } from 'vue'; +import { useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import XAntennaEditor from '@/components/MkAntennaEditor.vue'; @@ -40,7 +40,7 @@ const emit = defineEmits<{ (ev: 'closed'): void, }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); function onAntennaCreated(newAntenna: Misskey.entities.Antenna) { emit('created', newAntenna); diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 5c4d887e0c..20a953c72c 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSelect from '@/components/MkSelect.vue'; -import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js'; +import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js'; import MkFolder from '@/components/MkFolder.vue'; import MkPostForm from '@/components/MkPostForm.vue'; diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue index f78d2d38f0..00bf8e68d9 100644 --- a/packages/frontend/src/components/MkAuthConfirm.vue +++ b/packages/frontend/src/components/MkAuthConfirm.vue @@ -117,14 +117,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; - import MkButton from '@/components/MkButton.vue'; - -import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js'; +import { $i } from '@/i.js'; +import { getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { getProxiedImageUrl } from '@/utility/media-proxy.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; const props = defineProps<{ name?: string; @@ -158,7 +157,7 @@ async function init() { const accounts = await getAccounts(); - const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id)); + const accountIdsToFetch = accounts.map(a => a.user.id).filter(id => !users.value.has(id)); if (accountIdsToFetch.length > 0) { const usersRes = await misskeyApi('users/show', { @@ -170,7 +169,7 @@ async function init() { users.value.set(user.id, { ...user, - token: accounts.find(a => a.id === user.id)!.token, + token: accounts.find(a => a.user.id === user.id)!.token, }); } } diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts index af5dd4784d..64ccb708aa 100644 --- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts +++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts @@ -12,7 +12,7 @@ import { userDetailed } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkAutocomplete from './MkAutocomplete.vue'; import MkInput from './MkInput.vue'; -import { tick } from '@/scripts/test-utils.js'; +import { tick } from '@/utility/test-utils.js'; const common = { render(args) { return { diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 33495c8af6..e51a56fa7b 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -44,27 +44,28 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; +import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import sanitizeHtml from 'sanitize-html'; import { emojilist, getEmojiName } from '@@/js/emojilist.js'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js'; import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js'; -import type { EmojiDef } from '@/scripts/search-emoji.js'; -import contains from '@/scripts/contains.js'; +import type { EmojiDef } from '@/utility/search-emoji.js'; +import contains from '@/utility/contains.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 { misskeyApi } from '@/utility/misskey-api.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { searchEmoji } from '@/scripts/search-emoji.js'; +import { searchEmoji } from '@/utility/search-emoji.js'; +import { prefer } from '@/preferences.js'; const lib = emojilist.filter(x => x.category !== 'flags'); const emojiDb = computed(() => { //#region Unicode Emoji - const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; + const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({ emoji: x.char, @@ -72,7 +73,7 @@ const emojiDb = computed(() => { url: char2path(x.char), })); - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { for (const [emoji, keywords] of Object.entries(index)) { for (const k of keywords) { unicodeEmojiDB.push({ @@ -138,7 +139,7 @@ const emit = defineEmits<{ }>(); const suggests = ref<Element>(); -const rootEl = shallowRef<HTMLDivElement>(); +const rootEl = useTemplateRef('rootEl'); const fetching = ref(true); const users = ref<any[]>([]); @@ -154,10 +155,10 @@ function complete(type: string, value: any) { emit('done', { type, value }); emit('closed'); if (type === 'emoji') { - let recents = defaultStore.state.recentlyUsedEmojis; + let recents = store.s.recentlyUsedEmojis; recents = recents.filter((emoji: any) => emoji !== value); recents.unshift(value); - defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); + store.set('recentlyUsedEmojis', recents.splice(0, 32)); } } @@ -237,7 +238,7 @@ function exec() { } else if (props.type === 'emoji') { if (!props.q || props.q === '') { // 最近使った絵文字をサジェスト - emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; + emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; return; } @@ -358,7 +359,7 @@ onMounted(() => { props.textarea.addEventListener('keydown', onKeydown); - document.body.addEventListener('mousedown', onMousedown); + window.document.body.addEventListener('mousedown', onMousedown); nextTick(() => { exec(); @@ -374,7 +375,7 @@ onMounted(() => { onBeforeUnmount(() => { props.textarea.removeEventListener('keydown', onKeydown); - document.body.removeEventListener('mousedown', onMousedown); + window.document.body.removeEventListener('mousedown', onMousedown); }); </script> @@ -419,7 +420,7 @@ onBeforeUnmount(() => { } &:active { - background: var(--MI_THEME-accentDarken); + background: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); color: #fff !important; } } diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue index 8236d0ddb9..1c44ed60d8 100644 --- a/packages/frontend/src/components/MkAvatars.vue +++ b/packages/frontend/src/components/MkAvatars.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; const props = withDefaults(defineProps<{ userIds: string[]; diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 311facb4aa..891af7f696 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="!link" ref="el" class="_button" - :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" + :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]" :type="type" :name="name" :value="value" - :disabled="disabled" + :disabled="disabled || wait" @click="emit('click', $event)" @mousedown="onMousedown" > @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </button> <MkA v-else class="_button" - :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" + :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]" :to="to ?? '#'" :behavior="linkBehavior" @mousedown="onMousedown" @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, shallowRef } from 'vue'; +import { nextTick, onMounted, useTemplateRef } from 'vue'; const props = defineProps<{ type?: 'button' | 'submit' | 'reset'; @@ -57,14 +57,15 @@ const props = defineProps<{ name?: string; value?: string; disabled?: boolean; + iconOnly?: boolean; }>(); const emit = defineEmits<{ (ev: 'click', payload: MouseEvent): void; }>(); -const el = shallowRef<HTMLElement | null>(null); -const ripples = shallowRef<HTMLElement | null>(null); +const el = useTemplateRef('el'); +const ripples = useTemplateRef('ripples'); onMounted(() => { if (props.autofocus) { @@ -91,7 +92,7 @@ function onMousedown(evt: MouseEvent): void { const target = evt.target! as HTMLElement; const rect = target.getBoundingClientRect(); - const ripple = document.createElement('div'); + const ripple = window.document.createElement('div'); ripple.classList.add(ripples.value!.dataset.childrenClass!); ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; @@ -147,6 +148,11 @@ function onMousedown(evt: MouseEvent): void { background: var(--MI_THEME-buttonHoverBg); } + &.iconOnly { + padding: 7px; + min-width: auto; + } + &.small { font-size: 90%; padding: 6px 12px; @@ -220,28 +226,28 @@ function onMousedown(evt: MouseEvent): void { background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); + background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5))); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); + background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5))); } } &.danger { font-weight: bold; - color: #ff2a2a; + color: var(--MI_THEME-error); &.primary { color: #fff; - background: #ff2a2a; + background: var(--MI_THEME-error); &:not(:disabled):hover { - background: #ff4242; + background: hsl(from var(--MI_THEME-error) h s calc(l + 10)); } &:not(:disabled):active { - background: #d42e2e; + background: hsl(from var(--MI_THEME-error) h s calc(l - 10)); } } } @@ -250,6 +256,10 @@ function onMousedown(evt: MouseEvent): void { opacity: 0.5; } + &.wait { + cursor: wait !important; + } + &:focus-visible { outline-offset: 2px; } diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 134f8226d4..30940a34a9 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -26,8 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; -import { defaultStore } from '@/store.js'; +import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; +import { store } from '@/store.js'; // APIs provided by Captcha services // see: https://docs.hcaptcha.com/configuration/#javascript-api @@ -69,7 +69,7 @@ const emit = defineEmits<{ const available = ref(false); -const captchaEl = shallowRef<HTMLDivElement | undefined>(); +const captchaEl = useTemplateRef('captchaEl'); const captchaWidgetId = ref<string | undefined>(undefined); const testcaptchaInput = ref(''); const testcaptchaPassed = ref(false); @@ -112,7 +112,7 @@ watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => { if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') { available.value = true; } else if (src.value !== null) { - (document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), { + (window.document.getElementById(scriptId.value) ?? window.document.head.appendChild(Object.assign(window.document.createElement('script'), { async: true, id: scriptId.value, src: src.value, @@ -149,12 +149,12 @@ async function requestRender() { if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) { // reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する. // (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので) - const elem = document.createElement('div'); + const elem = window.document.createElement('div'); captchaEl.value.appendChild(elem); captchaWidgetId.value = captcha.value.render(elem, { sitekey: props.sitekey, - theme: defaultStore.state.darkMode ? 'dark' : 'light', + theme: store.s.darkMode ? 'dark' : 'light', callback: callback, 'expired-callback': () => callback(undefined), 'error-callback': () => callback(undefined), @@ -174,7 +174,7 @@ async function requestRender() { function clearWidget() { if (props.provider === 'mcaptcha') { - const container = document.getElementById('mcaptcha__widget-container'); + const container = window.document.getElementById('mcaptcha__widget-container'); if (container) { container.innerHTML = ''; } diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts index a42e80c27a..4304c2e2b7 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts +++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts @@ -2,20 +2,18 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; + import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; import { expect, userEvent, within } from '@storybook/test'; import { channel } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkChannelFollowButton from './MkChannelFollowButton.vue'; +import type { StoryObj } from '@storybook/vue3'; import { i18n } from '@/i18n.js'; function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => window.setTimeout(resolve, ms)); } export const Default = { diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index d4e4f6179a..1aec8d0c07 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ @@ -103,13 +103,13 @@ async function onClick() { background: var(--MI_THEME-accent); &:hover { - background: var(--MI_THEME-accentLighten); - border-color: var(--MI_THEME-accentLighten); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); + border-color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); } &:active { - background: var(--MI_THEME-accentDarken); - border-color: var(--MI_THEME-accentDarken); + background: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); + border-color: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); } } diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue index 23705f6ff8..fdb7d2a1c4 100644 --- a/packages/frontend/src/components/MkChannelList.vue +++ b/packages/frontend/src/components/MkChannelList.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPagination :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> + <img :src="infoImageUrl" draggable="false"/> <div>{{ i18n.ts.notFound }}</div> </div> </template> @@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import type { Paging } from '@/components/MkPagination.vue'; import MkChannelPreview from '@/components/MkChannelPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; -import type { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index d05f4921f6..7e164362c1 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -45,23 +45,19 @@ export type ChartSrc = </script> <script lang="ts" setup> -/* eslint-disable id-denylist -- - Chart.js has a `data` attribute in most chart definitions, which triggers the - id-denylist violation when setting it. This is causing about 60+ lint issues. - As this is part of Chart.js's API it makes sense to disable the check here. -*/ -import { onMounted, ref, shallowRef, watch } from 'vue'; + +import { onMounted, ref, useTemplateRef, watch } from 'vue'; import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { chartVLine } from '@/scripts/chart-vline.js'; -import { alpha } from '@/scripts/color.js'; +import { misskeyApiGet } from '@/utility/misskey-api.js'; +import { store } from '@/store.js'; +import { useChartTooltip } from '@/use/use-chart-tooltip.js'; +import { chartVLine } from '@/utility/chart-vline.js'; +import { alpha } from '@/utility/color.js'; import date from '@/filters/date.js'; import bytes from '@/filters/bytes.js'; -import { initChart } from '@/scripts/init-chart.js'; -import { chartLegend } from '@/scripts/chart-legend.js'; +import { initChart } from '@/utility/init-chart.js'; +import { chartLegend } from '@/utility/chart-legend.js'; import MkChartLegend from '@/components/MkChartLegend.vue'; initChart(); @@ -96,7 +92,7 @@ const props = withDefaults(defineProps<{ nowForChromatic: undefined, }); -const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); +const legendEl = useTemplateRef('legendEl'); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const negate = arr => arr.map(x => -x); @@ -134,7 +130,7 @@ let chartData: { bytes?: boolean; } | null = null; -const chartEl = shallowRef<HTMLCanvasElement | null>(null); +const chartEl = useTemplateRef('chartEl'); const fetching = ref(true); const getDate = (ago: number) => { @@ -161,7 +157,7 @@ const render = () => { chartInstance.destroy(); } - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y))); @@ -849,7 +845,7 @@ watch(() => [props.src, props.span], fetchAndRender); onMounted(() => { fetchAndRender(); }); -/* eslint-enable id-denylist */ + </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts index eb7e61f294..6e1eb13d61 100644 --- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts +++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts @@ -2,18 +2,16 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; + import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; import { expect, userEvent, within } from '@storybook/test'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkClickerGame from './MkClickerGame.vue'; +import type { StoryObj } from '@storybook/vue3'; function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => window.setTimeout(resolve, ms)); } export const Default = { diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 9a0a9fba05..775964af50 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -23,9 +23,9 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'; import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import * as os from '@/os.js'; import { useInterval } from '@@/js/use-interval.js'; -import * as game from '@/scripts/clicker-game.js'; +import * as game from '@/utility/clicker-game.js'; import number from '@/filters/number.js'; -import { claimAchievement } from '@/scripts/achievements.js'; +import { claimAchievement } from '@/utility/achievements.js'; const saveData = game.saveData; const cookies = computed(() => saveData.value?.cookies); diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue index 5b09ec90dd..2154c08ab3 100644 --- a/packages/frontend/src/components/MkClipPreview.vue +++ b/packages/frontend/src/components/MkClipPreview.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { computed } from 'vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; import number from '@/filters/number.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index 0d7a67eaec..8b39468d4c 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ref, watch } from 'vue'; import { bundledLanguagesInfo } from 'shiki/langs'; import type { BundledLanguage } from 'shiki/langs'; -import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js'; -import { defaultStore } from '@/store.js'; +import { getHighlighter, getTheme } from '@/utility/code-highlighter.js'; +import { store } from '@/store.js'; const props = defineProps<{ code: string; @@ -22,7 +22,7 @@ const props = defineProps<{ }>(); const highlighter = await getHighlighter(); -const darkMode = defaultStore.reactiveState.darkMode; +const darkMode = store.r.darkMode; const codeLang = ref<BundledLanguage | 'aiscript'>('js'); const [lightThemeName, darkThemeName] = await Promise.all([ @@ -74,10 +74,8 @@ watch(() => props.lang, (to) => { <style module lang="scss"> .codeBlockRoot :global(.shiki) { padding: 1em; - margin: .5em 0; + margin: 0; overflow: auto; - border-radius: 8px; - border: 1px solid var(--MI_THEME-divider); font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; color: var(--shiki-fallback); diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index cb82bfd98b..d2d9f320ee 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </button> <Suspense> <template #fallback> - <MkLoading /> + <MkLoading/> </template> <XCode v-if="show && lang" :code="code" :lang="lang"/> <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> @@ -28,9 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, ref } from 'vue'; import * as os from '@/os.js'; import MkLoading from '@/components/global/MkLoading.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ code: string; @@ -42,13 +42,12 @@ const props = withDefaults(defineProps<{ forceShow: false, }); -const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code); +const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code); const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); function copy() { copyToClipboard(props.code); - os.success(); } </script> diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 5bf2301e72..bdb2ba6a44 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, watch, toRefs, shallowRef, nextTick } from 'vue'; +import { ref, watch, toRefs, useTemplateRef, nextTick } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; @@ -61,7 +61,7 @@ const { modelValue } = toRefs(props); const v = ref<string>(modelValue.value ?? ''); const focused = ref(false); const changed = ref(false); -const inputEl = shallowRef<HTMLTextAreaElement>(); +const inputEl = useTemplateRef('inputEl'); const focus = () => inputEl.value?.focus(); @@ -140,7 +140,7 @@ watch(v, newValue => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index 55a32664de..80618ebfe4 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, toRefs } from 'vue'; +import { ref, useTemplateRef, toRefs } from 'vue'; const props = defineProps<{ modelValue: string | null; @@ -39,7 +39,7 @@ const emit = defineEmits<{ const { modelValue } = toRefs(props); const v = ref(modelValue.value); -const inputEl = shallowRef<HTMLElement>(); +const inputEl = useTemplateRef('inputEl'); const onInput = () => { emit('update:modelValue', v.value ?? ''); @@ -60,7 +60,7 @@ const onInput = () => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 6a278250fa..1993991106 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </header> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''" @enter="enter" @afterEnter="afterEnter" @leave="leave" @@ -39,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; -import { defaultStore } from '@/store.js'; +import { onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'; +import { prefer } from '@/preferences.js'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ @@ -58,9 +58,9 @@ const props = withDefaults(defineProps<{ maxHeight: null, }); -const rootEl = shallowRef<HTMLElement>(); -const contentEl = shallowRef<HTMLElement>(); -const headerEl = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); +const contentEl = useTemplateRef('contentEl'); +const headerEl = useTemplateRef('headerEl'); const showBody = ref(props.expanded); const ignoreOmit = ref(false); const omitted = ref(false); @@ -181,11 +181,16 @@ onUnmounted(() => { left: 0; color: var(--MI_THEME-panelHeaderFg); background: var(--MI_THEME-panelHeaderBg); - border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider); z-index: 2; line-height: 1.4em; } +@container style(--MI_THEME-panelHeaderBg: var(--MI_THEME-panel)) { + .header { + box-shadow: 0 0.5px 0 0 light-dark(#0002, #fff2); + } +} + .title { margin: 0; padding: 12px 16px; @@ -215,6 +220,14 @@ onUnmounted(() => { .content { --MI-stickyTop: 0px; + /* + 理屈は知らないけど、ここでbackgroundを設定しておかないと + スクロールコンテナーが少なくともChromeにおいて + main thread scrolling になってしまい、パフォーマンスが(多分)落ちる。 + backgroundが透明だと裏側を描画しないといけなくなるとかそういう理由かもしれない + */ + background: var(--MI_THEME-panel); + &.omitted { position: relative; max-height: var(--maxHeight); diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index f51fefa0c0..9c6397a72c 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition appear - :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''" > <div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> <MkMenu :items="items" :align="'left'" @close="emit('closed')"/> @@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue'; +import { onMounted, onBeforeUnmount, useTemplateRef, ref } from 'vue'; import MkMenu from './MkMenu.vue'; import type { MenuItem } from '@/types/menu.js'; -import contains from '@/scripts/contains.js'; -import { defaultStore } from '@/store.js'; +import contains from '@/utility/contains.js'; +import { prefer } from '@/preferences.js'; import * as os from '@/os.js'; const props = defineProps<{ @@ -34,7 +34,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const rootEl = shallowRef<HTMLDivElement>(); +const rootEl = useTemplateRef('rootEl'); const zIndex = ref<number>(os.claimZIndex('high')); @@ -68,11 +68,11 @@ onMounted(() => { rootEl.value.style.left = `${left}px`; } - document.body.addEventListener('mousedown', onMousedown); + window.document.body.addEventListener('mousedown', onMousedown); }); onBeforeUnmount(() => { - document.body.removeEventListener('mousedown', onMousedown); + window.document.body.removeEventListener('mousedown', onMousedown); }); function onMousedown(evt: Event) { diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts index 27ce60415b..78cb4120de 100644 --- a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts @@ -3,14 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; import { file } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkCropperDialog from './MkCropperDialog.vue'; +import type { StoryObj } from '@storybook/vue3'; export const Default = { render(args) { return { @@ -55,7 +53,7 @@ export const Default = { http.get('/proxy/image.webp', async ({ request }) => { const url = new URL(request.url).searchParams.get('url'); if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') { - const image = await (await fetch('client-assets/fedi.jpg')).blob(); + const image = await (await window.fetch('client-assets/fedi.jpg')).blob(); return new HttpResponse(image, { headers: { 'Content-Type': 'image/jpeg', diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 0186cfc2c0..ba21394cbc 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -31,17 +31,17 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, shallowRef, ref } from 'vue'; +import { onMounted, useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import Cropper from 'cropperjs'; import tinycolor from 'tinycolor2'; +import { apiUrl } from '@@/js/config.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; -import { apiUrl } from '@@/js/config.js'; +import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; +import { getProxiedImageUrl } from '@/utility/media-proxy.js'; +import { prefer } from '@/preferences.js'; const emit = defineEmits<{ (ev: 'ok', cropped: Misskey.entities.DriveFile): void; @@ -56,8 +56,8 @@ const props = defineProps<{ }>(); const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); -const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); -const imgEl = shallowRef<HTMLImageElement>(); +const dialogEl = useTemplateRef('dialogEl'); +const imgEl = useTemplateRef('imgEl'); let cropper: Cropper | null = null; const loading = ref(true); @@ -81,8 +81,8 @@ const ok = async () => { formData.append('i', $i!.token); if (props.uploadFolder) { formData.append('folderId', props.uploadFolder); - } else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) { - formData.append('folderId', defaultStore.state.uploadFolder); + } else if (props.uploadFolder !== null && prefer.s.uploadFolder) { + formData.append('folderId', prefer.s.uploadFolder); } window.fetch(apiUrl + '/drive/files/create', { @@ -122,7 +122,7 @@ onMounted(() => { cropper = new Cropper(imgEl.value!, { }); - const computedStyle = getComputedStyle(document.documentElement); + const computedStyle = getComputedStyle(window.document.documentElement); const selection = cropper.getCropperSelection()!; selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue index 86d6269c69..54fda6bf7c 100644 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -57,14 +57,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { shallowRef } from 'vue'; +import { useTemplateRef } from 'vue'; import MkLink from '@/components/MkLink.vue'; import { i18n } from '@/i18n.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; const props = defineProps<{ - emoji: Misskey.entities.EmojiDetailed, + emoji: Misskey.entities.EmojiDetailed, }>(); const emit = defineEmits<{ @@ -73,7 +73,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialogEl = useTemplateRef('dialogEl'); function cancel() { emit('cancel'); diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index b5f6e78b6c..cc8bbf1104 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed } from 'vue'; import * as Misskey from 'misskey-js'; import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; -import { concat } from '@/scripts/array.js'; +import { concat } from '@/utility/array.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 0d5a16126b..1cf6f0b744 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -3,16 +3,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project SPDX-License-Identifier: AGPL-3.0-only --> +<!-- TODO: 親からスタイルを当てにくいことや実装がトリッキーなことを鑑み廃止または使用の縮小(timeline-date-separate.tsを使う) --> + <script lang="ts"> import { defineComponent, h, TransitionGroup, useCssModule } from 'vue'; import type { PropType } from 'vue'; +import type { MisskeyEntity } from '@/types/date-separated-list.js'; import MkAd from '@/components/global/MkAd.vue'; import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js'; -import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; -import type { MisskeyEntity } from '@/types/date-separated-list.js'; +import { prefer } from '@/preferences.js'; +import { getDateText } from '@/utility/timeline-date-separate.js'; export default defineComponent({ props: { @@ -45,15 +47,6 @@ export default defineComponent({ setup(props, { slots, expose }) { const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫 - function getDateText(dateInstance: Date) { - const date = dateInstance.getDate(); - const month = dateInstance.getMonth() + 1; - return i18n.tsx.monthAndDay({ - month: month.toString(), - day: date.toString(), - }); - } - if (props.items.length === 0) return; const renderChildrenImpl = () => props.items.map((item, i) => { @@ -150,7 +143,7 @@ export default defineComponent({ [$style['direction-up']]: props.direction === 'up', }; - return () => defaultStore.state.animation ? h(TransitionGroup, { + return () => prefer.s.animation ? h(TransitionGroup, { class: classes, name: 'list', tag: 'div', @@ -168,21 +161,17 @@ export default defineComponent({ container-type: inline-size; &:global { - > .list-move { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } - - &.deny-move-transition > .list-move { - transition: none !important; - } + > .list-move { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); + } - > .list-enter-active { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } + > .list-enter-active { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); + } - > *:empty { - display: none; - } + > *:empty { + display: none; + } } &:not(.date-separated-list-nogap) > *:not(:last-child) { diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 6c9fa3167a..81d508c161 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i> <MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/> </div> - <header v-if="title" :class="$style.title"><Mfm :text="title"/></header> - <div v-if="text" :class="$style.text"><Mfm :text="text"/></div> + <header v-if="title" :class="$style.title" class="_selectable"><Mfm :text="title"/></header> + <div v-if="text" :class="$style.text" class="_selectable"><Mfm :text="text"/></div> <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> <template #caption> @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, computed } from 'vue'; +import { ref, useTemplateRef, computed } from 'vue'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -117,7 +117,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const modal = useTemplateRef('modal'); const inputValue = ref<string | number | null>(props.input?.default ?? null); const selectedValue = ref(props.select?.default ?? null); diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue index 2e2321e6ac..8198356a76 100644 --- a/packages/frontend/src/components/MkDigitalClock.vue +++ b/packages/frontend/src/components/MkDigitalClock.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref, watch } from 'vue'; -import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js'; +import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js'; const props = withDefaults(defineProps<{ showS?: boolean; diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index e45c3bd9ce..70ab60cfae 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -44,10 +44,10 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import bytes from '@/filters/bytes.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; -import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; -import { deviceKind } from '@/scripts/device-kind.js'; -import { useRouter } from '@/router/supplier.js'; +import { $i } from '@/i.js'; +import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; +import { deviceKind } from '@/utility/device-kind.js'; +import { useRouter } from '@/router.js'; const router = useRouter(); @@ -151,11 +151,11 @@ function onDragend() { background: var(--MI_THEME-accent); &:hover { - background: var(--MI_THEME-accentLighten); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); } &:active { - background: var(--MI_THEME-accentDarken); + background: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); } > .label { diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 44e3b59ade..9c72691d21 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> {{ folder.name }} </p> - <p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload"> + <p v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload"> {{ i18n.ts.uploadFolder }} </p> <button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked"> @@ -38,11 +38,11 @@ import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { claimAchievement } from '@/utility/achievements.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -244,8 +244,8 @@ function deleteFolder() { misskeyApi('drive/folders/delete', { folderId: props.folder.id, }).then(() => { - if (defaultStore.state.uploadFolder === props.folder.id) { - defaultStore.set('uploadFolder', null); + if (prefer.s.uploadFolder === props.folder.id) { + prefer.commit('uploadFolder', null); } }).catch(err => { switch (err.id) { @@ -266,7 +266,7 @@ function deleteFolder() { } function setAsUploadFolder() { - defaultStore.set('uploadFolder', props.folder.id); + prefer.commit('uploadFolder', props.folder.id); } function onContextmenu(ev: MouseEvent) { @@ -295,9 +295,9 @@ function onContextmenu(ev: MouseEvent) { danger: true, action: deleteFolder, }]; - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menu = menu.concat([{ type: 'divider' }, { - icon: 'ti ti-id', + icon: 'ti ti-hash', text: i18n.ts.copyFolderId, action: () => { copyToClipboard(props.folder.id); diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index 8df3c86ebf..7433aea061 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 8be6d6f53d..c212167c8f 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -4,34 +4,37 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.root"> - <nav :class="$style.nav"> - <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}"> - <XNavFolder - :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]" - :parentFolder="folder" - @move="move" - @upload="upload" - @removeFile="removeFile" - @removeFolder="removeFolder" - /> - <template v-for="f in hierarchyFolders"> - <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> +<MkStickyContainer> + <template #header> + <nav :class="$style.nav"> + <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}"> <XNavFolder - :folder="f" + :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]" :parentFolder="folder" - :class="[$style.navPathItem]" @move="move" @upload="upload" @removeFile="removeFile" @removeFolder="removeFolder" /> - </template> - <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> - <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> - </div> - <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> - </nav> + <template v-for="f in hierarchyFolders"> + <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> + <XNavFolder + :folder="f" + :parentFolder="folder" + :class="[$style.navPathItem]" + @move="move" + @upload="upload" + @removeFile="removeFile" + @removeFolder="removeFolder" + /> + </template> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> + </div> + <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> + </nav> + </template> + <div ref="main" :class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]" @@ -91,12 +94,11 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="fetching"/> </div> <div v-if="draghover" :class="$style.dropzone"></div> - <input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/> -</div> +</MkStickyContainer> </template> <script lang="ts" setup> -import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; +import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from './MkButton.vue'; import type { MenuItem } from '@/types/menu.js'; @@ -104,12 +106,13 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XFolder from '@/components/MkDrive.folder.vue'; import XFile from '@/components/MkDrive.file.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { useStream } from '@/stream.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { uploadFile, uploads } from '@/scripts/upload.js'; -import { claimAchievement } from '@/scripts/achievements.js'; +import { uploadFile, uploads } from '@/utility/upload.js'; +import { claimAchievement } from '@/utility/achievements.js'; +import { prefer } from '@/preferences.js'; +import { chooseFileFromPc } from '@/utility/select-file.js'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder; @@ -129,8 +132,7 @@ const emit = defineEmits<{ (ev: 'open-folder', v: Misskey.entities.DriveFolder): void; }>(); -const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>(); -const fileInput = shallowRef<HTMLInputElement>(); +const loadMoreFiles = useTemplateRef('loadMoreFiles'); const folder = ref<Misskey.entities.DriveFolder | null>(null); const files = ref<Misskey.entities.DriveFile[]>([]); @@ -142,7 +144,6 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); const uploadings = uploads; const connection = useStream().useChannel('drive'); -const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい // ドロップされようとしているか const draghover = ref(false); @@ -304,10 +305,6 @@ function onDrop(ev: DragEvent) { //#endregion } -function selectLocalFile() { - fileInput.value?.click(); -} - function urlUpload() { os.inputText({ title: i18n.ts.uploadFromUrl, @@ -383,15 +380,8 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { }); } -function onChangeFileInput() { - if (!fileInput.value?.files) return; - for (const file of Array.from(fileInput.value.files)) { - upload(file, folder.value); - } -} - -function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) { - uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => { +function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) { + uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => { addFile(res, true); }); } @@ -630,16 +620,20 @@ function getMenu() { const menu: MenuItem[] = []; menu.push({ - type: 'switch', - text: i18n.ts.keepOriginalUploading, - ref: keepOriginal, - }, { type: 'divider' }, { text: i18n.ts.addFile, type: 'label', }, { + text: i18n.ts.upload + ' (' + i18n.ts.compress + ')', + icon: 'ti ti-upload', + action: () => { + chooseFileFromPc(true, { keepOriginal: false }); + }, + }, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => { selectLocalFile(); }, + action: () => { + chooseFileFromPc(true, { keepOriginal: true }); + }, }, { text: i18n.ts.fromUrl, icon: 'ti ti-link', @@ -716,7 +710,7 @@ function onContextmenu(ev: MouseEvent) { } onMounted(() => { - if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) { + if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) { nextTick(() => { ilFilesObserver.observe(loadMoreFiles.value?.$el); }); @@ -737,7 +731,7 @@ onMounted(() => { }); onActivated(() => { - if (defaultStore.state.enableInfiniteScroll) { + if (prefer.s.enableInfiniteScroll) { nextTick(() => { ilFilesObserver.observe(loadMoreFiles.value?.$el); }); @@ -751,22 +745,17 @@ onBeforeUnmount(() => { </script> <style lang="scss" module> -.root { - display: flex; - flex-direction: column; - height: 100%; -} - .nav { display: flex; - z-index: 2; width: 100%; padding: 0 8px; box-sizing: border-box; overflow: auto; font-size: 0.9em; - box-shadow: 0 1px 0 var(--MI_THEME-divider); - user-select: none; + background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-bottom: solid 0.5px var(--MI_THEME-divider); } .navPath { diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue index f1ecc27123..1b9455e3f3 100644 --- a/packages/frontend/src/components/MkDriveSelectDialog.vue +++ b/packages/frontend/src/components/MkDriveSelectDialog.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef } from 'vue'; +import { ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import XDrive from '@/components/MkDrive.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; @@ -43,7 +43,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); const selected = ref<Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]>([]); diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue index 6e9eb75920..d18fe0ed0c 100644 --- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue'; +import { useTemplateRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue'; import { url } from '@@/js/config.js'; import { embedRouteWithScrollbar } from '@@/js/embed-page.js'; import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js'; @@ -105,8 +105,8 @@ import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; +import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js'; const emit = defineEmits<{ (ev: 'ok'): void; @@ -121,7 +121,7 @@ const props = defineProps<{ }>(); //#region Modalの制御 -const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialogEl = useTemplateRef('dialogEl'); function cancel() { emit('cancel'); @@ -180,7 +180,7 @@ function applyToPreview() { nextTick(() => { if (currentPreviewUrl === embedPreviewUrl.value) { // URLが変わらなくてもリロード - iframeEl.value?.contentWindow?.location.reload(); + iframeEl.value?.contentWindow?.window.location.reload(); } }); } @@ -194,14 +194,13 @@ function generate() { function doCopy() { copyToClipboard(result.value); - os.success(); } //#endregion //#region プレビューのリサイズ -const resizerRootEl = shallowRef<HTMLDivElement>(); +const resizerRootEl = useTemplateRef('resizerRootEl'); const iframeLoading = ref(true); -const iframeEl = shallowRef<HTMLIFrameElement>(); +const iframeEl = useTemplateRef('iframeEl'); const iframeHeight = ref(0); const iframeScale = ref(1); const iframeStyle = computed(() => { diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 62a1000674..d4367f6ee8 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, computed, watch, onMounted } from 'vue'; +import { ref, useTemplateRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import { emojilist, @@ -131,17 +131,18 @@ import type { 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'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { isTouchUsing } from '@/utility/touch.js'; +import { deviceKind } from '@/utility/device-kind.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; -import { $i } from '@/account.js'; -import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; +import { $i } from '@/i.js'; +import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ showPinned?: boolean; - pinnedEmojis?: string[]; + pinnedEmojis?: string[]; maxHeight?: number; asDrawer?: boolean; asWindow?: boolean; @@ -156,15 +157,16 @@ const emit = defineEmits<{ (ev: 'esc'): void; }>(); -const searchEl = shallowRef<HTMLInputElement>(); -const emojisEl = shallowRef<HTMLDivElement>(); +const searchEl = useTemplateRef('searchEl'); +const emojisEl = useTemplateRef('emojisEl'); const { emojiPickerScale, emojiPickerWidth, emojiPickerHeight, - recentlyUsedEmojis, -} = defaultStore.reactiveState; +} = prefer.r; + +const recentlyUsedEmojis = store.r.recentlyUsedEmojis; const recentlyUsedEmojisDef = computed(() => { return recentlyUsedEmojis.value.map(getDef); @@ -317,7 +319,7 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { for (const emoji of emojis) { if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) { matches.add(emoji); @@ -334,7 +336,7 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { for (const emoji of emojis) { if (index[emoji.char].some(k => k.startsWith(newQ))) { matches.add(emoji); @@ -351,7 +353,7 @@ watch(q, () => { } if (matches.size >= max) return matches; - for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { + for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { for (const emoji of emojis) { if (index[emoji.char].some(k => k.includes(newQ))) { matches.add(emoji); @@ -413,7 +415,7 @@ function computeButtonTitle(ev: MouseEvent): void { function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) { const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -427,10 +429,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, // 最近使った絵文字更新 if (!pinned.value?.includes(key)) { - let recents = defaultStore.state.recentlyUsedEmojis; + let recents = store.s.recentlyUsedEmojis; recents = recents.filter((emoji) => emoji !== key); recents.unshift(key); - defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); + store.set('recentlyUsedEmojis', recents.splice(0, 32)); } } diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 21c712b441..662e2a118d 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="modal" v-slot="{ type, maxHeight }" :zPriority="'middle'" - :preferType="defaultStore.state.emojiPickerStyle" + :preferType="prefer.s.emojiPickerStyle" :hasInteractionWithOtherFocusTrappedEls="true" :transparentBg="true" :manualShowing="manualShowing" @@ -37,19 +37,19 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { shallowRef } from 'vue'; +import { useTemplateRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ manualShowing?: boolean | null; src?: HTMLElement; showPinned?: boolean; - pinnedEmojis?: string[], + pinnedEmojis?: string[], asReactionPicker?: boolean; targetNote?: Misskey.entities.Note; - choseAndClose?: boolean; + choseAndClose?: boolean; }>(), { manualShowing: null, showPinned: true, @@ -64,8 +64,8 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const modal = shallowRef<InstanceType<typeof MkModal>>(); -const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>(); +const modal = useTemplateRef('modal'); +const picker = useTemplateRef('picker'); function chosen(emoji: string) { emit('done', emoji); @@ -79,7 +79,7 @@ function opening() { picker.value?.focus(); // 何故かちょっと待たないとフォーカスされない - setTimeout(() => { + window.setTimeout(() => { picker.value?.focus(); }, 10); } diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue index d59b20435e..a2247d844b 100644 --- a/packages/frontend/src/components/MkExtensionInstaller.vue +++ b/packages/frontend/src/components/MkExtensionInstaller.vue @@ -11,54 +11,91 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- 拡張用? --> <i v-else class="ti ti-download"></i> </div> - <h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2> - <div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div> - <MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo> - <FormSection> - <template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template> - <div class="_gaps_s"> - <FormSplit> + + <h2 v-if="isPlugin" :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller._plugin.title }}</h2> + <h2 v-else-if="isTheme" :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller._theme.title }}</h2> + + <MkInfo :warn="true">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</MkInfo> + + <div v-if="isPlugin" class="_gaps_s"> + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-info-circle"></i></template> + <template #label>{{ i18n.ts.metadata }}</template> + + <div class="_gaps_s"> + <FormSplit> + <MkKeyValue> + <template #key>{{ i18n.ts.name }}</template> + <template #value>{{ extension.meta.name }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.author }}</template> + <template #value>{{ extension.meta.author }}</template> + </MkKeyValue> + </FormSplit> + <MkKeyValue> + <template #key>{{ i18n.ts.description }}</template> + <template #value>{{ extension.meta.description ?? i18n.ts.none }}</template> + </MkKeyValue> <MkKeyValue> - <template #key>{{ i18n.ts.name }}</template> - <template #value>{{ extension.meta.name }}</template> + <template #key>{{ i18n.ts.version }}</template> + <template #value>{{ extension.meta.version }}</template> </MkKeyValue> <MkKeyValue> - <template #key>{{ i18n.ts.author }}</template> - <template #value>{{ extension.meta.author }}</template> + <template #key>{{ i18n.ts.permission }}</template> + <template #value> + <ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList"> + <li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li> + </ul> + <template v-else>{{ i18n.ts.none }}</template> + </template> </MkKeyValue> - </FormSplit> - <MkKeyValue v-if="isPlugin"> - <template #key>{{ i18n.ts.description }}</template> - <template #value>{{ extension.meta.description ?? i18n.ts.none }}</template> - </MkKeyValue> - <MkKeyValue v-if="isPlugin"> - <template #key>{{ i18n.ts.version }}</template> - <template #value>{{ extension.meta.version }}</template> - </MkKeyValue> - <MkKeyValue v-if="isPlugin"> - <template #key>{{ i18n.ts.permission }}</template> - <template #value> - <ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList"> - <li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li> - </ul> - <template v-else>{{ i18n.ts.none }}</template> - </template> - </MkKeyValue> - <MkKeyValue v-if="isTheme"> - <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template> - <template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template> - </MkKeyValue> - <MkFolder> - <template #icon><i class="ti ti-code"></i></template> - <template #label>{{ i18n.ts._plugin.viewSource }}</template> + </div> + </MkFolder> + + <MkFolder :withSpacer="false"> + <template #icon><i class="ti ti-code"></i></template> + <template #label>{{ i18n.ts._plugin.viewSource }}</template> + + <MkCode :code="extension.raw"/> + </MkFolder> + </div> + <div v-else-if="isTheme" class="_gaps_s"> + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-info-circle"></i></template> + <template #label>{{ i18n.ts.metadata }}</template> + + <div class="_gaps_s"> + <FormSplit> + <MkKeyValue> + <template #key>{{ i18n.ts.name }}</template> + <template #value>{{ extension.meta.name }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.author }}</template> + <template #value>{{ extension.meta.author }}</template> + </MkKeyValue> + </FormSplit> + <MkKeyValue> + <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template> + <template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template> + </MkKeyValue> + </div> + </MkFolder> + + <MkFolder :withSpacer="false"> + <template #icon><i class="ti ti-code"></i></template> + <template #label>{{ i18n.ts._theme.code }}</template> + + <MkCode :code="extension.raw"/> + </MkFolder> + </div> - <MkCode :code="extension.raw"/> - </MkFolder> - </div> - </FormSection> <slot name="additionalInfo"/> + <div class="_buttonsCenter"> - <MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> + <MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton> + <MkButton gradate rounded large @click="emits('confirm')"><i class="ti ti-download"></i> {{ i18n.ts.install }}</MkButton> </div> </div> </template> @@ -105,6 +142,7 @@ const props = defineProps<{ const emits = defineEmits<{ (ev: 'confirm'): void; + (ev: 'cancel'): void; }>(); </script> @@ -112,13 +150,13 @@ const emits = defineEmits<{ .extInstallerRoot { border-radius: var(--MI-radius); background: var(--MI_THEME-panel); - padding: 1.5rem; + padding: 20px; } .extInstallerIconWrapper { width: 48px; height: 48px; - font-size: 24px; + font-size: 20px; line-height: 48px; text-align: center; border-radius: 50%; @@ -135,10 +173,6 @@ const emits = defineEmits<{ margin: 0; } -.extInstallerNormDesc { - text-align: center; -} - .extInstallerKVList { margin-top: 0; margin-bottom: 0; diff --git a/packages/frontend/src/components/MkFeatureBanner.vue b/packages/frontend/src/components/MkFeatureBanner.vue new file mode 100644 index 0000000000..e990ffc8f0 --- /dev/null +++ b/packages/frontend/src/components/MkFeatureBanner.vue @@ -0,0 +1,43 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div v-panel :class="$style.root"> + <img :class="$style.img" :src="icon"/> + <div :class="$style.text"> + <slot></slot> + </div> +</div> +</template> + +<script setup lang="ts"> +withDefaults(defineProps<{ + icon: string; + color: string; +}>(), { +}); +</script> + +<style module lang="scss"> +.root { + padding: 20px 24px; + text-align: center; + border-radius: var(--MI-radius); + background: linear-gradient(180deg, color(from v-bind(color) srgb r g b / 0.1), color(from v-bind(color) srgb r g b / 0)); +} + +.img { + display: block; + margin: 0 auto; + width: 40px; + aspect-ratio: 1; +} + +.text { + margin-top: 12px; + font-size: 85%; + mix-blend-mode: luminosity; +} +</style> diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue index 76bb965101..120d5355b2 100644 --- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue +++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { shallowRef, ref } from 'vue'; +import { useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -42,7 +42,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); const caption = ref(props.default); diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index fb1b5220fb..b9888d9b64 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only </button> </header> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.folderToggleEnterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.folderToggleLeaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.folderToggleEnterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.folderToggleLeaveTo : ''" @enter="enter" @afterEnter="afterEnter" @leave="leave" @@ -31,10 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef, watch } from 'vue'; +import { onMounted, ref, useTemplateRef, watch } from 'vue'; import { miLocalStorage } from '@/local-storage.js'; -import { defaultStore } from '@/store.js'; -import { getBgColor } from '@/scripts/get-bg-color.js'; +import { prefer } from '@/preferences.js'; +import { getBgColor } from '@/utility/get-bg-color.js'; const miLocalStoragePrefix = 'ui:folder:' as const; @@ -46,7 +46,7 @@ const props = withDefaults(defineProps<{ persistKey: null, }); -const rootEl = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); const parentBg = ref<string | null>(null); // eslint-disable-next-line vue/no-setup-props-reactivity-loss const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded); diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 384c0c0b34..8b4bacba69 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -27,10 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened"> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''" @enter="enter" @afterEnter="afterEnter" @leave="leave" @@ -56,9 +56,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, ref, shallowRef } from 'vue'; -import { defaultStore } from '@/store.js'; -import { getBgColor } from '@/scripts/get-bg-color.js'; +import { nextTick, onMounted, ref, useTemplateRef } from 'vue'; +import { prefer } from '@/preferences.js'; +import { getBgColor } from '@/utility/get-bg-color.js'; const props = withDefaults(defineProps<{ defaultOpen?: boolean; @@ -74,7 +74,7 @@ const props = withDefaults(defineProps<{ spacerMax: 22, }); -const rootEl = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); const bgSame = ref(false); const opened = ref(props.defaultOpen); const openedAtLeastOnce = ref(props.defaultOpen); @@ -116,7 +116,7 @@ function toggle() { } onMounted(() => { - const computedStyle = getComputedStyle(document.documentElement); + const computedStyle = getComputedStyle(window.document.documentElement); const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent'; const myBg = computedStyle.getPropertyValue('--MI_THEME-panel'); bgSame.value = parentBg === myBg; @@ -175,7 +175,7 @@ onMounted(() => { } .headerLower { - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); font-size: .85em; padding-left: 4px; } @@ -209,13 +209,13 @@ onMounted(() => { } .headerTextSub { - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); font-size: .85em; } .headerRight { margin-left: auto; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); white-space: nowrap; } @@ -239,7 +239,7 @@ onMounted(() => { bottom: var(--MI-stickyBottom, 0px); left: 0; padding: 12px; - background: var(--MI_THEME-acrylicBg); + background: color(from var(--MI_THEME-bg) srgb r g b / 0.5); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); background-size: auto auto; diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index c1dc67f776..b65f610986 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -39,13 +39,13 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { pleaseLogin } from '@/scripts/please-login.js'; -import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { claimAchievement } from '@/utility/achievements.js'; +import { pleaseLogin } from '@/utility/please-login.js'; +import { $i } from '@/i.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed, @@ -100,7 +100,7 @@ async function onClick() { userId: props.user.id, }); } else { - if (defaultStore.state.alwaysConfirmFollow) { + if (prefer.s.alwaysConfirmFollow) { const { canceled } = await os.confirm({ type: 'question', text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }), @@ -120,11 +120,11 @@ async function onClick() { } else { await misskeyApi('following/create', { userId: props.user.id, - withReplies: defaultStore.state.defaultWithReplies, + withReplies: prefer.s.defaultFollowWithReplies, }); emit('update:user', { ...props.user, - withReplies: defaultStore.state.defaultWithReplies, + withReplies: prefer.s.defaultFollowWithReplies, }); hasPendingFollowRequestFromYou.value = true; @@ -211,13 +211,13 @@ onBeforeUnmount(() => { background: var(--MI_THEME-accent); &:hover { - background: var(--MI_THEME-accentLighten); - border-color: var(--MI_THEME-accentLighten); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); + border-color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); } &:active { - background: var(--MI_THEME-accentDarken); - border-color: var(--MI_THEME-accentDarken); + background: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); + border-color: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); } } diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue index ecb6cf882b..0a902f3400 100644 --- a/packages/frontend/src/components/MkFormDialog.file.vue +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -15,8 +15,8 @@ import * as Misskey from 'misskey-js'; import { computed, ref } from 'vue'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; -import { selectFile } from '@/scripts/select-file.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { selectFile } from '@/utility/select-file.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; const props = defineProps<{ fileId?: string | null; diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index a639eae208..4756079e76 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </div> <div v-else class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> + <img :src="infoImageUrl" draggable="false"/> <div>{{ i18n.ts.nothing }}</div> </div> </MkSpacer> @@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { reactive, shallowRef } from 'vue'; +import { reactive, useTemplateRef } from 'vue'; import MkInput from './MkInput.vue'; import MkTextarea from './MkTextarea.vue'; import MkSwitch from './MkSwitch.vue'; @@ -80,7 +80,7 @@ import MkRange from './MkRange.vue'; import MkButton from './MkButton.vue'; import MkRadios from './MkRadios.vue'; import XFile from './MkFormDialog.file.vue'; -import type { Form } from '@/scripts/form.js'; +import type { Form } from '@/utility/form.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; @@ -99,7 +99,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); const values = reactive({}); for (const item in props.form) { diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue index 8b1c56fca4..fba5dc854c 100644 --- a/packages/frontend/src/components/MkFukidashi.vue +++ b/packages/frontend/src/components/MkFukidashi.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only tail === 'left' ? $style.left : $style.right, negativeMargin === true && $style.negativeMargin, shadow === true && $style.shadow, + accented === true && $style.accented ]" > <div :class="$style.bg"> @@ -30,10 +31,12 @@ withDefaults(defineProps<{ tail?: 'left' | 'right' | 'none'; negativeMargin?: boolean; shadow?: boolean; + accented?: boolean; }>(), { tail: 'right', negativeMargin: false, shadow: false, + accented: false, }); </script> @@ -47,6 +50,10 @@ withDefaults(defineProps<{ min-height: calc(var(--fukidashi-radius) * 2); padding-top: calc(var(--fukidashi-radius) * .13); + &.accented { + --fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%); + } + &.shadow { filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow)); } @@ -77,7 +84,13 @@ withDefaults(defineProps<{ .content { position: relative; - padding: 8px 12px; + padding: 10px 14px; +} + +@container (max-width: 450px) { + .content { + padding: 8px 12px; + } } .tail { diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 22f8355acf..49a6c65170 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -35,14 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { computed, ref } from 'vue'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ post: Misskey.entities.GalleryPost; }>(); const hover = ref(false); -const safe = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive); +const safe = computed(() => prefer.s.nsfw === 'ignore' || prefer.s.nsfw === 'respect' && !props.post.isSensitive); const show = computed(() => safe.value || hover.value); function enterHover(): void { diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 0cc0df9911..28bb936755 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -13,14 +13,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; +import { onMounted, nextTick, watch, useTemplateRef, ref } from 'vue'; import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { alpha } from '@/scripts/color.js'; -import { initChart } from '@/scripts/init-chart.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { store } from '@/store.js'; +import { useChartTooltip } from '@/use/use-chart-tooltip.js'; +import { alpha } from '@/utility/color.js'; +import { initChart } from '@/utility/init-chart.js'; initChart(); @@ -35,8 +35,8 @@ const props = withDefaults(defineProps<{ label: '', }); -const rootEl = shallowRef<HTMLDivElement | null>(null); -const chartEl = shallowRef<HTMLCanvasElement | null>(null); +const rootEl = useTemplateRef('rootEl'); +const chartEl = useTemplateRef('chartEl'); const now = new Date(); let chartInstance: Chart | null = null; const fetching = ref(true); @@ -106,7 +106,7 @@ async function renderChart() { await nextTick(); - const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; + const color = store.s.darkMode ? '#b4e900' : '#86b300'; // 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue index 196c962a06..1d0ffaea11 100644 --- a/packages/frontend/src/components/MkHorizontalSwipe.vue +++ b/packages/frontend/src/components/MkHorizontalSwipe.vue @@ -19,21 +19,20 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_leaveTo : $style.swipeAnimationRight_leaveTo" :style="`--swipe: ${pullDistance}px;`" > - <!-- 【注意】slot内の最上位要素に動的にkeyを設定すること --> - <!-- 各最上位要素にユニークなkeyの指定がないとTransitionがうまく動きません --> - <slot></slot> + <div :key="tabModel"> + <slot></slot> + </div> </Transition> </div> </template> <script lang="ts" setup> -import { ref, shallowRef, computed, nextTick, watch } from 'vue'; +import { ref, useTemplateRef, computed, nextTick, watch } from 'vue'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; -import { defaultStore } from '@/store.js'; -import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js'; +import { isHorizontalSwipeSwiping as isSwiping } from '@/utility/touch.js'; +import { prefer } from '@/preferences.js'; -const rootEl = shallowRef<HTMLDivElement>(); +const rootEl = useTemplateRef('rootEl'); -// eslint-disable-next-line no-undef const tabModel = defineModel<string>('tab'); const props = defineProps<{ @@ -44,7 +43,7 @@ const emit = defineEmits<{ (ev: 'swiped', newKey: string, direction: 'left' | 'right'): void; }>(); -const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value); +const shouldAnimate = computed(() => prefer.r.enableHorizontalSwipe.value || prefer.r.animation.value); // ▼ しきい値 ▼ // @@ -72,7 +71,7 @@ const isSwipingForClass = ref(false); let swipeAborted = false; function touchStart(event: TouchEvent) { - if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; + if (!prefer.r.enableHorizontalSwipe.value) return; if (event.touches.length !== 1) return; @@ -83,7 +82,7 @@ function touchStart(event: TouchEvent) { } function touchMove(event: TouchEvent) { - if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; + if (!prefer.r.enableHorizontalSwipe.value) return; if (event.touches.length !== 1) return; @@ -101,7 +100,7 @@ function touchMove(event: TouchEvent) { pullDistance.value = 0; isSwiping.value = false; - setTimeout(() => { + window.setTimeout(() => { isSwipingForClass.value = false; }, 400); @@ -134,7 +133,7 @@ function touchEnd(event: TouchEvent) { return; } - if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; + if (!prefer.r.enableHorizontalSwipe.value) return; if (event.touches.length !== 0) return; diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index c04d0864fb..e3a0a371b4 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -6,16 +6,42 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''"> <TransitionGroup - :duration="defaultStore.state.animation && props.transition?.duration || undefined" - :enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined" - :leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" - :enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined" - :leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined" - :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" - :leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined" + :duration="prefer.s.animation && props.transition?.duration || undefined" + :enterActiveClass="prefer.s.animation && props.transition?.enterActiveClass || undefined" + :leaveActiveClass="prefer.s.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" + :enterFromClass="prefer.s.animation && props.transition?.enterFromClass || undefined" + :leaveToClass="prefer.s.animation && props.transition?.leaveToClass || undefined" + :enterToClass="prefer.s.animation && props.transition?.enterToClass || undefined" + :leaveFromClass="prefer.s.animation && props.transition?.leaveFromClass || undefined" > - <canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/> - <img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/> + <canvas + v-show="hide" + key="canvas" + ref="canvas" + :class="$style.canvas" + :width="canvasWidth" + :height="canvasHeight" + :title="title ?? undefined" + draggable="false" + tabindex="-1" + style="-webkit-user-drag: none;" + /> + <img + v-show="!hide" + key="img" + ref="img" + :height="imgHeight ?? undefined" + :width="imgWidth ?? undefined" + :class="$style.img" + :src="src ?? undefined" + :title="title ?? undefined" + :alt="alt ?? undefined" + loading="eager" + decoding="async" + draggable="false" + tabindex="-1" + style="-webkit-user-drag: none;" + /> </TransitionGroup> </div> </template> @@ -29,7 +55,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => { // テスト環境で Web Worker インスタンスは作成できない if (import.meta.env.MODE === 'test') { - const canvas = document.createElement('canvas'); + const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); @@ -43,13 +69,11 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol Math.min(navigator.hardwareConcurrency - 1, 4), ); resolve(workers); - if (_DEV_) console.log('WebGL2 in worker is supported!'); } else { - const canvas = document.createElement('canvas'); + const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); - if (_DEV_) console.log('WebGL2 in worker is not supported...'); } testWorker.terminate(); }); @@ -57,10 +81,10 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol </script> <script lang="ts" setup> -import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue'; +import { computed, nextTick, onMounted, onUnmounted, useTemplateRef, watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import { render } from 'buraha'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ transition?: { @@ -94,9 +118,9 @@ const props = withDefaults(defineProps<{ }); const viewId = uuid(); -const canvas = shallowRef<HTMLCanvasElement>(); -const root = shallowRef<HTMLDivElement>(); -const img = shallowRef<HTMLImageElement>(); +const canvas = useTemplateRef('canvas'); +const root = useTemplateRef('root'); +const img = useTemplateRef('img'); const loaded = ref(false); const canvasWidth = ref(64); const canvasHeight = ref(64); diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 90ca1b5a9d..f114ec8a71 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.root, { [$style.warn]: warn }]"> +<div :class="[$style.root, { [$style.warn]: warn }]" class="_selectable"> <i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i> <i v-else class="ti ti-info-circle" :class="$style.i"></i> <div><slot></slot></div> diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 739061bce1..b34b7aaf60 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div> +<div class="_selectable"> <div :class="$style.label" @click="focus"><slot name="label"></slot></div> <div :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]"> <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> @@ -44,14 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; -import type { InputHTMLAttributes } from 'vue'; +import { onMounted, onUnmounted, nextTick, ref, useTemplateRef, watch, computed, toRefs } from 'vue'; import { debounce } from 'throttle-debounce'; -import MkButton from '@/components/MkButton.vue'; import { useInterval } from '@@/js/use-interval.js'; +import type { InputHTMLAttributes } from 'vue'; +import type { SuggestionType } from '@/utility/autocomplete.js'; +import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { Autocomplete } from '@/scripts/autocomplete.js'; -import type { SuggestionType } from '@/scripts/autocomplete.js'; +import { Autocomplete } from '@/utility/autocomplete.js'; const props = defineProps<{ modelValue: string | number | null; @@ -92,9 +92,9 @@ const focused = ref(false); const changed = ref(false); const invalid = ref(false); const filled = computed(() => v.value !== '' && v.value != null); -const inputEl = shallowRef<HTMLInputElement>(); -const prefixEl = shallowRef<HTMLElement>(); -const suffixEl = shallowRef<HTMLElement>(); +const inputEl = useTemplateRef('inputEl'); +const prefixEl = useTemplateRef('prefixEl'); +const suffixEl = useTemplateRef('suffixEl'); const height = props.small ? 33 : props.large ? 39 : @@ -201,7 +201,7 @@ defineExpose({ .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts b/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts index b9d203ee80..bd69fb2f82 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts +++ b/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts @@ -3,13 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { federationInstance } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import { getChartResolver } from '../../.storybook/charts.js'; import MkInstanceCardMini from './MkInstanceCardMini.vue'; +import type { StoryObj } from '@storybook/vue3'; export const Default = { render(args) { @@ -48,7 +47,7 @@ export const Default = { const url = new URL(urlStr); if (url.href.startsWith('https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/')) { - const image = await (await fetch(`client-assets/${url.pathname.split('/').pop()}`)).blob(); + const image = await (await window.fetch(`client-assets/${url.pathname.split('/').pop()}`)).blob(); return new HttpResponse(image, { headers: { 'Content-Type': 'image/jpeg', diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index b0601cf7f9..7902151921 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkMiniChart from '@/components/MkMiniChart.vue'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; +import { misskeyApiGet } from '@/utility/misskey-api.js'; +import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; const props = defineProps<{ instance: Misskey.entities.FederationInstance; diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index c2860ed89b..90391005bc 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -84,22 +84,22 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, computed, shallowRef } from 'vue'; +import { onMounted, ref, computed, useTemplateRef } from 'vue'; import { Chart } from 'chart.js'; +import type { HeatmapSource } from '@/components/MkHeatmap.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkChart from '@/components/MkChart.vue'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { $i } from '@/account.js'; +import { useChartTooltip } from '@/use/use-chart-tooltip.js'; +import { $i } from '@/i.js'; import * as os from '@/os.js'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import { misskeyApiGet } from '@/utility/misskey-api.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import MkHeatmap from '@/components/MkHeatmap.vue'; -import type { HeatmapSource } from '@/components/MkHeatmap.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; -import { initChart } from '@/scripts/init-chart.js'; +import { initChart } from '@/utility/init-chart.js'; initChart(); @@ -109,8 +109,8 @@ const chartLimit = 500; const chartSpan = ref<'hour' | 'day'>('hour'); const chartSrc = ref('active-users'); const heatmapSrc = ref<HeatmapSource>('active-users'); -const subDoughnutEl = shallowRef<HTMLCanvasElement>(); -const pubDoughnutEl = shallowRef<HTMLCanvasElement>(); +const subDoughnutEl = useTemplateRef('subDoughnutEl'); +const pubDoughnutEl = useTemplateRef('pubDoughnutEl'); const { handler: externalTooltipHandler1 } = useChartTooltip({ position: 'middle', @@ -126,7 +126,7 @@ function createDoughnut(chartEl, tooltip, data) { labels: data.map(x => x.name), datasets: [{ backgroundColor: data.map(x => x.color), - borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'), + borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'), borderWidth: 2, hoverOffset: 0, data: data.map(x => x.value), diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 1da3f14ad4..eba8a73aec 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -15,7 +15,7 @@ import { computed } from 'vue'; import type { CSSProperties } from 'vue'; import { instanceName as localInstanceName } from '@@/js/config.js'; import { instance as localInstance } from '@/instance.js'; -import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; +import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; const props = defineProps<{ host: string | null; diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index 1a71f6574f..ab797459cc 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.items"> <div> <div :class="$style.label">{{ i18n.ts.invitationCode }}</div> - <div>{{ invite.code }}</div> + <div class="_selectableAtomic">{{ invite.code }}</div> </div> <div v-if="moderator"> <div :class="$style.label">{{ i18n.ts.inviteCodeCreator }}</div> @@ -64,7 +64,7 @@ import { computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; @@ -90,7 +90,6 @@ function deleteCode() { function copyInviteCode() { copyToClipboard(props.invite.code); - os.success(); } </script> diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue index 50c9e16e5e..b4185d2d0a 100644 --- a/packages/frontend/src/components/MkKeyValue.vue +++ b/packages/frontend/src/components/MkKeyValue.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.key"> <slot name="key"></slot> </div> - <div :class="$style.value"> + <div :class="$style.value" class="_selectable"> <slot name="value"></slot> <button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button> </div> @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -31,7 +31,6 @@ const props = withDefaults(defineProps<{ const copy_ = () => { copyToClipboard(props.copy); - os.success(); }; </script> diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 32c1a2d172..3e5a88a170 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -27,11 +27,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { shallowRef } from 'vue'; +import { useTemplateRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import { navbarItemDef } from '@/navbar.js'; -import { defaultStore } from '@/store.js'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { deviceKind } from '@/utility/device-kind.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ src?: HTMLElement; @@ -48,9 +48,9 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop deviceKind === 'smartphone' ? 'drawer' : 'dialog'; -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const modal = useTemplateRef('modal'); -const menu = defaultStore.state.menu; +const menu = prefer.s.menu; const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({ type: def.to ? 'link' : 'button', diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index a276bf68b4..4bac2bcea4 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; import { url as local } from '@@/js/config.js'; -import { useTooltip } from '@/scripts/use-tooltip.js'; +import { useTooltip } from '@/use/use-tooltip.js'; import * as os from '@/os.js'; import { isEnabledUrlPreview } from '@/instance.js'; import type { MkABehavior } from '@/components/global/MkA.vue'; diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 3d2795b37a..b7052ad918 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only tabindex="0" :class="[ $style.audioContainer, - (audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive, + (audio.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive, ]" @contextmenu.stop @keydown.stop > <button v-if="hide" :class="$style.hidden" @click="show"> <div :class="$style.hiddenTextWrapper"> - <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> + <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> <span style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </button> - <div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer"> + <div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.nativeAudioContainer"> <audio ref="audioEl" preload="metadata" @@ -88,18 +88,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue'; +import { useTemplateRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu.js'; -import type { Keymap } from '@/scripts/hotkey.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard'; -import { defaultStore } from '@/store.js'; +import type { Keymap } from '@/utility/hotkey.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; -import { $i, iAmModerator } from '@/account.js'; +import { $i, iAmModerator } from '@/i.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ audio: Misskey.entities.DriveFile; @@ -148,17 +148,17 @@ const keymap = { // PlayerElもしくはその子要素にフォーカスがあるかどうか function hasFocus() { if (!playerEl.value) return false; - return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement); + return playerEl.value === window.document.activeElement || playerEl.value.contains(window.document.activeElement); } -const playerEl = shallowRef<HTMLDivElement>(); -const audioEl = shallowRef<HTMLAudioElement>(); +const playerEl = useTemplateRef('playerEl'); +const audioEl = useTemplateRef('audioEl'); // eslint-disable-next-line vue/no-setup-props-reactivity-loss -const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore')); +const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore')); async function show() { - if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, @@ -240,9 +240,9 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider' }, ...details); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menu.push({ type: 'divider' }, { - icon: 'ti ti-id', + icon: 'ti ti-hash', text: i18n.ts.copyFileId, action: () => { copyToClipboard(props.audio.id); @@ -407,7 +407,7 @@ onDeactivated(() => { elapsedTimeMs.value = 0; durationMs.value = 0; bufferedEnd.value = 0; - hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'); + hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'); stopAudioElWatch(); onceInit = false; if (mediaTickFrameId) { diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 3e521e0a03..f23cf507fb 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -27,9 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; import MkMediaAudio from '@/components/MkMediaAudio.vue'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ media: Misskey.entities.DriveFile; @@ -38,7 +38,7 @@ const props = defineProps<{ const hide = ref(true); async function show() { - if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 8ab990b926..bb42cbecf9 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> +<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> <component :is="disableImageLink ? 'div' : 'a'" v-bind="disableImageLink ? { @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <ImgWithBlurhash :hash="image.blurhash" - :src="(defaultStore.state.dataSaver.media && hide) ? null : url" + :src="(prefer.s.dataSaver.media && hide) ? null : url" :forceBlurhash="hide" :cover="hide || cover" :alt="image.comment || image.name" @@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="hide"> <div :class="$style.hiddenText"> <div :class="$style.hiddenTextWrapper"> - <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> + <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> <span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </div> @@ -54,14 +54,14 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard'; +import { getStaticImageUrl } from '@/utility/media-proxy.js'; import bytes from '@/filters/bytes.js'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { $i, iAmModerator } from '@/account.js'; +import { $i, iAmModerator } from '@/i.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ image: Misskey.entities.DriveFile; @@ -77,9 +77,9 @@ const props = withDefaults(defineProps<{ const hide = ref(true); -const url = computed(() => (props.raw || defaultStore.state.loadRawImages) +const url = computed(() => (props.raw || prefer.s.loadRawImages) ? props.image.url - : defaultStore.state.disableShowingAnimatedImages + : prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(props.image.url) : props.image.thumbnailUrl, ); @@ -91,7 +91,7 @@ async function onclick(ev: MouseEvent) { if (hide.value) { ev.stopPropagation(); - if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.image.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, @@ -105,7 +105,7 @@ async function onclick(ev: MouseEvent) { // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする watch(() => props.image, () => { - hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore'); + hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.image.isSensitive && prefer.s.nsfw !== 'ignore'); }, { deep: true, immediate: true, @@ -166,9 +166,9 @@ function showMenu(ev: MouseEvent) { menuItems.push({ type: 'divider' }, ...details); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', + icon: 'ti ti-hash', text: i18n.ts.copyFileId, action: () => { copyToClipboard(props.image.id); @@ -220,7 +220,7 @@ function showMenu(ev: MouseEvent) { position: absolute; border-radius: 6px; background-color: var(--MI_THEME-fg); - color: var(--MI_THEME-accentLighten); + color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); font-size: 12px; opacity: .5; padding: 5px 8px; @@ -294,7 +294,7 @@ html[data-color-scheme=light] .visible { /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */ background-color: black; border-radius: 6px; - color: var(--MI_THEME-accentLighten); + color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); display: inline-block; font-weight: bold; font-size: 0.8em; diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 32766f2029..4a1100c324 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -12,9 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[ $style.medias, count === 1 ? [$style.n1, { - [$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9', - [$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1', - [$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3', + [$style.n116_9]: prefer.s.mediaListWithOneImageAppearance === '16_9', + [$style.n11_1]: prefer.s.mediaListWithOneImageAppearance === '1_1', + [$style.n12_3]: prefer.s.mediaListWithOneImageAppearance === '2_3', }] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany, ]" > @@ -28,27 +28,27 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted, shallowRef } from 'vue'; +import { computed, onMounted, onUnmounted, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; import 'photoswipe/style.css'; +import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js'; 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 '@@/js/const.js'; -import { defaultStore } from '@/store.js'; -import { focusParent } from '@/scripts/focus.js'; +import { focusParent } from '@/utility/focus.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ mediaList: Misskey.entities.DriveFile[]; raw?: boolean; }>(); -const gallery = shallowRef<HTMLDivElement>(); +const gallery = useTemplateRef('gallery'); const pswpZIndex = os.claimZIndex('middle'); -document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); +window.document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); const count = computed(() => props.mediaList.filter(media => previewable(media)).length); let lightbox: PhotoSwipeLightbox | null = null; @@ -75,7 +75,7 @@ async function calcAspectRatio() { return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; }; - switch (defaultStore.state.mediaListWithOneImageAppearance) { + switch (prefer.s.mediaListWithOneImageAppearance) { case '16_9': gallery.value.style.aspectRatio = ratioMax(16 / 9); break; @@ -166,7 +166,7 @@ onMounted(() => { className: 'pswp__alt-text-container', appendTo: 'wrapper', onInit: (el, pswp) => { - const textBox = document.createElement('p'); + const textBox = window.document.createElement('p'); textBox.className = 'pswp__alt-text _acrylic'; el.appendChild(textBox); @@ -178,19 +178,19 @@ onMounted(() => { }); lightbox.on('afterInit', () => { - activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null; + activeEl = window.document.activeElement instanceof HTMLElement ? window.document.activeElement : null; focusParent(activeEl, true, true); lightbox?.pswp?.element?.focus({ preventScroll: true, }); - history.pushState(null, '', '#pswp'); + window.history.pushState(null, '', '#pswp'); }); lightbox.on('destroy', () => { focusParent(activeEl, true, false); activeEl = null; if (window.location.hash === '#pswp') { - history.back(); + window.history.back(); } }); @@ -227,7 +227,6 @@ defineExpose({ .container { position: relative; width: 100%; - margin-top: 4px; } .medias { diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 682da22711..0e5f1e28b9 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[ $style.videoContainer, controlsShowing && $style.active, - (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive, + (video.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive, ]" @mouseover="onMouseOver" @mouseleave="onMouseLeave" @@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only > <button v-if="hide" :class="$style.hidden" @click="show"> <div :class="$style.hiddenTextWrapper"> - <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> + <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> <span style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </button> - <div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.videoRoot"> + <div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.videoRoot"> <video ref="videoEl" :class="$style.video" @@ -109,20 +109,20 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue'; +import { ref, useTemplateRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu.js'; -import type { Keymap } from '@/scripts/hotkey.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard'; +import type { Keymap } from '@/utility/hotkey.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js'; -import hasAudio from '@/scripts/media-has-audio.js'; +import { exitFullscreen, requestFullscreen } from '@/utility/fullscreen.js'; +import hasAudio from '@/utility/media-has-audio.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; -import { $i, iAmModerator } from '@/account.js'; +import { $i, iAmModerator } from '@/i.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ video: Misskey.entities.DriveFile; @@ -171,14 +171,14 @@ const keymap = { // PlayerElもしくはその子要素にフォーカスがあるかどうか function hasFocus() { if (!playerEl.value) return false; - return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement); + return playerEl.value === window.document.activeElement || playerEl.value.contains(window.document.activeElement); } // eslint-disable-next-line vue/no-setup-props-reactivity-loss -const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); +const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore')); async function show() { - if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { const { canceled } = await os.confirm({ type: 'question', text: i18n.ts.sensitiveMediaRevealConfirm, @@ -216,7 +216,7 @@ function showMenu(ev: MouseEvent) { '2.0x': 2, }, }, - ...(document.pictureInPictureEnabled ? [{ + ...(window.document.pictureInPictureEnabled ? [{ text: i18n.ts._mediaControls.pip, icon: 'ti ti-picture-in-picture', action: togglePictureInPicture, @@ -265,9 +265,9 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider' }, ...details); } - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menu.push({ type: 'divider' }, { - icon: 'ti ti-id', + icon: 'ti ti-hash', text: i18n.ts.copyFileId, action: () => { copyToClipboard(props.video.id); @@ -299,8 +299,8 @@ async function toggleSensitive(file: Misskey.entities.DriveFile) { } // MediaControl: Video State -const videoEl = shallowRef<HTMLVideoElement>(); -const playerEl = shallowRef<HTMLDivElement>(); +const videoEl = useTemplateRef('videoEl'); +const playerEl = useTemplateRef('playerEl'); const isHoverring = ref(false); const controlsShowing = computed(() => { if (!oncePlayed.value) return true; @@ -339,7 +339,7 @@ const bufferedDataRatio = computed(() => { // MediaControl Events function onMouseOver() { if (controlStateTimer) { - clearTimeout(controlStateTimer); + window.clearTimeout(controlStateTimer); } isHoverring.value = true; } @@ -384,8 +384,8 @@ function toggleFullscreen() { function togglePictureInPicture() { if (videoEl.value) { - if (document.pictureInPictureElement) { - document.exitPictureInPicture(); + if (window.document.pictureInPictureElement) { + window.document.exitPictureInPicture(); } else { videoEl.value.requestPictureInPicture(); } @@ -502,7 +502,7 @@ onDeactivated(() => { elapsedTimeMs.value = 0; durationMs.value = 0; bufferedEnd.value = 0; - hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'); + hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'); stopVideoElWatch(); onceInit = false; if (mediaTickFrameId) { @@ -553,7 +553,7 @@ onDeactivated(() => { /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */ background-color: black; border-radius: 6px; - color: var(--MI_THEME-accentLighten); + color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); display: inline-block; font-weight: bold; font-size: 0.8em; @@ -565,7 +565,7 @@ onDeactivated(() => { position: absolute; border-radius: 6px; background-color: var(--MI_THEME-fg); - color: var(--MI_THEME-accentLighten); + color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); font-size: 12px; opacity: .5; padding: 5px 8px; diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 87c82f0a89..f2cf33eb65 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -18,9 +18,9 @@ import { toUnicode } from 'punycode.js'; import { computed } from 'vue'; import { host as localHost } from '@@/js/config.js'; import type { MkABehavior } from '@/components/global/MkA.vue'; -import { $i } from '@/account.js'; -import { defaultStore } from '@/store.js'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +import { $i } from '@/i.js'; +import { getStaticImageUrl } from '@/utility/media-proxy.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ username: string; @@ -36,7 +36,7 @@ const isMe = $i && ( `@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase() ); -const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar +const avatarUrl = computed(() => prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar ? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) : `/avatar/@${props.username}@${props.host}`, ); diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue index 086573ba6d..f7cd72b6c6 100644 --- a/packages/frontend/src/components/MkMenu.child.vue +++ b/packages/frontend/src/components/MkMenu.child.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue'; +import { nextTick, onMounted, onUnmounted, provide, useTemplateRef, watch } from 'vue'; import MkMenu from './MkMenu.vue'; import type { MenuItem } from '@/types/menu.js'; @@ -28,7 +28,7 @@ const emit = defineEmits<{ provide('isNestingMenu', true); -const el = shallowRef<HTMLElement>(); +const el = useTemplateRef('el'); const align = 'left'; const SCROLLBAR_THICKNESS = 16; diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index d484c1b338..44f5a09404 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.center]: align === 'center', [$style.big]: big, [$style.asDrawer]: asDrawer, + [$style.widthSpecified]: width != null, }" @focusin.passive.stop="() => {}" > @@ -29,12 +30,19 @@ SPDX-License-Identifier: AGPL-3.0-only > <template v-for="item in (items2 ?? [])"> <div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div> + <span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]"> <span style="opacity: 0.7;">{{ item.text }}</span> </span> + <span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]"> <span><MkEllipsis/></span> </span> + + <div v-else-if="item.type === 'component'" role="menuitem" tabindex="-1" :class="[$style.componentItem]"> + <component :is="item.component" v-bind="item.props"/> + </div> + <MkA v-else-if="item.type === 'link'" role="menuitem" @@ -48,10 +56,14 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </MkA> + <a v-else-if="item.type === 'a'" role="menuitem" @@ -67,10 +79,14 @@ SPDX-License-Identifier: AGPL-3.0-only > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </a> + <button v-else-if="item.type === 'user'" role="menuitem" @@ -85,6 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> + <button v-else-if="item.type === 'switch'" role="menuitemcheckbox" @@ -98,10 +115,14 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> <div :class="$style.item_content"> - <span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span> + <div :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> </div> </button> + <button v-else-if="item.type === 'radio'" role="menuitem" @@ -114,10 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-only > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <div :class="$style.item_content"> - <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> + <div :class="$style.item_content_text" style="pointer-events: none;"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> </div> </button> + <button v-else-if="item.type === 'radioOption'" role="menuitemradio" @@ -131,9 +156,13 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span> </div> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> </div> </button> + <button v-else-if="item.type === 'parent'" role="menuitem" @@ -145,12 +174,17 @@ SPDX-License-Identifier: AGPL-3.0-only > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <div :class="$style.item_content"> - <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> + <div :class="$style.item_content_text" style="pointer-events: none;"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> </div> </button> + <button - v-else role="menuitem" + v-else + role="menuitem" tabindex="0" :class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]" @click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)" @@ -160,11 +194,15 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> - <span :class="$style.item_content_text">{{ item.text }}</span> + <div :class="$style.item_content_text"> + <div :class="$style.item_content_text_title">{{ item.text }}</div> + <div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div> + </div> <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> </template> + <span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]"> <span>{{ i18n.ts.none }}</span> </span> @@ -176,15 +214,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue'; -import MkSwitchButton from '@/components/MkSwitch.button.vue'; +import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, unref, watch, shallowRef } from 'vue'; import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js'; +import type { Keymap } from '@/utility/hotkey.js'; +import MkSwitchButton from '@/components/MkSwitch.button.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { isTouchUsing } from '@/scripts/touch.js'; -import type { Keymap } from '@/scripts/hotkey.js'; -import { isFocusable } from '@/scripts/focus.js'; -import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; +import { isTouchUsing } from '@/utility/touch.js'; +import { isFocusable } from '@/utility/focus.js'; +import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js'; const childrenCache = new WeakMap<MenuParent, MenuItem[]>(); </script> @@ -209,11 +247,11 @@ const big = isTouchUsing; const isNestingMenu = inject<boolean>('isNestingMenu', false); -const itemsEl = shallowRef<HTMLElement>(); +const itemsEl = useTemplateRef('itemsEl'); const items2 = ref<InnerMenuItem[]>(); -const child = shallowRef<InstanceType<typeof XChild>>(); +const child = useTemplateRef('child'); const keymap = { 'up|k|shift+tab': { @@ -254,7 +292,7 @@ watch(() => props.items, () => { }); const childMenu = ref<MenuItem[] | null>(); -const childTarget = shallowRef<HTMLElement | null>(); +const childTarget = shallowRef<HTMLElement>(); function closeChild() { childMenu.value = null; @@ -355,10 +393,10 @@ function switchItem(item: MenuSwitch & { ref: any }) { function focusUp() { if (disposed) return; - if (!itemsEl.value?.contains(document.activeElement)) return; + if (!itemsEl.value?.contains(window.document.activeElement)) return; const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable); - const activeIndex = focusableElements.findIndex(el => el === document.activeElement); + const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement); const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1); const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value; @@ -367,10 +405,10 @@ function focusUp() { function focusDown() { if (disposed) return; - if (!itemsEl.value?.contains(document.activeElement)) return; + if (!itemsEl.value?.contains(window.document.activeElement)) return; const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable); - const activeIndex = focusableElements.findIndex(el => el === document.activeElement); + const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement); const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0; const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value; @@ -397,9 +435,9 @@ const onGlobalMousedown = (ev: MouseEvent) => { const setupHandlers = () => { if (!isNestingMenu) { - document.addEventListener('focusin', onGlobalFocusin, { passive: true }); + window.document.addEventListener('focusin', onGlobalFocusin, { passive: true }); } - document.addEventListener('mousedown', onGlobalMousedown, { passive: true }); + window.document.addEventListener('mousedown', onGlobalMousedown, { passive: true }); }; let disposed = false; @@ -407,9 +445,9 @@ let disposed = false; const disposeHandlers = () => { disposed = true; if (!isNestingMenu) { - document.removeEventListener('focusin', onGlobalFocusin); + window.document.removeEventListener('focusin', onGlobalFocusin); } - document.removeEventListener('mousedown', onGlobalMousedown); + window.document.removeEventListener('mousedown', onGlobalMousedown); }; onMounted(() => { @@ -435,6 +473,12 @@ onBeforeUnmount(() => { } } + &:not(.asDrawer):not(.widthSpecified) { + > .menu { + max-width: 400px; + } + } + &.big:not(.asDrawer) { > .menu { min-width: 230px; @@ -558,11 +602,11 @@ onBeforeUnmount(() => { } &.danger { - --menuFg: #ff2a2a; + --menuFg: var(--MI_THEME-error); --menuHoverFg: #fff; - --menuHoverBg: #ff4242; + --menuHoverBg: var(--MI_THEME-error); --menuActiveFg: #fff; - --menuActiveBg: #d42e2e; + --menuActiveBg: hsl(from var(--MI_THEME-error) h s calc(l - 10)); } &.radio { @@ -604,10 +648,19 @@ onBeforeUnmount(() => { .item_content_text { max-width: calc(100vw - 4rem); +} + +.item_content_text_title { text-overflow: ellipsis; overflow: hidden; } +.item_content_text_caption { + text-wrap: auto; + font-size: 85%; + opacity: 0.7; +} + .switchButton { margin-left: -2px; --height: 1.35em; diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index 7ea585ecc2..98bd471438 100644 --- a/packages/frontend/src/components/MkMiniChart.vue +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -48,7 +48,7 @@ const polygonPoints = ref(''); const headX = ref<number | null>(null); const headY = ref<number | null>(null); const clock = ref<number | null>(null); -const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent')); +const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent')); const color = accent.toRgbString(); function draw(): void { diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 19588003fa..3bcf835ec9 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -42,14 +42,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue'; +import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, useTemplateRef, computed } from 'vue'; +import type { Keymap } from '@/utility/hotkey.js'; import * as os from '@/os.js'; -import { isTouchUsing } from '@/scripts/touch.js'; -import { defaultStore } from '@/store.js'; -import { deviceKind } from '@/scripts/device-kind.js'; -import type { Keymap } from '@/scripts/hotkey.js'; -import { focusTrap } from '@/scripts/focus-trap.js'; -import { focusParent } from '@/scripts/focus.js'; +import { isTouchUsing } from '@/utility/touch.js'; +import { deviceKind } from '@/utility/device-kind.js'; +import { focusTrap } from '@/utility/focus-trap.js'; +import { focusParent } from '@/utility/focus.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -94,19 +95,19 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -provide('modal', true); +provide(DI.inModal, true); const maxHeight = ref<number>(); const fixed = ref(false); const transformOrigin = ref('center'); const showing = ref(true); -const modalRootEl = shallowRef<HTMLElement>(); -const content = shallowRef<HTMLElement>(); +const modalRootEl = useTemplateRef('modalRootEl'); +const content = useTemplateRef('content'); const zIndex = os.claimZIndex(props.zPriority); const useSendAnime = ref(false); const type = computed<ModalTypes>(() => { if (props.preferType === 'auto') { - if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) { + if ((prefer.s.menuStyle === 'drawer') || (prefer.s.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) { return 'drawer'; } else { return props.src != null ? 'popup' : 'dialog'; @@ -117,7 +118,7 @@ const type = computed<ModalTypes>(() => { }); const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup')); const transitionName = computed((() => - defaultStore.state.animation + prefer.s.animation ? useSendAnime.value ? 'send' : type.value === 'drawer' diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index f06cfffee4..19989e375b 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, shallowRef, ref } from 'vue'; +import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue'; import MkModal from './MkModal.vue'; const props = withDefaults(defineProps<{ @@ -47,9 +47,9 @@ const emit = defineEmits<{ (event: 'esc'): void; }>(); -const modal = shallowRef<InstanceType<typeof MkModal>>(); -const rootEl = shallowRef<HTMLElement>(); -const headerEl = shallowRef<HTMLElement>(); +const modal = useTemplateRef('modal'); +const rootEl = useTemplateRef('rootEl'); +const headerEl = useTemplateRef('headerEl'); const bodyWidth = ref(0); const bodyHeight = ref(0); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 8e1d854660..ab70a11b9b 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -9,13 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" - :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]" + :class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]" :tabindex="isDeleted ? '-1' : '0'" > <MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div> - <!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>--> - <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>--> <div v-if="isRenote" :class="$style.renote"> <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> @@ -47,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <article v-else :class="$style.article" @contextmenu.stop="onContextmenu"> <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> - <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/> + <MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="appearNote.user" :link="!mock" :preview="!mock"/> <div :class="$style.main"> <MkNoteHeader :note="appearNote" :mini="true"/> <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/> @@ -76,12 +74,13 @@ SPDX-License-Identifier: AGPL-3.0-only :emojiUrls="appearNote.emojis" :enableEmojiMenu="true" :enableEmojiMenuReaction="true" + class="_selectable" /> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else-if="translation"> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/> </div> </div> </div> @@ -130,9 +129,9 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ti ti-plus"></i> - <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> + <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> <i class="ti ti-paperclip"></i> </button> <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()"> @@ -177,14 +176,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, onMounted, ref, shallowRef, watch, provide } from 'vue'; -import type { Ref } from 'vue'; +import { computed, inject, onMounted, ref, useTemplateRef, watch, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; import { shouldCollapsed } from '@@/js/collapsed.js'; import { host } from '@@/js/config.js'; +import type { Ref } from 'vue'; import type { MenuItem } from '@/types/menu.js'; +import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; +import type { Keymap } from '@/utility/hotkey.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; @@ -196,32 +197,32 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; -import { pleaseLogin } from '@/scripts/please-login.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; -import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { pleaseLogin } from '@/utility/please-login.js'; +import { checkWordMute } from '@/utility/check-word-mute.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import number from '@/filters/number.js'; import * as os from '@/os.js'; -import * as sound from '@/scripts/sound.js'; -import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { defaultStore, noteViewInterruptors } from '@/store.js'; -import { reactionPicker } from '@/scripts/reaction-picker.js'; -import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; -import { $i } from '@/account.js'; +import * as sound from '@/utility/sound.js'; +import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; +import { reactionPicker } from '@/utility/reaction-picker.js'; +import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; +import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js'; -import { useNoteCapture } from '@/scripts/use-note-capture.js'; -import { deepClone } from '@/scripts/clone.js'; -import { useTooltip } from '@/scripts/use-tooltip.js'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { getNoteSummary } from '@/scripts/get-note-summary.js'; +import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js'; +import { useNoteCapture } from '@/use/use-note-capture.js'; +import { deepClone } from '@/utility/clone.js'; +import { useTooltip } from '@/use/use-tooltip.js'; +import { claimAchievement } from '@/utility/achievements.js'; +import { getNoteSummary } from '@/utility/get-note-summary.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import { showMovedDialog } from '@/utility/show-moved-dialog.js'; import { isEnabledUrlPreview } from '@/instance.js'; -import type { Keymap } from '@/scripts/hotkey.js'; -import { focusPrev, focusNext } from '@/scripts/focus.js'; -import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { focusPrev, focusNext } from '@/utility/focus.js'; +import { getAppearNote } from '@/utility/get-appear-note.js'; +import { prefer } from '@/preferences.js'; +import { getPluginHandlers } from '@/plugin.js'; +import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -232,7 +233,7 @@ const props = withDefaults(defineProps<{ mock: false, }); -provide('mock', props.mock); +provide(DI.mock, props.mock); const emit = defineEmits<{ (ev: 'reaction', emoji: string): void; @@ -247,6 +248,7 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul const note = ref(deepClone(props.note)); // plugin +const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); if (noteViewInterruptors.length > 0) { onMounted(async () => { let result: Misskey.entities.Note | null = deepClone(note.value); @@ -267,14 +269,14 @@ if (noteViewInterruptors.length > 0) { const isRenote = Misskey.note.isPureRenote(note.value); -const rootEl = shallowRef<HTMLElement>(); -const menuButton = shallowRef<HTMLElement>(); -const renoteButton = shallowRef<HTMLElement>(); -const renoteTime = shallowRef<HTMLElement>(); -const reactButton = shallowRef<HTMLElement>(); -const clipButton = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); +const menuButton = useTemplateRef('menuButton'); +const renoteButton = useTemplateRef('renoteButton'); +const renoteTime = useTemplateRef('renoteTime'); +const reactButton = useTemplateRef('reactButton'); +const clipButton = useTemplateRef('clipButton'); const appearNote = computed(() => getAppearNote(note.value)); -const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); +const galleryEl = useTemplateRef('galleryEl'); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(false); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); @@ -284,13 +286,13 @@ const collapsed = ref(appearNote.value.cw == null && isLong); const isDeleted = ref(false); const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true)); -const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord); +const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord); const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translating = ref(false); -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); const renoteCollapsed = ref( - defaultStore.state.collapseRenotes && isRenote && ( + prefer.s.collapseRenotes && isRenote && ( ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 (appearNote.value.myReaction != null) ), @@ -345,7 +347,7 @@ const keymap = { }, 'c': () => { if (renoteCollapsed.value) return; - if (!defaultStore.state.showClipButtonInNoteFooter) return; + if (!prefer.s.showClipButtonInNoteFooter) return; clip(); }, 'o': () => { @@ -375,7 +377,8 @@ const keymap = { }, } as const satisfies Keymap; -provide('react', (reaction: string) => { +provide(DI.mfmEmojiReactCallback, (reaction) => { + sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, @@ -479,7 +482,7 @@ function react(): void { reaction: '❤️', }); const el = reactButton.value; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -490,7 +493,7 @@ function react(): void { } else { blur(); reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => { - if (defaultStore.state.confirmOnReact) { + if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }), @@ -549,7 +552,7 @@ function onContextmenu(ev: MouseEvent): void { if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; - if (defaultStore.state.useReactionPickerForContextMenu) { + if (prefer.s.useReactionPickerForContextMenu) { ev.preventDefault(); react(); } else { @@ -655,7 +658,6 @@ function emitUpdReaction(emoji: string, delta: number) { <style lang="scss" module> .root { position: relative; - transition: box-shadow 0.1s ease; font-size: 1.05em; overflow: clip; contain: content; @@ -721,6 +723,8 @@ function emitUpdReaction(emoji: string, delta: number) { } .skipRender { + // TODO: これが有効だとTransitionGroupでnoteを追加するときに一瞬がくっとなってしまうのをどうにかしたい + // Transitionが完了するのを待ってからskipRenderを付与すれば解決しそうだけどパフォーマンス的な影響が不明 content-visibility: auto; contain-intrinsic-size: 0 150px; } @@ -847,9 +851,12 @@ function emitUpdReaction(emoji: string, delta: number) { margin: 0 14px 0 0; width: 58px; height: 58px; - position: sticky !important; - top: calc(22px + var(--MI-stickyTop, 0px)); - left: 0; + + &.useSticky { + position: sticky !important; + top: calc(22px + var(--MI-stickyTop, 0px)); + left: 0; + } } .main { @@ -1036,7 +1043,10 @@ function emitUpdReaction(emoji: string, delta: number) { margin: 0 10px 0 0; width: 46px; height: 46px; - top: calc(14px + var(--MI-stickyTop, 0px)); + + &.useSticky { + top: calc(14px + var(--MI-stickyTop, 0px)); + } } } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 27d8a399cc..a26eb808e4 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -97,13 +97,14 @@ SPDX-License-Identifier: AGPL-3.0-only :emojiUrls="appearNote.emojis" :enableEmojiMenu="true" :enableEmojiMenuReaction="true" + class="_selectable" /> <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else-if="translation"> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/> </div> </div> <div v-if="appearNote.files && appearNote.files.length > 0"> @@ -146,9 +147,9 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ti ti-plus"></i> - <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()"> + <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()"> <i class="ti ti-paperclip"></i> </button> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()"> @@ -210,11 +211,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue'; +import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; import { host } from '@@/js/config.js'; +import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; +import type { Paging } from '@/components/MkPagination.vue'; +import type { Keymap } from '@/utility/hotkey.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -225,35 +229,34 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; -import { pleaseLogin } from '@/scripts/please-login.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; -import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { pleaseLogin } from '@/utility/please-login.js'; +import { checkWordMute } from '@/utility/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import { notePage } from '@/filters/note.js'; import number from '@/filters/number.js'; import * as os from '@/os.js'; -import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import * as sound from '@/scripts/sound.js'; -import { defaultStore, noteViewInterruptors } from '@/store.js'; -import { reactionPicker } from '@/scripts/reaction-picker.js'; -import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; -import { $i } from '@/account.js'; +import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; +import * as sound from '@/utility/sound.js'; +import { reactionPicker } from '@/utility/reaction-picker.js'; +import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; +import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js'; -import { useNoteCapture } from '@/scripts/use-note-capture.js'; -import { deepClone } from '@/scripts/clone.js'; -import { useTooltip } from '@/scripts/use-tooltip.js'; -import { claimAchievement } from '@/scripts/achievements.js'; +import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js'; +import { useNoteCapture } from '@/use/use-note-capture.js'; +import { deepClone } from '@/utility/clone.js'; +import { useTooltip } from '@/use/use-tooltip.js'; +import { claimAchievement } from '@/utility/achievements.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import { showMovedDialog } from '@/utility/show-moved-dialog.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination from '@/components/MkPagination.vue'; -import type { Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { isEnabledUrlPreview } from '@/instance.js'; -import { getAppearNote } from '@/scripts/get-appear-note.js'; -import type { Keymap } from '@/scripts/hotkey.js'; +import { getAppearNote } from '@/utility/get-appear-note.js'; +import { prefer } from '@/preferences.js'; +import { getPluginHandlers } from '@/plugin.js'; +import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -267,6 +270,7 @@ const inChannel = inject('inChannel', null); const note = ref(deepClone(props.note)); // plugin +const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); if (noteViewInterruptors.length > 0) { onMounted(async () => { let result: Misskey.entities.Note | null = deepClone(note.value); @@ -287,14 +291,14 @@ if (noteViewInterruptors.length > 0) { const isRenote = Misskey.note.isPureRenote(note.value); -const rootEl = shallowRef<HTMLElement>(); -const menuButton = shallowRef<HTMLElement>(); -const renoteButton = shallowRef<HTMLElement>(); -const renoteTime = shallowRef<HTMLElement>(); -const reactButton = shallowRef<HTMLElement>(); -const clipButton = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); +const menuButton = useTemplateRef('menuButton'); +const renoteButton = useTemplateRef('renoteButton'); +const renoteTime = useTemplateRef('renoteTime'); +const reactButton = useTemplateRef('reactButton'); +const clipButton = useTemplateRef('clipButton'); const appearNote = computed(() => getAppearNote(note.value)); -const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); +const galleryEl = useTemplateRef('galleryEl'); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(false); const isDeleted = ref(false); @@ -303,7 +307,7 @@ const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translating = ref(false); const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null; -const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); const conversation = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); @@ -319,7 +323,7 @@ const keymap = { 'q': () => renote(), 'm': () => showMenu(), 'c': () => { - if (!defaultStore.state.showClipButtonInNoteFooter) return; + if (!prefer.s.showClipButtonInNoteFooter) return; clip(); }, 'o': () => galleryEl.value?.openGallery(), @@ -334,7 +338,8 @@ const keymap = { }, } as const satisfies Keymap; -provide('react', (reaction: string) => { +provide(DI.mfmEmojiReactCallback, (reaction) => { + sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, @@ -442,7 +447,7 @@ function react(): void { reaction: '❤️', }); const el = reactButton.value; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -453,7 +458,7 @@ function react(): void { } else { blur(); reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => { - if (defaultStore.state.confirmOnReact) { + if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }), @@ -497,7 +502,7 @@ function onContextmenu(ev: MouseEvent): void { if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; - if (defaultStore.state.useReactionPickerForContextMenu) { + if (prefer.s.useReactionPickerForContextMenu) { ev.preventDefault(); react(); } else { diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 750e32a9ff..ea115c2cd8 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -40,13 +40,13 @@ import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; -import { defaultStore } from '@/store.js'; +import { DI } from '@/di.js'; defineProps<{ note: Misskey.entities.Note; }>(); -const mock = inject<boolean>('mock', false); +const mock = inject(DI.mock, false); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue index e51ea5a2de..764d9f6a32 100644 --- a/packages/frontend/src/components/MkNoteMediaGrid.vue +++ b/packages/frontend/src/components/MkNoteMediaGrid.vue @@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-for="file in note.files"> <div v-if="((( - (defaultStore.state.nsfw === 'force' || file.isSensitive) && - defaultStore.state.nsfw !== 'ignore' - ) || (defaultStore.state.dataSaver.media && file.type.startsWith('image/'))) && + (prefer.s.nsfw === 'force' || file.isSensitive) && + prefer.s.nsfw !== 'ignore' + ) || (prefer.s.dataSaver.media && file.type.startsWith('image/'))) && !showingFiles.has(file.id) )" :class="[$style.filePreview, { [$style.square]: square }]" @@ -18,15 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only <MkDriveFileThumbnail :file="file" fit="cover" - :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :highlightWhenSensitive="prefer.s.highlightSensitiveMedia" :forceBlurhash="true" :large="true" :class="$style.file" /> <div :class="$style.sensitive"> <div> - <div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div> - <div v-else><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div> + <div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div> + <div v-else><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div> <div>{{ i18n.ts.clickToShow }}</div> </div> </div> @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkDriveFileThumbnail :file="file" fit="cover" - :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :highlightWhenSensitive="prefer.s.highlightSensitiveMedia" :large="true" :class="$style.file" /> @@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import { notePage } from '@/filters/note.js'; import { i18n } from '@/i18n.js'; -import * as Misskey from 'misskey-js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import bytes from '@/filters/bytes.js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index e4bade309b..4fd1c210cb 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -46,11 +46,11 @@ import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import { notePage } from '@/filters/note.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; import { userPage } from '@/filters/user.js'; -import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { checkWordMute } from '@/utility/check-word-mute.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 344b7c4dd2..ad6210816d 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -7,38 +7,43 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad"> <template #empty> <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> + <img :src="infoImageUrl" draggable="false"/> <div>{{ i18n.ts.noNotes }}</div> </div> </template> <template #default="{ items: notes }"> - <div :class="[$style.root, { [$style.noGap]: noGap }]"> - <MkDateSeparatedList - ref="notes" - v-slot="{ item: note }" - :items="notes" - :direction="pagination.reversed ? 'up' : 'down'" - :reversed="pagination.reversed" - :noGap="noGap" - :ad="true" - :class="$style.notes" - > - <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> - </MkDateSeparatedList> - </div> + <component + :is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + :moveClass=" $style.transition_x_move" + tag="div" + > + <template v-for="(note, i) in notes" :key="note.id"> + <div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]"> + <MkNote :class="$style.note" :note="note" :withHardMute="true"/> + <div :class="$style.ad"> + <MkAd :preferForms="['horizontal', 'horizontal-big']"/> + </div> + </div> + <MkNote v-else :class="$style.note" :note="note" :withHardMute="true"/> + </template> + </component> </template> </MkPagination> </template> <script lang="ts" setup> -import { shallowRef } from 'vue'; +import { useTemplateRef, TransitionGroup } from 'vue'; +import type { Paging } from '@/components/MkPagination.vue'; import MkNote from '@/components/MkNote.vue'; -import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkPagination from '@/components/MkPagination.vue'; -import type { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ pagination: Paging; @@ -46,7 +51,7 @@ const props = defineProps<{ disableAutoLoad?: boolean; }>(); -const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); +const pagingComponent = useTemplateRef('pagingComponent'); defineExpose({ pagingComponent, @@ -54,22 +59,49 @@ defineExpose({ </script> <style lang="scss" module> +.transition_x_move, +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important; +} +.transition_x_enterFrom, +.transition_x_leaveTo { + opacity: 0; + transform: translateY(-50%); +} +.transition_x_leaveActive { + position: absolute; +} + .root { + container-type: inline-size; + &.noGap { - > .notes { - background: var(--MI_THEME-panel); + background: var(--MI_THEME-panel); + + .note { + border-bottom: solid 0.5px var(--MI_THEME-divider); + } + + .ad { + padding: 8px; + background-size: auto auto; + background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px); + border-bottom: solid 0.5px var(--MI_THEME-divider); } } &:not(.noGap) { - > .notes { - background: var(--MI_THEME-bg); + background: var(--MI_THEME-bg); - .note { - background: var(--MI_THEME-panel); - border-radius: var(--MI-radius); - } + .note { + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } } } + +.ad:empty { + display: none; +} </style> diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 80cb9a45bb..13ffd6b7cc 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_login]: notification.type === 'login', [$style.t_createToken]: notification.type === 'createToken', + [$style.t_chatRoomInvitationReceived]: notification.type === 'chatRoomInvitationReceived', [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, }]" > @@ -43,6 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> <i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i> <i v-else-if="notification.type === 'createToken'" class="ti ti-key"></i> + <i v-else-if="notification.type === 'chatRoomInvitationReceived'" class="ti ti-messages"></i> <template v-else-if="notification.type === 'roleAssigned'"> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <i v-else class="ti ti-badges"></i> @@ -61,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> + <span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span> <span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span> @@ -104,6 +107,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text"> {{ notification.role.name }} </div> + <div v-else-if="notification.type === 'chatRoomInvitationReceived'" :class="$style.text"> + {{ notification.invitation.room.name }} + </div> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> {{ i18n.ts._achievements._types['_' + notification.achievement].title }} </MkA> @@ -164,15 +170,15 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; -import { getNoteSummary } from '@/scripts/get-note-summary.js'; +import { getNoteSummary } from '@/utility/get-note-summary.js'; import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { signinRequired } from '@/account.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { ensureSignin } from '@/i.js'; import { infoImageUrl } from '@/instance.js'; -const $i = signinRequired(); +const $i = ensureSignin(); const props = withDefaults(defineProps<{ notification: Misskey.entities.Notification; @@ -369,6 +375,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_chatRoomInvitationReceived { + padding: 3px; + background: var(--eventOther); + pointer-events: none; +} + .tail { flex: 1; min-width: 0; diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index a0fb7fea83..d074dceb2f 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -30,13 +30,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef } from 'vue'; -import type { Ref } from 'vue'; +import { ref, useTemplateRef } from 'vue'; +import { notificationTypes } from '@@/js/const.js'; import MkSwitch from './MkSwitch.vue'; import MkInfo from './MkInfo.vue'; import MkButton from './MkButton.vue'; +import type { Ref } from 'vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; -import { notificationTypes } from '@@/js/const.js'; import { i18n } from '@/i18n.js'; type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>; @@ -52,7 +52,7 @@ const props = withDefaults(defineProps<{ excludeTypes: () => [], }); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap); diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 470837ace5..99eca35eb7 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -8,42 +8,51 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPagination ref="pagingComponent" :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> + <img :src="infoImageUrl" draggable="false"/> <div>{{ i18n.ts.noNotifications }}</div> </div> </template> <template #default="{ items: notifications }"> - <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/> - <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> - </MkDateSeparatedList> + <component + :is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + :moveClass=" $style.transition_x_move" + tag="div" + > + <template v-for="(notification, i) in notifications" :key="notification.id"> + <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/> + <XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/> + </template> + </component> </template> </MkPagination> </MkPullToRefresh> </template> <script lang="ts" setup> -import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; +import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue'; +import * as Misskey from 'misskey-js'; +import type { notificationTypes } from '@@/js/const.js'; import MkPagination from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; -import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import type { notificationTypes } from '@@/js/const.js'; import { infoImageUrl } from '@/instance.js'; -import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; -import * as Misskey from 'misskey-js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; }>(); -const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); +const pagingComponent = useTemplateRef('pagingComponent'); -const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? { +const pagination = computed(() => prefer.r.useGroupedNotifications.value ? { endpoint: 'i/notifications-grouped' as const, limit: 20, params: computed(() => ({ @@ -59,7 +68,7 @@ const pagination = computed(() => defaultStore.reactiveState.useGroupedNotificat function onNotification(notification) { const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false; - if (isMuted || document.visibilityState === 'visible') { + if (isMuted || window.document.visibilityState === 'visible') { useStream().send('readNotification'); } @@ -84,28 +93,36 @@ onMounted(() => { connection.on('notificationFlushed', reload); }); -onActivated(() => { - pagingComponent.value?.reload(); - connection = useStream().useChannel('main'); - connection.on('notification', onNotification); - connection.on('notificationFlushed', reload); -}); - onUnmounted(() => { if (connection) connection.dispose(); }); -onDeactivated(() => { - if (connection) connection.dispose(); -}); - defineExpose({ reload, }); </script> <style lang="scss" module> -.list { +.transition_x_move, +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important; +} +.transition_x_enterFrom, +.transition_x_leaveTo { + opacity: 0; + transform: translateY(-50%); +} +.transition_x_leaveActive { + position: absolute; +} + +.notifications { + container-type: inline-size; background: var(--MI_THEME-panel); } + +.item { + border-bottom: solid 0.5px var(--MI_THEME-divider); +} </style> diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue index a05176e2f4..04276b47fe 100644 --- a/packages/frontend/src/components/MkOmit.vue +++ b/packages/frontend/src/components/MkOmit.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, shallowRef, ref } from 'vue'; +import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ @@ -22,7 +22,7 @@ const props = withDefaults(defineProps<{ maxHeight: 200, }); -const content = shallowRef<HTMLElement>(); +const content = useTemplateRef('content'); const omitted = ref(false); const ignoreOmit = ref(false); diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index c3fc1961eb..32c2e48b01 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -22,29 +22,29 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </template> - <div ref="contents" :class="$style.root" style="container-type: inline-size;"> - <RouterView :key="reloadCount" :router="windowRouter"/> + <div :class="$style.root"> + <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :key="reloadCount" :router="windowRouter"/> + <RouterView v-else :key="reloadCount" :router="windowRouter"/> </div> </MkWindow> </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'; +import { computed, onMounted, onUnmounted, provide, ref, useTemplateRef } from 'vue'; import { url } from '@@/js/config.js'; -import { getScrollContainer } from '@@/js/scroll.js'; -import type { PageMetadata } from '@/scripts/page-metadata.js'; +import type { PageMetadata } from '@/page.js'; import RouterView from '@/components/global/RouterView.vue'; import MkWindow from '@/components/MkWindow.vue'; -import { popout as _popout } from '@/scripts/popout.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { useScrollPositionManager } from '@/nirax.js'; +import { popout as _popout } from '@/utility/popout.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; -import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; +import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; import { openingWindowsCount } from '@/os.js'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { useRouterFactory } from '@/router/supplier.js'; -import { mainRouter } from '@/router/main.js'; +import { claimAchievement } from '@/utility/achievements.js'; +import { createRouter, mainRouter } from '@/router.js'; import { analytics } from '@/analytics.js'; +import { DI } from '@/di.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ initialPath: string; @@ -54,15 +54,12 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const routerFactory = useRouterFactory(); -const windowRouter = routerFactory(props.initialPath); +const windowRouter = createRouter(props.initialPath); -const contents = shallowRef<HTMLElement | null>(null); const pageMetadata = ref<null | PageMetadata>(null); -const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); -const history = ref<{ path: string; key: string; }[]>([{ - path: windowRouter.getCurrentPath(), - key: windowRouter.getCurrentKey(), +const windowEl = useTemplateRef('windowEl'); +const history = ref<{ path: string; }[]>([{ + path: windowRouter.getCurrentFullPath(), }]); const buttonsLeft = computed(() => { const buttons: Record<string, unknown>[] = []; @@ -100,27 +97,27 @@ function getSearchMarker(path: string) { const searchMarkerId = ref<string | null>(getSearchMarker(props.initialPath)); windowRouter.addListener('push', ctx => { - history.value.push({ path: ctx.path, key: ctx.key }); + history.value.push({ path: ctx.fullPath }); }); windowRouter.addListener('replace', ctx => { history.value.pop(); - history.value.push({ path: ctx.path, key: ctx.key }); + history.value.push({ path: ctx.fullPath }); }); windowRouter.addListener('change', ctx => { - if (_DEV_) console.log('windowRouter: change', ctx.path); - searchMarkerId.value = getSearchMarker(ctx.path); + if (_DEV_) console.log('windowRouter: change', ctx.fullPath); + searchMarkerId.value = getSearchMarker(ctx.fullPath); analytics.page({ - path: ctx.path, - title: ctx.path, + path: ctx.fullPath, + title: ctx.fullPath, }); }); windowRouter.init(); -provide('router', windowRouter); -provide('inAppSearchMarkerId', searchMarkerId); +provide(DI.router, windowRouter); +provide(DI.inAppSearchMarkerId, searchMarkerId); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; @@ -128,7 +125,7 @@ provideMetadataReceiver((metadataGetter) => { provideReactiveMetadata(pageMetadata); provide('shouldOmitHeaderTitle', true); provide('shouldHeaderThin', true); -provide('forceSpacerMin', true); +provide(DI.forceSpacerMin, true); const contextmenu = computed(() => ([{ icon: 'ti ti-player-eject', @@ -142,20 +139,20 @@ const contextmenu = computed(() => ([{ icon: 'ti ti-external-link', text: i18n.ts.openInNewTab, action: () => { - window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener'); + window.open(url + windowRouter.getCurrentFullPath(), '_blank', 'noopener'); windowEl.value?.close(); }, }, { icon: 'ti ti-link', text: i18n.ts.copyLink, action: () => { - copyToClipboard(url + windowRouter.getCurrentPath()); + copyToClipboard(url + windowRouter.getCurrentFullPath()); }, }])); function back() { history.value.pop(); - windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key); + windowRouter.replace(history.value.at(-1)!.path); } function reload() { @@ -167,17 +164,15 @@ function close() { } function expand() { - mainRouter.push(windowRouter.getCurrentPath(), 'forcePage'); + mainRouter.push(windowRouter.getCurrentFullPath(), 'forcePage'); windowEl.value?.close(); } function popout() { - _popout(windowRouter.getCurrentPath(), windowEl.value?.$el); + _popout(windowRouter.getCurrentFullPath(), windowEl.value?.$el); windowEl.value?.close(); } -useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter); - onMounted(() => { analytics.page({ path: props.initialPath, @@ -201,9 +196,7 @@ defineExpose({ <style lang="scss" module> .root { - overscroll-behavior: contain; - - min-height: 100%; + height: 100%; background: var(--MI_THEME-bg); --MI-margin: var(--MI-marginHalf); diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index d9135ab517..9adc3d98da 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -5,54 +5,53 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''" mode="out-in" > <MkLoading v-if="fetching"/> <MkError v-else-if="error" @retry="init()"/> - <div v-else-if="empty" key="_empty_" class="empty"> + <div v-else-if="empty" key="_empty_"> <slot name="empty"> <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> + <img :src="infoImageUrl" draggable="false"/> <div>{{ i18n.ts.nothing }}</div> </div> </slot> </div> - <div v-else ref="rootEl"> - <div v-show="pagination.reversed && more" key="_more_" class="_margin"> - <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead"> + <div v-else ref="rootEl" class="_gaps"> + <div v-show="pagination.reversed && more" key="_more_"> + <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead"> {{ i18n.ts.loadMore }} </MkButton> - <MkLoading v-else class="loading"/> + <MkLoading v-else/> </div> <slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot> - <div v-show="!pagination.reversed && more" key="_more_" class="_margin"> - <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore"> + <div v-show="!pagination.reversed && more" key="_more_"> + <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore"> {{ i18n.ts.loadMore }} </MkButton> - <MkLoading v-else class="loading"/> + <MkLoading v-else/> </div> </div> </Transition> </template> <script lang="ts"> -import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; -import type { ComputedRef } from 'vue'; +import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, 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 { defaultStore } from '@/store.js'; +import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js'; +import type { ComputedRef } from 'vue'; import type { MisskeyEntity } from '@/types/date-separated-list.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; const SECOND_FETCH_LIMIT = 30; const TOLERANCE = 16; @@ -75,8 +74,6 @@ export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> reversed?: boolean; offsetMode?: boolean; - - pageEl?: HTMLElement; }; type MisskeyEntityMap = Map<string, MisskeyEntity>; @@ -107,7 +104,7 @@ const emit = defineEmits<{ (ev: 'status', error: boolean): void; }>(); -const rootEl = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); // 遡り中かどうか const backed = ref(false); @@ -140,10 +137,9 @@ const empty = computed(() => items.value.size === 0); const error = ref(false); const { enableInfiniteScroll, -} = defaultStore.reactiveState; +} = prefer.r; -const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value); -const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body); +const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body); const visibility = useDocumentVisibility(); @@ -174,13 +170,13 @@ watch(rootEl, () => { }); }); -watch([backed, contentEl], () => { +watch([backed, rootEl], () => { if (!backed.value) { - if (!contentEl.value) return; + if (!rootEl.value) return; scrollRemove.value = props.pagination.reversed - ? onScrollBottom(contentEl.value, executeQueue, TOLERANCE) - : onScrollTop(contentEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE); + ? onScrollBottom(rootEl.value, executeQueue, TOLERANCE) + : onScrollTop(rootEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE); } else { if (scrollRemove.value) scrollRemove.value(); scrollRemove.value = null; @@ -262,7 +258,7 @@ const fetchMore = async (): Promise<void> => { return nextTick(() => { if (scrollableElement.value) { - scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); + scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); } else { window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); } @@ -350,7 +346,7 @@ const appearFetchMoreAhead = async (): Promise<void> => { fetchMoreAppearTimeout(); }; -const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE); +const isHead = (): boolean => isBackTop.value || (props.pagination.reversed ? isTailVisible : isHeadVisible)(rootEl.value!, TOLERANCE); watch(visibility, () => { if (visibility.value === 'hidden') { @@ -361,11 +357,11 @@ watch(visibility, () => { BACKGROUND_PAUSE_WAIT_SEC * 1000); } else { // 'visible' if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } else { isPausingUpdate = false; - if (isTop()) { + if (isHead()) { executeQueue(); } } @@ -377,16 +373,18 @@ watch(visibility, () => { * ストリーミングから降ってきたアイテムはこれで追加する * @param item アイテム */ -const prepend = (item: MisskeyEntity): void => { +function prepend(item: MisskeyEntity): void { if (items.value.size === 0) { items.value.set(item.id, item); fetching.value = false; return; } - if (isTop() && !isPausingUpdate) unshiftItems([item]); + if (_DEV_) console.log(isHead(), isPausingUpdate); + + if (isHead() && !isPausingUpdate) unshiftItems([item]); else prependQueue(item); -}; +} /** * 新着アイテムをitemsの先頭に追加し、displayLimitを適用する @@ -448,18 +446,18 @@ onDeactivated(() => { }); function toBottom() { - scrollToBottom(contentEl.value!); + scrollToBottom(rootEl.value!); } onBeforeMount(() => { init().then(() => { if (props.pagination.reversed) { nextTick(() => { - setTimeout(toBottom, 800); + window.setTimeout(toBottom, 800); // scrollToBottomでmoreFetchingボタンが画面外まで出るまで // more = trueを遅らせる - setTimeout(() => { + window.setTimeout(() => { moreFetching.value = false; }, 2000); }); @@ -469,11 +467,11 @@ onBeforeMount(() => { onBeforeUnmount(() => { if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } if (preventAppearFetchMoreTimer.value) { - clearTimeout(preventAppearFetchMoreTimer.value); + window.clearTimeout(preventAppearFetchMoreTimer.value); preventAppearFetchMoreTimer.value = null; } scrollObserver.value?.disconnect(); diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue index e749725fea..2abf8669ed 100644 --- a/packages/frontend/src/components/MkPasswordDialog.vue +++ b/packages/frontend/src/components/MkPasswordDialog.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :marginMin="20" :marginMax="28"> <div style="padding: 0 0 16px 0; text-align: center;"> - <img src="/fluent-emoji/1f510.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;"> + <img src="/client-assets/locked_with_key_3d.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;"> <div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div> </div> @@ -39,14 +39,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, shallowRef, ref } from 'vue'; +import { onMounted, useTemplateRef, ref } from 'vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; -import { signinRequired } from '@/account.js'; +import { ensureSignin } from '@/i.js'; -const $i = signinRequired(); +const $i = ensureSignin(); const emit = defineEmits<{ (ev: 'done', v: { password: string; token: string | null; }): void; @@ -54,8 +54,8 @@ const emit = defineEmits<{ (ev: 'cancelled'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -const passwordInput = shallowRef<InstanceType<typeof MkInput>>(); +const dialog = useTemplateRef('dialog'); +const passwordInput = useTemplateRef('passwordInput'); const password = ref(''); const isBackupCode = ref(false); const token = ref<string | null>(null); diff --git a/packages/frontend/src/components/MkPolkadots.vue b/packages/frontend/src/components/MkPolkadots.vue new file mode 100644 index 0000000000..285c4d0b79 --- /dev/null +++ b/packages/frontend/src/components/MkPolkadots.vue @@ -0,0 +1,40 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[$style.root, accented ? $style.accented : null]"></div> +</template> + +<script lang="ts" setup> +const props = withDefaults(defineProps<{ + accented?: boolean; +}>(), { + accented: false, +}); +</script> + +<style lang="scss" module> +.root { + --c: var(--MI_THEME-divider); + + &.accented { + --c: var(--MI_THEME-accent); + opacity: 0.5; + } + + --dot-size: 2px; + --gap-size: 40px; + --offset: calc(var(--gap-size) / 2); + + height: 200px; + margin-bottom: -200px; + + background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)); + background-position: 0 0, 0 0, var(--offset) var(--offset); + background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size); + mask-image: linear-gradient(to bottom, black 0%, transparent 100%); + pointer-events: none; +} +</style> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 1b2b3e48ba..2d3ec45bca 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -31,11 +31,11 @@ import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; import { useInterval } from '@@/js/use-interval.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; -import { sum } from '@/scripts/array.js'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; +import { sum } from '@/utility/array.js'; +import { pleaseLogin } from '@/utility/please-login.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue index 3726ddf822..22fe189a63 100644 --- a/packages/frontend/src/components/MkPollEditor.vue +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -58,8 +58,8 @@ import MkInput from './MkInput.vue'; import MkSelect from './MkSelect.vue'; import MkSwitch from './MkSwitch.vue'; import MkButton from './MkButton.vue'; -import { formatDateTimeString } from '@/scripts/format-time-string.js'; -import { addTime } from '@/scripts/time.js'; +import { formatDateTimeString } from '@/utility/format-time-string.js'; +import { addTime } from '@/utility/time.js'; import { i18n } from '@/i18n.js'; export type PollEditorModelValue = { diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index df664e49f7..232cc005e1 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef } from 'vue'; +import { ref, useTemplateRef } from 'vue'; import MkModal from './MkModal.vue'; import MkMenu from './MkMenu.vue'; import type { MenuItem } from '@/types/menu.js'; @@ -28,7 +28,7 @@ const emit = defineEmits<{ (ev: 'closing'): void; }>(); -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const modal = useTemplateRef('modal'); const manualShowing = ref(true); const hiding = ref(false); diff --git a/packages/frontend/src/components/MkPostForm.TextCounter.vue b/packages/frontend/src/components/MkPostForm.TextCounter.vue new file mode 100644 index 0000000000..b1d39df5d3 --- /dev/null +++ b/packages/frontend/src/components/MkPostForm.TextCounter.vue @@ -0,0 +1,95 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[$style.textCountRoot]"> + <div :class="$style.textCountLabel">{{ i18n.ts.textCount }}</div> + <div + :class="[$style.textCount, + { [$style.danger]: textCountPercentage > 100 }, + { [$style.warning]: textCountPercentage > 90 && textCountPercentage <= 100 }, + ]" + > + <div :class="$style.textCountGraph"></div> + <div><span :class="$style.textCountCurrent">{{ number(textLength) }}</span> / {{ number(maxTextLength) }}</div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed, useTemplateRef } from 'vue'; +import { instance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import number from '@/filters/number.js'; + +const props = defineProps<{ + textLength: number; +}>(); + +const maxTextLength = computed(() => { + return instance ? instance.maxNoteTextLength : 1000; +}); + +const textCountPercentage = computed(() => { + return props.textLength / maxTextLength.value * 100; +}); +</script> + +<style lang="scss" module> +.textCountRoot { + padding: 4px 14px; +} + +.textCountLabel { + font-size: 11px; + opacity: 0.8; + margin-bottom: 4px; +} + +.textCount { + display: flex; + gap: var(--MI-marginHalf); + align-items: center; + font-size: 12px; + --countColor: var(--MI_THEME-accent); + + &.danger { + --countColor: var(--MI_THEME-error); + } + + &.warning { + --countColor: var(--MI_THEME-warn); + } + + .textCountGraph { + position: relative; + width: 24px; + height: 24px; + border-radius: 50%; + background-image: conic-gradient( + var(--countColor) 0% v-bind("Math.min(100, textCountPercentage) + '%'"), + rgba(0, 0, 0, .2) v-bind("Math.min(100, textCountPercentage) + '%'") 100% + ); + + &::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: var(--MI_THEME-popup); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + .textCountCurrent { + color: var(--countColor); + font-weight: 700; + font-size: 18px; + } +} +</style> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index b39a4ad708..e43ff65e1d 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.headerRight"> <template v-if="!(channel != null && fixed)"> - <button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> + <button v-if="channel == null" ref="visibilityButton" v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> @@ -32,15 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.headerRightButtonText">{{ channel.name }}</span> </button> </template> - <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> + <button v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span> </button> - <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> - <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> - <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> - <span v-else><i class="ti ti-icons"></i></span> - </button> + <button ref="otherSettingsButton" v-tooltip="i18n.ts.other" class="_button" :class="$style.headerRightItem" @click="showOtherSettings"><i class="ti ti-dots"></i></button> <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <div :class="$style.submitInner"> <template v-if="posted"></template> @@ -67,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <div v-show="useCw" :class="$style.cwOuter"> <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd"> - <div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div> + <div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div> </div> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> @@ -103,42 +99,48 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue'; -import type { ShallowRef } from 'vue'; +import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { toASCII } from 'punycode.js'; import { host, url } from '@@/js/config.js'; +import type { ShallowRef } from 'vue'; import type { PostFormProps } from '@/types/post-form.js'; -import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import type { MenuItem } from '@/types/menu.js'; +import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; +import XTextCounter from '@/components/MkPostForm.TextCounter.vue'; import MkPollEditor from '@/components/MkPollEditor.vue'; -import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; -import { erase, unique } from '@/scripts/array.js'; -import { extractMentions } from '@/scripts/extract-mentions.js'; -import { formatTimeString } from '@/scripts/format-time-string.js'; -import { Autocomplete } from '@/scripts/autocomplete.js'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import { erase, unique } from '@/utility/array.js'; +import { extractMentions } from '@/utility/extract-mentions.js'; +import { formatTimeString } from '@/utility/format-time-string.js'; +import { Autocomplete } from '@/utility/autocomplete.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { selectFiles } from '@/scripts/select-file.js'; -import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { selectFiles } from '@/utility/select-file.js'; +import { store } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { uploadFile } from '@/scripts/upload.js'; -import { deepClone } from '@/scripts/clone.js'; +import { ensureSignin, notesCount, incNotesCount } from '@/i.js'; +import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { uploadFile } from '@/utility/upload.js'; +import { deepClone } from '@/utility/clone.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { miLocalStorage } from '@/local-storage.js'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { emojiPicker } from '@/scripts/emoji-picker.js'; -import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; +import { claimAchievement } from '@/utility/achievements.js'; +import { emojiPicker } from '@/utility/emoji-picker.js'; +import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js'; +import { prefer } from '@/preferences.js'; +import { getPluginHandlers } from '@/plugin.js'; +import { DI } from '@/di.js'; -const $i = signinRequired(); +const $i = ensureSignin(); -const modal = inject('modal'); +const modal = inject(DI.inModal, false); const props = withDefaults(defineProps<PostFormProps & { fixed?: boolean; @@ -152,7 +154,7 @@ const props = withDefaults(defineProps<PostFormProps & { initialLocalOnly: undefined, }); -provide('mock', props.mock); +provide(DI.mock, props.mock); const emit = defineEmits<{ (ev: 'posted'): void; @@ -163,10 +165,11 @@ const emit = defineEmits<{ (ev: 'fileChangeSensitive', fileId: string, to: boolean): void; }>(); -const textareaEl = shallowRef<HTMLTextAreaElement | null>(null); -const cwInputEl = shallowRef<HTMLInputElement | null>(null); -const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null); -const visibilityButton = shallowRef<HTMLElement>(); +const textareaEl = useTemplateRef('textareaEl'); +const cwInputEl = useTemplateRef('cwInputEl'); +const hashtagsInputEl = useTemplateRef('hashtagsInputEl'); +const visibilityButton = useTemplateRef('visibilityButton'); +const otherSettingsButton = useTemplateRef('otherSettingsButton'); const posting = ref(false); const posted = ref(false); @@ -174,19 +177,18 @@ const text = ref(props.initialText ?? ''); const files = ref(props.initialFiles ?? []); const poll = ref<PollEditorModelValue | null>(null); const useCw = ref<boolean>(!!props.initialCw); -const showPreview = ref(defaultStore.state.showPreview); -watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); -const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction); -watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value)); +const showPreview = ref(store.s.showPreview); +watch(showPreview, () => store.set('showPreview', showPreview.value)); +const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction); +watch(showAddMfmFunction, () => prefer.commit('enableQuickAddMfmFunction', showAddMfmFunction.value)); const cw = ref<string | null>(props.initialCw ?? null); -const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly)); -const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility)); +const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly)); +const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility)); const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]); if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(u => pushVisibleUser(u)); } -const reactionAcceptance = ref(defaultStore.state.reactionAcceptance); -const autocomplete = ref(null); +const reactionAcceptance = ref(store.s.reactionAcceptance); const draghover = ref(false); const quoteId = ref<string | null>(null); const hasNotSpecifiedMentions = ref(false); @@ -196,6 +198,7 @@ const showingOptions = ref(false); const textAreaReadOnly = ref(false); const justEndedComposition = ref(false); const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote); +const postFormActions = getPluginHandlers('post_form_action'); const draftKey = computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; @@ -263,13 +266,19 @@ const canPost = computed((): boolean => { quoteId.value != null ) && (textLength.value <= maxTextLength.value) && - (cwTextLength.value <= maxCwTextLength) && + ( + useCw.value ? + ( + cw.value != null && cw.value.trim() !== '' && + cwTextLength.value <= maxCwTextLength + ) : true + ) && (files.value.length <= 16) && (!poll.value || poll.value.choices.length >= 2); }); -const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags')); -const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags')); +const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags')); +const hashtags = computed(store.makeGetterSetter('postFormHashtags')); watch(text, () => { checkMissingMention(); @@ -357,7 +366,7 @@ if (props.specified) { } // keep cw when reply -if (defaultStore.state.keepCw && props.reply && props.reply.cw) { +if (prefer.s.keepCw && props.reply && props.reply.cw) { useCw.value = true; cw.value = props.reply.cw; } @@ -456,7 +465,7 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities function upload(file: File, name?: string): void { if (props.mock) return; - uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { + uploadFile(file, prefer.s.uploadFolder, name).then(res => { files.value.push(res); }); } @@ -477,8 +486,8 @@ function setVisibility() { }, { changeVisibility: v => { visibility.value = v; - if (defaultStore.state.rememberNoteVisibility) { - defaultStore.set('visibility', visibility.value); + if (prefer.s.rememberNoteVisibility) { + store.set('visibility', visibility.value); } }, closed: () => dispose(), @@ -525,8 +534,8 @@ async function toggleLocalOnly() { } localOnly.value = !localOnly.value; - if (defaultStore.state.rememberNoteVisibility) { - defaultStore.set('localOnly', localOnly.value); + if (prefer.s.rememberNoteVisibility) { + store.set('localOnly', localOnly.value); } } @@ -546,6 +555,47 @@ async function toggleReactionAcceptance() { reactionAcceptance.value = select.result; } +//#region その他の設定メニューpopup +function showOtherSettings() { + let reactionAcceptanceIcon = 'ti ti-icons'; + + if (reactionAcceptance.value === 'likeOnly') { + reactionAcceptanceIcon = 'ti ti-heart _love'; + } else if (reactionAcceptance.value === 'likeOnlyForRemote') { + reactionAcceptanceIcon = 'ti ti-heart-plus'; + } + + const menuItems = [{ + type: 'component', + component: XTextCounter, + props: { + textLength: textLength, + }, + }, { type: 'divider' }, { + icon: reactionAcceptanceIcon, + text: i18n.ts.reactionAcceptance, + action: () => { + toggleReactionAcceptance(); + }, + }, { type: 'divider' }, { + icon: 'ti ti-trash', + text: i18n.ts.reset, + danger: true, + action: async () => { + if (props.mock) return; + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.resetAreYouSure, + }); + if (canceled) return; + clear(); + }, + }] satisfies MenuItem[]; + + os.popupMenu(menuItems, otherSettingsButton.value); +} +//#endregion + function pushVisibleUser(user: Misskey.entities.UserDetailed) { if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { visibleUsers.value.push(user); @@ -594,6 +644,8 @@ function onCompositionEnd(ev: CompositionEvent) { justEndedComposition.value = true; } +const pastedFileName = 'yyyy-MM-dd HH-mm-ss [{{number}}]'; + async function onPaste(ev: ClipboardEvent) { if (props.mock) return; if (!ev.clipboardData) return; @@ -604,7 +656,7 @@ async function onPaste(ev: ClipboardEvent) { if (!file) continue; const lio = file.name.lastIndexOf('.'); const ext = lio >= 0 ? file.name.slice(lio) : ''; - const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; + const formatted = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; upload(file, formatted); } } @@ -638,7 +690,7 @@ async function onPaste(ev: ClipboardEvent) { return; } - const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0'); + const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0'); const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' }); upload(file, `${fileName}.txt`); }); @@ -740,18 +792,10 @@ function isAnnoying(text: string): boolean { } async function post(ev?: MouseEvent) { - if (useCw.value && (cw.value == null || cw.value.trim() === '')) { - os.alert({ - type: 'error', - text: i18n.ts.cwNotationRequired, - }); - return; - } - if (ev) { const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; - if (el && defaultStore.state.animation) { + if (el && prefer.s.animation) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); @@ -820,6 +864,7 @@ async function post(ev?: MouseEvent) { } // plugin + const notePostInterruptors = getPluginHandlers('note_post_interruptor'); if (notePostInterruptors.length > 0) { for (const interruptor of notePostInterruptors) { try { @@ -834,7 +879,7 @@ async function post(ev?: MouseEvent) { if (postAccount.value) { const storedAccounts = await getAccounts(); - token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token; + token = storedAccounts.find(x => x.user.id === postAccount.value?.id)?.token; } posting.value = true; @@ -1269,7 +1314,7 @@ html[data-color-scheme=light] .preview { padding: 0 24px; margin: 0; width: 100%; - font-size: 16px; + font-size: 110%; border: none; border-radius: 0; background: transparent; diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index c7774d50b2..e8404cbd4f 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -36,12 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, inject } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu'; -import { defaultStore } from '@/store'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard'; +import { copyToClipboard } from '@/utility/copy-to-clipboard'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -50,7 +51,7 @@ const props = defineProps<{ detachMediaFn?: (id: string) => void; }>(); -const mock = inject<boolean>('mock', false); +const mock = inject(DI.mock, false); const emit = defineEmits<{ (ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void; @@ -198,9 +199,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar action: () => { detachAndDeleteMedia(file); }, }); - if (defaultStore.state.devMode) { + if (prefer.s.devMode) { menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', + icon: 'ti ti-hash', text: i18n.ts.copyFileId, action: () => { copyToClipboard(file.id); diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index 4bef5824fe..c467e29df6 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -25,10 +25,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { shallowRef } from 'vue'; +import { useTemplateRef } from 'vue'; +import type { PostFormProps } from '@/types/post-form.js'; import MkModal from '@/components/MkModal.vue'; import MkPostForm from '@/components/MkPostForm.vue'; -import type { PostFormProps } from '@/types/post-form.js'; const props = withDefaults(defineProps<PostFormProps & { instant?: boolean; @@ -42,8 +42,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const modal = shallowRef<InstanceType<typeof MkModal>>(); -const form = shallowRef<InstanceType<typeof MkPostForm>>(); +const modal = useTemplateRef('modal'); function onPosted() { modal.value?.close({ diff --git a/packages/frontend/src/components/MkPreferenceContainer.vue b/packages/frontend/src/components/MkPreferenceContainer.vue new file mode 100644 index 0000000000..70b111513c --- /dev/null +++ b/packages/frontend/src/components/MkPreferenceContainer.vue @@ -0,0 +1,103 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root" @contextmenu.prevent.stop="showMenu($event, true)"> + <div :class="$style.body"> + <slot></slot> + </div> + <div :class="$style.menu"> + <i v-if="isSyncEnabled" class="ti ti-cloud-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i> + <i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i> + <div :class="$style.buttons"> + <button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu($event)"><i class="ti ti-dots"></i></button> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import type { PREF_DEF } from '@/preferences/def.js'; +import * as os from '@/os.js'; +import { prefer } from '@/preferences.js'; + +const props = withDefaults(defineProps<{ + k: keyof typeof PREF_DEF; +}>(), { +}); + +const isAccountOverrided = ref(prefer.isAccountOverrided(props.k)); +const isSyncEnabled = ref(prefer.isSyncEnabled(props.k)); + +function showMenu(ev: MouseEvent, contextmenu?: boolean) { + const i = window.setInterval(() => { + isAccountOverrided.value = prefer.isAccountOverrided(props.k); + isSyncEnabled.value = prefer.isSyncEnabled(props.k); + }, 100); + if (contextmenu) { + os.contextMenu(prefer.getPerPrefMenu(props.k), ev).then(() => { + window.clearInterval(i); + }); + } else { + os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, { + onClosing: () => { + window.clearInterval(i); + }, + }); + } +} +</script> + +<style lang="scss" module> +.root { + position: relative; + display: flex; + + &:hover { + &::before { + content: ''; + position: absolute; + top: -8px; + left: -8px; + width: calc(100% + 16px); + height: calc(100% + 16px); + border-radius: 8px; + background: light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.02)); + pointer-events: none; + } + + .menu { + .buttons { + opacity: 0.7; + } + } + } + + .body { + flex: 1; + } + + .menu { + display: flex; + gap: 8px; + align-items: center; + margin-left: 12px; + font-size: 12px; + padding-left: 8px; + border-left: solid 1px var(--MI_THEME-divider); + + &:hover { + .buttons { + opacity: 1; + } + } + + .buttons { + opacity: 0.3; + } + } +} +</style> diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue index 6efd99d14b..d8dfbd1655 100644 --- a/packages/frontend/src/components/MkPreview.vue +++ b/packages/frontend/src/components/MkPreview.vue @@ -43,7 +43,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkRadio from '@/components/MkRadio.vue'; import * as os from '@/os.js'; import * as config from '@@/js/config.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; const text = ref(''); const flag = ref(true); diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index 4fb4c6fe56..22ae563d13 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -16,17 +16,16 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </div> - <div :class="{ [$style.slotClip]: isPullStart }"> - <slot/> - </div> + + <slot/> </div> </template> <script lang="ts" setup> -import { onMounted, onUnmounted, ref, shallowRef } from 'vue'; -import { i18n } from '@/i18n.js'; +import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue'; import { getScrollContainer } from '@@/js/scroll.js'; -import { isHorizontalSwipeSwiping } from '@/scripts/touch.js'; +import { i18n } from '@/i18n.js'; +import { isHorizontalSwipeSwiping } from '@/utility/touch.js'; const SCROLL_STOP = 10; const MAX_PULL_DISTANCE = Infinity; @@ -43,7 +42,7 @@ const pullDistance = ref(0); let supportPointerDesktop = false; let startScreenY: number | null = null; -const rootEl = shallowRef<HTMLDivElement>(); +const rootEl = useTemplateRef('rootEl'); let scrollEl: HTMLElement | null = null; let disabled = false; @@ -82,11 +81,11 @@ function moveBySystem(to: number): Promise<void> { return; } const startTime = Date.now(); - let intervalId = setInterval(() => { + let intervalId = window.setInterval(() => { const time = Date.now() - startTime; if (time > RELEASE_TRANSITION_DURATION) { pullDistance.value = to; - clearInterval(intervalId); + window.clearInterval(intervalId); r(); return; } @@ -261,8 +260,4 @@ defineExpose({ margin: 5px 0; } } - -.slotClip { - overflow-y: clip; -} </style> diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue index 5e42df4795..9c37eb5e72 100644 --- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue +++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue @@ -42,12 +42,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { ref } from 'vue'; -import { $i, getAccounts } from '@/account.js'; +import { $i } from '@/i.js'; import MkButton from '@/components/MkButton.vue'; import { instance } from '@/instance.js'; import { apiWithDialog, promiseDialog } from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { getAccounts } from '@/accounts.js'; defineProps<{ primary?: boolean; diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index 559399d1d4..884890bf70 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -78,7 +78,7 @@ export default defineComponent({ > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 264b559222..4b2e6910db 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -33,8 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; -import { isTouchUsing } from '@/scripts/touch.js'; +import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'; +import { isTouchUsing } from '@/utility/touch.js'; import * as os from '@/os.js'; const props = withDefaults(defineProps<{ @@ -58,8 +58,8 @@ const emit = defineEmits<{ (ev: 'dragEnded', value: number): void; }>(); -const containerEl = shallowRef<HTMLElement>(); -const thumbEl = shallowRef<HTMLElement>(); +const containerEl = useTemplateRef('containerEl'); +const thumbEl = useTemplateRef('thumbEl'); const rawValue = ref((props.modelValue - props.min) / (props.max - props.min)); const steppedRawValue = computed(() => { @@ -151,20 +151,21 @@ function onMousedown(ev: MouseEvent | TouchEvent) { closed: () => dispose(), }); - const style = document.createElement('style'); - style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); - document.head.appendChild(style); + const style = window.document.createElement('style'); + style.appendChild(window.document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); + window.document.head.appendChild(style); const thumbWidth = getThumbWidth(); const onDrag = (ev: MouseEvent | TouchEvent) => { ev.preventDefault(); + let beforeValue = finalValue.value; const containerRect = containerEl.value!.getBoundingClientRect(); const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0; const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2)); rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth))); - if (props.continuousUpdate) { + if (props.continuousUpdate && beforeValue !== finalValue.value) { emit('update:modelValue', finalValue.value); } }; @@ -172,7 +173,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { let beforeValue = finalValue.value; const onMouseup = () => { - document.head.removeChild(style); + window.document.head.removeChild(style); tooltipForDragShowing.value = false; window.removeEventListener('mousemove', onDrag); window.removeEventListener('touchmove', onDrag); @@ -212,7 +213,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; @@ -286,7 +287,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { border-radius: 999px; &:hover { - background: var(--MI_THEME-accentLighten); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); } } } diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue index c0cbd8a65d..453253f0fc 100644 --- a/packages/frontend/src/components/MkReactionIcon.vue +++ b/packages/frontend/src/components/MkReactionIcon.vue @@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, shallowRef } from 'vue'; -import { useTooltip } from '@/scripts/use-tooltip.js'; +import { defineAsyncComponent, useTemplateRef } from 'vue'; +import { useTooltip } from '@/use/use-tooltip.js'; import * as os from '@/os.js'; const props = defineProps<{ @@ -20,7 +20,7 @@ const props = defineProps<{ withTooltip?: boolean; }>(); -const elRef = shallowRef(); +const elRef = useTemplateRef('elRef'); if (props.withTooltip) { useTooltip(elRef, (showing) => { diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 41e475eade..9d941a949a 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -8,33 +8,34 @@ SPDX-License-Identifier: AGPL-3.0-only ref="buttonEl" v-ripple="canToggle" class="_button" - :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" + :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]" @click="toggleReaction()" @contextmenu.prevent.stop="menu" > - <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> + <MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> <span :class="$style.count">{{ count }}</span> </button> </template> <script lang="ts" setup> -import { computed, inject, onMounted, shallowRef, watch } from 'vue'; +import { computed, inject, onMounted, useTemplateRef, 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'; import * as os from '@/os.js'; -import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useTooltip } from '@/scripts/use-tooltip.js'; -import { $i } from '@/account.js'; +import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; +import { useTooltip } from '@/use/use-tooltip.js'; +import { $i } from '@/i.js'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; -import { claimAchievement } from '@/scripts/achievements.js'; -import { defaultStore } from '@/store.js'; +import { claimAchievement } from '@/utility/achievements.js'; import { i18n } from '@/i18n.js'; -import * as sound from '@/scripts/sound.js'; -import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; +import * as sound from '@/utility/sound.js'; +import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; import { customEmojisMap } from '@/custom-emojis.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const props = defineProps<{ reaction: string; @@ -43,13 +44,13 @@ const props = defineProps<{ note: Misskey.entities.Note; }>(); -const mock = inject<boolean>('mock', false); +const mock = inject(DI.mock, false); const emit = defineEmits<{ (ev: 'reactionToggled', emoji: string, newCount: number): void; }>(); -const buttonEl = shallowRef<HTMLElement>(); +const buttonEl = useTemplateRef('buttonEl'); const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, '')); const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction)); @@ -90,7 +91,7 @@ async function toggleReaction() { } }); } else { - if (defaultStore.state.confirmOnReact) { + if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', text: i18n.tsx.reactAreYouSure({ emoji: props.reaction.replace('@.', '') }), @@ -135,7 +136,7 @@ async function menu(ev) { } function anime() { - if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return; + if (window.document.hidden || !prefer.s.animation || buttonEl.value == null) return; const rect = buttonEl.value.getBoundingClientRect(); const x = rect.left + 16; diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 63b202f9f3..6e23709be4 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -4,24 +4,27 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<TransitionGroup - :enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''" - :moveClass="defaultStore.state.animation ? $style.transition_x_move : ''" +<component + :is="prefer.s.animation ? TransitionGroup : 'div'" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + :moveClass="$style.transition_x_move" tag="div" :class="$style.root" > <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/> <slot v-if="hasMoreReactions" name="more"/> -</TransitionGroup> +</component> </template> <script lang="ts" setup> import * as Misskey from 'misskey-js'; import { inject, watch, ref } from 'vue'; +import { TransitionGroup } from 'vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -30,7 +33,7 @@ const props = withDefaults(defineProps<{ maxNumber: Infinity, }); -const mock = inject<boolean>('mock', false); +const mock = inject(DI.mock, false); const emit = defineEmits<{ (ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; diff --git a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue index 873b276b3d..dc9bacf481 100644 --- a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue +++ b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue @@ -125,7 +125,7 @@ async function done() { left: 0; padding: 12px; border-top: solid 0.5px var(--MI_THEME-divider); - background: var(--MI_THEME-acrylicBg); + background: color(from var(--MI_THEME-bg) srgb r g b / 0.5); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); } diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index 64b573c4d3..1ab2397337 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -13,18 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, nextTick, shallowRef, ref } from 'vue'; +import { onMounted, nextTick, useTemplateRef, ref } from 'vue'; import { Chart } from 'chart.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { alpha } from '@/scripts/color.js'; -import { initChart } from '@/scripts/init-chart.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { store } from '@/store.js'; +import { useChartTooltip } from '@/use/use-chart-tooltip.js'; +import { alpha } from '@/utility/color.js'; +import { initChart } from '@/utility/init-chart.js'; initChart(); -const rootEl = shallowRef<HTMLDivElement | null>(null); -const chartEl = shallowRef<HTMLCanvasElement | null>(null); +const rootEl = useTemplateRef('rootEl'); +const chartEl = useTemplateRef('chartEl'); let chartInstance: Chart | null = null; const fetching = ref(true); @@ -75,7 +75,7 @@ async function renderChart() { await nextTick(); - const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; + const color = store.s.darkMode ? '#b4e900' : '#86b300'; const getYYYYMMDD = (date: Date) => { const y = date.getFullYear().toString().padStart(2, '0'); diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue index d41793b0fa..ba66ffecc0 100644 --- a/packages/frontend/src/components/MkRetentionLineChart.vue +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -8,19 +8,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, shallowRef } from 'vue'; +import { onMounted, useTemplateRef } from 'vue'; import { Chart } from 'chart.js'; import tinycolor from 'tinycolor2'; -import { defaultStore } from '@/store.js'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { chartVLine } from '@/scripts/chart-vline.js'; -import { alpha } from '@/scripts/color.js'; -import { initChart } from '@/scripts/init-chart.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { store } from '@/store.js'; +import { useChartTooltip } from '@/use/use-chart-tooltip.js'; +import { chartVLine } from '@/utility/chart-vline.js'; +import { alpha } from '@/utility/color.js'; +import { initChart } from '@/utility/init-chart.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; initChart(); -const chartEl = shallowRef<HTMLCanvasElement | null>(null); +const chartEl = useTemplateRef('chartEl'); const { handler: externalTooltipHandler } = useChartTooltip(); @@ -42,9 +42,9 @@ const getDate = (ymd: string) => { onMounted(async () => { let raw = await misskeyApi('retention', { }); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent')); + const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent')); const color = accent.toHex(); if (chartEl.value == null) return; diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue index 32f35ed5ad..fd56e4902c 100644 --- a/packages/frontend/src/components/MkRoleSelectDialog.vue +++ b/packages/frontend/src/components/MkRoleSelectDialog.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :width="400" :height="500" @close="onCloseModalWindow" - @closed="console.log('MkRoleSelectDialog: closed') ; $emit('dispose')" + @closed="emit('closed')" > <template #header>{{ title }}</template> <MkSpacer :marginMin="20" :marginMax="28"> @@ -49,7 +49,7 @@ import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkRolePreview from '@/components/MkRolePreview.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import * as os from '@/os.js'; import MkSpacer from '@/components/global/MkSpacer.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; @@ -58,7 +58,7 @@ import MkLoading from '@/components/global/MkLoading.vue'; const emit = defineEmits<{ (ev: 'done', value: Misskey.entities.Role[]), (ev: 'close'), - (ev: 'dispose'), + (ev: 'closed'), }>(); const props = withDefaults(defineProps<{ @@ -144,7 +144,7 @@ fetchRoles(); } .roleItemArea { - background-color: var(--MI_THEME-acrylicBg); + background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.5); border-radius: var(--MI-radius); padding: 12px; overflow-y: auto; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 15717802ec..58a4edfddf 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -268,7 +268,7 @@ function show() { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue index e98ac9cfd2..aacd1eae2a 100644 --- a/packages/frontend/src/components/MkSignin.input.vue +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -58,7 +58,7 @@ import { toUnicode } from 'punycode.js'; import { query, extractDomain } from '@@/js/url.js'; import { host as configHost } from '@@/js/config.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 2fb97e8e46..b0fbe3c490 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -67,20 +67,19 @@ SPDX-License-Identifier: AGPL-3.0-only import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; - import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; -import { login } from '@/account.js'; +import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; +import type { PwResponse } from '@/components/MkSignin.password.vue'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import XInput from '@/components/MkSignin.input.vue'; import XPassword from '@/components/MkSignin.password.vue'; -import type { PwResponse } from '@/components/MkSignin.password.vue'; import XTotp from '@/components/MkSignin.totp.vue'; import XPasskey from '@/components/MkSignin.passkey.vue'; +import { login } from '@/accounts.js'; const emit = defineEmits<{ (ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 676a336ec7..60c99880cd 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { shallowRef } from 'vue'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import { useTemplateRef } from 'vue'; +import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import MkSignin from '@/components/MkSignin.vue'; import MkModal from '@/components/MkModal.vue'; import { i18n } from '@/i18n.js'; @@ -46,7 +46,7 @@ const emit = defineEmits<{ (ev: 'cancelled'): void; }>(); -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const modal = useTemplateRef('modal'); function onClose() { emit('cancelled'); @@ -84,7 +84,7 @@ function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) align-items: center; font-weight: bold; backdrop-filter: var(--MI-blur, blur(15px)); - background: var(--MI_THEME-acrylicBg); + background: color(from var(--MI_THEME-bg) srgb r g b / 0.5); z-index: 1; } diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 4227e1317a..65b879235f 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -85,13 +85,13 @@ import * as Misskey from 'misskey-js'; import * as config from '@@/js/config.js'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; -import MkCaptcha from '@/components/MkCaptcha.vue'; import type { Captcha } from '@/components/MkCaptcha.vue'; +import MkCaptcha from '@/components/MkCaptcha.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { login } from '@/account.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { login } from '@/accounts.js'; const props = withDefaults(defineProps<{ autoSet?: boolean; @@ -267,7 +267,7 @@ async function onSubmit(): Promise<void> { 'testcaptcha-response': testcaptchaResponse.value, }; - const res = await fetch(`${config.apiUrl}/signup`, { + const res = await window.fetch(`${config.apiUrl}/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 6fb9d77837..5c9047dd43 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { shallowRef, ref } from 'vue'; +import { useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XSignup from '@/components/MkSignupDialog.form.vue'; import XServerRules from '@/components/MkSignupDialog.rules.vue'; @@ -52,7 +52,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); const isAcceptedServerRule = ref(false); diff --git a/packages/frontend/src/components/MkSparkle.vue b/packages/frontend/src/components/MkSparkle.vue index b3fc67c0df..2400c5ec7f 100644 --- a/packages/frontend/src/components/MkSparkle.vue +++ b/packages/frontend/src/components/MkSparkle.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, ref, shallowRef } from 'vue'; +import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue'; const particles = ref<{ id: string, @@ -66,7 +66,7 @@ const particles = ref<{ dur: number, color: string }[]>([]); -const el = shallowRef<HTMLElement>(); +const el = useTemplateRef('el'); const width = ref(0); const height = ref(0); const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202']; diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index d8dec3aa2f..3f8d92a61d 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -6,16 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="rootEl" class="rrevdjwu" :class="{ grid }"> <MkInput - v-model="search" + v-if="searchIndex && searchIndex.length > 0" + v-model="searchQuery" :placeholder="i18n.ts.search" type="search" style="margin-bottom: 16px;" + @input.passive="searchOnInput" @keydown="searchOnKeyDown" > <template #prefix><i class="ti ti-search"></i></template> </MkInput> - <template v-if="search == ''"> + <template v-if="rawSearchQuery == ''"> <div v-for="group in def" class="group"> <div v-if="group.title" class="title">{{ group.title }}</div> @@ -91,23 +93,28 @@ export type SuperMenuDef = { </script> <script lang="ts" setup> -import { useTemplateRef, ref, watch, nextTick } from 'vue'; -import type { SearchIndexItem } from '@/scripts/autogen/settings-search-index.js'; +import { useTemplateRef, ref, watch, nextTick, computed } from 'vue'; +import { getScrollContainer } from '@@/js/scroll.js'; +import type { SearchIndexItem } from '@/utility/settings-search-index.js'; import MkInput from '@/components/MkInput.vue'; import { i18n } from '@/i18n.js'; -import { getScrollContainer } from '@@/js/scroll.js'; -import { useRouter } from '@/router/supplier.js'; +import { useRouter } from '@/router.js'; +import { initIntlString, compareStringIncludes } from '@/utility/intl-string.js'; const props = defineProps<{ def: SuperMenuDef[]; grid?: boolean; - searchIndex: SearchIndexItem[]; + searchIndex?: SearchIndexItem[]; }>(); +initIntlString(); + const router = useRouter(); const rootEl = useTemplateRef('rootEl'); -const search = ref(''); +const searchQuery = ref(''); +const rawSearchQuery = ref(''); + const searchSelectedIndex = ref<null | number>(null); const searchResult = ref<{ id: string; @@ -117,8 +124,13 @@ const searchResult = ref<{ isRoot: boolean; parentLabels: string[]; }[]>([]); +const searchIndexItemByIdComputed = computed(() => props.searchIndex && new Map<string, SearchIndexItem>(props.searchIndex.map(i => [i.id, i]))); -watch(search, (value) => { +watch(searchQuery, (value) => { + rawSearchQuery.value = value; +}); + +watch(rawSearchQuery, (value) => { searchResult.value = []; searchSelectedIndex.value = null; @@ -126,32 +138,49 @@ watch(search, (value) => { return; } - const dive = (items: SearchIndexItem[], parents: SearchIndexItem[] = []) => { - for (const item of items) { - const matched = - item.label.includes(value.toLowerCase()) || - item.keywords.some((x) => x.toLowerCase().includes(value.toLowerCase())); + const searchIndexItemById = searchIndexItemByIdComputed.value; + if (searchIndexItemById != null) { + const addSearchResult = (item: SearchIndexItem) => { + let path: string | undefined = item.path; + let icon: string | undefined = item.icon; + const parentLabels: string[] = []; - if (matched) { - searchResult.value.push({ - id: item.id, - path: item.path ?? parents.find((x) => x.path != null)?.path, - label: item.label, - parentLabels: parents.map((x) => x.label).toReversed(), - icon: item.icon ?? parents.find((x) => x.icon != null)?.icon, - isRoot: parents.length === 0, - }); + for (let current = searchIndexItemById.get(item.parentId ?? ''); + current != null; + current = searchIndexItemById.get(current.parentId ?? '')) { + path ??= current.path; + icon ??= current.icon; + parentLabels.push(current.label); } - if (item.children) { - dive(item.children, [item, ...parents]); + if (_DEV_ && path == null) throw new Error('path is null for ' + item.id); + + searchResult.value.push({ + id: item.id, + path: path ?? '/', // never gets `/` + label: item.label, + parentLabels: parentLabels.toReversed(), + icon, + isRoot: item.parentId == null, + }); + }; + + for (const item of searchIndexItemById.values()) { + if ( + compareStringIncludes(item.label, value) || + item.keywords.some((x) => compareStringIncludes(x, value)) + ) { + addSearchResult(item); } } - }; - - dive(props.searchIndex); + } }); +function searchOnInput(ev: InputEvent) { + searchSelectedIndex.value = null; + rawSearchQuery.value = (ev.target as HTMLInputElement).value; +} + function searchOnKeyDown(ev: KeyboardEvent) { if (ev.isComposing) return; diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index 797e577fa4..92359b773a 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -100,7 +100,7 @@ const toggle = () => { .caption { margin: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); font-size: 0.85em; &:empty { diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue index 7e92726dcb..86e755a3c3 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.vue +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -92,18 +92,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { computed, onMounted, ref, shallowRef, toRefs } from 'vue'; +import { computed, onMounted, ref, useTemplateRef, toRefs } from 'vue'; import * as Misskey from 'misskey-js'; -import MkInput from '@/components/MkInput.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; import type { MkSystemWebhookEditorProps, MkSystemWebhookResult, SystemWebhookEventType, } from '@/components/MkSystemWebhookEditor.impl.js'; +import MkInput from '@/components/MkInput.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; @@ -122,7 +122,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialogEl = useTemplateRef('dialogEl'); const props = defineProps<MkSystemWebhookEditorProps>(); @@ -280,7 +280,7 @@ onMounted(async () => { left: 0; padding: 12px; border-top: solid 0.5px var(--MI_THEME-divider); - background: var(--MI_THEME-acrylicBg); + background: color(from var(--MI_THEME-bg) srgb r g b / 0.5); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); } @@ -307,6 +307,6 @@ onMounted(async () => { .description { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); } </style> diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue index 87aa046963..9d541c8acb 100644 --- a/packages/frontend/src/components/MkTagCloud.vue +++ b/packages/frontend/src/components/MkTagCloud.vue @@ -15,18 +15,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, watch, onBeforeUnmount, ref, shallowRef } from 'vue'; +import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue'; import tinycolor from 'tinycolor2'; const loaded = !!window.TagCanvas; const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz'; -const computedStyle = getComputedStyle(document.documentElement); +const computedStyle = getComputedStyle(window.document.documentElement); const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); const available = ref(false); -const rootEl = shallowRef<HTMLElement | null>(null); -const canvasEl = shallowRef<HTMLCanvasElement | null>(null); -const tagsEl = shallowRef<HTMLElement | null>(null); +const rootEl = useTemplateRef('rootEl'); +const canvasEl = useTemplateRef('canvasEl'); +const tagsEl = useTemplateRef('tagsEl'); const width = ref(300); watch(available, () => { @@ -57,7 +57,7 @@ onMounted(() => { if (loaded) { available.value = true; } else { - document.head.appendChild(Object.assign(document.createElement('script'), { + window.document.head.appendChild(Object.assign(window.document.createElement('script'), { async: true, src: '/client-assets/tagcanvas.min.js', })).addEventListener('load', () => available.value = true); diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue index 3e8588018c..407ac33add 100644 --- a/packages/frontend/src/components/MkTextarea.vue +++ b/packages/frontend/src/components/MkTextarea.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div> +<div class="_selectable"> <div :class="$style.label" @click="focus"><slot name="label"></slot></div> <div :class="{ [$style.disabled]: disabled, [$style.focused]: focused, [$style.tall]: tall, [$style.pre]: pre }" style="position: relative;"> <textarea @@ -36,12 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue'; +import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, useTemplateRef } from 'vue'; import { debounce } from 'throttle-debounce'; +import type { SuggestionType } from '@/utility/autocomplete.js'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { Autocomplete } from '@/scripts/autocomplete.js'; -import type { SuggestionType } from '@/scripts/autocomplete.js'; +import { Autocomplete } from '@/utility/autocomplete.js'; const props = defineProps<{ modelValue: string | null; @@ -75,7 +75,7 @@ const focused = ref(false); const changed = ref(false); const invalid = ref(false); const filled = computed(() => v.value !== '' && v.value != null); -const inputEl = shallowRef<HTMLTextAreaElement>(); +const inputEl = useTemplateRef('inputEl'); const preview = ref(false); let autocompleteWorker: Autocomplete | null = null; @@ -160,7 +160,7 @@ onUnmounted(() => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkThemePreview.vue b/packages/frontend/src/components/MkThemePreview.vue new file mode 100644 index 0000000000..013ab9d6a4 --- /dev/null +++ b/packages/frontend/src/components/MkThemePreview.vue @@ -0,0 +1,112 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<svg viewBox="0 0 200 150"> + <g fill-rule="evenodd"> + <rect width="200" height="150" :fill="themeVariables.bg"/> + <rect width="64" height="150" :fill="themeVariables.navBg"/> + <rect x="64" width="136" height="41" :fill="themeVariables.bg"/> + <path transform="scale(.26458)" d="m439.77 247.19c-43.673 0-78.832 35.157-78.832 78.83v249.98h407.06v-328.81z" :fill="themeVariables.panel"/> + </g> + <circle cx="32" cy="83" r="21" :fill="themeVariables.accentedBg"/> + <g> + <rect x="120" y="88" width="40" height="6" ry="3" :fill="themeVariables.fg"/> + <rect x="170" y="88" width="20" height="6" ry="3" :fill="themeVariables.mention"/> + <rect x="120" y="108" width="20" height="6" ry="3" :fill="themeVariables.hashtag"/> + <rect x="150" y="108" width="40" height="6" ry="3" :fill="themeVariables.fg"/> + <rect x="120" y="128" width="40" height="6" ry="3" :fill="themeVariables.fg"/> + <rect x="170" y="128" width="20" height="6" ry="3" :fill="themeVariables.link"/> + </g> + <path d="m65.498 40.892h137.7" :stroke="themeVariables.divider" stroke-width="0.75"/> + <g transform="matrix(.60823 0 0 .60823 25.45 75.755)" fill="none" :stroke="themeVariables.accent" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> + <path d="m0 0h24v24h-24z" fill="none" stroke="none"/> + <path d="m5 12h-2l9-9 9 9h-2"/> + <path d="m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7"/> + <path d="m9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6"/> + </g> + <g transform="matrix(.61621 0 0 .61621 25.354 117.92)" fill="none" :stroke="themeVariables.fg" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> + <path d="m0 0h24v24h-24z" fill="none" stroke="none"/> + <path d="m10 5a2 2 0 1 1 4 0 7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6"/> + <path d="m9 17v1a3 3 0 0 0 6 0v-1"/> + </g> + <circle cx="32" cy="32" r="16" :fill="themeVariables.accent"/> + <circle cx="140" cy="20" r="6" :fill="themeVariables.success"/> + <circle cx="160" cy="20" r="6" :fill="themeVariables.warn"/> + <circle cx="180" cy="20" r="6" :fill="themeVariables.error"/> +</svg> +</template> + +<script setup lang="ts"> +import { ref, watch } from 'vue'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; +import type { Theme } from '@/theme.js'; +import { compile } from '@/theme.js'; +import { deepClone } from '@/utility/clone.js'; + +const props = defineProps<{ + theme: Theme; +}>(); + +const themeVariables = ref<{ + bg: string; + panel: string; + fg: string; + mention: string; + hashtag: string; + link: string; + divider: string; + accent: string; + accentedBg: string; + navBg: string; + success: string; + warn: string; + error: string; +}>({ + bg: 'var(--MI_THEME-bg)', + panel: 'var(--MI_THEME-panel)', + fg: 'var(--MI_THEME-fg)', + mention: 'var(--MI_THEME-mention)', + hashtag: 'var(--MI_THEME-hashtag)', + link: 'var(--MI_THEME-link)', + divider: 'var(--MI_THEME-divider)', + accent: 'var(--MI_THEME-accent)', + accentedBg: 'var(--MI_THEME-accentedBg)', + navBg: 'var(--MI_THEME-navBg)', + success: 'var(--MI_THEME-success)', + warn: 'var(--MI_THEME-warn)', + error: 'var(--MI_THEME-error)', +}); + +watch(() => props.theme, (theme) => { + if (theme == null) return; + + const _theme = deepClone(theme); + + if (_theme.base != null) { + const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); + if (base) _theme.props = Object.assign({}, base.props, _theme.props); + } + + const compiled = compile(_theme); + + themeVariables.value = { + bg: compiled.bg ?? 'var(--MI_THEME-bg)', + panel: compiled.panel ?? 'var(--MI_THEME-panel)', + fg: compiled.fg ?? 'var(--MI_THEME-fg)', + mention: compiled.mention ?? 'var(--MI_THEME-mention)', + hashtag: compiled.hashtag ?? 'var(--MI_THEME-hashtag)', + link: compiled.link ?? 'var(--MI_THEME-link)', + divider: compiled.divider ?? 'var(--MI_THEME-divider)', + accent: compiled.accent ?? 'var(--MI_THEME-accent)', + accentedBg: compiled.accentedBg ?? 'var(--MI_THEME-accentedBg)', + navBg: compiled.navBg ?? 'var(--MI_THEME-navBg)', + success: compiled.success ?? 'var(--MI_THEME-success)', + warn: compiled.warn ?? 'var(--MI_THEME-warn)', + error: compiled.error ?? 'var(--MI_THEME-error)', + }; +}, { immediate: true }); +</script> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 7bae240ddd..73057e4644 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-if="paginationQuery" ref="tlComponent" :pagination="paginationQuery" - :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" + :noGap="!prefer.s.showGapBetweenNotesInTimeline" @queue="emit('queue', $event)" @status="prComponent?.setDisabled($event)" /> @@ -17,17 +17,17 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; +import { computed, watch, onUnmounted, provide, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import type { BasicTimelineType } from '@/timelines.js'; +import type { Paging } from '@/components/MkPagination.vue'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; -import * as sound from '@/scripts/sound.js'; -import { $i } from '@/account.js'; +import * as sound from '@/utility/sound.js'; +import { $i } from '@/i.js'; import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; -import type { Paging } from '@/components/MkPagination.vue'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; @@ -67,8 +67,8 @@ type TimelineQueryType = { roleId?: string }; -const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>(); -const tlComponent = shallowRef<InstanceType<typeof MkNotes>>(); +const prComponent = useTemplateRef('prComponent'); +const tlComponent = useTemplateRef('tlComponent'); let tlNotesCount = 0; @@ -239,7 +239,7 @@ function updatePaginationQuery() { } function refreshEndpointAndChannel() { - if (!defaultStore.state.disableStreamingTimeline) { + if (!prefer.s.disableStreamingTimeline) { disconnectChannel(); connectChannel(); } diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue index e256640649..ac795e312c 100644 --- a/packages/frontend/src/components/MkToast.vue +++ b/packages/frontend/src/components/MkToast.vue @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_toast_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_toast_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_toast_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_toast_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_toast_leaveTo : ''" appear @afterLeave="emit('closed')" > <div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }"> @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; defineProps<{ message: string; diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index 73aef68964..b449155edb 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { shallowRef, ref } from 'vue'; +import { useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkInput from './MkInput.vue'; import MkSwitch from './MkSwitch.vue'; @@ -55,7 +55,7 @@ import MkButton from './MkButton.vue'; import MkInfo from './MkInfo.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; -import { iAmAdmin } from '@/account.js'; +import { iAmAdmin } from '@/i.js'; const props = withDefaults(defineProps<{ title?: string | null; @@ -77,10 +77,10 @@ const emit = defineEmits<{ const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin')); const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admin') || p.startsWith('write:admin')); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); const name = ref(props.initialName); -const permissionSwitches = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{}); -const permissionSwitchesForAdmin = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{}); +const permissionSwitches = ref({} as Record<(typeof Misskey.permissions)[number], boolean>); +const permissionSwitchesForAdmin = ref({} as Record<(typeof Misskey.permissions)[number], boolean>); if (props.initialPermissions) { for (const kind of props.initialPermissions) { diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index 10365d29b1..3fe80f4ab4 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -5,11 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''" - appear @afterLeave="emit('closed')" + :enterActiveClass="prefer.s.animation ? $style.transition_tooltip_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''" + appear :css="prefer.s.animation" + @afterLeave="emit('closed')" > <div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> <slot> @@ -23,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue'; +import { nextTick, onMounted, onUnmounted, useTemplateRef } from 'vue'; import * as os from '@/os.js'; -import { calcPopupPosition } from '@/scripts/popup-position.js'; -import { defaultStore } from '@/store.js'; +import { calcPopupPosition } from '@/utility/popup-position.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ showing: boolean; @@ -51,7 +52,7 @@ const emit = defineEmits<{ // タイミングによっては最初から showing = false な場合があり、その場合に closed 扱いにしないと永久にDOMに残ることになる if (!props.showing) emit('closed'); -const el = shallowRef<HTMLElement>(); +const el = useTemplateRef('el'); const zIndex = os.claimZIndex('high'); function setPosition() { diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index b26a01737e..59e1b096ae 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js'; import { ref, reactive } from 'vue'; import { i18n } from '@/i18n.js'; import { globalEvents } from '@/events.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; import MkNote from '@/components/MkNote.vue'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue index f7b60fbc45..8ae6c1ceaa 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -31,7 +31,7 @@ import MkPostForm from '@/components/MkPostForm.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkNote from '@/components/MkNote.vue'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; const emit = defineEmits<{ (ev: 'succeeded'): void; diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 11d7c8dc4d..92f71b01af 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -148,7 +148,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, watch } from 'vue'; +import { ref, useTemplateRef, watch } from 'vue'; +import { host } from '@@/js/config.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import XNote from '@/components/MkTutorialDialog.Note.vue'; @@ -158,8 +159,7 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { host } from '@@/js/config.js'; -import { claimAchievement } from '@/scripts/achievements.js'; +import { claimAchievement } from '@/utility/achievements.js'; import * as os from '@/os.js'; const props = defineProps<{ @@ -170,7 +170,7 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialog = useTemplateRef('dialog'); // eslint-disable-next-line vue/no-setup-props-reactivity-loss const page = ref(props.initialPage ?? 0); @@ -249,6 +249,7 @@ async function close(skip: boolean) { .pageFooter { position: sticky; + z-index: 1; bottom: 0; left: 0; flex-shrink: 0; diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index c937b4ce59..79ab464cb0 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -15,15 +15,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, shallowRef } from 'vue'; +import { onMounted, useTemplateRef } from 'vue'; +import { version } from '@@/js/config.js'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkSparkle from '@/components/MkSparkle.vue'; -import { version } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; -import { confetti } from '@/scripts/confetti.js'; +import { confetti } from '@/utility/confetti.js'; -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const modal = useTemplateRef('modal'); function whatIsNew() { modal.value?.close(); diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 063e5dcad2..20dab6f028 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin" scrolling="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }" - :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`" + :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${store.s.darkMode ? 'dark' : 'light'}&id=${tweetId}`" ></iframe> </div> <div :class="$style.action"> @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div v-else> <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> - <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`"> + <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`"> </div> <article :class="$style.body"> <header :class="$style.header"> @@ -89,10 +89,11 @@ import { versatileLang } from '@@/js/intl-const.js'; import type { summaly } from '@misskey-dev/summaly'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { deviceKind } from '@/utility/device-kind.js'; import MkButton from '@/components/MkButton.vue'; -import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; -import { defaultStore } from '@/store.js'; +import { transformPlayerUrl } from '@/utility/player-url-transform.js'; +import { store } from '@/store.js'; +import { prefer } from '@/preferences.js'; type SummalyResult = Awaited<ReturnType<typeof summaly>>; @@ -245,6 +246,7 @@ onUnmounted(() => { box-shadow: 0 0 0 1px var(--MI_THEME-divider); border-radius: 8px; overflow: clip; + text-align: left; &:hover { text-decoration: none; diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue index e972973dba..fd36d6a82b 100644 --- a/packages/frontend/src/components/MkUrlPreviewPopup.vue +++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> - <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')"> + <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')"> <MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/> </Transition> </div> @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, ref } from 'vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ showing: boolean; diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 34991fa0dd..34e86444ad 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -56,7 +56,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index 7a2e878931..dde2efd8ee 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { onMounted, ref } from 'vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import { misskeyApiGet } from '@/utility/misskey-api.js'; import { acct } from '@/filters/user.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index 0164515a8a..cff531b2ca 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_panel" :class="$style.root"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div> + <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div> <MkAvatar :class="$style.avatar" :user="user" indicator/> <div :class="$style.title"> <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> @@ -39,10 +39,10 @@ import MkFollowButton from '@/components/MkFollowButton.vue'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; -import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { defaultStore } from '@/store.js'; +import { $i } from '@/i.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js'; +import { getStaticImageUrl } from '@/utility/media-proxy.js'; +import { prefer } from '@/preferences.js'; defineProps<{ user: Misskey.entities.UserDetailed; diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index d1881ec3fc..0d1ffd715f 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPagination :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> + <img :src="infoImageUrl" draggable="false"/> <div>{{ i18n.ts.noUsers }}</div> </div> </template> @@ -21,9 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import type { Paging } from '@/components/MkPagination.vue'; import MkUserInfo from '@/components/MkUserInfo.vue'; import MkPagination from '@/components/MkPagination.vue'; -import type { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 740202f28b..17a882a3a6 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_popup_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_popup_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_popup_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_popup_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_popup_leaveTo : ''" appear @afterLeave="emit('closed')" > <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> <div v-if="user != null"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"> + <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"> <span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> </div> <svg viewBox="0 0 128 128" :class="$style.avatarBack"> @@ -60,14 +60,14 @@ import * as Misskey from 'misskey-js'; import MkFollowButton from '@/components/MkFollowButton.vue'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { getUserMenu } from '@/scripts/get-user-menu.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { getUserMenu } from '@/utility/get-user-menu.js'; import number from '@/filters/number.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; -import { $i } from '@/account.js'; -import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +import { prefer } from '@/preferences.js'; +import { $i } from '@/i.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js'; +import { getStaticImageUrl } from '@/utility/media-proxy.js'; const props = defineProps<{ showing: boolean; @@ -220,7 +220,7 @@ onMounted(() => { .statusItemLabel { font-size: 0.7em; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); } .menu { diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 1e93d9dbea..057af49a36 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -61,16 +61,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, computed, shallowRef } from 'vue'; +import { onMounted, ref, computed, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import { host as currentHost, hostname } from '@@/js/config.js'; import MkInput from '@/components/MkInput.vue'; import FormSplit from '@/components/form/split.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; import { instance } from '@/instance.js'; const emit = defineEmits<{ @@ -94,7 +94,7 @@ const host = ref(''); const users = ref<Misskey.entities.UserLite[]>([]); const recentUsers = ref<Misskey.entities.UserDetailed[]>([]); const selected = ref<Misskey.entities.UserLite | null>(null); -const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); +const dialogEl = useTemplateRef('dialogEl'); function search() { if (username.value === '' && host.value === '') { @@ -128,10 +128,10 @@ async function ok() { dialogEl.value?.close(); // 最近使ったユーザー更新 - let recents = defaultStore.state.recentlyUsedUsers; + let recents = store.s.recentlyUsedUsers; recents = recents.filter(x => x !== selected.value?.id); recents.unshift(selected.value.id); - defaultStore.set('recentlyUsedUsers', recents.splice(0, 16)); + store.set('recentlyUsedUsers', recents.splice(0, 16)); } function cancel() { @@ -141,7 +141,7 @@ function cancel() { onMounted(() => { misskeyApi('users/show', { - userIds: defaultStore.state.recentlyUsedUsers, + userIds: store.s.recentlyUsedUsers, }).then(foundUsers => { let _users = foundUsers; _users = _users.filter((u) => { diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue index 62e5d1da8a..629bc30667 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue @@ -49,7 +49,7 @@ import { i18n } from '@/i18n.js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; const isLocked = ref(false); const hideOnlineStatus = ref(false); diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 7cb48f6afb..30925b854c 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -37,11 +37,11 @@ import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import FormSlot from '@/components/form/slot.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { chooseFileFromPc } from '@/scripts/select-file.js'; +import { chooseFileFromPc } from '@/utility/select-file.js'; import * as os from '@/os.js'; -import { signinRequired } from '@/account.js'; +import { ensureSignin } from '@/i.js'; -const $i = signinRequired(); +const $i = ensureSignin(); const name = ref($i.name ?? ''); const description = ref($i.description ?? ''); diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index 004edab630..4accc6183b 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js'; import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index b7261129ef..767f5c591a 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -128,7 +128,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue'; +import { ref, useTemplateRef, watch, nextTick, defineAsyncComponent } from 'vue'; +import { host } from '@@/js/config.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import MkButton from '@/components/MkButton.vue'; import XProfile from '@/components/MkUserSetupDialog.Profile.vue'; @@ -137,22 +138,20 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { host } from '@@/js/config.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import * as os from '@/os.js'; const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); - -// eslint-disable-next-line vue/no-setup-props-reactivity-loss -const page = ref(defaultStore.state.accountSetupWizard); +const dialog = useTemplateRef('dialog'); + +const page = ref(store.s.accountSetupWizard); watch(page, () => { - defaultStore.set('accountSetupWizard', page.value); + store.set('accountSetupWizard', page.value); }); async function close(skip: boolean) { @@ -165,11 +164,11 @@ async function close(skip: boolean) { } dialog.value?.close(); - defaultStore.set('accountSetupWizard', -1); + store.set('accountSetupWizard', -1); } function setupComplete() { - defaultStore.set('accountSetupWizard', -1); + store.set('accountSetupWizard', -1); dialog.value?.close(); } @@ -194,7 +193,7 @@ async function later(later: boolean) { } dialog.value?.close(); - defaultStore.set('accountSetupWizard', 0); + store.set('accountSetupWizard', 0); } </script> diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index 650e639c4f..cb402b1a57 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -42,12 +42,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, shallowRef, ref } from 'vue'; +import { nextTick, useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkModal from '@/components/MkModal.vue'; import { i18n } from '@/i18n.js'; -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const modal = useTemplateRef('modal'); const props = withDefaults(defineProps<{ currentVisibility: typeof Misskey.noteVisibilities[number]; diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue index d098dad9a1..79c9e739c4 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue @@ -13,19 +13,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, shallowRef, ref, nextTick } from 'vue'; +import { onMounted, useTemplateRef, ref, nextTick } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; import tinycolor from 'tinycolor2'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { chartVLine } from '@/scripts/chart-vline.js'; -import { initChart } from '@/scripts/init-chart.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { store } from '@/store.js'; +import { useChartTooltip } from '@/use/use-chart-tooltip.js'; +import { chartVLine } from '@/utility/chart-vline.js'; +import { initChart } from '@/utility/init-chart.js'; initChart(); -const chartEl = shallowRef<HTMLCanvasElement | null>(null); +const chartEl = useTemplateRef('chartEl'); const now = new Date(); let chartInstance: Chart | null = null; const chartLimit = 30; @@ -59,9 +59,9 @@ async function renderChart() { await nextTick(); - const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - const computedStyle = getComputedStyle(document.documentElement); + const computedStyle = getComputedStyle(window.document.documentElement); const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); const colorRead = accent; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 898ab75fc0..1a4d14a3f0 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -62,7 +62,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instanceName } from '@@/js/config.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import MkNumber from '@/components/MkNumber.vue'; @@ -181,7 +181,7 @@ function showMenu(ev: MouseEvent) { } .statsItemLabel { - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); font-size: 0.9em; } diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue index 34fa6b0723..820cf05e1f 100644 --- a/packages/frontend/src/components/MkWaitingDialog.vue +++ b/packages/frontend/src/components/MkWaitingDialog.vue @@ -14,15 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch, shallowRef } from 'vue'; +import { watch, useTemplateRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; -const modal = shallowRef<InstanceType<typeof MkModal>>(); +const modal = useTemplateRef('modal'); const props = defineProps<{ success: boolean; showing: boolean; - text?: string; + text?: string | null; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 2953f656d4..e5ac791d0b 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_window_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_window_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_window_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_window_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_window_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_window_leaveTo : ''" appear @afterLeave="emit('closed')" > @@ -53,12 +53,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; +import { onBeforeUnmount, onMounted, provide, useTemplateRef, ref } from 'vue'; import type { MenuItem } from '@/types/menu.js'; -import contains from '@/scripts/contains.js'; +import contains from '@/utility/contains.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; type WindowButton = { title: string; @@ -114,7 +114,7 @@ const emit = defineEmits<{ provide('inWindow', true); -const rootEl = shallowRef<HTMLElement | null>(); +const rootEl = useTemplateRef('rootEl'); const showing = ref(true); let beforeClickedAt = 0; const maximized = ref(false); @@ -240,7 +240,7 @@ function onHeaderMousedown(evt: MouseEvent | TouchEvent) { const main = rootEl.value; if (main == null) return; - if (!contains(main, document.activeElement)) main.focus(); + if (!contains(main, window.document.activeElement)) main.focus(); const position = main.getBoundingClientRect(); diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index 1122976436..ab62a5113d 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div class="poamfof"> - <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> + <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> <div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player"> <iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe> </div> @@ -25,10 +25,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import MkWindow from '@/components/MkWindow.vue'; import { versatileLang } from '@@/js/intl-const.js'; -import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; -import { defaultStore } from '@/store.js'; +import MkWindow from '@/components/MkWindow.vue'; +import { transformPlayerUrl } from '@/utility/player-url-transform.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ url: string; diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index 8fa9e4affb..e60155f4af 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -70,7 +70,7 @@ const props = defineProps<{ margin-right: 0.75em; flex-shrink: 0; text-align: center; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue index 5fca3acc31..b23ed51a83 100644 --- a/packages/frontend/src/components/form/section.vue +++ b/packages/frontend/src/components/form/section.vue @@ -49,7 +49,7 @@ defineProps<{ .description { font-size: 0.85em; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); margin: 0 0 8px 0; } </style> diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue index da94b7abbb..cc3af9aca4 100644 --- a/packages/frontend/src/components/form/slot.vue +++ b/packages/frontend/src/components/form/slot.vue @@ -35,7 +35,7 @@ function focus() { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts index 1ccf105dbb..deb2b8a52b 100644 --- a/packages/frontend/src/components/global/MkA.stories.impl.ts +++ b/packages/frontend/src/components/global/MkA.stories.impl.ts @@ -7,7 +7,7 @@ import { expect, userEvent, within } from '@storybook/test'; import type { StoryObj } from '@storybook/vue3'; import MkA from './MkA.vue'; -import { tick } from '@/scripts/test-utils.js'; +import { tick } from '@/utility/test-utils.js'; export const Default = { render(args) { return { diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 8eacf16d6d..4004db5b12 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -14,12 +14,12 @@ export type MkABehavior = 'window' | 'browser' | null; </script> <script lang="ts" setup> -import { computed, inject, shallowRef } from 'vue'; +import { computed, inject, useTemplateRef } from 'vue'; import { url } from '@@/js/config.js'; import * as os from '@/os.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router/supplier.js'; +import { useRouter } from '@/router.js'; const props = withDefaults(defineProps<{ to: string; @@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{ const behavior = props.behavior ?? inject<MkABehavior>('linkNavigationBehavior', null); -const el = shallowRef<HTMLElement>(); +const el = useTemplateRef('el'); defineExpose({ $el: el }); @@ -87,7 +87,7 @@ function openWindow() { function nav(ev: MouseEvent) { if (behavior === 'browser') { - location.href = props.to; + window.location.href = props.to; return; } diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 08a78c8d81..2f55700b47 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -36,7 +36,6 @@ SPDX-License-Identifier: AGPL-3.0-only <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button> </div> </div> -<div v-else></div> </template> <script lang="ts" setup> @@ -45,14 +44,15 @@ import { url as local, host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import MkButton from '@/components/MkButton.vue'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; +import { prefer } from '@/preferences.js'; type Ad = (typeof instance)['ads'][number]; const props = defineProps<{ - prefer: string[]; + preferForms: string[]; specify?: Ad; }>(); @@ -66,12 +66,12 @@ const choseAd = (): Ad | null => { return props.specify; } - const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? { + const allAds = instance.ads.map(ad => store.s.mutedAds.includes(ad.id) ? { ...ad, ratio: 0, } : ad); - let ads = allAds.filter(ad => props.prefer.includes(ad.place)); + let ads = allAds.filter(ad => props.preferForms.includes(ad.place)); if (ads.length === 0) { ads = allAds.filter(ad => ad.place === 'square'); @@ -107,12 +107,12 @@ const chosen = ref(choseAd()); const self = computed(() => chosen.value?.url.startsWith(local)); -const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); +const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); function reduceFrequency(): void { if (chosen.value == null) return; - if (defaultStore.state.mutedAds.includes(chosen.value.id)) return; - defaultStore.push('mutedAds', chosen.value.id); + if (store.s.mutedAds.includes(chosen.value.id)) return; + store.push('mutedAds', chosen.value.id); os.success(); chosen.value = choseAd(); showMenu.value = false; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 35c07bc80c..97c2069a2f 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -34,6 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only translate: getDecorationOffset(decoration), }" alt="" + draggable="false" + style="-webkit-user-drag: none;" > </template> </component> @@ -45,14 +47,13 @@ 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 { getStaticImageUrl } from '@/utility/media-proxy.js'; import { acct, userPage } from '@/filters/user.js'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; -const animation = ref(defaultStore.state.animation); -const squareAvatars = ref(defaultStore.state.squareAvatars); -const useBlurEffect = ref(defaultStore.state.useBlurEffect); +const animation = ref(prefer.s.animation); +const squareAvatars = ref(prefer.s.squareAvatars); const props = withDefaults(defineProps<{ user: Misskey.entities.User; @@ -75,7 +76,7 @@ const emit = defineEmits<{ (ev: 'click', v: MouseEvent): void; }>(); -const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations; +const showDecoration = props.forceShowDecoration || prefer.s.showAvatarDecorations; const bound = computed(() => props.link ? { to: userPage(props.user), target: props.target } @@ -83,7 +84,7 @@ const bound = computed(() => props.link const url = computed(() => { if (props.user.avatarUrl == null) return null; - if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl); + if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl); return props.user.avatarUrl; }); @@ -93,7 +94,7 @@ function onClick(ev: MouseEvent): void { } function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { - if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(decoration.url); + if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(decoration.url); return decoration.url; } diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index ec1d859080..dda45ceaa2 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -9,6 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" src="/client-assets/dummy.png" :title="alt" + draggable="false" + style="-webkit-user-drag: none;" /> <span v-else-if="errored">:{{ customEmojiName }}:</span> <img @@ -18,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only :alt="alt" :title="alt" decoding="async" + draggable="false" @error="errored = true" @load="errored = false" @click="onClick" @@ -27,16 +30,16 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, defineAsyncComponent, inject, ref } from 'vue'; import type { MenuItem } from '@/types/menu.js'; -import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { defaultStore } from '@/store.js'; +import { getProxiedImageUrl, getStaticImageUrl } from '@/utility/media-proxy.js'; import { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; -import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import * as sound from '@/scripts/sound.js'; +import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const props = defineProps<{ name: string; @@ -50,7 +53,7 @@ const props = defineProps<{ fallbackToImage?: boolean; }>(); -const react = inject<((name: string) => void) | null>('react', null); +const react = inject(DI.mfmEmojiReactCallback); const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', '')); const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); @@ -77,7 +80,7 @@ const url = computed(() => { false, true, ); - return defaultStore.reactiveState.disableShowingAnimatedImages.value + return prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(proxied) : proxied; }); @@ -97,7 +100,6 @@ function onClick(ev: MouseEvent) { icon: 'ti ti-copy', action: () => { copyToClipboard(`:${props.name}:`); - os.success(); }, }); @@ -107,7 +109,6 @@ function onClick(ev: MouseEvent) { icon: 'ti ti-plus', action: () => { react(`:${props.name}:`); - sound.playMisskeySfx('reaction'); }, }); } @@ -157,6 +158,7 @@ async function edit(name: string) { .root { height: 2em; vertical-align: middle; + -webkit-user-drag: none; transition: transform 0.2s ease; &:hover { diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index f0acd3bc27..fa55fd888b 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -12,12 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject } from 'vue'; import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js'; import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js'; -import { defaultStore } from '@/store.js'; +import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import * as sound from '@/scripts/sound.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const props = defineProps<{ emoji: string; @@ -25,11 +25,11 @@ const props = defineProps<{ menuReaction?: boolean; }>(); -const react = inject<((name: string) => void) | null>('react', null); +const react = inject(DI.mfmEmojiReactCallback, null); -const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; +const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; -const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native'); +const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native'); const url = computed(() => char2path(props.emoji)); const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji)); @@ -50,7 +50,6 @@ function onClick(ev: MouseEvent) { icon: 'ti ti-copy', action: () => { copyToClipboard(props.emoji); - os.success(); }, }); @@ -60,7 +59,6 @@ function onClick(ev: MouseEvent) { icon: 'ti ti-plus', action: () => { react(props.emoji); - sound.playMisskeySfx('reaction'); }, }); } diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue index c594cc752b..95ed255189 100644 --- a/packages/frontend/src/components/global/MkError.vue +++ b/packages/frontend/src/components/global/MkError.vue @@ -4,9 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> +<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear> <div :class="$style.root"> - <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> + <img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/> <p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p> <MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton> </div> @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { serverErrorImageUrl } from '@/instance.js'; const emit = defineEmits<{ diff --git a/packages/frontend/src/components/global/MkFooterSpacer.vue b/packages/frontend/src/components/global/MkFooterSpacer.vue deleted file mode 100644 index 1a75855fa1..0000000000 --- a/packages/frontend/src/components/global/MkFooterSpacer.vue +++ /dev/null @@ -1,32 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div :class="[$style.spacer, defaultStore.reactiveState.darkMode.value ? $style.dark : $style.light]"></div> -</template> - -<script lang="ts" setup> -import { defaultStore } from '@/store.js'; -</script> - -<style lang="scss" module> -.spacer { - box-sizing: border-box; - padding: 32px; - margin: 0 auto; - height: 300px; - background-clip: content-box; - background-size: auto auto; - background-color: rgba(255, 255, 255, 0); - - &.light { - background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #00000010 16px, #00000010 20px ); - } - - &.dark { - background-image: repeating-linear-gradient(135deg, transparent, transparent 16px, #FFFFFF16 16px, #FFFFFF16 20px ); - } -} -</style> diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue index f35932ae77..b352296469 100644 --- a/packages/frontend/src/components/global/MkLazy.vue +++ b/packages/frontend/src/components/global/MkLazy.vue @@ -11,9 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue'; +import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, useTemplateRef } from 'vue'; -const rootEl = shallowRef<HTMLDivElement>(); +const rootEl = useTemplateRef('rootEl'); const showing = ref(false); const observer = new IntersectionObserver( diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts index 48d7e34d76..337e326ccd 100644 --- a/packages/frontend/src/components/global/MkMfm.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { h, provide } from 'vue'; -import type { VNode, SetupContext } from 'vue'; +import { h } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; +import type { VNode, SetupContext } from 'vue'; +import type { MkABehavior } from '@/components/global/MkA.vue'; import MkUrl from '@/components/global/MkUrl.vue'; import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; @@ -19,8 +20,7 @@ import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; import MkA from '@/components/global/MkA.vue'; -import type { MkABehavior } from '@/components/global/MkA.vue'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; function safeParseFloat(str: unknown): number | null { if (typeof str !== 'string' || str === '') return null; @@ -81,7 +81,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven return c.match(/^[0-9a-f]{3,6}$/i) ? c : null; }; - const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm; + const useAnim = prefer.s.advancedMfm && prefer.s.animatedMfm; /** * Gen Vue Elements from MFM AST @@ -188,17 +188,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'x2': { return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x2' : '', + class: prefer.s.advancedMfm ? 'mfm-x2' : '', }, genEl(token.children, scale * 2)); } case 'x3': { return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x3' : '', + class: prefer.s.advancedMfm ? 'mfm-x3' : '', }, genEl(token.children, scale * 3)); } case 'x4': { return h('span', { - class: defaultStore.state.advancedMfm ? 'mfm-x4' : '', + class: prefer.s.advancedMfm ? 'mfm-x4' : '', }, genEl(token.children, scale * 4)); } case 'font': { @@ -241,14 +241,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven break; } case 'position': { - if (!defaultStore.state.advancedMfm) break; + if (!prefer.s.advancedMfm) break; const x = safeParseFloat(token.props.args.x) ?? 0; const y = safeParseFloat(token.props.args.y) ?? 0; style = `transform: translateX(${x}em) translateY(${y}em);`; break; } case 'scale': { - if (!defaultStore.state.advancedMfm) { + if (!prefer.s.advancedMfm) { style = ''; break; } diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts index c9af5f4ea4..15938d0495 100644 --- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts +++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts @@ -2,11 +2,10 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ + import { waitFor } from '@storybook/test'; -import type { StoryObj } from '@storybook/vue3'; import MkPageHeader from './MkPageHeader.vue'; +import type { StoryObj } from '@storybook/vue3'; export const Empty = { render(args) { return { @@ -29,7 +28,7 @@ export const Empty = { }; }, async play() { - const wait = new Promise((resolve) => setTimeout(resolve, 800)); + const wait = new Promise((resolve) => window.setTimeout(resolve, 800)); await waitFor(async () => await wait); }, args: { diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index aaef8b8fca..255fca8f86 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -8,13 +8,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.tabsInner"> <button v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title" - class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]" + class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]" @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)" > <div :class="$style.tabInner"> <i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i> <div - v-if="!t.iconOnly || (!defaultStore.reactiveState.animation.value && t.key === tab)" + v-if="!t.iconOnly || (!prefer.s.animation && t.key === tab)" :class="$style.tabTitle" > {{ t.title }} @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div ref="tabHighlightEl" - :class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]" + :class="[$style.tabHighlight, { [$style.animate]: prefer.s.animation }]" ></div> </div> </template> @@ -41,20 +41,20 @@ export type Tab = { onClick?: (ev: MouseEvent) => void; } & ( | { - iconOnly?: false; - title: string; - icon?: string; - } + iconOnly?: false; + title: string; + icon?: string; + } | { - iconOnly: true; - icon: string; - } + iconOnly: true; + icon: string; + } ); </script> <script lang="ts" setup> -import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; -import { defaultStore } from '@/store.js'; +import { nextTick, onMounted, onUnmounted, useTemplateRef, watch } from 'vue'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ tabs?: Tab[]; @@ -69,9 +69,9 @@ const emit = defineEmits<{ (ev: 'tabClick', key: string); }>(); -const el = shallowRef<HTMLElement | null>(null); +const el = useTemplateRef('el'); +const tabHighlightEl = useTemplateRef('tabHighlightEl'); const tabRefs: Record<string, HTMLElement | null> = {}; -const tabHighlightEl = shallowRef<HTMLElement | null>(null); function onTabMousedown(tab: Tab, ev: MouseEvent): void { // ユーザビリティの観点からmousedown時にはonClickは呼ばない @@ -133,7 +133,7 @@ async function enter(el: Element) { entering = false; }); - setTimeout(renderTab, 170); + window.setTimeout(renderTab, 170); } function afterEnter(el: Element) { @@ -170,7 +170,7 @@ onMounted(() => { if (props.rootEl) { ro2 = new ResizeObserver((entries, observer) => { - if (document.body.contains(el.value as HTMLElement)) { + if (window.document.body.contains(el.value as HTMLElement)) { nextTick(() => renderTab()); } }); diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 1070c0c83b..93f46a866a 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }"> +<div v-if="show" ref="el" :class="[$style.root]"> <div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]"> <div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu"> <MkAvatar :class="$style.avatar" :user="$i"/> @@ -41,16 +41,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue'; -import tinycolor from 'tinycolor2'; +import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue'; +import { scrollToTop } from '@@/js/scroll.js'; import XTabs from './MkPageHeader.tabs.vue'; import type { Tab } from './MkPageHeader.tabs.vue'; -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'; import type { PageHeaderItem } from '@/types/page-header.js'; -import type { PageMetadata } from '@/scripts/page-metadata.js'; +import type { PageMetadata } from '@/page.js'; +import { globalEvents } from '@/events.js'; +import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { $i } from '@/i.js'; +import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ overridePageMetadata?: PageMetadata; @@ -68,14 +68,14 @@ const emit = defineEmits<{ (ev: 'update:tab', key: string); }>(); -const injectedPageMetadata = injectReactiveMetadata(); +//const viewId = inject(DI.viewId); +const injectedPageMetadata = inject(DI.pageMetadata, ref(null)); const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value); const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle); const thin_ = props.thin || inject('shouldHeaderThin', false); -const el = shallowRef<HTMLElement | undefined>(undefined); -const bg = ref<string | undefined>(undefined); +const el = useTemplateRef('el'); const narrow = ref(false); const hasTabs = computed(() => props.tabs.length > 0); const hasActions = computed(() => props.actions && props.actions.length > 0); @@ -103,23 +103,13 @@ function onTabClick(): void { top(); } -const calcBg = () => { - const rawBg = 'var(--MI_THEME-bg)'; - const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - tinyBg.setAlpha(0.85); - bg.value = tinyBg.toRgbString(); -}; - let ro: ResizeObserver | null; onMounted(() => { - calcBg(); - globalEvents.on('themeChanged', calcBg); - if (el.value && el.value.parentElement) { narrow.value = el.value.parentElement.offsetWidth < 500; ro = new ResizeObserver((entries, observer) => { - if (el.value && el.value.parentElement && document.body.contains(el.value as HTMLElement)) { + if (el.value && el.value.parentElement && window.document.body.contains(el.value as HTMLElement)) { narrow.value = el.value.parentElement.offsetWidth < 500; } }); @@ -128,13 +118,13 @@ onMounted(() => { }); onUnmounted(() => { - globalEvents.off('themeChanged', calcBg); if (ro) ro.disconnect(); }); </script> <style lang="scss" module> .root { + background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); border-bottom: solid 0.5px var(--MI_THEME-divider); diff --git a/packages/frontend/src/components/global/MkSpacer.vue b/packages/frontend/src/components/global/MkSpacer.vue index db01c10eb0..c3bc37cb92 100644 --- a/packages/frontend/src/components/global/MkSpacer.vue +++ b/packages/frontend/src/components/global/MkSpacer.vue @@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { inject } from 'vue'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { deviceKind } from '@/utility/device-kind.js'; +import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ contentMax?: number | null; @@ -25,7 +26,7 @@ const props = withDefaults(defineProps<{ marginMax: 24, }); -const forceSpacerMin = inject('forceSpacerMin', false) || deviceKind === 'smartphone'; +const forceSpacerMin = inject(DI.forceSpacerMin, false) || deviceKind === 'smartphone'; </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index 7ee3952083..05245716c2 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -23,9 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, provide, inject, ref, watch, useTemplateRef } from 'vue'; -import type { Ref } from 'vue'; - -import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js'; +import { DI } from '@/di.js'; const rootEl = useTemplateRef('rootEl'); const headerEl = useTemplateRef('headerEl'); @@ -33,13 +31,13 @@ const footerEl = useTemplateRef('footerEl'); const headerHeight = ref<string | undefined>(); const childStickyTop = ref(0); -const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0)); -provide(CURRENT_STICKY_TOP, childStickyTop); +const parentStickyTop = inject(DI.currentStickyTop, ref(0)); +provide(DI.currentStickyTop, childStickyTop); const footerHeight = ref<string | undefined>(); const childStickyBottom = ref(0); -const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0)); -provide(CURRENT_STICKY_BOTTOM, childStickyBottom); +const parentStickyBottom = inject(DI.currentStickyBottom, ref(0)); +provide(DI.currentStickyBottom, childStickyBottom); const calc = () => { // コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 3d2036e376..0c248b041d 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -29,7 +29,7 @@ import { defineAsyncComponent, ref } from 'vue'; import { toUnicode as decodePunycode } from 'punycode.js'; import { url as local } from '@@/js/config.js'; import * as os from '@/os.js'; -import { useTooltip } from '@/scripts/use-tooltip.js'; +import { useTooltip } from '@/use/use-tooltip.js'; import { isEnabledUrlPreview } from '@/instance.js'; import type { MkABehavior } from '@/components/global/MkA.vue'; diff --git a/packages/frontend/src/components/global/NestedRouterView.vue b/packages/frontend/src/components/global/NestedRouterView.vue new file mode 100644 index 0000000000..af00347db8 --- /dev/null +++ b/packages/frontend/src/components/global/NestedRouterView.vue @@ -0,0 +1,60 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<Suspense :timeout="0"> + <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> + + <template #fallback> + <MkLoading/> + </template> +</Suspense> +</template> + +<script lang="ts" setup> +import { inject, provide, ref, shallowRef } from 'vue'; +import type { Router } from '@/router.js'; +import type { PathResolvedResult } from '@/lib/nirax.js'; +import MkLoadingPage from '@/pages/_loading_.vue'; +import { DI } from '@/di.js'; + +const props = defineProps<{ + router?: Router; +}>(); + +const router = props.router ?? inject(DI.router); + +if (router == null) { + throw new Error('no router provided'); +} + +const currentDepth = inject(DI.routerCurrentDepth, 0); +provide(DI.routerCurrentDepth, currentDepth + 1); + +function resolveNested(current: PathResolvedResult, d = 0): PathResolvedResult | null { + if (d === currentDepth) { + return current; + } else { + if (current.child) { + return resolveNested(current.child, d + 1); + } else { + return null; + } + } +} + +const current = resolveNested(router.current)!; +const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); +const currentPageProps = ref(current.props); +const key = ref(router.getCurrentFullPath()); + +router.useListener('change', ({ resolved }) => { + const current = resolveNested(resolved); + if (current == null || 'redirect' in current.route) return; + currentPageComponent.value = current.route.component; + currentPageProps.value = current.props; + key.value = router.getCurrentFullPath(); +}); +</script> diff --git a/packages/frontend/src/components/global/PageWithAnimBg.vue b/packages/frontend/src/components/global/PageWithAnimBg.vue new file mode 100644 index 0000000000..7106ae20cd --- /dev/null +++ b/packages/frontend/src/components/global/PageWithAnimBg.vue @@ -0,0 +1,29 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <MkAnimBg style="position: absolute;"/> + <div class="_pageScrollable" :class="$style.body"> + <slot></slot> + </div> +</div> +</template> + +<script lang="ts" setup> +import MkAnimBg from '@/components/MkAnimBg.vue'; +</script> + +<style lang="scss" module> +.body { + position: absolute; + top: 0; + width: 100%; + height: 100%; + + // _pageScrollable はパフォーマンス上の理由で背景色が設定されているため + background: transparent !important; +} +</style> diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue new file mode 100644 index 0000000000..7ea0b5c97f --- /dev/null +++ b/packages/frontend/src/components/global/PageWithHeader.vue @@ -0,0 +1,53 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']"> + <MkStickyContainer> + <template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs"/></template> + <div :class="$style.body"> + <slot></slot> + </div> + <template #footer><slot name="footer"></slot></template> + </MkStickyContainer> +</div> +</template> + +<script lang="ts" setup> +import { useTemplateRef } from 'vue'; +import { scrollInContainer } from '@@/js/scroll.js'; +import type { PageHeaderItem } from '@/types/page-header.js'; +import type { Tab } from './MkPageHeader.tabs.vue'; + +const props = withDefaults(defineProps<{ + tabs?: Tab[]; + actions?: PageHeaderItem[] | null; + thin?: boolean; + hideTitle?: boolean; + displayMyAvatar?: boolean; + reversed?: boolean; +}>(), { + tabs: () => ([] as Tab[]), +}); + +const tab = defineModel<string>('tab'); +const rootEl = useTemplateRef('rootEl'); + +defineExpose({ + scrollToTop: () => { + if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' }); + }, +}); +</script> + +<style lang="scss" module> +.root { + +} + +.body { + min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px))); +} +</style> diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 3ab3d10a40..78ac6900a3 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -4,98 +4,110 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<KeepAlive - :max="defaultStore.state.numberOfPageCache" - :exclude="pageCacheController" -> - <Suspense :timeout="0"> - <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> +<div ref="rootEl" class="_pageContainer" :class="$style.root"> + <KeepAlive :max="prefer.s.numberOfPageCache"> + <Suspense :timeout="0"> + <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> - <template #fallback> - <MkLoading/> - </template> - </Suspense> -</KeepAlive> + <template #fallback> + <MkLoading/> + </template> + </Suspense> + </KeepAlive> +</div> </template> <script lang="ts" setup> -import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue'; -import type { IRouter, Resolved, RouteDef } from '@/nirax.js'; -import { defaultStore } from '@/store.js'; -import { globalEvents } from '@/events.js'; +import { inject, nextTick, onMounted, provide, ref, shallowRef, useTemplateRef } from 'vue'; +import type { Router } from '@/router.js'; +import { prefer } from '@/preferences.js'; import MkLoadingPage from '@/pages/_loading_.vue'; +import { DI } from '@/di.js'; +import { randomId } from '@/utility/random-id.js'; +import { deepEqual } from '@/utility/deep-equal.js'; const props = defineProps<{ - router?: IRouter; - nested?: boolean; + router?: Router; }>(); -const router = props.router ?? inject('router'); +const router = props.router ?? inject(DI.router); if (router == null) { throw new Error('no router provided'); } -const currentDepth = inject('routerCurrentDepth', 0); -provide('routerCurrentDepth', currentDepth + 1); +const viewId = randomId(); +provide(DI.viewId, viewId); -function resolveNested(current: Resolved, d = 0): Resolved | null { - if (!props.nested) return current; +const currentDepth = inject(DI.routerCurrentDepth, 0); +provide(DI.routerCurrentDepth, currentDepth + 1); - if (d === currentDepth) { - return current; - } else { - if (current.child) { - return resolveNested(current.child, d + 1); - } else { - return null; - } +const rootEl = useTemplateRef('rootEl'); +onMounted(() => { + if (prefer.s.animation) { + rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入 } +}); + +// view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成 +const viewTransitionStylesTag = window.document.createElement('style'); +viewTransitionStylesTag.textContent = ` +@keyframes ${viewId}-old { + to { transform: scale(0.95); opacity: 0; } } -const current = resolveNested(router.current)!; -const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); -const currentPageProps = ref(current.props); -const key = ref(router.getCurrentKey() + JSON.stringify(Object.fromEntries(current.props))); +@keyframes ${viewId}-new { + from { transform: scale(0.95); opacity: 0; } +} -function onChange({ resolved, key: newKey }) { - const current = resolveNested(resolved); - if (current == null || 'redirect' in current.route) return; - currentPageComponent.value = current.route.component; - currentPageProps.value = current.props; - key.value = newKey + JSON.stringify(Object.fromEntries(current.props)); +::view-transition-old(${viewId}) { + animation-duration: 0.2s; + animation-name: ${viewId}-old; +} - nextTick(() => { - // ページ遷移完了後に再びキャッシュを有効化 - if (clearCacheRequested.value) { - clearCacheRequested.value = false; - } - }); +::view-transition-new(${viewId}) { + animation-duration: 0.2s; + animation-name: ${viewId}-new; } +`; -router.addListener('change', onChange); +window.document.head.appendChild(viewTransitionStylesTag); -// #region キャッシュ制御 +const current = router.current!; +const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); +const currentPageProps = ref(current.props); +let currentRoutePath = current.route.path; +const key = ref(router.getCurrentFullPath()); -/** - * キャッシュクリアが有効になったら、全キャッシュをクリアする - * - * keepAlive側にwatcherがあるのですぐ消えるとはおもうけど、念のためページ遷移完了まではキャッシュを無効化しておく。 - * キャッシュ有効時向けにexcludeを使いたい場合は、pageCacheControllerに並列に突っ込むのではなく、下に追記すること - */ -const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined); -const clearCacheRequested = ref(false); +router.useListener('change', ({ resolved }) => { + if (resolved == null || 'redirect' in resolved.route) return; + if (resolved.route.path === currentRoutePath && deepEqual(resolved.props, currentPageProps.value)) return; -globalEvents.on('requestClearPageCache', () => { - if (_DEV_) console.log('clear page cache requested'); - if (!clearCacheRequested.value) { - clearCacheRequested.value = true; + function _() { + currentPageComponent.value = resolved.route.component; + currentPageProps.value = resolved.props; + key.value = router.getCurrentFullPath(); + currentRoutePath = resolved.route.path; } -}); -// #endregion - -onBeforeUnmount(() => { - router.removeListener('change', onChange); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (prefer.s.animation && window.document.startViewTransition) { + window.document.startViewTransition(() => new Promise((res) => { + _(); + nextTick(() => { + res(); + //setTimeout(res, 100); + }); + })); + } else { + _(); + } }); </script> + +<style lang="scss" module> +.root { + height: 100%; + background-color: var(--MI_THEME-bg); +} +</style> diff --git a/packages/frontend/src/components/global/SearchIcon.vue b/packages/frontend/src/components/global/SearchIcon.vue new file mode 100644 index 0000000000..27a284faf0 --- /dev/null +++ b/packages/frontend/src/components/global/SearchIcon.vue @@ -0,0 +1,14 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<slot></slot> +</template> + +<script lang="ts" setup> +</script> + +<style lang="scss" module> +</style> diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue index c5ec626cf4..ded1f9a28b 100644 --- a/packages/frontend/src/components/global/SearchMarker.vue +++ b/packages/frontend/src/components/global/SearchMarker.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="root" :class="[$style.root, { [$style.highlighted]: highlighted }]"> - <slot></slot> + <slot :isParentOfTarget="isParentOfTarget"></slot> </div> </template> @@ -21,7 +21,7 @@ import { useTemplateRef, inject, } from 'vue'; -import type { Ref } from 'vue'; +import { DI } from '@/di.js'; const props = defineProps<{ markerId?: string; @@ -36,13 +36,14 @@ const rootEl = useTemplateRef('root'); const rootElMutationObserver = new MutationObserver(() => { checkChildren(); }); -const injectedSearchMarkerId = inject<Ref<string | null>>('inAppSearchMarkerId'); +const injectedSearchMarkerId = inject(DI.inAppSearchMarkerId, null); const searchMarkerId = computed(() => injectedSearchMarkerId?.value ?? window.location.hash.slice(1)); const highlighted = ref(props.markerId === searchMarkerId.value); +const isParentOfTarget = computed(() => props.children?.includes(searchMarkerId.value)); function checkChildren() { - if (props.children?.includes(searchMarkerId.value)) { - const el = document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`); + if (isParentOfTarget.value) { + const el = window.document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`); highlighted.value = el == null; } } @@ -105,8 +106,8 @@ onBeforeUnmount(dispose); @keyframes blink { 0%, 100% { - background: color(from var(--MI_THEME-accent) srgb r g b / 0.05); - border: 1px solid color(from var(--MI_THEME-accent) srgb r g b / 0.7); + background: color(from var(--MI_THEME-accent) srgb r g b / 0.1); + border: 1px solid color(from var(--MI_THEME-accent) srgb r g b / 0.75); } 50% { background: transparent; diff --git a/packages/frontend/src/components/global/StackingRouterView.vue b/packages/frontend/src/components/global/StackingRouterView.vue new file mode 100644 index 0000000000..c95c74aef3 --- /dev/null +++ b/packages/frontend/src/components/global/StackingRouterView.vue @@ -0,0 +1,243 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<TransitionGroup + :enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''" + :moveClass="prefer.s.animation ? $style.transition_x_move : ''" + :duration="200" + tag="div" :class="$style.tabs" +> + <div v-for="(tab, i) in tabs" :key="tab.fullPath" :class="$style.tab" :style="{ '--i': i - 1 }"> + <div v-if="i > 0" :class="$style.tabBg" @click="back()"></div> + <div :class="$style.tabFg" @click.stop="back()"> + <div v-if="i > 0" :class="$style.tabMenu"> + <svg :class="$style.tabMenuShape" viewBox="0 0 24 16"> + <g transform="matrix(2.04108e-17,-0.333333,0.222222,1.36072e-17,21.3333,15.9989)"> + <path d="M23.997,-42C47.903,-23.457 47.997,12 47.997,12L-0.003,12L-0.003,-96C-0.003,-96 0.091,-60.543 23.997,-42Z" style="fill:var(--MI_THEME-panel);"/> + </g> + </svg> + <button :class="$style.tabMenuButton" class="_button" @click.stop="mount"><i class="ti ti-rectangle"></i></button> + <button :class="$style.tabMenuButton" class="_button" @click.stop="back"><i class="ti ti-x"></i></button> + </div> + <div v-if="i > 0" :class="$style.tabBorder"></div> + <div :class="$style.tabContent" class="_pageContainer" @click.stop=""> + <Suspense :timeout="0"> + <component :is="tab.component" v-bind="Object.fromEntries(tab.props)"/> + + <template #fallback> + <MkLoading/> + </template> + </Suspense> + </div> + </div> + </div> +</TransitionGroup> +</template> + +<script lang="ts" setup> +import { inject, provide, shallowRef } from 'vue'; +import type { Router } from '@/router.js'; +import { prefer } from '@/preferences.js'; +import MkLoadingPage from '@/pages/_loading_.vue'; +import { DI } from '@/di.js'; +import { deepEqual } from '@/utility/deep-equal.js'; + +const props = defineProps<{ + router?: Router; +}>(); + +const router = props.router ?? inject(DI.router); + +if (router == null) { + throw new Error('no router provided'); +} + +const currentDepth = inject(DI.routerCurrentDepth, 0); +provide(DI.routerCurrentDepth, currentDepth + 1); + +const tabs = shallowRef([{ + fullPath: router.getCurrentFullPath(), + routePath: router.current.route.path, + component: 'component' in router.current.route ? router.current.route.component : MkLoadingPage, + props: router.current.props, +}]); + +function mount() { + const currentTab = tabs.value[tabs.value.length - 1]; + tabs.value = [currentTab]; +} + +function back() { + const prev = tabs.value[tabs.value.length - 2]; + tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)]; + router.replace(prev.fullPath); +} + +router.useListener('change', ({ resolved }) => { + const currentTab = tabs.value[tabs.value.length - 1]; + const routePath = resolved.route.path; + if (resolved == null || 'redirect' in resolved.route) return; + if (resolved.route.path === currentTab.routePath && deepEqual(resolved.props, currentTab.props)) return; + const fullPath = router.getCurrentFullPath(); + + if (tabs.value.some(tab => tab.routePath === routePath && deepEqual(resolved.props, tab.props))) { + const newTabs = []; + for (const tab of tabs.value) { + newTabs.push(tab); + + if (tab.routePath === routePath && deepEqual(resolved.props, tab.props)) { + break; + } + } + tabs.value = newTabs; + return; + } + + tabs.value = tabs.value.length >= prefer.s.numberOfPageCache ? [ + ...tabs.value.slice(1), + { + fullPath: fullPath, + routePath, + component: resolved.route.component, + props: resolved.props, + }, + ] : [...tabs.value, { + fullPath: fullPath, + routePath, + component: resolved.route.component, + props: resolved.props, + }]; +}); + +router.useListener('replace', ({ fullPath }) => { + const currentTab = tabs.value[tabs.value.length - 1]; + currentTab.fullPath = fullPath; + tabs.value = [...tabs.value.slice(0, tabs.value.length - 1), currentTab]; +}); +</script> + +<style lang="scss" module> +.transition_x_move, +.transition_x_enterActive, +.transition_x_leaveActive { + .tabBg { + transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important; + } + + .tabFg { + transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important; + } +} +.transition_x_enterFrom, +.transition_x_leaveTo { + .tabBg { + opacity: 0; + } + + .tabFg { + opacity: 0; + transform: translateY(100px); + } +} +.transition_x_leaveActive { + .tabFg { + //position: absolute; + } +} + +.tabs { + position: relative; + width: 100%; + height: 100%; +} + +.tab { + overflow: clip; + + &:first-child { + position: relative; + width: 100%; + height: 100%; + + .tabFg { + position: relative; + width: 100%; + height: 100%; + } + + .tabContent { + position: relative; + width: 100%; + height: 100%; + } + } + + &:not(:first-child) { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + + .tabBg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #0003; + -webkit-backdrop-filter: var(--MI-blur, blur(3px)); + backdrop-filter: var(--MI-blur, blur(3px)); + } + + .tabFg { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: calc(100% - (10px + (20px * var(--i)))); + display: flex; + flex-direction: column; + } + + .tabContent { + flex: 1; + width: 100%; + height: 100%; + background: var(--MI_THEME-bg); + } + } +} + +.tabMenu { + position: relative; + margin-left: auto; + padding: 0 4px; + background: var(--MI_THEME-panel); +} + +.tabMenuShape { + position: absolute; + bottom: -1px; + left: -100%; + height: calc(100% + 1px); + width: 129%; + pointer-events: none; +} + +.tabBorder { + height: 6px; + background: var(--MI_THEME-panel); +} + +.tabMenuButton { + padding: 8px; + font-size: 13px; +} +</style> diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue index c2dc05efe6..55de0df690 100644 --- a/packages/frontend/src/components/grid/MkDataCell.vue +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -39,10 +39,12 @@ SPDX-License-Identifier: AGPL-3.0-only {{ cell.value }} </div> <div v-else-if="cellType === 'boolean'"> - <div :class="[$style.bool, { - [$style.boolTrue]: cell.value === true, - 'ti ti-check': cell.value === true, - }]"></div> + <div + :class="[$style.bool, { + [$style.boolTrue]: cell.value === true, + 'ti ti-check': cell.value === true, + }]" + ></div> </div> <div v-else-if="cellType === 'image'"> <img @@ -88,14 +90,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'; -import { GridEventEmitter } from '@/components/grid/grid.js'; -import { useTooltip } from '@/scripts/use-tooltip.js'; -import * as os from '@/os.js'; -import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js'; +import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, useTemplateRef, toRefs, watch } from 'vue'; import type { Size } from '@/components/grid/grid.js'; import type { CellValue, GridCell } from '@/components/grid/cell.js'; import type { GridRowSetting } from '@/components/grid/row.js'; +import { GridEventEmitter } from '@/components/grid/grid.js'; +import { useTooltip } from '@/use/use-tooltip.js'; +import * as os from '@/os.js'; +import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js'; const emit = defineEmits<{ (ev: 'operation:beginEdit', sender: GridCell): void; @@ -111,9 +113,9 @@ const props = defineProps<{ const { cell, bus } = toRefs(props); -const rootEl = shallowRef<InstanceType<typeof HTMLTableCellElement>>(); -const contentAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>(); -const inputAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>(); +const rootEl = useTemplateRef('rootEl'); +const contentAreaEl = useTemplateRef('contentAreaEl'); +const inputAreaEl = useTemplateRef('inputAreaEl'); /** 値が編集中かどうか */ const editing = ref<boolean>(false); @@ -343,7 +345,7 @@ $cellHeight: 28px; border: solid 0.5px transparent; &.selected { - border: solid 0.5px var(--MI_THEME-accentLighten); + border: solid 0.5px hsl(from var(--MI_THEME-accent) h s calc(l + 10)); } &.ranged { diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index c89e23c135..c37f3df0d3 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { computed, onMounted, ref, toRefs, watch } from 'vue'; +import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js'; +import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; +import type { GridContext, GridEvent } from '@/components/grid/grid-event.js'; +import type { GridColumn } from '@/components/grid/column.js'; +import type { GridRow, GridRowSetting } from '@/components/grid/row.js'; +import type { MenuItem } from '@/types/menu.js'; import { GridEventEmitter } from '@/components/grid/grid.js'; import MkDataRow from '@/components/grid/MkDataRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; @@ -66,14 +72,7 @@ import { import * as os from '@/os.js'; import { createColumn } from '@/components/grid/column.js'; import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js'; -import { handleKeyEvent } from '@/scripts/key-event.js'; - -import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js'; -import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; -import type { GridContext, GridEvent } from '@/components/grid/grid-event.js'; -import type { GridColumn } from '@/components/grid/column.js'; -import type { GridRow, GridRowSetting } from '@/components/grid/row.js'; -import type { MenuItem } from '@/types/menu.js'; +import { handleKeyEvent } from '@/utility/key-event.js'; type RowHolder = { row: GridRow, @@ -130,7 +129,7 @@ const bus = new GridEventEmitter(); * * @see {@link onResize} */ -const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries))); +const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries))); const rootEl = ref<InstanceType<typeof HTMLTableElement>>(); /** diff --git a/packages/frontend/src/components/grid/grid-utils.ts b/packages/frontend/src/components/grid/grid-utils.ts index 4f48af194c..9e5402354e 100644 --- a/packages/frontend/src/components/grid/grid-utils.ts +++ b/packages/frontend/src/components/grid/grid-utils.ts @@ -10,7 +10,7 @@ import { CELL_ADDRESS_NONE } from '@/components/grid/cell.js'; import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; import type { GridRow } from '@/components/grid/row.js'; import type { GridContext } from '@/components/grid/grid-event.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import type { GridColumn, GridColumnSetting } from '@/components/grid/column.js'; export function isCellElement(elem: HTMLElement): boolean { diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index ebbad3e5b8..34cf598b84 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -16,17 +16,21 @@ import MkTime from './global/MkTime.vue'; import MkUrl from './global/MkUrl.vue'; import I18n from './global/I18n.vue'; import RouterView from './global/RouterView.vue'; +import NestedRouterView from './global/NestedRouterView.vue'; +import StackingRouterView from './global/StackingRouterView.vue'; import MkLoading from './global/MkLoading.vue'; import MkError from './global/MkError.vue'; import MkAd from './global/MkAd.vue'; import MkPageHeader from './global/MkPageHeader.vue'; import MkSpacer from './global/MkSpacer.vue'; -import MkFooterSpacer from './global/MkFooterSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; import MkLazy from './global/MkLazy.vue'; +import PageWithHeader from './global/PageWithHeader.vue'; +import PageWithAnimBg from './global/PageWithAnimBg.vue'; import SearchMarker from './global/SearchMarker.vue'; import SearchLabel from './global/SearchLabel.vue'; import SearchKeyword from './global/SearchKeyword.vue'; +import SearchIcon from './global/SearchIcon.vue'; import type { App } from 'vue'; @@ -39,6 +43,8 @@ export default function(app: App) { export const components = { I18n: I18n, RouterView: RouterView, + NestedRouterView: NestedRouterView, + StackingRouterView: StackingRouterView, Mfm: Mfm, MkA: MkA, MkAcct: MkAcct, @@ -55,18 +61,22 @@ export const components = { MkAd: MkAd, MkPageHeader: MkPageHeader, MkSpacer: MkSpacer, - MkFooterSpacer: MkFooterSpacer, MkStickyContainer: MkStickyContainer, MkLazy: MkLazy, + PageWithHeader: PageWithHeader, + PageWithAnimBg: PageWithAnimBg, SearchMarker: SearchMarker, SearchLabel: SearchLabel, SearchKeyword: SearchKeyword, + SearchIcon: SearchIcon, }; declare module '@vue/runtime-core' { export interface GlobalComponents { I18n: typeof I18n; RouterView: typeof RouterView; + NestedRouterView: typeof NestedRouterView; + StackingRouterView: typeof StackingRouterView; Mfm: typeof Mfm; MkA: typeof MkA; MkAcct: typeof MkAcct; @@ -83,11 +93,13 @@ declare module '@vue/runtime-core' { MkAd: typeof MkAd; MkPageHeader: typeof MkPageHeader; MkSpacer: typeof MkSpacer; - MkFooterSpacer: typeof MkFooterSpacer; MkStickyContainer: typeof MkStickyContainer; MkLazy: typeof MkLazy; + PageWithHeader: typeof PageWithHeader; + PageWithAnimBg: typeof PageWithAnimBg; SearchMarker: typeof SearchMarker; SearchLabel: typeof SearchLabel; SearchKeyword: typeof SearchKeyword; + SearchIcon: typeof SearchIcon; } } diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index 84436e7adb..df26874c17 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -15,7 +15,7 @@ import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkNote from '@/components/MkNote.vue'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; const props = defineProps<{ block: Misskey.entities.PageBlock, diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue index e0c7956f6e..7702e250e4 100644 --- a/packages/frontend/src/components/page/page.text.vue +++ b/packages/frontend/src/components/page/page.text.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; -import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; +import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { isEnabledUrlPreview } from '@/instance.js'; const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')); |