diff options
Diffstat (limited to 'packages/frontend/src')
375 files changed, 5758 insertions, 3370 deletions
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 875353f8a4..5c39955c28 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -10,7 +10,7 @@ import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; import { subBoot } from '@/boot/sub-boot.js'; -const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete']; +const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete']; if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) { subBoot(); diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts index 1601f247d7..f312765dcf 100644 --- a/packages/frontend/src/_dev_boot_.ts +++ b/packages/frontend/src/_dev_boot_.ts @@ -43,7 +43,7 @@ async function main() { const theme = localStorage.getItem('theme'); if (theme) { for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); + document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); // HTMLの theme-color 適用 if (k === 'htmlThemeColor') { diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index f0a464084f..366345b5b3 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -5,12 +5,12 @@ import { defineAsyncComponent, reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { apiUrl } from '@@/js/config.js'; +import type { MenuItem, MenuButton } from '@/types/menu.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; -import type { MenuItem, MenuButton } from '@/types/menu.js'; import { del, get, set } from '@/scripts/idb-proxy.js'; -import { apiUrl } from '@@/js/config.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; @@ -173,7 +173,18 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr }); } -export function updateAccount(accountData: Partial<Account>) { +export function updateAccount(accountData: Account) { + if (!$i) return; + for (const key of Object.keys($i)) { + delete $i[key]; + } + for (const [key, value] of Object.entries(accountData)) { + $i[key] = value; + } + miLocalStorage.setItem('account', JSON.stringify($i)); +} + +export function updateAccountPartial(accountData: Partial<Account>) { if (!$i) return; for (const [key, value] of Object.entries(accountData)) { $i[key] = value; @@ -232,26 +243,6 @@ export async function openAccountMenu(opts: { }, ev: MouseEvent) { if (!$i) return; - function showSigninDialog() { - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { - done: res => { - addAccount(res.id, res.i); - success(); - }, - closed: () => dispose(), - }); - } - - function createAccount() { - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { - done: res => { - addAccount(res.id, res.i); - switchAccountWithToken(res.i); - }, - closed: () => dispose(), - }); - } - async function switchAccount(account: Misskey.entities.UserDetailed) { const storedAccounts = await getAccounts(); const found = storedAccounts.find(x => x.id === account.id); @@ -320,10 +311,22 @@ export async function openAccountMenu(opts: { text: i18n.ts.addAccount, children: [{ text: i18n.ts.existingAccount, - action: () => { showSigninDialog(); }, + action: () => { + getAccountWithSigninDialog().then(res => { + if (res != null) { + success(); + } + }); + }, }, { text: i18n.ts.createAccount, - action: () => { createAccount(); }, + action: () => { + getAccountWithSignupDialog().then(res => { + if (res != null) { + switchAccountWithToken(res.token); + } + }); + }, }], }, { type: 'link', @@ -349,6 +352,40 @@ export async function openAccountMenu(opts: { }); } +export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> { + return new Promise((resolve) => { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { + done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => { + await addAccount(res.id, res.i); + resolve({ id: res.id, token: res.i }); + }, + cancelled: () => { + resolve(null); + }, + closed: () => { + dispose(); + }, + }); + }); +} + +export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> { + return new Promise((resolve) => { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { + done: async (res: Misskey.entities.SignupResponse) => { + await addAccount(res.id, res.token); + resolve({ id: res.id, token: res.token }); + }, + cancelled: () => { + resolve(null); + }, + closed: () => { + dispose(); + }, + }); + }); +} + if (_DEV_) { (window as any).$i = $i; } diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index af8bbf57d2..d43a2b0799 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -11,11 +11,11 @@ import directives from '@/directives/index.js'; import components from '@/components/index.js'; import { applyTheme } from '@/scripts/theme.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; -import { updateI18n } from '@/i18n.js'; +import { updateI18n, i18n } from '@/i18n.js'; import { $i, refreshAccount, login } from '@/account.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { fetchInstance, instance } from '@/instance.js'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js'; import { reloadChannel } from '@/scripts/unison-reload.js'; import { getUrlWithoutLoginId } from '@/scripts/login-id.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js'; @@ -183,24 +183,22 @@ export async function common(createVue: () => App<Element>) { if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); defaultStore.set('themeInitial', false); - } else { - if (defaultStore.state.darkMode) { - applyTheme(darkTheme.value); - } else { - applyTheme(lightTheme.value); - } } }); + watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => { + updateDeviceKind(kind); + }, { immediate: true }); + watch(defaultStore.reactiveState.useBlurEffectForModal, v => { - document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); + document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none'); }, { immediate: true }); watch(defaultStore.reactiveState.useBlurEffect, v => { if (v) { - document.documentElement.style.removeProperty('--blur'); + document.documentElement.style.removeProperty('--MI-blur'); } else { - document.documentElement.style.setProperty('--blur', 'none'); + document.documentElement.style.setProperty('--MI-blur', 'none'); } }, { immediate: true }); @@ -280,6 +278,27 @@ export async function common(createVue: () => App<Element>) { removeSplash(); + //#region Self-XSS 対策メッセージ + console.log( + `%c${i18n.ts._selfXssPrevention.warning}`, + 'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;', + ); + console.log( + `%c${i18n.ts._selfXssPrevention.title}`, + 'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;', + ); + console.log( + `%c${i18n.ts._selfXssPrevention.description1}`, + 'font-size: 16px; font-weight: 700;', + ); + console.log( + `%c${i18n.ts._selfXssPrevention.description2}`, + 'font-size: 16px;', + 'font-size: 20px; font-weight: 700; color: #f00;', + ); + console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' })); + //#endregion + return { isClientUpdated, app, diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 395d7d9ad1..eb8a4d30d2 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -4,14 +4,14 @@ */ import { createApp, defineAsyncComponent, markRaw } from 'vue'; +import { ui } from '@@/js/config.js'; import { common } from './common.js'; import type * as Misskey from 'misskey-js'; -import { ui } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; -import { $i, signout, updateAccount } from '@/account.js'; +import { $i, signout, updateAccountPartial } from '@/account.js'; import { instance } from '@/instance.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -230,11 +230,41 @@ export async function mainBoot() { } if (!claimedAchievements.includes('justPlainLucky')) { - window.setInterval(() => { + let justPlainLuckyTimer: number | null = null; + let lastVisibilityChangedAt = Date.now(); + + function claimPlainLucky() { + if (document.visibilityState !== 'visible') { + if (justPlainLuckyTimer != null) window.clearTimeout(justPlainLuckyTimer); + return; + } + if (Math.floor(Math.random() * 20000) === 0) { claimAchievement('justPlainLucky'); + } else { + justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10); } - }, 1000 * 10); + } + + window.addEventListener('visibilitychange', () => { + const now = Date.now(); + + if (document.visibilityState === 'visible') { + // タブを高速で切り替えたら取得処理が何度も走るのを防ぐ + if ((now - lastVisibilityChangedAt) < 1000 * 10) { + justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10); + } else { + claimPlainLucky(); + } + } else if (justPlainLuckyTimer != null) { + window.clearTimeout(justPlainLuckyTimer); + justPlainLuckyTimer = null; + } + + lastVisibilityChangedAt = now; + }, { passive: true }); + + claimPlainLucky(); } if (!claimedAchievements.includes('client30min')) { @@ -298,13 +328,13 @@ export async function mainBoot() { // 自分の情報が更新されたとき main.on('meUpdated', i => { - updateAccount(i); + updateAccountPartial(i); }); main.on('readAllNotifications', () => { setFavIconDot(false); - updateAccount({ + updateAccountPartial({ hasUnreadNotification: false, unreadNotificationsCount: 0, }); @@ -314,39 +344,39 @@ export async function mainBoot() { attemptShowNotificationDot(); const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1; - updateAccount({ + updateAccountPartial({ hasUnreadNotification: true, unreadNotificationsCount, }); }); main.on('unreadMention', () => { - updateAccount({ hasUnreadMentions: true }); + updateAccountPartial({ hasUnreadMentions: true }); }); main.on('readAllUnreadMentions', () => { - updateAccount({ hasUnreadMentions: false }); + updateAccountPartial({ hasUnreadMentions: false }); }); main.on('unreadSpecifiedNote', () => { - updateAccount({ hasUnreadSpecifiedNotes: true }); + updateAccountPartial({ hasUnreadSpecifiedNotes: true }); }); main.on('readAllUnreadSpecifiedNotes', () => { - updateAccount({ hasUnreadSpecifiedNotes: false }); + updateAccountPartial({ hasUnreadSpecifiedNotes: false }); }); main.on('readAllAntennas', () => { - updateAccount({ hasUnreadAntenna: false }); + updateAccountPartial({ hasUnreadAntenna: false }); }); main.on('unreadAntenna', () => { - updateAccount({ hasUnreadAntenna: true }); + updateAccountPartial({ hasUnreadAntenna: true }); sound.playMisskeySfx('antenna'); }); main.on('readAllAnnouncements', () => { - updateAccount({ hasUnreadAnnouncement: false }); + updateAccountPartial({ hasUnreadAnnouncement: false }); }); // 個人宛てお知らせが発行されたとき diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts deleted file mode 100644 index cf09c96fd4..0000000000 --- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { abuseUserReport } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkAbuseReport from './MkAbuseReport.vue'; -export const Default = { - render(args) { - return { - components: { - MkAbuseReport, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - resolved: action('resolved'), - }; - }, - }, - template: '<MkAbuseReport v-bind="props" v-on="events" />', - }; - }, - args: { - report: abuseUserReport(), - }, - parameters: { - layout: 'fullscreen', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => { - action('POST /api/admin/resolve-abuse-user-report')(await request.json()); - return HttpResponse.json({}); - }), - ], - }, - }, -} satisfies StoryObj<typeof MkAbuseReport>; diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index d13eedaade..d59c5b2c57 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -4,141 +4,151 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> - <div class="bcekxzvu _margin _panel"> - <div class="target"> - <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'"> - <MkAvatar class="avatar" :user="report.targetUser" indicator/> - <div class="names"> - <MkUserName class="name" :user="report.targetUser"/> - <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> - </div> - </MkA> - <div class="keyvalCtn"> - <MkKeyValue> - <template #key>{{ i18n.ts.registeredDate }}</template> - <template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.reporter }}</template> - <template #value><MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.createdAt }}</template> - <template #value><MkTime :time="report.createdAt" mode="absolute"/> (<MkTime :time="report.createdAt" mode="relative"/>)</template> - </MkKeyValue> - </div> - <hr> +<MkFolder> + <template #icon> + <i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--MI_THEME-success)"></i> + <i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--MI_THEME-error)"></i> + <i v-else-if="report.resolved" class="ti ti-slash"></i> + <i v-else class="ti ti-exclamation-circle" style="color: var(--MI_THEME-warn)"></i> + </template> + <template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template> + <template #caption>{{ report.comment }}</template> + <template #suffix><MkTime :time="report.createdAt"/></template> + <template #footer> + <div class="_buttons"> + <template v-if="!report.resolved"> + <MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--MI_THEME-success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton> + <MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--MI_THEME-error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton> + <MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton> + </template> + <template v-if="report.targetUser.host != null"> + <MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton> + <div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div> + </template> + <button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button> </div> - <div class="detail"> - <div> + </template> + + <div class="_gaps_s"> + <MkFolder :withSpacer="false"> + <template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template> + <template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template> + <template #suffix>#{{ report.targetUserId.toUpperCase() }}</template> + + <div style="container-type: inline-size;"> + <RouterView :router="targetRouter"/> + </div> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-message-2"></i></template> + <template #label>{{ i18n.ts.details }}</template> + <div class="_gaps_s"> <Mfm :text="report.comment" :isBlock="true" :linkNavigationBehavior="'window'"/> </div> - <hr/> - <div v-if="report.assignee" class="assignee"> - {{ i18n.ts.moderator }}: - <MkA :to="`/admin/user/${report.assignee.id}`" class="_link" :behavior="'window'">@{{ report.assignee.username }}</MkA> + </MkFolder> + + <MkFolder :withSpacer="false"> + <template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template> + <template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template> + <template #suffix>#{{ report.reporterId.toUpperCase() }}</template> + + <div style="container-type: inline-size;"> + <RouterView :router="reporterRouter"/> </div> - <div class="action"> - <MkSwitch v-model="forward" c:disabled="report.targetUser.host == null || report.resolved"> - {{ i18n.ts.forwardReport }} - <template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template> - </MkSwitch> - <MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton> + </MkFolder> + + <MkFolder :defaultOpen="false"> + <template #icon><i class="ti ti-message-2"></i></template> + <template #label>{{ i18n.ts.moderationNote }}</template> + <template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template> + <div class="_gaps_s"> + <MkTextarea v-model="moderationNote" manualSave> + <template #caption>{{ i18n.ts.moderationNoteDescription }}</template> + </MkTextarea> </div> + </MkFolder> + + <div v-if="report.assignee"> + {{ i18n.ts.moderator }}: + <MkAcct :user="report.assignee"/> </div> </div> +</MkFolder> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { provide, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; 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'; const props = defineProps<{ - report: any; + report: Misskey.entities.AdminAbuseUserReportsResponse[number]; }>(); const emit = defineEmits<{ (ev: 'resolved', reportId: string): void; }>(); -const forward = ref(props.report.forwarded); +const routerFactory = useRouterFactory(); +const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`); +targetRouter.init(); +const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`); +reporterRouter.init(); + +const moderationNote = ref(props.report.moderationNote ?? ''); -function resolve() { +watch(moderationNote, async () => { + os.apiWithDialog('admin/update-abuse-user-report', { + reportId: props.report.id, + moderationNote: moderationNote.value, + }).then(() => { + }); +}); + +function resolve(resolvedAs) { os.apiWithDialog('admin/resolve-abuse-user-report', { - forward: forward.value, reportId: props.report.id, + resolvedAs, }).then(() => { emit('resolved', props.report.id); }); } -</script> -<style lang="scss" scoped> -.bcekxzvu { - display: flex; - flex-direction: column; - transition: .1s; - - > .target { - box-sizing: border-box; - text-align: left; - padding: 24px 24px 0px 24px; - - > .info { - display: flex; - box-sizing: border-box; - align-items: center; - padding: 14px; - border-radius: var(--radius-sm); - --c: rgb(255 196 0 / 15%); - background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); - background-size: 16px 16px; - - > .avatar { - width: 42px; - height: 42px; - } - - > .names { - margin-left: 0.3em; - padding: 0 8px; - flex: 1; - - white-space: pre; - overflow: hidden; - - > .name { - font-weight: bold; - } - } - } - - > .keyvalCtn { - display: inline-flex; - gap: 15px; - margin-top: 15px; - } - } - - > .detail { - display: flex; - flex-direction: column; - padding: 0px 24px 24px 24px; +function forward() { + os.apiWithDialog('admin/forward-abuse-user-report', { + reportId: props.report.id, + }).then(() => { - .assignee { - margin-bottom: 15px; - } + }); +} - .action { - display: flex; - flex-direction: column; - gap: 15px; - } - } +function showMenu(ev: MouseEvent) { + os.popupMenu([{ + icon: 'ti ti-id', + text: 'Copy ID', + action: () => { + copyToClipboard(props.report.id); + }, + }, { + icon: 'ti ti-json', + text: 'Copy JSON', + action: () => { + copyToClipboard(JSON.stringify(props.report, null, '\t')); + }, + }], ev.currentTarget ?? ev.target); } +</script> + +<style lang="scss" module> </style> diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 796524fce9..0839955d9d 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -32,9 +32,9 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u); .root { padding: 16px; font-size: 90%; - background: var(--infoWarnBg); - color: var(--error); - border-radius: var(--radius); + background: var(--MI_THEME-infoWarnBg); + color: var(--MI_THEME-error); + border-radius: var(--MI-radius); } .link { diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index ab7bafc47a..087ad51fe3 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -121,10 +121,10 @@ onMounted(() => { .iconFrame { position: relative; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); padding: 6px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); box-sizing: border-box; pointer-events: none; user-select: none; @@ -191,7 +191,7 @@ onMounted(() => { position: relative; width: 100%; height: 100%; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); box-shadow: 0 1px 0px #ffffff88 inset; } diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index 835efbd6cd..c8fa6246e0 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -193,12 +193,12 @@ tick(); function calcColors() { const computedStyle = getComputedStyle(document.documentElement); - const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark(); - const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + 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)'; //minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; - mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString(); + mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString(); hHandColor.value = accent; nowColor.value = accent; } diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue index c81fea175c..6c335d71d9 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.vue +++ b/packages/frontend/src/components/MkAnnouncementDialog.vue @@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.header"> <span :class="$style.icon"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i> </span> <span :class="$style.title">{{ announcement.title }}</span> </div> @@ -29,7 +29,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { $i, updateAccount } from '@/account.js'; +import { $i, updateAccountPartial } from '@/account.js'; const props = withDefaults(defineProps<{ announcement: Misskey.entities.Announcement; @@ -51,7 +51,7 @@ async function ok() { modal.value?.close(); misskeyApi('i/read-announcement', { announcementId: props.announcement.id }); - updateAccount({ + updateAccountPartial({ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id), }); } @@ -83,8 +83,8 @@ onMounted(() => { min-width: 320px; max-width: 480px; box-sizing: border-box; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } .header { diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue index cb7ee3d6ca..e622d57f1e 100644 --- a/packages/frontend/src/components/MkAntennaEditor.vue +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -160,7 +160,7 @@ async function deleteAntenna() { function addUser() { os.selectUser({ includeSelf: true }).then(user => { users.value = users.value.trim(); - users.value += '\n@' + Misskey.acct.toString(user as any); + users.value += '\n@' + Misskey.acct.toString(user); users.value = users.value.trim(); }); } @@ -170,6 +170,6 @@ function addUser() { .actions { margin-top: 16px; padding: 24px 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index e2af4f034e..c28dbc7ffa 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -106,7 +106,7 @@ const containerStyle = computed(() => { const border = isBordered ? { borderWidth: c.borderWidth ?? '1px', - borderColor: c.borderColor ?? 'var(--divider)', + borderColor: c.borderColor ?? 'var(--MI_THEME-divider)', borderStyle: c.borderStyle ?? 'solid', } : undefined; @@ -165,7 +165,7 @@ function openPostForm() { } .postForm { - background: var(--bg); - border-radius: var(--radius-sm); + background: var(--MI_THEME-bg); + border-radius: var(--MI-radius-sm); } </style> diff --git a/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts new file mode 100644 index 0000000000..0adc44e204 --- /dev/null +++ b/packages/frontend/src/components/MkAuthConfirm.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkAuthConfirm from './MkAuthConfirm.vue'; +void MkAuthConfirm; diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue new file mode 100644 index 0000000000..f78d2d38f0 --- /dev/null +++ b/packages/frontend/src/components/MkAuthConfirm.vue @@ -0,0 +1,450 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_enterActive" + :leaveActiveClass="$style.transition_leaveActive" + :enterFromClass="$style.transition_enterFrom" + :leaveToClass="$style.transition_leaveTo" + + :inert="_waiting" + > + <div v-if="phase === 'accountSelect'" key="accountSelect" :class="$style.root" class="_gaps"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-user"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts.pleaseSelectAccount }}</div> + </div> + <div> + <div :class="$style.accountSelectorLabel">{{ i18n.ts.selectAccount }}</div> + <div :class="$style.accountSelectorList"> + <template v-for="[id, user] in users"> + <input :id="'account-' + id" v-model="selectedUser" type="radio" name="accountSelector" :value="id" :class="$style.accountSelectorRadio"/> + <label :for="'account-' + id" :class="$style.accountSelectorItem"> + <MkAvatar :user="user" :class="$style.accountSelectorAvatar"/> + <div :class="$style.accountSelectorBody"> + <MkUserName :user="user" :class="$style.accountSelectorName"/> + <MkAcct :user="user" :class="$style.accountSelectorAcct"/> + </div> + </label> + </template> + <button class="_button" :class="[$style.accountSelectorItem, $style.accountSelectorAddAccountRoot]" @click="clickAddAccount"> + <div :class="[$style.accountSelectorAvatar, $style.accountSelectorAddAccountAvatar]"> + <i class="ti ti-user-plus"></i> + </div> + <div :class="[$style.accountSelectorBody, $style.accountSelectorName]">{{ i18n.ts.addAccount }}</div> + </button> + </div> + </div> + <div class="_buttonsCenter"> + <MkButton rounded gradate :disabled="selectedUser === null" @click="clickChooseAccount">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + <div v-else-if="phase === 'consent'" key="consent" :class="$style.root" class="_gaps"> + <div :class="$style.header" class="_gaps_s"> + <img v-if="icon" :class="$style.icon" :src="getProxiedImageUrl(icon, 'preview')"/> + <div v-else :class="$style.iconFallback"> + <i class="ti ti-apps"></i> + </div> + <div :class="$style.headerText">{{ name ? i18n.tsx._auth.shareAccess({ name }) : i18n.ts._auth.shareAccessAsk }}</div> + </div> + <div v-if="permissions && permissions.length > 0" class="_gaps_s" :class="$style.permissionRoot"> + <div>{{ name ? i18n.tsx._auth.permission({ name }) : i18n.ts._auth.permissionAsk }}</div> + <div :class="$style.permissionListWrapper"> + <ul :class="$style.permissionList"> + <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li> + </ul> + </div> + </div> + <slot name="consentAdditionalInfo"></slot> + <div> + <div :class="$style.accountSelectorLabel"> + {{ i18n.ts._auth.scopeUser }} <button class="_textButton" @click="clickBackToAccountSelect">{{ i18n.ts.switchAccount }}</button> + </div> + <div :class="$style.accountSelectorList"> + <div :class="[$style.accountSelectorItem, $style.static]"> + <MkAvatar :user="users.get(selectedUser!)!" :class="$style.accountSelectorAvatar"/> + <div :class="$style.accountSelectorBody"> + <MkUserName :user="users.get(selectedUser!)!" :class="$style.accountSelectorName"/> + <MkAcct :user="users.get(selectedUser!)!" :class="$style.accountSelectorAcct"/> + </div> + </div> + </div> + </div> + <div class="_buttonsCenter"> + <MkButton rounded @click="clickCancel">{{ i18n.ts.reject }}</MkButton> + <MkButton rounded gradate @click="clickAccept">{{ i18n.ts.accept }}</MkButton> + </div> + </div> + <div v-else-if="phase === 'success'" key="success" :class="$style.root" class="_gaps_s"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-check"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts._auth.accepted }}</div> + <div :class="$style.headerTextSub">{{ i18n.ts._auth.pleaseGoBack }}</div> + </div> + </div> + <div v-else-if="phase === 'denied'" key="denied" :class="$style.root" class="_gaps_s"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-x"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts._auth.denied }}</div> + </div> + </div> + <div v-else-if="phase === 'failed'" key="failed" :class="$style.root" class="_gaps_s"> + <div :class="$style.header" class="_gaps_s"> + <div :class="$style.iconFallback"> + <i class="ti ti-x"></i> + </div> + <div :class="$style.headerText">{{ i18n.ts.somethingHappened }}</div> + </div> + </div> + </Transition> + <div v-if="_waiting" :class="$style.waitingRoot"> + <MkLoading/> + </div> +</div> +</template> + +<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 { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; + +const props = defineProps<{ + name?: string; + icon?: string; + permissions?: (typeof Misskey.permissions[number])[]; + manualWaiting?: boolean; + waitOnDeny?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'accept', token: string): void; + (ev: 'deny', token: string): void; +}>(); + +const waiting = ref(true); +const _waiting = computed(() => waiting.value || props.manualWaiting); +const phase = ref<'accountSelect' | 'consent' | 'success' | 'denied' | 'failed'>('accountSelect'); + +const selectedUser = ref<string | null>(null); + +const users = ref(new Map<string, Misskey.entities.UserDetailed & { token: string; }>()); + +async function init() { + waiting.value = true; + + users.value.clear(); + + if ($i) { + users.value.set($i.id, $i); + } + + const accounts = await getAccounts(); + + const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id)); + + if (accountIdsToFetch.length > 0) { + const usersRes = await misskeyApi('users/show', { + userIds: accountIdsToFetch, + }); + + for (const user of usersRes) { + if (users.value.has(user.id)) continue; + + users.value.set(user.id, { + ...user, + token: accounts.find(a => a.id === user.id)!.token, + }); + } + } + + waiting.value = false; +} + +init(); + +function clickAddAccount(ev: MouseEvent) { + selectedUser.value = null; + + os.popupMenu([{ + text: i18n.ts.existingAccount, + action: () => { + getAccountWithSigninDialog().then(async (res) => { + if (res != null) { + os.success(); + await init(); + if (users.value.has(res.id)) { + selectedUser.value = res.id; + } + } + }); + }, + }, { + text: i18n.ts.createAccount, + action: () => { + getAccountWithSignupDialog().then(async (res) => { + if (res != null) { + os.success(); + await init(); + if (users.value.has(res.id)) { + selectedUser.value = res.id; + } + } + }); + }, + }], ev.currentTarget ?? ev.target); +} + +function clickChooseAccount() { + if (selectedUser.value === null) return; + + phase.value = 'consent'; +} + +function clickBackToAccountSelect() { + selectedUser.value = null; + phase.value = 'accountSelect'; +} + +function clickCancel() { + if (selectedUser.value === null) return; + + const user = users.value.get(selectedUser.value)!; + + const token = user.token; + + if (props.waitOnDeny) { + waiting.value = true; + } + emit('deny', token); +} + +async function clickAccept() { + if (selectedUser.value === null) return; + + const user = users.value.get(selectedUser.value)!; + + const token = user.token; + + waiting.value = true; + emit('accept', token); +} + +function showUI(state: 'success' | 'denied' | 'failed') { + phase.value = state; + waiting.value = false; +} + +defineExpose({ + showUI, +}); +</script> + +<style lang="scss" module> +.transition_enterActive, +.transition_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.wrapper { + overflow-x: hidden; + overflow-x: clip; + + position: relative; + width: 100%; + height: 100%; +} + +.waitingRoot { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%); + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + cursor: wait; +} + +.root { + position: relative; + box-sizing: border-box; + width: 100%; + padding: 48px 24px; +} + +.header { + margin: 0 auto; + max-width: 320px; +} + +.icon, +.iconFallback { + display: block; + margin: 0 auto; + width: 54px; + height: 54px; +} + +.icon { + border-radius: 50%; + border: 1px solid var(--MI_THEME-divider); + background-color: #fff; + object-fit: contain; +} + +.iconFallback { + border-radius: 50%; + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + text-align: center; + line-height: 54px; + font-size: 18px; +} + +.headerText, +.headerTextSub { + text-align: center; + word-break: normal; + word-break: auto-phrase; +} + +.headerText { + font-size: 16px; + font-weight: 700; +} + +.permissionRoot { + padding: 16px; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-bg); +} + +.permissionListWrapper { + max-height: 350px; + overflow-y: auto; + padding: 12px; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-panel); +} + +.permissionList { + margin: 0 0 0 1.5em; + padding: 0; + font-size: 90%; +} + +.accountSelectorLabel { + font-size: 0.85em; + opacity: 0.7; + margin-bottom: 8px; +} + +.accountSelectorList { + border-radius: var(--MI-radius); + border: 1px solid var(--MI_THEME-divider); + overflow: hidden; + overflow: clip; +} + +.accountSelectorRadio { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; + + &:focus-visible + .accountSelectorItem { + outline: 2px solid var(--MI_THEME-accent); + outline-offset: -4px; + } + + &:checked:focus-visible + .accountSelectorItem { + outline-color: #fff; + } + + &:checked + .accountSelectorItem { + background: var(--MI_THEME-accent); + color: #fff; + } +} + +.accountSelectorItem { + display: flex; + align-items: center; + padding: 8px; + font-size: 14px; + -webkit-tap-highlight-color: transparent; + cursor: pointer; + + &:hover { + background: var(--MI_THEME-buttonHoverBg); + } + + &.static { + cursor: unset; + + &:hover { + background: none; + } + } +} + +.accountSelectorAddAccountRoot { + width: 100%; +} + +.accountSelectorBody { + padding: 0 8px; + min-width: 0; +} + +.accountSelectorAvatar { + width: 45px; + height: 45px; +} + +.accountSelectorAddAccountAvatar { + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + font-size: 16px; + line-height: 45px; + text-align: center; + border-radius: 50%; +} + +.accountSelectorName { + display: block; + font-weight: bold; +} + +.accountSelectorAcct { + opacity: 0.5; +} +</style> diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index de5207f350..a0cd066c06 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -407,16 +407,16 @@ onBeforeUnmount(() => { text-overflow: ellipsis; &:hover { - background: var(--X3); + background: var(--MI_THEME-X3); } &[data-selected='true'] { - background: var(--accent); + background: var(--MI_THEME-accent); color: #fff !important; } &:active { - background: var(--accentDarken); + background: var(--MI_THEME-accentDarken); color: #fff !important; } } @@ -427,7 +427,7 @@ onBeforeUnmount(() => { max-width: 28px; max-height: 28px; margin: 0 8px 0 0; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .userName { diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index e30f74460d..a6e5651d63 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -129,8 +129,8 @@ function onMousedown(evt: MouseEvent): void { font-size: 95%; box-shadow: none; text-decoration: none; - background: var(--buttonBg); - border-radius: var(--radius-xs); + background: var(--MI_THEME-buttonBg); + border-radius: var(--MI-radius-xs); overflow: clip; box-sizing: border-box; transition: background 0.1s ease; @@ -140,11 +140,11 @@ function onMousedown(evt: MouseEvent): void { } &:not(:disabled):hover { - background: var(--buttonHoverBg); + background: var(--MI_THEME-buttonHoverBg); } &:not(:disabled):active { - background: var(--buttonHoverBg); + background: var(--MI_THEME-buttonHoverBg); } &.small { @@ -162,20 +162,20 @@ function onMousedown(evt: MouseEvent): void { } &.rounded { - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } &.primary { font-weight: bold; - color: var(--fgOnAccent) !important; - background: var(--accent); + color: var(--MI_THEME-fgOnAccent) !important; + background: var(--MI_THEME-accent); &:not(:disabled):hover { - background: hsl(from var(--accent) h s calc(l + 5)); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); } &:not(:disabled):active { - background: hsl(from var(--accent) h s calc(l + 5)); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); } } @@ -216,15 +216,15 @@ function onMousedown(evt: MouseEvent): void { &.gradate { font-weight: bold; - color: var(--fgOnAccent) !important; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + color: var(--MI_THEME-fgOnAccent) !important; + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + 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))); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + 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))); } } @@ -272,7 +272,7 @@ function onMousedown(evt: MouseEvent): void { left: 0; width: 100%; height: 100%; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; pointer-events: none; } @@ -281,7 +281,7 @@ function onMousedown(evt: MouseEvent): void { position: absolute; width: 2px; height: 2px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); background: rgba(0, 0, 0, 0.1); opacity: 1; transform: scale(1); diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index ab00ea9930..e9493edbd1 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only <div id="mcaptcha__widget-container" class="m-captcha-style"></div> <div ref="captchaEl"></div> </div> + <div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;"> + <img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/> + <div v-if="testcaptchaPassed"> + <div style="color: green;">Test captcha passed!</div> + </div> + <div v-else> + <div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div> + <input v-model="testcaptchaInput" data-cy-testcaptcha-input/> + <button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button> + </div> + </div> <div v-else ref="captchaEl"></div> </div> </template> @@ -32,7 +43,7 @@ export type Captcha = { }): void; }; -export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc' | 'testcaptcha'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; @@ -57,6 +68,9 @@ const available = ref(false); const captchaEl = shallowRef<HTMLDivElement | undefined>(); +const testcaptchaInput = ref(''); +const testcaptchaPassed = ref(false); + const variable = computed(() => { switch (props.provider) { case 'hcaptcha': return 'hcaptcha'; @@ -64,6 +78,7 @@ const variable = computed(() => { case 'turnstile': return 'turnstile'; case 'mcaptcha': return 'mcaptcha'; case 'fc': return 'friendlyChallenge'; + case 'testcaptcha': return 'testcaptcha'; } }); @@ -76,6 +91,7 @@ const src = computed(() => { case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; case 'fc': return 'https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.18/widget.min.js'; case 'mcaptcha': return null; + case 'testcaptcha': return null; } }); @@ -83,7 +99,7 @@ const scriptId = computed(() => `script-${props.provider}`); const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); -if (loaded || props.provider === 'mcaptcha') { +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'), { @@ -96,6 +112,8 @@ if (loaded || props.provider === 'mcaptcha') { function reset() { if (captcha.value.reset) captcha.value.reset(); + testcaptchaPassed.value = false; + testcaptchaInput.value = ''; } async function requestRender() { @@ -104,8 +122,8 @@ async function requestRender() { sitekey: props.sitekey, theme: defaultStore.state.darkMode ? 'dark' : 'light', callback: callback, - 'expired-callback': callback, - 'error-callback': callback, + 'expired-callback': () => callback(undefined), + 'error-callback': () => callback(undefined), }); } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { const { default: Widget } = await import('@mcaptcha/vanilla-glue'); @@ -140,6 +158,12 @@ function onReceivedMessage(message: MessageEvent) { } } +function testcaptchaSubmit() { + testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii'; + callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined); + if (!testcaptchaPassed.value) testcaptchaInput.value = ''; +} + onMounted(() => { if (available.value) { window.addEventListener('message', onReceivedMessage); diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 6dace43fde..7aa916134f 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -68,13 +68,13 @@ async function onClick() { position: relative; display: inline-block; font-weight: bold; - color: var(--accent); + color: var(--MI_THEME-accent); background: transparent; - border: solid 1px var(--accent); + border: solid 1px var(--MI_THEME-accent); padding: 0; height: 31px; font-size: 16px; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); background: #fff; &.full { @@ -99,17 +99,17 @@ async function onClick() { } &.active { - color: var(--fgOnAccent); - background: var(--accent); + color: var(--MI_THEME-fgOnAccent); + background: var(--MI_THEME-accent); &:hover { - background: var(--accentLighten); - border-color: var(--accentLighten); + background: var(--MI_THEME-accentLighten); + border-color: var(--MI_THEME-accentLighten); } &:active { - background: var(--accentDarken); - border-color: var(--accentDarken); + background: var(--MI_THEME-accentDarken); + border-color: var(--MI_THEME-accentDarken); } } diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index 2f7ec34d44..e036fec528 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; const props = defineProps<{ - channel: Record<string, any>; + channel: Misskey.entities.Channel; }>(); const getLastReadedAt = (): number | null => { @@ -100,7 +101,7 @@ const bannerStyle = computed(() => { height: 100%; border-radius: inherit; pointer-events: none; - box-shadow: inset 0 0 0 2px var(--focus); + box-shadow: inset 0 0 0 2px var(--MI_THEME-focus); } } @@ -117,7 +118,7 @@ const bannerStyle = computed(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); } > .name { @@ -138,7 +139,7 @@ const bannerStyle = computed(() => { padding: 8px 12px; font-size: 80%; background: rgba(0, 0, 0, 0.7); - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); color: #fff; } @@ -148,8 +149,8 @@ const bannerStyle = computed(() => { bottom: 16px; left: 16px; background: rgba(0, 0, 0, 0.7); - color: var(--warn); - border-radius: var(--radius-sm); + color: var(--MI_THEME-warn); + border-radius: var(--MI-radius-sm); font-weight: bold; font-size: 1em; padding: 4px 7px; @@ -167,7 +168,7 @@ const bannerStyle = computed(() => { > footer { padding: 12px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); > span { opacity: 0.7; @@ -213,9 +214,9 @@ const bannerStyle = computed(() => { top: 0; right: 0; transform: translate(25%, -25%); - background-color: var(--accent); - border: solid var(--bg) 4px; - border-radius: var(--radius-full); + background-color: var(--MI_THEME-accent); + border: solid var(--MI_THEME-bg) 4px; + border-radius: var(--MI-radius-full); width: 1.5rem; height: 1.5rem; aspect-ratio: 1 / 1; diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 57d325b11a..d05f4921f6 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -863,8 +863,8 @@ onMounted(() => { left: 0; width: 100%; height: 100%; - -webkit-backdrop-filter: var(--blur, blur(12px)); - backdrop-filter: var(--blur, blur(12px)); + -webkit-backdrop-filter: var(--MI-blur, blur(12px)); + backdrop-filter: var(--MI-blur, blur(12px)); display: flex; justify-content: center; align-items: center; diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue index 240c9c919e..e28d6ad6ba 100644 --- a/packages/frontend/src/components/MkChartLegend.vue +++ b/packages/frontend/src/components/MkChartLegend.vue @@ -53,11 +53,11 @@ defineExpose({ > .item { font-size: 85%; padding: 4px 12px 4px 8px; - border: solid 1px var(--divider); - border-radius: var(--radius-ellipse); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-ellipse); &:hover { - border-color: var(--inputBorderHover); + border-color: var(--MI_THEME-inputBorderHover); } &.disabled { @@ -69,7 +69,7 @@ defineExpose({ display: inline-block; width: 12px; height: 12px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); vertical-align: -10%; } } diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue index dd550733cb..5b09ec90dd 100644 --- a/packages/frontend/src/components/MkClipPreview.vue +++ b/packages/frontend/src/components/MkClipPreview.vue @@ -49,13 +49,13 @@ const remaining = computed(() => { outline: none; .root { - box-shadow: inset 0 0 0 2px var(--focus); + box-shadow: inset 0 0 0 2px var(--MI_THEME-focus); } } &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -65,7 +65,7 @@ const remaining = computed(() => { .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .description { diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index 9e54420034..e253b1b55f 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -95,8 +95,8 @@ watch(() => props.lang, (to) => { padding: 1em; margin: .5em 0; overflow: auto; - border-radius: var(--radius-sm); - border: 1px solid var(--divider); + border-radius: var(--MI-radius-sm); + border: 1px solid var(--MI_THEME-divider); font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; color: var(--shiki-fallback); @@ -140,7 +140,7 @@ watch(() => props.lang, (to) => { & :global(.shiki) { padding: 12px; margin: 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); border: none; min-height: 130px; pointer-events: none; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index 05cde89dd9..a46b220101 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -71,11 +71,11 @@ function copy() { .codeBlockFallbackRoot { display: block; overflow-wrap: anywhere; - background: var(--bg); + background: var(--MI_THEME-bg); padding: 1em; margin: .5em 0; overflow: auto; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .codeBlockFallbackCode { @@ -91,11 +91,11 @@ function copy() { cursor: pointer; box-sizing: border-box; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); padding: 24px; margin-top: 4px; - color: var(--fg); - background: var(--bg); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-bg); } .codePlaceholderContainer { diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index b233189ab0..49b083815a 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -140,7 +140,7 @@ watch(v, newValue => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -158,20 +158,20 @@ watch(v, newValue => { overflow-y: hidden; box-sizing: border-box; margin: 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); padding: 0; - color: var(--fg); - border: solid 1px var(--panel); + color: var(--MI_THEME-fg); + border: solid 1px var(--MI_THEME-panel); transition: border-color 0.1s ease-out; font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } .focused.codeEditorRoot { - border-color: var(--accent) !important; - border-radius: var(--radius-sm); + border-color: var(--MI_THEME-accent) !important; + border-radius: var(--MI-radius-sm); } .codeEditorScroller { @@ -196,10 +196,10 @@ watch(v, newValue => { resize: none; text-align: left; color: transparent; - caret-color: var(--fg); + caret-color: var(--MI_THEME-fg); background-color: transparent; border: 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); box-sizing: border-box; outline: 0; min-width: calc(100% - 24px); @@ -213,6 +213,6 @@ watch(v, newValue => { } .textarea::selection { - color: var(--bg); + color: var(--MI_THEME-bg); } </style> diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue index 6add80d1bc..04b6e54108 100644 --- a/packages/frontend/src/components/MkCodeInline.vue +++ b/packages/frontend/src/components/MkCodeInline.vue @@ -18,7 +18,7 @@ const props = defineProps<{ display: inline-block; font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; overflow-wrap: anywhere; - background: var(--bg); + background: var(--MI_THEME-bg); padding: .1em; border-radius: .3em; } diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index 99aa46d561..babf356942 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -60,7 +60,7 @@ const onInput = () => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -72,8 +72,8 @@ const onInput = () => { &.focused { > .inputCore { - border-color: var(--accent) !important; - //box-shadow: 0 0 0 4px var(--focus); + border-color: var(--MI_THEME-accent) !important; + //box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } @@ -98,17 +98,17 @@ const onInput = () => { font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; transition: border-color 0.1s ease-out; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } </style> diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 5f71e289b8..f4d20c7d8c 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -64,26 +64,30 @@ const showBody = ref(props.expanded); const ignoreOmit = ref(false); const omitted = ref(false); -function enter(el) { +function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; + el.style.height = '0'; el.offsetHeight; // reflow - el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; + el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`; } -function afterEnter(el) { - el.style.height = null; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } -function leave(el) { +function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; el.offsetHeight; // reflow - el.style.height = 0; + el.style.height = '0'; } -function afterLeave(el) { - el.style.height = null; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } const calcOmit = () => { @@ -138,7 +142,7 @@ onUnmounted(() => { position: relative; overflow: clip; contain: content; - background: color-mix(in srgb, var(--panel) 65%, transparent); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); &.naked { background: transparent !important; box-shadow: none !important; @@ -165,13 +169,14 @@ onUnmounted(() => { .header { position: sticky; - top: var(--stickyTop, 0px); + top: var(--MI-stickyTop, 0px); left: 0; - color: var(--panelHeaderFg); - border-bottom: solid 0.5px var(--panelHeaderDivider); + 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; - background: color-mix(in srgb, var(--panelHeaderBg) 35%, transparent); + background: color-mix(in srgb, var(--MI_THEME-panelHeaderBg) 35%, transparent); } .title { @@ -201,7 +206,7 @@ onUnmounted(() => { } .content { - --stickyTop: 0px; + --MI-stickyTop: 0px; &.omitted { position: relative; @@ -216,20 +221,20 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } &:hover { > .fadeLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } } diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 2e1e92cbdf..0186cfc2c0 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withOkButton="true" @close="cancel()" @ok="ok()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header>{{ i18n.ts.cropImage }}</template> <template #default="{ width, height }"> @@ -125,7 +125,7 @@ onMounted(() => { const computedStyle = getComputedStyle(document.documentElement); const selection = cropper.getCropperSelection()!; - selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); selection.aspectRatio = props.aspectRatio; selection.initialAspectRatio = props.aspectRatio; selection.outlined = true; @@ -170,8 +170,8 @@ onMounted(() => { display: flex; align-items: center; justify-content: center; - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); + -webkit-backdrop-filter: var(--MI-blur, blur(10px)); + backdrop-filter: var(--MI-blur, blur(10px)); background: rgba(0, 0, 0, 0.5); } diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue index c7f1288729..ecbee864dc 100644 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')"> +<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')"> <template #header>:{{ emoji.name }}:</template> <template #default> <MkSpacer> @@ -85,8 +85,8 @@ function cancel() { .emojiImgWrapper { max-width: 100%; height: 40cqh; - background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--X5) 8px, var(--X5) 14px); - border-radius: var(--radius); + background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px); + border-radius: var(--MI-radius); margin: auto; overflow-y: hidden; } @@ -101,8 +101,8 @@ function cancel() { display: inline-block; word-break: break-all; padding: 3px 10px; - background-color: var(--X5); - border: solid 1px var(--divider); - border-radius: var(--radius); + background-color: var(--MI_THEME-X5); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius); } </style> diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 98bf5191f7..4b9666c55c 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -9,6 +9,7 @@ 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 { MisskeyEntity } from '@/types/date-separated-list.js'; @@ -99,11 +100,13 @@ export default defineComponent({ return [el, separator]; } else { - if (props.ad && item._shouldInsertAd_) { - return [h(MkAd, { + if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) { + return [h('div', { key: item.id + ':ad', + class: $style['ad-wrapper'], + }, [h(MkAd, { prefer: ['horizontal', 'horizontal-big'], - }), el]; + })]), el]; } else { return el; } @@ -125,14 +128,14 @@ export default defineComponent({ return children; }; - function onBeforeLeave(element: Element) { - const el = element as HTMLElement; + function onBeforeLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; el.style.top = `${el.offsetTop}px`; el.style.left = `${el.offsetLeft}px`; } - function onLeaveCancelled(element: Element) { - const el = element as HTMLElement; + function onLeaveCancelled(el: Element) { + if (!(el instanceof HTMLElement)) return; el.style.top = ''; el.style.left = ''; } @@ -182,12 +185,12 @@ export default defineComponent({ } &:not(.date-separated-list-nogap) > *:not(:last-child) { - margin-bottom: var(--margin); + margin-bottom: var(--MI-margin); } } .date-separated-list-nogap { - border-radius: var(--radius); + border-radius: var(--MI-radius); > * { margin: 0 !important; @@ -196,7 +199,7 @@ export default defineComponent({ box-shadow: none; &:not(:last-child) { - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); } } } @@ -237,7 +240,7 @@ export default defineComponent({ line-height: 32px; text-align: center; font-size: 12px; - color: var(--dateLabelFg); + color: var(--MI_THEME-dateLabelFg); } .date-1 { @@ -255,5 +258,11 @@ export default defineComponent({ .date-2-icon { margin-left: 8px; } + +.ad-wrapper { + 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); +} </style> diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 7dc381b662..9a59a9aac7 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </MkSelect> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> - <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> + <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> <MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton> </div> <div v-if="actions" :class="$style.buttons"> @@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{ text: string; primary?: boolean, danger?: boolean, - callback: (...args: any[]) => void; + callback: (...args: unknown[]) => void; }[]; showOkButton?: boolean; showCancelButton?: boolean; @@ -185,8 +185,8 @@ function onInputKeydown(evt: KeyboardEvent) { max-width: 480px; box-sizing: border-box; text-align: center; - background: var(--panel); - border-radius: var(--radius-md); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-md); } .icon { @@ -207,15 +207,15 @@ function onInputKeydown(evt: KeyboardEvent) { } .type_success { - color: var(--success); + color: var(--MI_THEME-success); } .type_error { - color: var(--error); + color: var(--MI_THEME-error); } .type_warning { - color: var(--warn); + color: var(--MI_THEME-warn); } .title { diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue index e4e3af99e4..f72f091383 100644 --- a/packages/frontend/src/components/MkDivider.vue +++ b/packages/frontend/src/components/MkDivider.vue @@ -27,6 +27,6 @@ defineProps<{ <style scoped lang="scss"> .default { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index 1dfdebf0d4..43d2002204 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -75,22 +75,22 @@ function neverShow() { .root { position: fixed; z-index: v-bind(zIndex); - bottom: var(--margin); + bottom: var(--MI-margin); left: 0; right: 0; margin: auto; box-sizing: border-box; - width: calc(100% - (var(--margin) * 2)); + width: calc(100% - (var(--MI-margin) * 2)); max-width: 500px; display: flex; - backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .icon { text-align: center; padding-top: 25px; width: 100px; - color: var(--accent); + color: var(--MI_THEME-accent); } @media (max-width: 500px) { .icon { diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 20ad2984d8..5dee448329 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -112,7 +112,7 @@ function onDragend() { position: relative; padding: 8px 0 0 0; min-height: 180px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); cursor: pointer; &:hover { @@ -152,14 +152,14 @@ function onDragend() { } &.isSelected { - background: var(--accent); + background: var(--MI_THEME-accent); &:hover { - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } &:active { - background: var(--accentDarken); + background: var(--MI_THEME-accentDarken); } > .label { @@ -248,7 +248,7 @@ function onDragend() { font-size: 0.8em; text-align: center; word-break: break-all; - color: var(--fg); + color: var(--MI_THEME-fg); overflow: hidden; } </style> diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 44788a6ffb..8496890f60 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> 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 { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import type { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -313,8 +313,8 @@ function onContextmenu(ev: MouseEvent) { position: relative; padding: 8px; height: 64px; - background: var(--driveFolderBg); - border-radius: var(--radius-xs); + background: var(--MI_THEME-driveFolderBg); + border-radius: var(--MI-radius-xs); cursor: pointer; &.draghover { @@ -326,8 +326,8 @@ function onContextmenu(ev: MouseEvent) { right: -4px; bottom: -4px; left: -4px; - border: 2px dashed var(--focus); - border-radius: var(--radius-xs); + border: 2px dashed var(--MI_THEME-focus); + border-radius: var(--MI-radius-xs); } } } @@ -345,13 +345,13 @@ function onContextmenu(ev: MouseEvent) { width: 18px; height: 18px; background: #fff; - border: solid 2px var(--divider); + border: solid 2px var(--MI_THEME-divider); border-radius: 4px; box-sizing: border-box; &.checked { - border-color: var(--accent); - background: var(--accent); + border-color: var(--MI_THEME-accent); + background: var(--MI_THEME-accent); &::after { content: "\ea5e"; @@ -368,14 +368,13 @@ function onContextmenu(ev: MouseEvent) { } &:hover { - background: var(--accentedBg); + background: var(--MI_THEME-accentedBg); } } .name { margin: 0; font-size: 0.9em; - color: var(--desktopDriveFolderFg); } .icon { @@ -388,6 +387,5 @@ function onContextmenu(ev: MouseEvent) { margin: 4px 4px; font-size: 0.8em; text-align: right; - color: var(--desktopDriveFolderFg); } </style> diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index a471457b44..5a0803f1e3 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -167,7 +167,12 @@ const ilFilesObserver = new IntersectionObserver( (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(), ); +const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt'); + watch(folder, () => emit('cd', folder.value)); +watch(sortModeSelect, () => { + fetch(); +}); function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) { addFile(file, true); @@ -203,7 +208,7 @@ function onStreamDriveFolderDeleted(folderId: string) { removeFolder(folderId); } -function onDragover(ev: DragEvent): any { +function onDragover(ev: DragEvent) { if (!ev.dataTransfer) return; // ドラッグ元が自分自身の所有するアイテムだったら @@ -248,7 +253,7 @@ function onDragleave() { draghover.value = false; } -function onDrop(ev: DragEvent): any { +function onDrop(ev: DragEvent) { draghover.value = false; if (!ev.dataTransfer) return; @@ -337,7 +342,7 @@ function createFolder() { title: i18n.ts.createFolder, placeholder: i18n.ts.folderName, }).then(({ canceled, result: name }) => { - if (canceled) return; + if (canceled || name == null) return; misskeyApi('drive/folders/create', { name: name, parentId: folder.value ? folder.value.id : undefined, @@ -570,6 +575,7 @@ async function fetch() { type: props.type, limit: filesMax + 1, searchQuery: searchQuery.value.toString().trim(), + sort: sortModeSelect.value, }).then(fetchedFiles => { if (fetchedFiles.length === filesMax + 1) { moreFiles.value = true; @@ -621,6 +627,7 @@ function fetchMoreFiles() { untilId: files.value.at(-1)?.id, limit: max + 1, searchQuery: searchQuery.value.toString().trim(), + sort: sortModeSelect.value, }).then(files => { if (files.length === max + 1) { moreFiles.value = true; @@ -656,6 +663,43 @@ function getMenu() { type: 'label', }); + menu.push({ + type: 'parent', + text: i18n.ts.sort, + icon: 'ti ti-arrows-sort', + children: [{ + text: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`, + icon: 'ti ti-sort-descending-letters', + action: () => { sortModeSelect.value = '+createdAt'; }, + active: sortModeSelect.value === '+createdAt', + }, { + text: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`, + icon: 'ti ti-sort-ascending-letters', + action: () => { sortModeSelect.value = '-createdAt'; }, + active: sortModeSelect.value === '-createdAt', + }, { + text: `${i18n.ts.size} (${i18n.ts.descendingOrder})`, + icon: 'ti ti-sort-descending-letters', + action: () => { sortModeSelect.value = '+size'; }, + active: sortModeSelect.value === '+size', + }, { + text: `${i18n.ts.size} (${i18n.ts.ascendingOrder})`, + icon: 'ti ti-sort-ascending-letters', + action: () => { sortModeSelect.value = '-size'; }, + active: sortModeSelect.value === '-size', + }, { + text: `${i18n.ts.name} (${i18n.ts.descendingOrder})`, + icon: 'ti ti-sort-descending-letters', + action: () => { sortModeSelect.value = '+name'; }, + active: sortModeSelect.value === '+name', + }, { + text: `${i18n.ts.name} (${i18n.ts.ascendingOrder})`, + icon: 'ti ti-sort-ascending-letters', + action: () => { sortModeSelect.value = '-name'; }, + active: sortModeSelect.value === '-name', + }], + }); + if (folder.value) { menu.push({ text: i18n.ts.renameFolder, @@ -735,7 +779,7 @@ onBeforeUnmount(() => { box-sizing: border-box; overflow: auto; font-size: 0.9em; - box-shadow: 0 1px 0 var(--divider); + box-shadow: 0 1px 0 var(--MI_THEME-divider); user-select: none; } @@ -787,7 +831,7 @@ onBeforeUnmount(() => { .main { flex: 1; overflow: auto; - padding: var(--margin); + padding: var(--MI-margin); user-select: none; &.fetching { @@ -834,7 +878,7 @@ onBeforeUnmount(() => { top: 38px; width: 100%; height: calc(100% - 38px); - border: dashed 2px var(--focus); + border: dashed 2px var(--MI_THEME-focus); pointer-events: none; } </style> diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 543cf24022..1079e52030 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -69,8 +69,8 @@ const isThumbnailAvailable = computed(() => { .root { position: relative; display: flex; - background: var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -83,7 +83,7 @@ const isThumbnailAvailable = computed(() => { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } .iconSub { diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue index c060c3a659..6e9eb75920 100644 --- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :scroll="false" :withOkButton="false" @close="cancel()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template> @@ -306,9 +306,9 @@ onUnmounted(() => { .embedCodeGenPreviewRoot { position: relative; - background-color: var(--bg); + background-color: var(--MI_THEME-bg); background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px); + background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px); cursor: not-allowed; } @@ -381,8 +381,8 @@ onUnmounted(() => { .embedCodeGenResultHeadingIcon { margin: 0 auto; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); text-align: center; height: 64px; width: 64px; diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 151843b18c..a4f763e895 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと --> <!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) --> -<section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);"> +<section v-if="!hasChildSection" v-panel style="border-radius: var(--MI-radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);"> <header class="_acrylic" @click="shown = !shown"> <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ph-smiley-sticker ph-bold ph-lg"></i>:{{ emojis.length }}) </header> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </section> <!-- フォルダの中にはカスタム絵文字やフォルダがある --> -<section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);"> +<section v-else v-panel style="border-radius: var(--MI-radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);"> <header class="_acrylic" @click="shown = !shown"> <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }}) </header> @@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void { elm.title = getEmojiName(emoji); } -function nestedChosen(emoji: any, ev: MouseEvent) { +function nestedChosen(emoji: string, ev: MouseEvent) { emit('chosen', emoji, ev); } </script> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 949ed4db91..dc589a28e0 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void { elm.title = getEmojiName(emoji); } -function chosen(emoji: any, ev?: MouseEvent) { +function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) { const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); @@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) { // 最近使った絵文字更新 if (!pinned.value?.includes(key)) { let recents = defaultStore.state.recentlyUsedEmojis; - recents = recents.filter((emoji: any) => emoji !== key); + recents = recents.filter((emoji) => emoji !== key); recents.unshift(key); defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); } @@ -580,7 +580,7 @@ defineExpose({ &:disabled { cursor: not-allowed; - background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%); opacity: 1; > .emoji { @@ -615,7 +615,7 @@ defineExpose({ &:disabled { cursor: not-allowed; - background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%); opacity: 1; > .emoji { @@ -638,7 +638,7 @@ defineExpose({ outline: none; border: none; background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); &:not(:focus):not(.filled) { margin-bottom: env(safe-area-inset-bottom, 0px); @@ -647,7 +647,7 @@ defineExpose({ &:not(.filled) { order: 1; z-index: 2; - box-shadow: 0px -1px 0 0px var(--divider); + box-shadow: 0px -1px 0 0px var(--MI_THEME-divider); } } @@ -658,11 +658,11 @@ defineExpose({ > .tab { flex: 1; height: 38px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); &.active { - border-top: solid 1px var(--accent); - color: var(--accent); + border-top: solid 1px var(--MI_THEME-accent); + color: var(--MI_THEME-accent); } } } @@ -681,7 +681,7 @@ defineExpose({ > .group { &:not(.index) { padding: 4px 0 8px 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } > header { @@ -708,7 +708,7 @@ defineExpose({ cursor: pointer; &:hover { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -722,7 +722,7 @@ defineExpose({ width: var(--eachSize); height: var(--eachSize); contain: strict; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); font-size: 24px; &:hover { @@ -730,13 +730,13 @@ defineExpose({ } &:active { - background: var(--accent); + background: var(--MI_THEME-accent); box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); } &:disabled { cursor: not-allowed; - background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%); opacity: 1; > .emoji { @@ -757,7 +757,7 @@ defineExpose({ } &.result { - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); &:empty { display: none; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 18e80d8445..3178f72498 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -87,7 +87,7 @@ function opening() { <style lang="scss" module> .drawer { - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; } diff --git a/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts new file mode 100644 index 0000000000..6763f7c546 --- /dev/null +++ b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import MkExtensionInstaller from './MkExtensionInstaller.vue'; +import lightTheme from '@@/themes/_light.json5'; + +export const Plugin = { + render(args) { + return { + components: { + MkExtensionInstaller, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkExtensionInstaller v-bind="props" />', + }; + }, + args: { + extension: { + type: 'plugin', + raw: '"do nothing"', + meta: { + name: 'do nothing plugin', + version: '1.0', + author: 'syuilo and misskey-project', + description: 'a plugin that does nothing', + permissions: ['read:account'], + config: { + 'doNothing': true, + }, + }, + }, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkExtensionInstaller>; + +export const Theme = { + render(args) { + return { + components: { + MkExtensionInstaller, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkExtensionInstaller v-bind="props" />', + }; + }, + args: { + extension: { + type: 'theme', + raw: JSON.stringify(lightTheme), + meta: lightTheme, + }, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkExtensionInstaller>; diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue new file mode 100644 index 0000000000..d59b20435e --- /dev/null +++ b/packages/frontend/src/components/MkExtensionInstaller.vue @@ -0,0 +1,146 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps_m" :class="$style.extInstallerRoot"> + <div :class="$style.extInstallerIconWrapper"> + <i v-if="isPlugin" class="ti ti-plug"></i> + <i v-else-if="isTheme" class="ti ti-palette"></i> + <!-- 拡張用? --> + <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> + <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 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> + + <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> + </div> +</div> +</template> + +<script lang="ts"> +export type Extension = { + type: 'plugin'; + raw: string; + meta: { + name: string; + version: string; + author: string; + description?: string; + permissions?: string[]; + config?: Record<string, unknown>; + }; +} | { + type: 'theme'; + raw: string; + meta: { + name: string; + author: string; + base?: 'light' | 'dark'; + }; +}; +</script> +<script lang="ts" setup> +import { computed } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import FormSection from '@/components/form/section.vue'; +import FormSplit from '@/components/form/split.vue'; +import MkCode from '@/components/MkCode.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import { i18n } from '@/i18n.js'; + +const isPlugin = computed(() => props.extension.type === 'plugin'); +const isTheme = computed(() => props.extension.type === 'theme'); + +const props = defineProps<{ + extension: Extension; +}>(); + +const emits = defineEmits<{ + (ev: 'confirm'): void; +}>(); +</script> + +<style lang="scss" module> +.extInstallerRoot { + border-radius: var(--MI-radius); + background: var(--MI_THEME-panel); + padding: 1.5rem; +} + +.extInstallerIconWrapper { + width: 48px; + height: 48px; + font-size: 24px; + line-height: 48px; + text-align: center; + border-radius: 50%; + margin-left: auto; + margin-right: auto; + + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); +} + +.extInstallerTitle { + font-size: 1.2rem; + text-align: center; + margin: 0; +} + +.extInstallerNormDesc { + text-align: center; +} + +.extInstallerKVList { + margin-top: 0; + margin-bottom: 0; +} +</style> diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue index 7af68a32ba..204bf69f48 100644 --- a/packages/frontend/src/components/MkFileListForAdmin.vue +++ b/packages/frontend/src/components/MkFileListForAdmin.vue @@ -66,7 +66,7 @@ const props = defineProps<{ align-items: center; &:hover { - color: var(--accent); + color: var(--MI_THEME-accent); } > .thumbnail { @@ -108,7 +108,7 @@ const props = defineProps<{ padding: 2px 4px; background: #ff0000bf; color: #fff; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); font-size: 85%; animation: sensitive-blink 1s infinite; } diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue index 85f7917658..0bb1450f87 100644 --- a/packages/frontend/src/components/MkFlashPreview.vue +++ b/packages/frontend/src/components/MkFlashPreview.vue @@ -35,7 +35,7 @@ const props = defineProps<{ &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); } &:focus-visible { @@ -82,7 +82,6 @@ const props = defineProps<{ > p { display: inline-block; margin: 0; - color: var(--urlPreviewInfo); font-size: 0.8em; line-height: 16px; vertical-align: top; @@ -91,7 +90,7 @@ const props = defineProps<{ } &:global(.gray) { - --c: var(--bg); + --c: var(--MI_THEME-bg); background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); background-size: 16px 16px; } diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index f10d58b38a..5bf3fdfe76 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="rootEl" :class="$style.root"> - <header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody"> + <header :class="$style.header" class="_button" @click="showBody = !showBody"> <div :class="$style.title"><div><slot name="header"></slot></div></div> <div :class="$style.divider"></div> <button class="_button" :class="$style.button"> @@ -32,21 +32,23 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref, shallowRef, watch } from 'vue'; -import tinycolor from 'tinycolor2'; import { miLocalStorage } from '@/local-storage.js'; import { defaultStore } from '@/store.js'; +import { getBgColor } from '@/scripts/get-bg-color.js'; const miLocalStoragePrefix = 'ui:folder:' as const; const props = withDefaults(defineProps<{ expanded?: boolean; - persistKey?: string; + persistKey?: string | null; }>(), { expanded: true, + persistKey: null, }); -const rootEl = shallowRef<HTMLDivElement>(); -const bg = ref<string>(); +const rootEl = shallowRef<HTMLElement>(); +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); watch(showBody, () => { @@ -55,47 +57,34 @@ watch(showBody, () => { } }); -function enter(element: Element) { - const el = element as HTMLElement; +function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; el.style.height = '0'; el.offsetHeight; // reflow - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; } -function afterEnter(element: Element) { - const el = element as HTMLElement; - el.style.height = 'unset'; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } -function leave(element: Element) { - const el = element as HTMLElement; +function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; el.offsetHeight; // reflow el.style.height = '0'; } -function afterLeave(element: Element) { - const el = element as HTMLElement; - el.style.height = 'unset'; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } onMounted(() => { - function getParentBg(el?: HTMLElement | null): string { - if (el == null || el.tagName === 'BODY') return 'var(--bg)'; - const background = el.style.background || el.style.backgroundColor; - if (background) { - return background; - } else { - return getParentBg(el.parentElement); - } - } - - const rawBg = getParentBg(rootEl.value); - const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - _bg.setAlpha(0.85); - bg.value = _bg.toRgbString(); + parentBg.value = getBgColor(rootEl.value?.parentElement); }); </script> @@ -118,9 +107,10 @@ onMounted(() => { position: relative; z-index: 10; position: sticky; - top: var(--stickyTop, 0px); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(20px)); + top: var(--MI-stickyTop, 0px); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(20px)); + background-color: color(from v-bind("parentBg ?? 'var(--MI_THEME-bg)'") srgb r g b / 0.85); } .title { @@ -134,7 +124,7 @@ onMounted(() => { flex: 1; margin: auto; height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .button { diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 392963fdb9..0b4114d252 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -38,9 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only > <KeepAlive> <div v-show="opened"> - <MkSpacer :marginMin="14" :marginMax="22"> + <MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22"> <slot></slot> </MkSpacer> + <div v-else> + <slot></slot> + </div> <div v-if="$slots.footer" :class="$style.footer"> <slot name="footer"></slot> </div> @@ -53,51 +56,49 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, shallowRef, ref } from 'vue'; +import { nextTick, onMounted, ref, shallowRef } from 'vue'; import { defaultStore } from '@/store.js'; +import { getBgColor } from '@/scripts/get-bg-color.js'; const props = withDefaults(defineProps<{ defaultOpen?: boolean; maxHeight?: number | null; + withSpacer?: boolean; }>(), { defaultOpen: false, maxHeight: null, + withSpacer: true, }); -const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } -}; - const rootEl = shallowRef<HTMLElement>(); const bgSame = ref(false); const opened = ref(props.defaultOpen); const openedAtLeastOnce = ref(props.defaultOpen); -function enter(el) { +function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; + el.style.height = '0'; el.offsetHeight; // reflow - el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; + el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`; } -function afterEnter(el) { - el.style.height = null; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } -function leave(el) { +function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; + el.style.height = `${elementHeight}px`; el.offsetHeight; // reflow - el.style.height = 0; + el.style.height = '0'; } -function afterLeave(el) { - el.style.height = null; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; + el.style.height = ''; } function toggle() { @@ -112,8 +113,8 @@ function toggle() { onMounted(() => { const computedStyle = getComputedStyle(document.documentElement); - const parentBg = getBgColor(rootEl.value!.parentElement!); - const myBg = computedStyle.getPropertyValue('--panel'); + const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent'; + const myBg = computedStyle.getPropertyValue('--MI_THEME-panel'); bgSame.value = parentBg === myBg; }); </script> @@ -139,15 +140,15 @@ onMounted(() => { width: 100%; box-sizing: border-box; padding: 9px 12px 9px 12px; - background: var(--folderHeaderBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - border-radius: var(--radius-sm); + background: var(--MI_THEME-folderHeaderBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-radius: var(--MI-radius-sm); transition: border-radius 0.3s; &:hover { text-decoration: none; - background: var(--folderHeaderHoverBg); + background: var(--MI_THEME-folderHeaderHoverBg); } &:focus-within { @@ -155,12 +156,12 @@ onMounted(() => { } &.active { - color: var(--accent); - background: var(--folderHeaderHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-folderHeaderHoverBg); } &.opened { - border-radius: var(--radius-sm) var(--radius-sm) 0 0; + border-radius: var(--MI-radius-sm) var(--MI-radius-sm) 0 0; } } @@ -170,7 +171,7 @@ onMounted(() => { } .headerLower { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: .85em; padding-left: 4px; } @@ -204,13 +205,13 @@ onMounted(() => { } .headerTextSub { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: .85em; } .headerRight { margin-left: auto; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); white-space: nowrap; } @@ -219,26 +220,26 @@ onMounted(() => { } .body { - background: var(--panel); - border-radius: 0 0 var(--radius-sm) var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: 0 0 var(--MI-radius-sm) var(--MI-radius-sm); container-type: inline-size; &.bgSame { - background: var(--bg); + background: var(--MI_THEME-bg); } } .footer { position: sticky !important; z-index: 1; - bottom: var(--stickyBottom, 0px); + bottom: var(--MI-stickyBottom, 0px); left: 0; padding: 12px; - background: var(--acrylicBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px); + background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px); border-radius: 0 0 6px 6px; } </style> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index 52497a2994..c7965aaac4 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -37,13 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> 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 { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { pleaseLogin } from '@/scripts/please-login.js'; -import { host } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; @@ -80,7 +80,7 @@ function onFollowChange(user: Misskey.entities.UserDetailed) { } async function onClick() { - pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` }); + pleaseLogin({ openOnRemote: { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` } }); wait.value = true; @@ -91,7 +91,10 @@ async function onClick() { text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }), }); - if (canceled) return; + if (canceled) { + wait.value = false; + return; + } await misskeyApi('following/delete', { userId: props.user.id, @@ -125,7 +128,10 @@ async function onClick() { }); hasPendingFollowRequestFromYou.value = true; - if ($i == null) return; + if ($i == null) { + wait.value = false; + return; + } claimAchievement('following1'); @@ -165,12 +171,12 @@ onBeforeUnmount(() => { position: relative; display: inline-block; font-weight: bold; - color: var(--fgOnWhite); - border: solid 1px var(--accent); + color: var(--MI_THEME-fgOnWhite); + border: solid 1px var(--MI_THEME-accent); padding: 0; height: 31px; font-size: 16px; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); background: #fff; &.full { @@ -201,17 +207,17 @@ onBeforeUnmount(() => { } &.active { - color: var(--fgOnAccent); - background: var(--accent); + color: var(--MI_THEME-fgOnAccent); + background: var(--MI_THEME-accent); &:hover { - background: var(--accentLighten); - border-color: var(--accentLighten); + background: var(--MI_THEME-accentLighten); + border-color: var(--MI_THEME-accentLighten); } &:active { - background: var(--accentDarken); - border-color: var(--accentDarken); + background: var(--MI_THEME-accentDarken); + border-color: var(--MI_THEME-accentDarken); } } diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue index 9360594236..ecb6cf882b 100644 --- a/packages/frontend/src/components/MkFormDialog.file.vue +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -66,6 +66,6 @@ function selectButton(ev: MouseEvent) { <style module> .fileNotSelected { font-weight: 700; - color: var(--infoWarnFg); + color: var(--MI_THEME-infoWarnFg); } </style> diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 124f114111..a639eae208 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click="cancel()" @ok="ok()" @close="cancel()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header> {{ title }} diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue index 1e88d59d8e..f409f6ce50 100644 --- a/packages/frontend/src/components/MkFormFooter.vue +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -36,7 +36,7 @@ const props = defineProps<{ } .text { - color: var(--warn); + color: var(--MI_THEME-warn); font-size: 90%; animation: modified-blink 2s infinite; } diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue new file mode 100644 index 0000000000..8b1c56fca4 --- /dev/null +++ b/packages/frontend/src/components/MkFukidashi.vue @@ -0,0 +1,100 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + :class="[ + $style.root, + tail === 'left' ? $style.left : $style.right, + negativeMargin === true && $style.negativeMargin, + shadow === true && $style.shadow, + ]" +> + <div :class="$style.bg"> + <svg v-if="tail !== 'none'" :class="$style.tail" version="1.1" viewBox="0 0 14.597 14.58" xmlns="http://www.w3.org/2000/svg"> + <g transform="translate(-173.71 -87.184)"> + <path d="m188.19 87.657c-1.469 2.3218-3.9315 3.8312-6.667 4.0865-2.2309-1.7379-4.9781-2.6816-7.8061-2.6815h-5.1e-4v12.702h12.702v-5.1e-4c2e-5 -1.9998-0.47213-3.9713-1.378-5.754 2.0709-1.6834 3.2732-4.2102 3.273-6.8791-6e-5 -0.49375-0.0413-0.98662-0.1235-1.4735z" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".33225" style="paint-order:stroke fill markers"/> + </g> + </svg> + <div :class="$style.content"> + <slot></slot> + </div> + </div> +</div> +</template> + +<script setup lang="ts"> +withDefaults(defineProps<{ + tail?: 'left' | 'right' | 'none'; + negativeMargin?: boolean; + shadow?: boolean; +}>(), { + tail: 'right', + negativeMargin: false, + shadow: false, +}); +</script> + +<style module lang="scss"> +.root { + --fukidashi-radius: var(--MI-radius); + --fukidashi-bg: var(--MI_THEME-panel); + + position: relative; + display: inline-block; + min-height: calc(var(--fukidashi-radius) * 2); + padding-top: calc(var(--fukidashi-radius) * .13); + + &.shadow { + filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow)); + } + + &.left { + padding-left: calc(var(--fukidashi-radius) * .13); + + &.negativeMargin { + margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1); + } + } + + &.right { + padding-right: calc(var(--fukidashi-radius) * .13); + + &.negativeMargin { + margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1); + } + } +} + +.bg { + width: 100%; + height: 100%; + background: var(--fukidashi-bg); + border-radius: var(--fukidashi-radius); +} + +.content { + position: relative; + padding: 8px 12px; +} + +.tail { + position: absolute; + top: 0; + display: block; + width: calc(var(--fukidashi-radius) * 1.13); + height: auto; + fill: var(--fukidashi-bg); +} + +.left .tail { + left: 0; + transform: rotateY(180deg); +} + +.right .tail { + right: 0; +} +</style> diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 2bb5b8762a..22f8355acf 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -75,7 +75,7 @@ function leaveHover(): void { &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); > .thumbnail { transform: scale(1.1); diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue index 54c7585f18..6cdab2479e 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -41,8 +41,8 @@ const search = () => { width: 100%; height: 40px; font-size: 16px; - border: solid 1px var(--divider); - border-radius: var(--radius-xs) 0 0 var(--radius-xs); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs) 0 0 var(--MI-radius-xs); -webkit-appearance: textfield; } @@ -50,9 +50,9 @@ const search = () => { flex-shrink: 0; margin: 0; padding: 0 16px; - border: solid 1px var(--divider); + border: solid 1px var(--MI_THEME-divider); border-left: none; - border-radius: 0 var(--radius-xs) var(--radius-xs) 0; + border-radius: 0 var(--MI-radius-xs) var(--MI-radius-xs) 0; &:active { box-shadow: 0 2px 4px rgba(#000, 0.15) inset; diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 46c2db4b1f..bdc38f5142 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -36,15 +36,15 @@ function close() { align-items: center; padding: 12px 14px; font-size: 90%; - background: color-mix(in srgb, var(--infoBg) 65%, transparent); - color: var(--infoFg); - border-radius: var(--radius); + background: color-mix(in srgb, var(--MI_THEME-infoBg) 65%, transparent); + color: var(--MI_THEME-infoFg); + border-radius: var(--MI-radius); white-space: pre-wrap; z-index: 1; &.warn { - background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent); - color: var(--infoWarnFg); + background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent); + color: var(--MI_THEME-infoWarnFg); } } diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 42e1146e27..ec299dce36 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; +import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; import { useInterval } from '@@/js/use-interval.js'; @@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js'; const props = defineProps<{ modelValue: string | number | null; - type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local'; + type?: InputHTMLAttributes['type']; required?: boolean; readonly?: boolean; disabled?: boolean; @@ -64,8 +64,8 @@ const props = defineProps<{ mfmAutocomplete?: boolean | SuggestionType[], autocapitalize?: string; spellcheck?: boolean; - inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal'; - step?: any; + inputmode?: InputHTMLAttributes['inputmode']; + step?: InputHTMLAttributes['step']; datalist?: string[]; min?: number; max?: number | string; @@ -199,7 +199,7 @@ defineExpose({ .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -216,8 +216,8 @@ defineExpose({ &.focused { > .inputCore { - border-color: var(--accent) !important; - //box-shadow: 0 0 0 4px var(--focus); + border-color: var(--MI_THEME-accent) !important; + //box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } @@ -242,17 +242,17 @@ defineExpose({ font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; transition: border-color 0.1s ease-out; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index 10b390e7f9..b063b82b17 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -46,15 +46,15 @@ function getInstanceIcon(instance): string { display: flex; align-items: center; padding: 16px; - background: var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); > :global(.icon) { display: block; width: ($bodyTitleHieght + $bodyInfoHieght); height: ($bodyTitleHieght + $bodyInfoHieght); object-fit: cover; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); margin-right: 10px; } @@ -62,7 +62,7 @@ function getInstanceIcon(instance): string { flex: 1; overflow: hidden; font-size: 0.9em; - color: var(--fg); + color: var(--MI_THEME-fg); padding-right: 8px; > :global(.host) { @@ -109,7 +109,7 @@ function getInstanceIcon(instance): string { } &:global(.gray) { - --c: var(--bg); + --c: var(--MI_THEME-bg); background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); background-size: 16px 16px; } diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index d74c885041..8ccbf61e48 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -121,7 +121,7 @@ function createDoughnut(chartEl, tooltip, data) { labels: data.map(x => x.name), datasets: [{ backgroundColor: data.map(x => x.color), - borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'), + borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'), borderWidth: 2, hoverOffset: 0, data: data.map(x => x.value), @@ -256,8 +256,8 @@ onMounted(() => { flex: 1; min-width: 0; position: relative; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); padding: 24px; max-height: 300px; diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 46d42248d3..2a8d5c9f71 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -46,7 +46,7 @@ $height: 2ex; display: flex; align-items: center; height: $height; - border-radius: var(--radius-xs) 0 0 var(--radius-xs); + border-radius: var(--MI-radius-xs) 0 0 var(--MI-radius-xs); overflow: clip; color: #fff; text-shadow: /* .866 ≈ sin(60deg) */ diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index 4aee64f78e..1a71f6574f 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ invite.code }}</template> <template #suffix> <span v-if="invite.used">{{ i18n.ts.used }}</span> - <span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span> - <span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span> + <span v-else-if="isExpired" style="color: var(--MI_THEME-error)">{{ i18n.ts.expired }}</span> + <span v-else style="color: var(--MI_THEME-success)">{{ i18n.ts.unused }}</span> </template> <template #footer> <div class="_buttons"> diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index e0880ec3e7..c7af75e2e7 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -12,13 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="icon" :class="item.icon"></i> <div class="text">{{ item.text }}</div> <span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> - <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> + <span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span> </button> <MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()"> <i class="icon" :class="item.icon"></i> <div class="text">{{ item.text }}</div> <span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span> - <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> + <span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span> </MkA> </template> </div> @@ -77,12 +77,12 @@ function close() { overflow: auto; overscroll-behavior: contain; text-align: left; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); &.asDrawer { width: 100%; padding: 16px 16px max(env(safe-area-inset-bottom, 0px), 16px) 16px; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; text-align: center; @@ -100,13 +100,13 @@ function close() { justify-content: center; vertical-align: bottom; height: 100px; - border-radius: var(--radius); + border-radius: var(--MI-radius); padding: 10px; box-sizing: border-box; &:hover { - color: var(--accent); - background: var(--accentedBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-accentedBg); text-decoration: none; } @@ -137,9 +137,8 @@ function close() { position: absolute; top: 32px; left: 32px; - color: var(--indicator); + color: var(--MI_THEME-indicator); font-size: 8px; - animation: global-blink 1s infinite; @media (max-width: 500px) { top: 16px; diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 2d7cde1af2..10450fb621 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -394,8 +394,8 @@ onDeactivated(() => { .audioContainer { container-type: inline-size; position: relative; - border: .5px solid var(--divider); - border-radius: var(--radius); + border: .5px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); overflow: clip; &:focus-visible { @@ -415,7 +415,7 @@ onDeactivated(() => { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } } @@ -457,12 +457,12 @@ onDeactivated(() => { .controlButton { padding: 6px; - border-radius: calc(var(--radius) / 2); + border-radius: calc(var(--MI-radius) / 2); font-size: 1.05rem; &:hover { - color: var(--accent); - background-color: var(--accentedBg); + color: var(--MI_THEME-accent); + background-color: var(--MI_THEME-accentedBg); } &:focus-visible { diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 77a86ff2fb..56048a33d8 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -53,7 +53,7 @@ async function show() { <style lang="scss" module> .root { width: 100%; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); margin-top: 4px; overflow: clip; } @@ -68,7 +68,6 @@ async function show() { } .download { - background: var(--noteAttachedFile); } .sensitive { @@ -77,7 +76,7 @@ async function show() { } .audio { - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; } </style> diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 02c054956c..4aca7256a5 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.indicators"> <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> <div v-if="image.comment" :class="$style.indicator">ALT</div> - <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> + <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> <div v-if="!image.comment" :class="$style.indicator" title="Image lacks descriptive text"><i class="ph-pencil-simple ph-bold ph-lg-off"></i></div> </div> <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button> @@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } } @@ -186,9 +186,9 @@ function showMenu(ev: MouseEvent) { .hide { display: block; position: absolute; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); background-color: black; - color: var(--accentLighten); + color: var(--MI_THEME-accentLighten); font-size: 12px; opacity: .5; padding: 5px 8px; @@ -207,28 +207,28 @@ function showMenu(ev: MouseEvent) { .visible { position: relative; - //box-shadow: 0 0 0 1px var(--divider) inset; - background: var(--bg); + //box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset; + background: var(--MI_THEME-bg); background-size: 16px 16px; } html[data-color-scheme=dark] .visible { --c: rgb(255 255 255 / 2%); - background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%); } html[data-color-scheme=light] .visible { --c: rgb(0 0 0 / 2%); - background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%); } .menu { display: block; position: absolute; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); background-color: rgba(0, 0, 0, 0.3); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); color: #fff; font-size: 0.8em; width: 28px; @@ -259,10 +259,10 @@ html[data-color-scheme=light] .visible { } .indicator { - /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + /* 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: var(--radius-sm); - color: var(--accentLighten); + border-radius: var(--MI-radius-sm); + color: var(--MI_THEME-accentLighten); 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 11529c186f..487cf509d6 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -339,19 +339,19 @@ defineExpose({ .media { overflow: hidden; // clipにするとバグる - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } :global(.pswp) { --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important; - --pswp-bg: var(--modalBg) !important; + --pswp-bg: var(--MI_THEME-modalBg) !important; } </style> <style lang="scss"> .pswp__bg { - background: var(--modalBg); - backdrop-filter: var(--modalBgFilter); + background: var(--MI_THEME-modalBg); + backdrop-filter: var(--MI-modalBgFilter); } .pswp__alt-text-container { @@ -369,14 +369,14 @@ defineExpose({ } .pswp__alt-text { - color: var(--fg); + color: var(--MI_THEME-fg); margin: 0 auto; text-align: center; - padding: var(--margin); - border-radius: var(--radius); + padding: var(--MI-margin); + border-radius: var(--MI-radius); max-height: 8em; overflow-y: auto; - text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px; + text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px; white-space: pre-line; } </style> diff --git a/packages/frontend/src/components/MkMediaRange.vue b/packages/frontend/src/components/MkMediaRange.vue index 86ed8ba2cf..df7505b0c3 100644 --- a/packages/frontend/src/components/MkMediaRange.vue +++ b/packages/frontend/src/components/MkMediaRange.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- Media系専用のinput range --> <template> -<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'"> +<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--MI_THEME-scrollbarHandle);'"> <div :class="$style.controlsSeekbar"> <progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress> <input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/> @@ -48,7 +48,7 @@ const modelValue = computed({ background: transparent; border: 0; border-radius: 26px; - color: var(--accent); + color: var(--MI_THEME-accent); display: block; height: 19px; margin: 0; diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 0502bdd401..04f8e9f8d8 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i> <div :class="$style.indicators"> <div v-if="video.comment" :class="$style.indicator">ALT</div> - <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> + <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> </div> </div> @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i> <div :class="$style.indicators"> <div v-if="video.comment" :class="$style.indicator">ALT</div> - <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> + <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> </div> <div :class="$style.videoControls" @click.self="togglePlayPause"> <div :class="[$style.controlsChild, $style.controlsLeft]"> @@ -121,7 +121,7 @@ import { hms } from '@/filters/hms.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; +import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js'; import hasAudio from '@/scripts/media-has-audio.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; import { $i, iAmModerator } from '@/account.js'; @@ -337,26 +337,21 @@ function togglePlayPause() { } function toggleFullscreen() { - if (isFullscreenNotSupported && videoEl.value) { - if (isFullscreen.value) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - videoEl.value.webkitExitFullscreen(); - isFullscreen.value = false; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - videoEl.value.webkitEnterFullscreen(); - isFullscreen.value = true; - } - } else if (playerEl.value) { - if (isFullscreen.value) { - document.exitFullscreen(); - isFullscreen.value = false; - } else { - playerEl.value.requestFullscreen({ navigationUI: 'hide' }); - isFullscreen.value = true; - } + if (playerEl.value == null || videoEl.value == null) return; + if (isFullscreen.value) { + exitFullscreen({ + videoEl: videoEl.value, + }); + isFullscreen.value = false; + } else { + requestFullscreen({ + videoEl: videoEl.value, + playerEl: playerEl.value, + options: { + navigationUI: 'hide', + }, + }); + isFullscreen.value = true; } } @@ -457,8 +452,10 @@ watch(loop, (to) => { }); watch(hide, (to) => { - if (to && isFullscreen.value) { - document.exitFullscreen(); + if (videoEl.value && to && isFullscreen.value) { + exitFullscreen({ + videoEl: videoEl.value, + }); isFullscreen.value = false; } }); @@ -511,7 +508,7 @@ onDeactivated(() => { height: 100%; pointer-events: none; border-radius: inherit; - box-shadow: inset 0 0 0 4px var(--warn); + box-shadow: inset 0 0 0 4px var(--MI_THEME-warn); } } @@ -526,10 +523,10 @@ onDeactivated(() => { } .indicator { - /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + /* 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(--accentLighten); + color: var(--MI_THEME-accentLighten); display: inline-block; font-weight: bold; font-size: 0.8em; @@ -539,9 +536,9 @@ onDeactivated(() => { .hide { display: block; position: absolute; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); background-color: black; - color: var(--accentLighten); + color: var(--MI_THEME-accentLighten); font-size: 12px; opacity: .5; padding: 5px 8px; @@ -595,7 +592,7 @@ onDeactivated(() => { opacity: 0; transition: opacity .4s ease-in-out; - background: var(--accent); + background: var(--MI_THEME-accent); color: #fff; padding: 1rem; border-radius: 99rem; @@ -661,12 +658,12 @@ onDeactivated(() => { .controlButton { padding: 6px; - border-radius: calc(var(--radius) / 2); + border-radius: calc(var(--MI-radius) / 2); transition: background-color .2s ease-in-out; font-size: 1.05rem; &:hover { - background-color: var(--accent); + background-color: var(--MI_THEME-accent); } &:focus-visible { @@ -728,10 +725,10 @@ onDeactivated(() => { } .indicator { - /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + /* 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: var(--radius-sm); - color: var(--accentLighten); + border-radius: var(--MI-radius-sm); + color: var(--MI_THEME-accentLighten); display: inline-block; font-weight: bold; font-size: 0.8em; diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index de2048b6f2..f64ca4bc77 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior"> +<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :behavior="navigationBehavior"> <img :class="$style.icon" :src="avatarUrl" alt=""> <span> <span>@{{ username }}</span> @@ -16,7 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { toUnicode } from 'punycode'; import { computed } from 'vue'; -import tinycolor from 'tinycolor2'; import { host as localHost } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; @@ -37,11 +36,7 @@ const isMe = $i && ( `@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase() ); -const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention')); -bg.setAlpha(0.1); -const bgCss = bg.toRgbString(); - -const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages +const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar ? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) : `/avatar/@${props.username}@${props.host}`, ); @@ -51,12 +46,14 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages .root { display: inline-block; padding: 4px 8px 4px 4px; - border-radius: var(--radius-ellipse); - color: var(--mention); + border-radius: var(--MI-radius-ellipse); + color: var(--MI_THEME-mention); + background: color(from var(--MI_THEME-mention) srgb r g b / 0.1); white-space: nowrap; &.isMe { - color: var(--mentionMe); + color: var(--MI_THEME-mentionMe); + background: color(from var(--MI_THEME-mentionMe) srgb r g b / 0.1); } } @@ -66,7 +63,7 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages object-fit: cover; margin: 0 0.2em 0 0; vertical-align: bottom; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .host { diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index fe6df7090c..b0a1b80210 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </MkA> <a @@ -68,7 +68,7 @@ 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> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </a> <button @@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> <div v-if="item.indicate" :class="$style.item_content"> - <span :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> <button @@ -161,7 +161,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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> - <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> + <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </div> </button> </template> @@ -437,9 +437,11 @@ onBeforeUnmount(() => { &.big:not(.asDrawer) { > .menu { + min-width: 230px; + > .item { padding: 6px 20px; - font-size: 1em; + font-size: 0.95em; line-height: 24px; } } @@ -452,7 +454,7 @@ onBeforeUnmount(() => { > .menu { padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; width: 100%; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; @@ -462,7 +464,7 @@ onBeforeUnmount(() => { &::before { width: calc(100% - 24px); - border-radius: var(--radius); + border-radius: var(--MI-radius); } > .icon { @@ -505,7 +507,7 @@ onBeforeUnmount(() => { overflow: hidden; text-overflow: ellipsis; text-decoration: none !important; - color: var(--menuFg, var(--fg)); + color: var(--menuFg, var(--MI_THEME-fg)); &::before { content: ""; @@ -518,14 +520,14 @@ onBeforeUnmount(() => { margin: auto; width: calc(100% - 16px); height: 100%; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } &:focus-visible { outline: none; &:not(:hover):not(:active)::before { - outline: var(--focus) solid 2px; + outline: var(--MI_THEME-focus) solid 2px; outline-offset: -2px; } } @@ -534,19 +536,19 @@ onBeforeUnmount(() => { &:hover, &:focus-visible:active, &:focus-visible.active { - color: var(--menuHoverFg, var(--accent)); + color: var(--menuHoverFg, var(--MI_THEME-accent)); &::before { - background-color: var(--menuHoverBg, var(--accentedBg)); + background-color: var(--menuHoverBg, var(--MI_THEME-accentedBg)); } } &:not(:focus-visible):active, &:not(:focus-visible).active { - color: var(--menuActiveFg, var(--fgOnAccent)); + color: var(--menuActiveFg, var(--MI_THEME-fgOnAccent)); &::before { - background-color: var(--menuActiveBg, var(--accent)); + background-color: var(--menuActiveBg, var(--MI_THEME-accent)); } } } @@ -564,13 +566,13 @@ onBeforeUnmount(() => { } &.radio { - --menuActiveFg: var(--accent); - --menuActiveBg: var(--accentedBg); + --menuActiveFg: var(--MI_THEME-accent); + --menuActiveBg: var(--MI_THEME-accentedBg); } &.parent { - --menuActiveFg: var(--accent); - --menuActiveBg: var(--accentedBg); + --menuActiveFg: var(--MI_THEME-accent); + --menuActiveBg: var(--MI_THEME-accentedBg); } &.label { @@ -635,14 +637,13 @@ onBeforeUnmount(() => { .indicator { display: flex; align-items: center; - color: var(--indicator); + color: var(--MI_THEME-indicator); font-size: 12px; - animation: global-blink 1s infinite; } .divider { margin: 8px 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .radioIcon { @@ -652,11 +653,11 @@ onBeforeUnmount(() => { height: 1em; vertical-align: -0.125em; border-radius: 50%; - border: solid 2px var(--divider); - background-color: var(--panel); + border: solid 2px var(--MI_THEME-divider); + background-color: var(--MI_THEME-panel); &.radioChecked { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); &::after { content: ""; @@ -668,7 +669,7 @@ onBeforeUnmount(() => { width: 50%; height: 50%; border-radius: 50%; - background-color: var(--accent); + background-color: var(--MI_THEME-accent); } } } diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index 1b6f6cef31..7ea585ecc2 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('--accent')); +const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent')); const color = accent.toRgbString(); function draw(): void { diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index f26959888b..f06cfffee4 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -26,11 +26,11 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue'; import MkModal from './MkModal.vue'; const props = withDefaults(defineProps<{ - withOkButton: boolean; - withCloseButton: boolean; - okButtonDisabled: boolean; - width: number; - height: number; + withOkButton?: boolean; + withCloseButton?: boolean; + okButtonDisabled?: boolean; + width?: number; + height?: number; }>(), { withOkButton: false, withCloseButton: true, @@ -90,12 +90,12 @@ defineExpose({ display: flex; flex-direction: column; contain: content; - border-radius: var(--radius); + border-radius: var(--MI-radius); --root-margin: 24px; - --headerHeight: 46px; - --headerHeightNarrow: 42px; + --MI_THEME-headerHeight: 46px; + --MI_THEME-headerHeightNarrow: 42px; @media (max-width: 500px) { --root-margin: 16px; @@ -105,24 +105,24 @@ defineExpose({ .header { display: flex; flex-shrink: 0; - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + background: var(--MI_THEME-windowHeader); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .headerButton { - height: var(--headerHeight); - width: var(--headerHeight); + height: var(--MI_THEME-headerHeight); + width: var(--MI_THEME-headerHeight); @media (max-width: 500px) { - height: var(--headerHeightNarrow); - width: var(--headerHeightNarrow); + height: var(--MI_THEME-headerHeightNarrow); + width: var(--MI_THEME-headerHeightNarrow); } } .title { flex: 1; - line-height: var(--headerHeight); + line-height: var(--MI_THEME-headerHeight); padding-left: 32px; font-weight: bold; white-space: nowrap; @@ -131,7 +131,7 @@ defineExpose({ pointer-events: none; @media (max-width: 500px) { - line-height: var(--headerHeightNarrow); + line-height: var(--MI_THEME-headerHeightNarrow); padding-left: 16px; } } @@ -143,7 +143,7 @@ defineExpose({ .body { flex: 1; overflow: auto; - background: var(--panel); + background: var(--MI_THEME-panel); container-type: size; } </style> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 323f6e994e..39f4806f0c 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" - :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" + :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]" :tabindex="isDeleted ? '-1' : '0'" > <div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo"> @@ -132,7 +132,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" :class="$style.footerButton" class="_button" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @click.stop @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()" > @@ -156,8 +156,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <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="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -201,6 +201,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro import * as mfm from '@transfem-org/sfm-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 { MenuItem } from '@/types/menu.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; @@ -215,6 +218,7 @@ import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkButton from '@/components/MkButton.vue'; import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/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'; @@ -233,13 +237,10 @@ 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 type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { useRouter } from '@/router/supplier.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; -import { shouldCollapsed } from '@@/js/collapsed.js'; -import { host } from '@@/js/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; @@ -264,6 +265,7 @@ const emit = defineEmits<{ const router = useRouter(); const inTimeline = inject<boolean>('inTimeline', false); +const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true)); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); @@ -343,15 +345,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; */ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { - if (mutedWords == null) return false; - - if (checkWordMute(noteToCheck, $i, mutedWords)) return true; - if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; - if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + if (mutedWords != null) { + if (checkWordMute(noteToCheck, $i, mutedWords)) return true; + if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; + if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + } if (checkOnly) return false; - if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; + if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) { + return 'sensitiveMute'; + } + return false; } @@ -514,7 +519,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -564,7 +569,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (props.mock) { return; @@ -625,7 +630,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (props.mock) { return; } @@ -638,7 +643,7 @@ function reply(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.mock) { @@ -660,7 +665,7 @@ function like(): void { } function react(viaKeyboard = false): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -808,15 +813,24 @@ function showRenoteMenu(): void { }; } + const renoteDetailsMenu: MenuItem = { + type: 'link', + text: i18n.ts.renoteDetails, + icon: 'ti ti-info-circle', + to: notePage(note.value), + }; + if (isMyRenote) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), ], renoteTime.value); } else { os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), @@ -877,14 +891,6 @@ function emitUpdReaction(emoji: string, delta: number) { overflow: clip; contain: content; - // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 - // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう - // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、 - // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる - // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?) - //content-visibility: auto; - //contain-intrinsic-size: 0 128px; - &:focus-visible { outline: none; @@ -901,8 +907,8 @@ function emitUpdReaction(emoji: string, delta: number) { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 2px var(--focus); - border-radius: var(--radius); + border: dashed 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -929,9 +935,9 @@ function emitUpdReaction(emoji: string, delta: number) { right: 12px; padding: 0 4px; margin-bottom: 0 !important; - background: var(--popup); - border-radius: var(--radius-sm); - box-shadow: 0px 4px 32px var(--shadow); + background: var(--MI_THEME-popup); + border-radius: var(--MI-radius-sm); + box-shadow: 0px 4px 32px var(--MI_THEME-shadow); } .footerButton { @@ -950,6 +956,11 @@ function emitUpdReaction(emoji: string, delta: number) { } } +.skipRender { + content-visibility: auto; + contain-intrinsic-size: 0 150px; +} + .tip { display: flex; align-items: center; @@ -976,7 +987,7 @@ function emitUpdReaction(emoji: string, delta: number) { padding: 16px 32px 8px 32px; line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); & + .article { padding-top: 8px; @@ -1070,7 +1081,7 @@ function emitUpdReaction(emoji: string, delta: number) { left: 8px; width: 5px; height: calc(100% - 16px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -1078,10 +1089,10 @@ function emitUpdReaction(emoji: string, delta: number) { flex-shrink: 0; display: block !important; margin: 0 14px 0 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); position: sticky !important; - top: calc(22px + var(--stickyTop, 0px)); + top: calc(22px + var(--MI-stickyTop, 0px)); left: 0; } @@ -1101,15 +1112,15 @@ function emitUpdReaction(emoji: string, delta: number) { width: 100%; margin-top: 14px; position: sticky; - bottom: calc(var(--stickyBottom, 0px) - 100px); + bottom: calc(var(--MI-stickyBottom, 0px) - 100px); } .showLessLabel { display: inline-block; - background: var(--popup); + background: var(--MI_THEME-popup); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1127,19 +1138,19 @@ function emitUpdReaction(emoji: string, delta: number) { z-index: 2; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); &:hover > .collapsedLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } .collapsedLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1149,13 +1160,13 @@ function emitUpdReaction(emoji: string, delta: number) { } .replyIcon { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1178,8 +1189,8 @@ function emitUpdReaction(emoji: string, delta: number) { .quoteNote { padding: 16px; - border: dashed 1px var(--renote); - border-radius: var(--radius-sm); + border: dashed 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1202,7 +1213,7 @@ function emitUpdReaction(emoji: string, delta: number) { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1277,7 +1288,7 @@ function emitUpdReaction(emoji: string, delta: number) { margin: 0 10px 0 0; width: 46px; height: 46px; - top: calc(14px + var(--stickyTop, 0px)); + top: calc(14px + var(--MI-stickyTop, 0px)); } } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 5acb18c871..a537d8afb9 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -63,7 +63,14 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> - <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> + <div :class="$style.noteHeaderUsernameAndBadgeRoles"> + <div :class="$style.noteHeaderUsername"> + <MkAcct :user="appearNote.user"/> + </div> + <div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles"> + <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/> + </div> + </div> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> </div> </header> @@ -135,7 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown.prevent="renoted ? undoRenote() : boostVisibility()" > <i class="ti ti-repeat"></i> @@ -157,8 +164,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <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="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -236,6 +243,7 @@ import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vu import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; +import { host } from '@@/js/config.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -259,10 +267,8 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { host } from '@@/js/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; - import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -507,7 +513,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -553,7 +559,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -611,7 +617,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: appearNote.value, @@ -622,7 +628,7 @@ function reply(): void { } function react(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -659,7 +665,7 @@ function react(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -741,7 +747,7 @@ async function clip(): Promise<void> { function showRenoteMenu(): void { if (!isMyRenote) return; - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([{ text: i18n.ts.unrenote, icon: 'ti ti-trash', @@ -843,8 +849,8 @@ function animatedMFM() { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 2px var(--focus); - border-radius: var(--radius); + border: dashed 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -874,7 +880,7 @@ function animatedMFM() { padding: 16px 32px 8px 32px; line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); } .renoteAvatar { @@ -883,7 +889,7 @@ function animatedMFM() { width: 28px; height: 28px; margin: 0 8px 0 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .renoteText { @@ -932,8 +938,8 @@ function animatedMFM() { .noteHeaderAvatar { display: block; flex-shrink: 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .noteHeaderBody { @@ -956,16 +962,21 @@ function animatedMFM() { padding: 4px 6px; font-size: 80%; line-height: 1; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .noteHeaderInfo { float: right; } +.noteHeaderUsernameAndBadgeRoles { + display: flex; +} + .noteHeaderUsername { margin-bottom: 2px; + margin-right: 0.5em; line-height: 1.3; word-wrap: anywhere; } @@ -974,6 +985,19 @@ function animatedMFM() { margin-top: 5px; } +.noteHeaderBadgeRoles { + margin: 0 .5em 0 0; +} + +.noteHeaderBadgeRole { + height: 1.3em; + vertical-align: -20%; + + & + .noteHeaderBadgeRole { + margin-left: 0.2em; + } +} + .noteContent { container-type: inline-size; overflow-wrap: break-word; @@ -989,19 +1013,19 @@ function animatedMFM() { } .noteReplyTarget { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .rn { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1016,8 +1040,8 @@ function animatedMFM() { .quoteNote { padding: 16px; - border: dashed 1px var(--renote); - border-radius: var(--radius-sm); + border: dashed 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1042,7 +1066,7 @@ function animatedMFM() { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1052,17 +1076,17 @@ function animatedMFM() { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } .reply:not(:first-child) { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .tabs { - border-top: solid 0.5px var(--divider); - border-bottom: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); display: flex; } @@ -1074,7 +1098,7 @@ function animatedMFM() { } .tabActive { - border-bottom: solid 2px var(--accent); + border-bottom: solid 2px var(--MI_THEME-accent); } .tab_renotes { @@ -1094,12 +1118,12 @@ function animatedMFM() { .reactionTab { padding: 4px 6px; - border: solid 1px var(--divider); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); } .reactionTabActive { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } @container (max-width: 500px) { diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 2c69048ec5..3b15242685 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,18 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <header :class="$style.root"> - <component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;"> - <div style="display: flex; white-space: nowrap; align-items: baseline;"> - <div v-if="mock" :class="$style.name"> - <MkUserName :user="note.user"/> - </div> - <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - <div v-if="note.user.isBot" :class="$style.isBot">bot</div> - <div :class="$style.username"><MkAcct :user="note.user"/></div> - </div> - </component> + <div v-if="mock" :class="$style.name"> + <MkUserName :user="note.user"/> + </div> + <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + <div v-if="note.user.isBot" :class="$style.isBot">bot</div> + <div :class="$style.username"><MkAcct :user="note.user"/></div> <div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> </div> @@ -98,8 +94,8 @@ const mock = inject<boolean>('mock', false); margin: 0 .5em 0 0; padding: 1px 6px; font-size: 80%; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .username { diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 7ccc2c0320..4f907231bb 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -56,7 +56,7 @@ const props = defineProps<{ margin: 0 10px 0 0 !important; width: 40px !important; height: 40px !important; - border-radius: var(--radius-sm) !important; + border-radius: var(--MI-radius-sm) !important; pointer-events: none !important; } diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index a369d84783..9ef45a0ec3 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -95,8 +95,8 @@ watch(() => props.expandAllCws, (expandAllCws) => { } .button{ - margin-right: var(--margin); - margin-bottom: var(--margin); + margin-right: var(--MI-margin); + margin-bottom: var(--MI-margin); } .avatar { @@ -105,9 +105,9 @@ watch(() => props.expandAllCws, (expandAllCws) => { margin: 0 10px 0 0; width: 34px; height: 34px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); position: sticky !important; - top: calc(16px + var(--stickyTop, 0px)); + top: calc(16px + var(--MI-stickyTop, 0px)); left: 0; } diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 45276839ad..3523babe46 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown="renoted ? undoRenote() : boostVisibility()" > <i class="ph-rocket-launch ph-bold ph-lg"></i> @@ -98,7 +98,8 @@ import { $i } from '@/account.js'; import { userPage } from '@/filters/user.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { defaultStore } from '@/store.js'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import { host } from '@@/js/config.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -145,6 +146,11 @@ const isRenote = ( props.note.poll == null ); +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + async function addReplyTo(replyNote: Misskey.entities.Note) { replies.value.unshift(replyNote); appearNote.value.repliesCount += 1; @@ -182,7 +188,7 @@ function focus() { } function reply(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: props.note, @@ -194,7 +200,7 @@ function reply(viaKeyboard = false): void { } function react(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.note.reactionAcceptance === 'likeOnly') { @@ -228,7 +234,7 @@ function react(viaKeyboard = false): void { } function like(): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -288,7 +294,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -332,7 +338,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -438,7 +444,7 @@ if (props.detail) { left: 8px; width: 5px; height: calc(100% - 8px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -448,7 +454,7 @@ if (props.detail) { margin: 0 8px 0 0; width: 38px; height: 38px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .body { @@ -475,7 +481,7 @@ if (props.detail) { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -493,7 +499,7 @@ if (props.detail) { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -510,7 +516,7 @@ if (props.detail) { } .reply, .more { - border-left: solid 0.5px var(--divider); + border-left: solid 0.5px var(--MI_THEME-divider); margin-top: 10px; } @@ -531,8 +537,8 @@ if (props.detail) { .muted { text-align: center; padding: 8px !important; - border: 1px solid var(--divider); + border: 1px solid var(--MI_THEME-divider); margin: 8px 8px 0 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } </style> diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 15173fbd99..b13df2813b 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -61,20 +61,20 @@ defineExpose({ <style lang="scss" module> .root { &.noGap { - border-radius: var(--radius); + border-radius: var(--MI-radius); > .notes { - background: color-mix(in srgb, var(--panel) 65%, transparent); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); } } &:not(.noGap) { > .notes { - background: var(--bg); + background: var(--MI_THEME-bg); .note { - background: color-mix(in srgb, var(--panel) 65%, transparent); - border-radius: var(--radius); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); + border-radius: var(--MI-radius); } } } diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index f713d46137..4620b966af 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -7,13 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <div :class="$style.head"> <MkAvatar v-if="['pollEnded', 'note', 'edited', 'scheduledNotePosted'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> - <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> - <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> + <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> + <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/> - <MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/> <img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> <div :class="[$style.subIcon, { @@ -27,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_exportCompleted]: notification.type === 'exportCompleted', + [$style.t_login]: notification.type === 'login', [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_pollEnded]: notification.type === 'edited', [$style.t_roleAssigned]: notification.type === 'scheduledNoteFailed', @@ -43,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <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> <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> @@ -66,6 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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 === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> + <span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> @@ -243,13 +245,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) overflow-wrap: break-word; display: flex; contain: content; + content-visibility: auto; + contain-intrinsic-size: 0 100px; --eventFollow: #36aed2; --eventRenote: #36d298; --eventReply: #007aff; - --eventReactionHeart: var(--love); + --eventReactionHeart: var(--MI_THEME-love); --eventReaction: #e99a0b; --eventAchievement: #cb9a11; + --eventLogin: #007aff; --eventOther: #88a6b7; } @@ -277,7 +282,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) width: 80%; height: 80%; font-size: 15px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); color: #fff; } @@ -294,7 +299,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) } .icon_app { - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .subIcon { @@ -305,9 +310,9 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) width: 20px; height: 20px; box-sizing: border-box; - border-radius: var(--radius-full); - background: var(--panel); - box-shadow: 0 0 0 3px var(--panel); + border-radius: var(--MI-radius-full); + background: var(--MI_THEME-panel); + box-shadow: 0 0 0 3px var(--MI_THEME-panel); font-size: 11px; text-align: center; color: #fff; @@ -371,6 +376,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_login { + padding: 3px; + background: var(--eventLogin); + pointer-events: none; +} + .tail { flex: 1; min-width: 0; @@ -452,9 +463,9 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) width: 20px; height: 20px; box-sizing: border-box; - border-radius: var(--radius-full); - background: var(--panel); - box-shadow: 0 0 0 3px var(--panel); + border-radius: var(--MI-radius-full); + background: var(--MI_THEME-panel); + box-shadow: 0 0 0 3px var(--MI_THEME-panel); font-size: 11px; text-align: center; color: #fff; diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 47a9c79e45..d07827d11a 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any); +const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap); function ok() { emit('done', { diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index a395734add..51c4ea7ce4 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -111,6 +111,6 @@ defineExpose({ <style lang="scss" module> .list { - background: var(--panel); + background: var(--MI_THEME-panel); } </style> diff --git a/packages/frontend/src/components/MkNumberDiff.vue b/packages/frontend/src/components/MkNumberDiff.vue index 1825cc5405..80c634fdce 100644 --- a/packages/frontend/src/components/MkNumberDiff.vue +++ b/packages/frontend/src/components/MkNumberDiff.vue @@ -24,11 +24,11 @@ const isZero = computed(() => props.value === 0); <style lang="scss" module> .isPlus { - color: var(--success); + color: var(--MI_THEME-success); } .isMinus { - color: var(--error); + color: var(--MI_THEME-error); } .isZero { diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue index 870599aa94..7fa8c23c6c 100644 --- a/packages/frontend/src/components/MkObjectView.value.vue +++ b/packages/frontend/src/components/MkObjectView.value.vue @@ -39,7 +39,7 @@ import number from '@/filters/number.js'; import XValue from '@/components/MkObjectView.value.vue'; const props = defineProps<{ - value: any; + value: unknown; }>(); const collapsed = reactive({}); @@ -50,19 +50,19 @@ if (isObject(props.value)) { } } -function isObject(v): boolean { +function isObject(v: unknown): v is Record<PropertyKey, unknown> { return typeof v === 'object' && !Array.isArray(v) && v !== null; } -function isArray(v): boolean { +function isArray(v: unknown): v is unknown[] { return Array.isArray(v); } -function isEmpty(v): boolean { +function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] { return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0); } -function collapsable(v): boolean { +function collapsable(v: unknown): boolean { return (isObject(v) || isArray(v)) && !isEmpty(v); } </script> @@ -78,7 +78,7 @@ function collapsable(v): boolean { > .boolean { display: inline; - color: var(--codeBoolean); + color: var(--MI_THEME-codeBoolean); &.true { font-weight: bold; @@ -91,12 +91,12 @@ function collapsable(v): boolean { > .string { display: inline; - color: var(--codeString); + color: var(--MI_THEME-codeString); } > .number { display: inline; - color: var(--codeNumber); + color: var(--MI_THEME-codeNumber); } > .array.empty { @@ -127,7 +127,7 @@ function collapsable(v): boolean { > .toggle { width: 16px; - color: var(--accent); + color: var(--MI_THEME-accent); visibility: hidden; &.visible { diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue index 94cbaf5c91..b978a71c15 100644 --- a/packages/frontend/src/components/MkOmit.vue +++ b/packages/frontend/src/components/MkOmit.vue @@ -47,7 +47,7 @@ onUnmounted(() => { <style lang="scss" module> .content { - --stickyTop: 0px; + --MI-stickyTop: 0px; &.omitted { position: relative; @@ -62,20 +62,20 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } &:hover { > .fadeLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } } diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue index 361cc520de..29fdc91a2b 100644 --- a/packages/frontend/src/components/MkPagePreview.vue +++ b/packages/frontend/src/components/MkPagePreview.vue @@ -41,7 +41,7 @@ const props = defineProps<{ .eyeCatchingImageRoot { width: 100%; height: 200px; - border-radius: var(--radius) var(--radius) 0 0; + border-radius: var(--MI-radius) var(--MI-radius) 0 0; overflow: hidden; } </style> @@ -53,7 +53,7 @@ const props = defineProps<{ &:hover { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); } &:focus-within { @@ -66,22 +66,22 @@ const props = defineProps<{ left: 0; width: 100%; height: 100%; - border-radius: var(--radius); + border-radius: var(--MI-radius); pointer-events: none; - box-shadow: inset 0 0 0 2px var(--focus); + box-shadow: inset 0 0 0 2px var(--MI_THEME-focus); } } > .thumbnail { & + article { - border-radius: 0 0 var(--radius) var(--radius); + border-radius: 0 0 var(--MI-radius) var(--MI-radius); } } > article { - background-color: var(--panel); + background-color: var(--MI_THEME-panel); padding: 16px; - border-radius: var(--radius); + border-radius: var(--MI-radius); > header { margin-bottom: 8px; @@ -114,7 +114,6 @@ const props = defineProps<{ > p { display: inline-block; margin: 0; - color: var(--urlPreviewInfo); font-size: 0.8em; line-height: 16px; vertical-align: top; diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index f67a1e5b63..84189211b6 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only :buttonsLeft="buttonsLeft" :buttonsRight="buttonsRight" :contextmenu="contextmenu" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header> <template v-if="pageMetadata"> @@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'; +import { url } from '@@/js/config.js'; +import { getScrollContainer } from '@@/js/scroll.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 { url } from '@@/js/config.js'; import { useScrollPositionManager } from '@/nirax.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { openingWindowsCount } from '@/os.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { getScrollContainer } from '@@/js/scroll.js'; import { useRouterFactory } from '@/router/supplier.js'; import { mainRouter } from '@/router/main.js'; import MkUserName from './global/MkUserName.vue'; @@ -49,7 +49,7 @@ const props = defineProps<{ initialPath: string; }>(); -defineEmits<{ +const emit = defineEmits<{ (ev: 'closed'): void; }>(); @@ -59,7 +59,7 @@ const windowRouter = routerFactory(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: any; }[]>([{ +const history = ref<{ path: string; key: string; }[]>([{ path: windowRouter.getCurrentPath(), key: windowRouter.getCurrentKey(), }]); @@ -181,8 +181,8 @@ defineExpose({ overscroll-behavior: contain; min-height: 100%; - background: var(--bg); + background: var(--MI_THEME-bg); - --margin: var(--marginHalf); + --MI-margin: var(--MI-marginHalf); } </style> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 592a511fb0..a414676bda 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <li v-for="(choice, i) in props.poll.choices" :key="i" :class="$style.choice" @click="vote(i)"> <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <span :class="$style.fg"> - <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> + <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template> <Mfm :text="choice.text" :plain="true"/> <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span> </span> @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <p v-if="!readOnly" :class="$style.info"> <span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span> <span v-if="poll.multiple"> · </span> - <span v-if="poll.multiple" style="color: var(--accent); font-weight: bolder;">{{ i18n.ts._poll.multiple }}</span> + <span v-if="poll.multiple" style="color: var(--MI_THEME-accent); font-weight: bolder;">{{ i18n.ts._poll.multiple }}</span> <span> · </span> <a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> @@ -33,14 +33,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> 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 * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { host } from '@@/js/config.js'; -import { useInterval } from '@@/js/use-interval.js'; import { $i } from '@/account.js'; const props = defineProps<{ @@ -91,7 +91,7 @@ if (props.poll.expiresAt) { const vote = async (id) => { if (props.readOnly || closed.value || isVoted.value) return; - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (!props.poll.multiple) { const { canceled } = await os.confirm({ @@ -115,7 +115,7 @@ const vote = async (id) => { }; const refreshVotes = async () => { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (props.readOnly || closed.value) return; await misskeyApi('notes/polls/refresh', { @@ -139,9 +139,9 @@ const refreshVotes = async () => { position: relative; margin: 4px 0; padding: 4px; - //border: solid 0.5px var(--divider); - background: var(--accentedBg); - border-radius: var(--radius-xs); + //border: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-accentedBg); + border-radius: var(--MI-radius-xs); overflow: clip; cursor: pointer; } @@ -151,8 +151,8 @@ const refreshVotes = async () => { top: 0; left: 0; height: 100%; - background: var(--accent); - background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB)); + background: var(--MI_THEME-accent); + background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB)); transition: width 1s ease; } @@ -160,12 +160,12 @@ const refreshVotes = async () => { position: relative; display: inline-block; padding: 3px 5px; - background: var(--panel); - border-radius: var(--radius-xs); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-xs); } .info { - color: var(--fg); + color: var(--MI_THEME-fg); } .done { diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index d14873008b..f1b5ff4de0 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -19,7 +19,7 @@ defineProps<{ items: MenuItem[]; align?: 'center' | string; width?: number; - src?: any; + src?: HTMLElement | null; returnFocusTo?: HTMLElement | null; }>(); @@ -73,7 +73,7 @@ function close() { <style lang="scss" module> .drawer { - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; } diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 93328fe52f..11ae6dbd6a 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -61,17 +61,17 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAcct :user="u"/> <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button> </span> - <button class="_buttonPrimary" style="padding: 4px; border-radius: var(--radius-sm);" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> + <button class="_buttonPrimary" style="padding: 4px; border-radius: var(--MI-radius-sm);" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> </div> </div> <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.cwFrame"> - <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> + <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd"> <div v-if="maxCwLength - cwLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: cwLength > maxCwLength }]">{{ maxCwLength - cwLength }}</div> </div> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> - <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> @@ -136,28 +136,14 @@ 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 type { PostFormProps } from '@/types/post-form.js'; import MkScheduleEditor from '@/components/MkScheduleEditor.vue'; const $i = signinRequired(); const modal = inject('modal'); -const props = withDefaults(defineProps<{ - reply?: Misskey.entities.Note; - renote?: Misskey.entities.Note; - channel?: Misskey.entities.Channel; // TODO - mention?: Misskey.entities.User; - specified?: Misskey.entities.UserDetailed; - initialText?: string; - initialCw?: string; - initialVisibility?: (typeof Misskey.noteVisibilities)[number]; - initialFiles?: Misskey.entities.DriveFile[]; - initialLocalOnly?: boolean; - initialVisibleUsers?: Misskey.entities.UserDetailed[]; - initialNote?: Misskey.entities.Note & { - isSchedule?: boolean, - }; - instant?: boolean; +const props = withDefaults(defineProps<PostFormProps & { fixed?: boolean; autofocus?: boolean; freezeAfterPosted?: boolean; @@ -212,6 +198,7 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]' const imeText = ref(''); const showingOptions = ref(false); const textAreaReadOnly = ref(false); +const justEndedComposition = ref(false); const scheduleNote = ref<{ scheduledAt: number | null; } | null>(null); @@ -602,7 +589,13 @@ function clear() { function onKeydown(ev: KeyboardEvent) { if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); - if (ev.key === 'Escape') emit('esc'); + // justEndedComposition.value is for Safari, which keyDown occurs after compositionend. + // ev.isComposing is for another browsers. + if (ev.key === 'Escape' && !justEndedComposition.value && !ev.isComposing) emit('esc'); +} + +function onKeyup(ev: KeyboardEvent) { + justEndedComposition.value = false; } function onCompositionUpdate(ev: CompositionEvent) { @@ -611,6 +604,7 @@ function onCompositionUpdate(ev: CompositionEvent) { function onCompositionEnd(ev: CompositionEvent) { imeText.value = ''; + justEndedComposition.value = true; } async function onPaste(ev: ClipboardEvent) { @@ -1015,8 +1009,8 @@ function showActions(ev: MouseEvent) { action.handler({ text: text.value, cw: cw.value, - }, (key, value: any) => { - if (typeof key !== 'string') return; + }, (key, value) => { + if (typeof key !== 'string' || typeof value !== 'string') return; if (key === 'text') { text.value = value; } if (key === 'cw') { useCw.value = value !== null; cw.value = value; } }); @@ -1228,8 +1222,8 @@ defineExpose({ &:focus-visible { outline: none; - .submitInner { - outline: 2px solid var(--fgOnAccent); + > .submitInner { + outline: 2px solid var(--MI_THEME-fgOnAccent); outline-offset: -4px; } } @@ -1243,14 +1237,14 @@ defineExpose({ } &:not(:disabled):hover { - > .inner { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + > .submitInner { + 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))); } } &:not(:disabled):active { - > .inner { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + > .submitInner { + 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))); } } } @@ -1261,7 +1255,7 @@ defineExpose({ left: 12px; width: 5px; height: 100% ; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -1269,20 +1263,20 @@ defineExpose({ padding: 0 12px; line-height: 34px; font-weight: bold; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); min-width: 90px; box-sizing: border-box; - color: var(--fgOnAccent); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + color: var(--MI_THEME-fgOnAccent); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } .headerRightItem { margin: 0; padding: 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); &:hover { - background: var(--X5); + background: var(--MI_THEME-X5); } &:disabled { @@ -1326,7 +1320,7 @@ defineExpose({ .withQuote { margin: 0 0 8px 0; - color: var(--accent); + color: var(--MI_THEME-accent); } .toSpecified { @@ -1345,8 +1339,8 @@ defineExpose({ .visibleUser { margin-right: 14px; padding: 8px 0 8px 8px; - border-radius: var(--radius-sm); - background: var(--X4); + border-radius: var(--MI-radius-sm); + background: var(--MI_THEME-X4); } .hasNotSpecifiedMentions { @@ -1365,7 +1359,7 @@ defineExpose({ border: none; border-radius: 0; background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); font-family: inherit; &:focus { @@ -1380,7 +1374,7 @@ defineExpose({ .cwFrame { z-index: 1; padding-bottom: 8px; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); width: 100%; position: relative; @@ -1390,7 +1384,7 @@ defineExpose({ z-index: 1; padding-top: 8px; padding-bottom: 8px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .textOuter { @@ -1416,8 +1410,8 @@ defineExpose({ right: 2px; padding: 4px 6px; font-size: .9em; - color: var(--warn); - border-radius: var(--radius-sm); + color: var(--MI_THEME-warn); + border-radius: var(--MI-radius-sm); min-width: 1.6em; text-align: center; @@ -1457,19 +1451,19 @@ defineExpose({ font-size: 1em; width: auto; height: 100%; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); &:hover { - background: var(--X5); + background: var(--MI_THEME-X5); } &.footerButtonActive { - color: var(--accent); + color: var(--MI_THEME-accent); } } .previewButtonActive { - color: var(--accent); + color: var(--MI_THEME-accent); } @container (max-width: 500px) { diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index f90fcfef33..11444d8d78 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div v-show="props.modelValue.length != 0" :class="$style.root"> <Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)"> - <template #item="{element}"> + <template #item="{ element }"> <div :class="$style.file" role="button" @@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const props = defineProps<{ - modelValue: any[]; + modelValue: Misskey.entities.DriveFile[]; detachMediaFn?: (id: string) => void; }>(); const mock = inject<boolean>('mock', false); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any[]): void; + (ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void; (ev: 'detach', id: string): void; (ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void; (ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void; @@ -113,7 +113,7 @@ async function rename(file) { }); } -async function describe(file) { +async function describe(file: Misskey.entities.DriveFile) { if (mock) return; const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { @@ -203,7 +203,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar width: 64px; height: 64px; margin-right: 4px; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); overflow: hidden; cursor: move; @@ -216,7 +216,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar width: 100%; height: 100%; z-index: 1; - color: var(--fg); + color: var(--MI_THEME-fg); } .sensitive { diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index 811a6378f2..0fd17e12c7 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -11,23 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { shallowRef } from 'vue'; -import * as Misskey from 'misskey-js'; import MkModal from '@/components/MkModal.vue'; import MkPostForm from '@/components/MkPostForm.vue'; +import * as Misskey from 'misskey-js'; +import type { PostFormProps } from '@/types/post-form.js'; -const props = withDefaults(defineProps<{ - reply?: Misskey.entities.Note; - renote?: Misskey.entities.Note; - channel?: any; // TODO - mention?: Misskey.entities.User; - specified?: Misskey.entities.UserDetailed; - initialText?: string; - initialCw?: string; - initialVisibility?: (typeof Misskey.noteVisibilities)[number]; - initialFiles?: Misskey.entities.DriveFile[]; - initialLocalOnly?: boolean; - initialVisibleUsers?: Misskey.entities.UserDetailed[]; - initialNote?: Misskey.entities.Note; +const props = withDefaults(defineProps<PostFormProps & { instant?: boolean; fixed?: boolean; autofocus?: boolean; diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index e02f76a58f..5bd50170d8 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> +<script lang="ts" setup generic="T extends unknown"> import { computed } from 'vue'; const props = defineProps<{ - modelValue: any; - value: any; + modelValue: T; + value: T; disabled?: boolean; }>(); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any): void; + (ev: 'update:modelValue', value: T): void; }>(); const checked = computed(() => props.modelValue === props.value); @@ -53,10 +53,10 @@ function toggle(): void { cursor: pointer; padding: 7px 10px; min-width: 60px; - background-color: var(--panel); + background-color: var(--MI_THEME-panel); background-clip: padding-box !important; - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); font-size: 90%; transition: all 0.2s; user-select: none; @@ -67,25 +67,25 @@ function toggle(): void { } &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } &:focus-within { outline: none; - box-shadow: 0 0 0 2px var(--focus); + box-shadow: 0 0 0 2px var(--MI_THEME-focus); } &.checked { - background-color: var(--accentedBg) !important; - border-color: var(--accentedBg) !important; - color: var(--accent); + background-color: var(--MI_THEME-accentedBg) !important; + border-color: var(--MI_THEME-accentedBg) !important; + color: var(--MI_THEME-accent); cursor: default !important; > .button { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); &::after { - background-color: var(--accent); + background-color: var(--MI_THEME-accent); transform: scale(1); opacity: 1; } @@ -106,8 +106,8 @@ function toggle(): void { width: 14px; height: 14px; background: none; - border: solid 2px var(--inputBorder); - border-radius: var(--radius-full); + border: solid 2px var(--MI_THEME-inputBorder); + border-radius: var(--MI-radius-full); transition: inherit; &::after { @@ -118,7 +118,7 @@ function toggle(): void { right: 3px; bottom: 3px; left: 3px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); opacity: 0; transform: scale(0); transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index 705c93f770..af81eb814d 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -77,7 +77,7 @@ export default defineComponent({ > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 22c187c357..d009f3858c 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -212,7 +212,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -224,9 +224,9 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .body { padding: 7px 12px; - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); > .container { position: relative; @@ -242,7 +242,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { width: calc(100% - #{$thumbWidth}); height: 3px; background: rgba(0, 0, 0, 0.1); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); overflow: clip; > .highlight { @@ -250,7 +250,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { top: 0; left: 0; height: 100%; - background: var(--accent); + background: var(--MI_THEME-accent); opacity: 0.5; } } @@ -272,8 +272,8 @@ function onMousedown(ev: MouseEvent | TouchEvent) { width: $tickWidth; height: 3px; margin-left: - math.div($tickWidth, 2); - background: var(--divider); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-divider); + border-radius: var(--MI-radius-ellipse); } } @@ -282,11 +282,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) { width: $thumbWidth; height: $thumbHeight; cursor: grab; - background: var(--accent); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-accent); + border-radius: var(--MI-radius-ellipse); &:hover { - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } } } diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue index 361e246e9f..5a59a5e055 100644 --- a/packages/frontend/src/components/MkReactionEffect.vue +++ b/packages/frontend/src/components/MkReactionEffect.vue @@ -60,7 +60,7 @@ onMounted(() => { right: 0; bottom: 0; margin: auto; - color: var(--accent); + color: var(--MI_THEME-accent); font-size: 18px; font-weight: bold; transform: translateY(-30px); diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 6fdeb3a3ab..e60ac86315 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; +import * as Misskey from 'misskey-js'; import { getEmojiName } from '@@/js/emojilist.js'; import MkTooltip from './MkTooltip.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue'; defineProps<{ showing: boolean; reaction: string; - users: any[]; // TODO + users: Misskey.entities.UserLite[]; count: number; targetElement: HTMLElement; }>(); @@ -57,7 +58,7 @@ function getReactionName(reaction: string): string { max-width: 100px; padding-right: 10px; text-align: center; - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--MI_THEME-divider); } .reactionIcon { diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 957ee0e76b..9cd972639f 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -175,12 +175,12 @@ if (!mock) { margin: 2px; padding: 0 6px; font-size: 1.5em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); align-items: center; justify-content: center; &.canToggle { - background: var(--buttonBg); + background: var(--MI_THEME-buttonBg); &:hover { background: rgba(0, 0, 0, 0.1); @@ -194,7 +194,7 @@ if (!mock) { &.small { height: 32px; font-size: 1em; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); > .count { font-size: 0.9em; @@ -205,7 +205,7 @@ if (!mock) { &.large { height: 52px; font-size: 2em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); > .count { font-size: 0.6em; @@ -214,12 +214,12 @@ if (!mock) { } &.reacted, &.reacted:hover { - background: var(--accentedBg); - color: var(--accent); - box-shadow: 0 0 0 1px var(--accent) inset; + background: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset; > .count { - color: var(--accent); + color: var(--MI_THEME-accent); } > .icon { diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue index 2b59eab9d9..6391468204 100644 --- a/packages/frontend/src/components/MkRemoteCaution.vue +++ b/packages/frontend/src/components/MkRemoteCaution.vue @@ -19,15 +19,15 @@ defineProps<{ .root { font-size: 0.8em; padding: 16px; - background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent); - color: var(--infoWarnFg); - border-radius: var(--radius); + background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent); + color: var(--MI_THEME-infoWarnFg); + border-radius: var(--MI-radius); overflow: clip; z-index: 1; } .link { margin-left: 4px; - color: var(--accent); + color: var(--MI_THEME-accent); } </style> diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue index c3daa9c9a4..d41793b0fa 100644 --- a/packages/frontend/src/components/MkRetentionLineChart.vue +++ b/packages/frontend/src/components/MkRetentionLineChart.vue @@ -44,7 +44,7 @@ onMounted(async () => { const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; - const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')); + const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent')); const color = accent.toHex(); if (chartEl.value == null) return; diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue index ee5bb73ebf..2949cf156d 100644 --- a/packages/frontend/src/components/MkRippleEffect.vue +++ b/packages/frontend/src/components/MkRippleEffect.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> <svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> - <circle fill="none" cx="64" cy="64" style="stroke: var(--accent);"> + <circle fill="none" cx="64" cy="64" style="stroke: var(--MI_THEME-accent);"> <animate attributeName="r" begin="0s" dur="0.5s" @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only /> </circle> <g fill="none" fill-rule="evenodd"> - <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);"> + <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--MI_THEME-accent);"> <animate attributeName="r" begin="0s" dur="0.8s" diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index ce17ae08e0..3f14c5b5e0 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }"> <template v-if="forModeration"> - <i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i> - <i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i> + <i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i> + <i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i> </template> <div v-adaptive-bg class="_panel" :class="$style.body"> @@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only <img :class="$style.bodyBadge" :src="role.iconUrl"/> </template> <template v-else> - <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i> - <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i> + <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--MI_THEME-accent);"></i> + <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--MI_THEME-accent);"></i> <i v-else class="ti ti-user" style="opacity: 0.7;"></i> </template> </span> diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 150a5c6d54..79a56b68a8 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only @keydown.space.enter="show" > <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> - <select + <div ref="inputEl" - v-model="v" v-adaptive-border tabindex="-1" :class="$style.inputCore" @@ -26,55 +25,48 @@ SPDX-License-Identifier: AGPL-3.0-only :required="required" :readonly="readonly" :placeholder="placeholder" - @input="onInput" @mousedown.prevent="() => {}" @keydown.prevent="() => {}" > - <slot></slot> - </select> + <div style="pointer-events: none;">{{ currentValueText ?? '' }}</div> + <div style="display: none;"> + <slot></slot> + </div> + </div> <div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> </div> <div :class="$style.caption"><slot name="caption"></slot></div> - - <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> <script lang="ts" setup> import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue'; -import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; import { useInterval } from '@@/js/use-interval.js'; -import { i18n } from '@/i18n.js'; import type { MenuItem } from '@/types/menu.js'; +import * as os from '@/os.js'; const props = defineProps<{ - modelValue: string | null; + modelValue: string | number | null; required?: boolean; readonly?: boolean; disabled?: boolean; placeholder?: string; autofocus?: boolean; inline?: boolean; - manualSave?: boolean; small?: boolean; large?: boolean; }>(); const emit = defineEmits<{ - (ev: 'changeByUser', value: string | null): void; - (ev: 'update:modelValue', value: string | null): void; + (ev: 'update:modelValue', value: string | number | null): void; }>(); const slots = useSlots(); const { modelValue, autofocus } = toRefs(props); -const v = ref(modelValue.value); const focused = ref(false); const opening = ref(false); -const changed = ref(false); -const invalid = ref(false); -const filled = computed(() => v.value !== '' && v.value != null); +const currentValueText = ref<string | null>(null); const inputEl = ref<HTMLObjectElement | null>(null); const prefixEl = ref<HTMLElement | null>(null); const suffixEl = ref<HTMLElement | null>(null); @@ -85,26 +77,6 @@ const height = 36; const focus = () => container.value?.focus(); -const onInput = (ev) => { - changed.value = true; -}; - -const updated = () => { - changed.value = false; - emit('update:modelValue', v.value); -}; - -watch(modelValue, newValue => { - v.value = newValue; -}); - -watch(v, () => { - if (!props.manualSave) { - updated(); - } - - invalid.value = inputEl.value?.validity.badInput ?? true; -}); // このコンポーネントが作成された時、非表示状態である場合がある // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する @@ -134,6 +106,31 @@ onMounted(() => { }); }); +watch(modelValue, () => { + const scanOptions = (options: VNodeChild[]) => { + for (const vnode of options) { + if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue; + if (vnode.type === 'optgroup') { + const optgroup = vnode; + if (Array.isArray(optgroup.children)) scanOptions(optgroup.children); + } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある + const fragment = vnode; + if (Array.isArray(fragment.children)) scanOptions(fragment.children); + } else if (vnode.props == null) { // v-if で条件が false のときにこうなる + // nop? + } else { + const option = vnode; + if (option.props?.value === modelValue.value) { + currentValueText.value = option.children as string; + break; + } + } + } + }; + + scanOptions(slots.default!()); +}, { immediate: true }); + function show() { if (opening.value) return; focus(); @@ -146,11 +143,9 @@ function show() { const pushOption = (option: VNode) => { menu.push({ text: option.children as string, - active: computed(() => v.value === option.props?.value), + active: computed(() => modelValue.value === option.props?.value), action: () => { - v.value = option.props?.value; - changed.value = true; - emit('changeByUser', v.value); + emit('update:modelValue', option.props?.value); }, }); }; @@ -202,7 +197,7 @@ function show() { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -220,8 +215,8 @@ function show() { &.focused { > .inputCore { - border-color: var(--accent) !important; - //box-shadow: 0 0 0 4px var(--focus); + border-color: var(--MI_THEME-accent) !important; + //box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } @@ -240,7 +235,7 @@ function show() { &:hover { > .inputCore { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } } @@ -248,7 +243,8 @@ function show() { .inputCore { appearance: none; -webkit-appearance: none; - display: block; + display: flex; + align-items: center; height: v-bind("height + 'px'"); width: 100%; margin: 0; @@ -256,10 +252,10 @@ function show() { font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue new file mode 100644 index 0000000000..34c22abc31 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -0,0 +1,206 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper" data-cy-signin-page-input> + <div :class="$style.root"> + <div :class="$style.avatar"> + <i class="ti ti-user"></i> + </div> + + <!-- ログイン画面メッセージ --> + <MkInfo v-if="message"> + {{ message }} + </MkInfo> + + <!-- 外部サーバーへの転送 --> + <div v-if="openOnRemote" class="_gaps_m"> + <div class="_gaps_s"> + <MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)"> + {{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i> + </MkButton> + <button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)"> + {{ i18n.ts.specifyServerHost }} + </button> + </div> + <div :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + </div> + + <!-- username入力 --> + <form class="_gaps_s" @submit.prevent="emit('usernameSubmitted', username)"> + <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + </MkInput> + <MkButton type="submit" large primary rounded style="margin: 0 auto;" data-cy-signin-page-input-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </form> + + <!-- パスワードレスログイン --> + <div :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + <div> + <MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)"> + <i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }} + </MkButton> + </div> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import { toUnicode } from 'punycode/'; + +import { query, extractDomain } from '@@/js/url.js'; +import { host as configHost } from '@@/js/config.js'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +const props = withDefaults(defineProps<{ + message?: string, + openOnRemote?: OpenOnRemoteOptions, +}>(), { + message: '', + openOnRemote: undefined, +}); + +const emit = defineEmits<{ + (ev: 'usernameSubmitted', v: string): void; + (ev: 'passkeyClick', v: MouseEvent): void; +}>(); + +const host = toUnicode(configHost); + +const username = ref(''); + +//#region Open on remote +function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void { + switch (options.type) { + case 'web': + case 'lookup': { + let _path: string; + + if (options.type === 'lookup') { + // TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼ + // _path = `/lookup?uri=${encodeURIComponent(_path)}`; + _path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`; + } else { + _path = options.path; + } + + if (targetHost) { + window.open(`https://${targetHost}${_path}`, '_blank', 'noopener'); + } else { + window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener'); + } + break; + } + case 'share': { + const params = query(options.params); + if (targetHost) { + window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener'); + } else { + window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener'); + } + break; + } + } +} + +async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> { + const { canceled, result: hostTemp } = await os.inputText({ + title: i18n.ts.inputHostName, + placeholder: 'misskey.example.com', + }); + + if (canceled) return; + + let targetHost: string | null = hostTemp; + + // ドメイン部分だけを取り出す + targetHost = extractDomain(targetHost ?? ''); + if (targetHost == null) { + os.alert({ + type: 'error', + title: i18n.ts.invalidValue, + text: i18n.ts.tryAgain, + }); + return; + } + openRemote(options, targetHost); +} +//#endregion +</script> + +<style lang="scss" module> +.root { + display: flex; + flex-direction: column; + gap: 20px; +} + +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.avatar { + margin: 0 auto; + background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%); + color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.instanceManualSelectButton { + display: block; + text-align: center; + opacity: .7; + font-size: .8em; + + &:hover { + text-decoration: underline; + } +} + +.orHr { + position: relative; + margin: .4em auto; + width: 100%; + height: 1px; + background: var(--MI_THEME-divider); +} + +.orMsg { + position: absolute; + top: -.6em; + display: inline-block; + padding: 0 1em; + background: var(--MI_THEME-panel); + font-size: 0.8em; + color: var(--MI_THEME-fgOnPanel); + margin: 0; + left: 50%; + transform: translateX(-50%); +} +</style> diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue new file mode 100644 index 0000000000..e5a56ab66d --- /dev/null +++ b/packages/frontend/src/components/MkSignin.passkey.vue @@ -0,0 +1,92 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper"> + <div class="_gaps" :class="$style.root"> + <div class="_gaps_s"> + <div :class="$style.passkeyIcon"> + <i class="ti ti-fingerprint"></i> + </div> + <div :class="$style.passkeyDescription">{{ i18n.ts.useSecurityKey }}</div> + </div> + + <MkButton large primary rounded :disabled="queryingKey" style="margin: 0 auto;" @click="queryKey">{{ i18n.ts.retry }}</MkButton> + + <MkButton v-if="isPerformingPasswordlessLogin !== true" transparent rounded :disabled="queryingKey" style="margin: 0 auto;" @click="emit('useTotp')">{{ i18n.ts.useTotp }}</MkButton> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref, onMounted } from 'vue'; +import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill'; + +import { i18n } from '@/i18n.js'; + +import MkButton from '@/components/MkButton.vue'; + +import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; + +const props = defineProps<{ + credentialRequest: CredentialRequestOptions; + isPerformingPasswordlessLogin?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'done', credential: AuthenticationPublicKeyCredential): void; + (ev: 'useTotp'): void; +}>(); + +const queryingKey = ref(true); + +async function queryKey() { + queryingKey.value = true; + await webAuthnRequest(props.credentialRequest) + .catch(() => { + return Promise.reject(null); + }) + .then((credential) => { + emit('done', credential); + }) + .finally(() => { + queryingKey.value = false; + }); +} + +onMounted(() => { + queryKey(); +}); +</script> + +<style lang="scss" module> +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.passkeyIcon { + margin: 0 auto; + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.passkeyDescription { + text-align: center; + font-size: 1.1em; +} +</style> diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue new file mode 100644 index 0000000000..ff7c598b50 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.password.vue @@ -0,0 +1,195 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper" data-cy-signin-page-password> + <div class="_gaps" :class="$style.root"> + <div :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined }"></div> + <div :class="$style.welcomeBackMessage"> + <I18n :src="i18n.ts.welcomeBackWithName" tag="span"> + <template #name><Mfm :text="user.name ?? user.username" :plain="true"/></template> + </I18n> + </div> + + <!-- password入力 --> + <form class="_gaps_s" @submit.prevent="onSubmit"> + <!-- ブラウザ オートコンプリート用 --> + <input type="hidden" name="username" autocomplete="username" :value="user.username"> + + <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required autofocus data-cy-signin-password> + <template #prefix><i class="ti ti-lock"></i></template> + <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> + </MkInput> + + <div v-if="needCaptcha"> + <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> + <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> + <MkCaptcha v-if="instance.enableFC" ref="fc" v-model="fcResponse" provider="fc" :sitekey="instance.fcSiteKey"/> + <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/> + </div> + + <MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </form> + </div> +</div> +</template> + +<script lang="ts"> +export type PwResponse = { + password: string; + captcha: { + hCaptchaResponse: string | null; + mCaptchaResponse: string | null; + reCaptchaResponse: string | null; + turnstileResponse: string | null; + fcResponse: string | null; + testcaptchaResponse: string | null; + }; +}; +</script> + +<script setup lang="ts"> +import { ref, computed, useTemplateRef, defineAsyncComponent } from 'vue'; +import * as Misskey from 'misskey-js'; + +import { instance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkCaptcha from '@/components/MkCaptcha.vue'; + +const props = defineProps<{ + user: Misskey.entities.UserDetailed; + needCaptcha: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'passwordSubmitted', v: PwResponse): void; +}>(); + +const password = ref(''); + +const hCaptcha = useTemplateRef('hcaptcha'); +const mCaptcha = useTemplateRef('mcaptcha'); +const reCaptcha = useTemplateRef('recaptcha'); +const turnstile = useTemplateRef('turnstile'); +const fc = useTemplateRef('fc'); +const testcaptcha = useTemplateRef('testcaptcha'); + +const hCaptchaResponse = ref<string | null>(null); +const mCaptchaResponse = ref<string | null>(null); +const reCaptchaResponse = ref<string | null>(null); +const turnstileResponse = ref<string | null>(null); +const fcResponse = ref<string | null>(null); +const testcaptchaResponse = ref<string | null>(null); + +const captchaFailed = computed((): boolean => { + return ( + (instance.enableHcaptcha && !hCaptchaResponse.value) || + (instance.enableMcaptcha && !mCaptchaResponse.value) || + (instance.enableRecaptcha && !reCaptchaResponse.value) || + (instance.enableTurnstile && !turnstileResponse.value) || + (instance.enableFC && !fcResponse.value) || + (instance.enableTestcaptcha && !testcaptchaResponse.value) + ); +}); + +function resetPassword(): void { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { + closed: () => dispose(), + }); +} + +function onSubmit() { + emit('passwordSubmitted', { + password: password.value, + captcha: { + hCaptchaResponse: hCaptchaResponse.value, + mCaptchaResponse: mCaptchaResponse.value, + reCaptchaResponse: reCaptchaResponse.value, + turnstileResponse: turnstileResponse.value, + fcResponse: fcResponse.value, + testcaptchaResponse: testcaptchaResponse.value, + }, + }); +} + +function resetCaptcha() { + hCaptcha.value?.reset(); + mCaptcha.value?.reset(); + reCaptcha.value?.reset(); + turnstile.value?.reset(); + fc.value?.reset(); + testcaptcha.value?.reset(); +} + +defineExpose({ + resetCaptcha, +}); +</script> + +<style lang="scss" module> +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.avatar { + margin: 0 auto 0 auto; + width: 64px; + height: 64px; + background: #ddd; + background-position: center; + background-size: cover; + border-radius: 100%; +} + +.welcomeBackMessage { + text-align: center; + font-size: 1.1em; +} + +.instanceManualSelectButton { + display: block; + text-align: center; + opacity: .7; + font-size: .8em; + + &:hover { + text-decoration: underline; + } +} + +.orHr { + position: relative; + margin: .4em auto; + width: 100%; + height: 1px; + background: var(--MI_THEME-divider); +} + +.orMsg { + position: absolute; + top: -.6em; + display: inline-block; + padding: 0 1em; + background: var(--MI_THEME-panel); + font-size: 0.8em; + color: var(--MI_THEME-fgOnPanel); + margin: 0; + left: 50%; + transform: translateX(-50%); +} +</style> diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue new file mode 100644 index 0000000000..670b8057c2 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.totp.vue @@ -0,0 +1,74 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.wrapper"> + <div class="_gaps" :class="$style.root"> + <div class="_gaps_s"> + <div :class="$style.totpIcon"> + <i class="ti ti-key"></i> + </div> + <div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div> + </div> + + <!-- totp入力 --> + <form class="_gaps_s" @submit.prevent="emit('totpSubmitted', token)"> + <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required autofocus :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'"> + <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template> + <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> + <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template> + </MkInput> + + <MkButton type="submit" large primary rounded style="margin: 0 auto;">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </form> + </div> +</div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; + +import { i18n } from '@/i18n.js'; + +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; + +const emit = defineEmits<{ + (ev: 'totpSubmitted', token: string): void; +}>(); + +const token = ref(''); +const isBackupCode = ref(false); +</script> + +<style lang="scss" module> +.wrapper { + display: flex; + align-items: center; + width: 100%; + min-height: 336px; + + > .root { + width: 100%; + } +} + +.totpIcon { + margin: 0 auto; + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.totpDescription { + text-align: center; + font-size: 1.1em; +} +</style> diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 82e0df8a01..4a6219071b 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -4,245 +4,290 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> - <div class="_gaps_m"> - <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div> - <MkInfo v-if="message"> - {{ message }} - </MkInfo> - <div v-if="openOnRemote" class="_gaps_m"> - <div class="_gaps_s"> - <MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)"> - {{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i> - </MkButton> - <button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)"> - {{ i18n.ts.specifyServerHost }} - </button> - </div> - <div :class="$style.orHr"> - <p :class="$style.orMsg">{{ i18n.ts.or }}</p> - </div> - </div> - <div v-if="!totpLogin" class="normal-signin _gaps_m"> - <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange"> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - </MkInput> - <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password> - <template #prefix><i class="ti ti-lock"></i></template> - <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> - </MkInput> - <MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> - </div> - <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> - <div v-if="user && user.securityKeys" class="twofa-group tap-group"> - <p>{{ i18n.ts.useSecurityKey }}</p> - <MkButton v-if="!queryingKey" @click="query2FaKey"> - {{ i18n.ts.retry }} - </MkButton> - </div> - <div v-if="user && user.securityKeys" :class="$style.orHr"> - <p :class="$style.orMsg">{{ i18n.ts.or }}</p> - </div> - <div class="twofa-group totp-group _gaps"> - <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'"> - <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template> - <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> - <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template> - </MkInput> - <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> - </div> - </div> - <div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr"> - <p :class="$style.orMsg">{{ i18n.ts.or }}</p> - </div> - <div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group"> - <MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin"> - <i class="ti ti-device-usb" style="font-size: medium;"></i> - {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} - </MkButton> - <p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p> - </div> +<div :class="$style.signinRoot"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_enterActive" + :leaveActiveClass="$style.transition_leaveActive" + :enterFromClass="$style.transition_enterFrom" + :leaveToClass="$style.transition_leaveTo" + + :inert="waiting" + > + <!-- 1. 外部サーバーへの転送・username入力・パスキー --> + <XInput + v-if="page === 'input'" + key="input" + :message="message" + :openOnRemote="openOnRemote" + + @usernameSubmitted="onUsernameSubmitted" + @passkeyClick="onPasskeyLogin" + /> + + <!-- 2. パスワード入力 --> + <XPassword + v-else-if="page === 'password'" + key="password" + ref="passwordPageEl" + + :user="userInfo!" + :needCaptcha="needCaptcha" + + @passwordSubmitted="onPasswordSubmitted" + /> + + <!-- 3. ワンタイムパスワード --> + <XTotp + v-else-if="page === 'totp'" + key="totp" + + @totpSubmitted="onTotpSubmitted" + /> + + <!-- 4. パスキー --> + <XPasskey + v-else-if="page === 'passkey'" + key="passkey" + + :credentialRequest="credentialRequest!" + :isPerformingPasswordlessLogin="doingPasskeyFromInputPage" + + @done="onPasskeyDone" + @useTotp="onUseTotp" + /> + </Transition> + <div v-if="waiting" :class="$style.waitingRoot"> + <MkLoading/> </div> -</form> +</div> </template> -<script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; -import { toUnicode } from 'punycode/'; +<script setup lang="ts"> +import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; -import { SigninWithPasskeyResponse } from 'misskey-js/entities.js'; -import { query, extractDomain } from '@@/js/url.js'; -import { host as configHost } from '@@/js/config.js'; -import MkDivider from './MkDivider.vue'; +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 { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; -import MkButton from '@/components/MkButton.vue'; -import MkInput from '@/components/MkInput.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; +import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js'; +import * as os from '@/os.js'; -const signing = ref(false); -const user = ref<Misskey.entities.UserDetailed | null>(null); -const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true); -const username = ref(''); -const password = ref(''); -const token = ref(''); -const host = ref(toUnicode(configHost)); -const totpLogin = ref(false); -const isBackupCode = ref(false); -const queryingKey = ref(false); -let credentialRequest: CredentialRequestOptions | null = null; -const passkey_context = ref(''); +import XInput from '@/components/MkSignin.input.vue'; +import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue'; +import XTotp from '@/components/MkSignin.totp.vue'; +import XPasskey from '@/components/MkSignin.passkey.vue'; const emit = defineEmits<{ - (ev: 'login', v: any): void; + (ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; }>(); const props = withDefaults(defineProps<{ - withAvatar?: boolean; autoSet?: boolean; message?: string, openOnRemote?: OpenOnRemoteOptions, }>(), { - withAvatar: true, autoSet: false, message: '', openOnRemote: undefined, }); -function onUsernameChange(): void { - const usernameRequested = username.value; - misskeyApi('users/show', { - username: usernameRequested, - }).then(userResponse => { - if (userResponse.username === username.value) { - user.value = userResponse; - usePasswordLessLogin.value = userResponse.usePasswordLessLogin; - } - }, () => { - if (usernameRequested === username.value) { - user.value = null; - usePasswordLessLogin.value = true; - } - }); -} +const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input'); +const waiting = ref(false); -function onLogin(res: any): Promise<void> | void { - if (props.autoSet) { - return login(res.i); - } -} +const passwordPageEl = useTemplateRef('passwordPageEl'); +const needCaptcha = ref(false); -async function query2FaKey(): Promise<void> { - if (credentialRequest == null) return; - queryingKey.value = true; - await webAuthnRequest(credentialRequest) - .catch(() => { - queryingKey.value = false; - return Promise.reject(null); - }).then(credential => { - credentialRequest = null; - queryingKey.value = false; - signing.value = true; - return misskeyApi('signin', { - username: username.value, - password: password.value, - credential: credential.toJSON(), - }); - }).then(res => { - emit('login', res); - return onLogin(res); - }).catch(err => { - if (err === null) return; - os.alert({ - type: 'error', - text: i18n.ts.signinFailed, - }); - signing.value = false; - }); -} +const userInfo = ref<null | Misskey.entities.UserDetailed>(null); +const password = ref(''); + +//#region Passkey Passwordless +const credentialRequest = shallowRef<CredentialRequestOptions | null>(null); +const passkeyContext = ref(''); +const doingPasskeyFromInputPage = ref(false); function onPasskeyLogin(): void { - signing.value = true; if (webAuthnSupported()) { + doingPasskeyFromInputPage.value = true; + waiting.value = true; misskeyApi('signin-with-passkey', {}) - .then((res: SigninWithPasskeyResponse) => { - totpLogin.value = false; - signing.value = false; - queryingKey.value = true; - passkey_context.value = res.context ?? ''; - credentialRequest = parseRequestOptionsFromJSON({ + .then((res) => { + passkeyContext.value = res.context ?? ''; + credentialRequest.value = parseRequestOptionsFromJSON({ publicKey: res.option, }); + + page.value = 'passkey'; + waiting.value = false; }) - .then(() => queryPasskey()) - .catch(loginFailed); + .catch(onSigninApiError); } } -async function queryPasskey(): Promise<void> { - if (credentialRequest == null) return; - queryingKey.value = true; - console.log('Waiting passkey auth...'); - await webAuthnRequest(credentialRequest) - .catch((err) => { - console.warn('Passkey Auth fail!: ', err); - queryingKey.value = false; - return Promise.reject(null); - }).then(credential => { - credentialRequest = null; - queryingKey.value = false; - signing.value = true; - return misskeyApi('signin-with-passkey', { - credential: credential.toJSON(), - context: passkey_context.value, - }); - }).then((res: SigninWithPasskeyResponse) => { +function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void { + waiting.value = true; + + if (doingPasskeyFromInputPage.value) { + misskeyApi('signin-with-passkey', { + credential: credential.toJSON(), + context: passkeyContext.value, + }).then((res) => { + if (res.signinResponse == null) { + onSigninApiError(); + return; + } emit('login', res.signinResponse); - return onLogin(res.signinResponse); + }).catch(onSigninApiError); + } else if (userInfo.value != null) { + tryLogin({ + username: userInfo.value.username, + password: password.value, + credential: credential.toJSON(), }); + } } -function onSubmit(): void { - signing.value = true; - if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { - if (webAuthnSupported() && user.value.securityKeys) { - misskeyApi('signin', { - username: username.value, - password: password.value, - }).then(res => { - totpLogin.value = true; - signing.value = false; - credentialRequest = parseRequestOptionsFromJSON({ - publicKey: res, - }); - }) - .then(() => query2FaKey()) - .catch(loginFailed); - } else { - totpLogin.value = true; - signing.value = false; - } +function onUseTotp(): void { + page.value = 'totp'; +} +//#endregion + +async function onUsernameSubmitted(username: string) { + waiting.value = true; + + userInfo.value = await misskeyApi('users/show', { + username, + }).catch(() => null); + + await tryLogin({ + username, + }); +} + +async function onPasswordSubmitted(pw: PwResponse) { + waiting.value = true; + password.value = pw.password; + + if (userInfo.value == null) { + await os.alert({ + type: 'error', + title: i18n.ts.noSuchUser, + text: i18n.ts.signinFailed, + }); + waiting.value = false; + return; + } else { + await tryLogin({ + username: userInfo.value.username, + password: pw.password, + 'hcaptcha-response': pw.captcha.hCaptchaResponse, + 'm-captcha-response': pw.captcha.mCaptchaResponse, + 'g-recaptcha-response': pw.captcha.reCaptchaResponse, + 'frc-captcha-solution': pw.captcha.fcResponse, + 'turnstile-response': pw.captcha.turnstileResponse, + 'testcaptcha-response': pw.captcha.testcaptchaResponse, + }); + } +} + +async function onTotpSubmitted(token: string) { + waiting.value = true; + + if (userInfo.value == null) { + await os.alert({ + type: 'error', + title: i18n.ts.noSuchUser, + text: i18n.ts.signinFailed, + }); + waiting.value = false; + return; } else { - misskeyApi('signin', { - username: username.value, + await tryLogin({ + username: userInfo.value.username, password: password.value, - token: user.value?.twoFactorEnabled ? token.value : undefined, - }).then(res => { + token, + }); + } +} + +async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> { + const _req = { + username: req.username ?? userInfo.value?.username, + ...req, + }; + + function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest { + return x.username != null; + } + + if (!assertIsSigninFlowRequest(_req)) { + throw new Error('Invalid request'); + } + + return await misskeyApi('signin-flow', _req).then(async (res) => { + if (res.finished) { emit('login', res); - onLogin(res); - }).catch(loginFailed); + await onLoginSucceeded(res); + } else { + switch (res.next) { + case 'captcha': { + needCaptcha.value = true; + page.value = 'password'; + break; + } + case 'password': { + needCaptcha.value = false; + page.value = 'password'; + break; + } + case 'totp': { + page.value = 'totp'; + break; + } + case 'passkey': { + if (webAuthnSupported()) { + credentialRequest.value = parseRequestOptionsFromJSON({ + publicKey: res.authRequest, + }); + page.value = 'passkey'; + } else { + page.value = 'totp'; + } + break; + } + } + + if (doingPasskeyFromInputPage.value === true) { + doingPasskeyFromInputPage.value = false; + page.value = 'input'; + password.value = ''; + } + passwordPageEl.value?.resetCaptcha(); + nextTick(() => { + waiting.value = false; + }); + } + return res; + }).catch((err) => { + onSigninApiError(err); + return Promise.reject(err); + }); +} + +async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true }) { + if (props.autoSet) { + await login(res.i); } } -function loginFailed(err: any): void { - switch (err.id) { +function onSigninApiError(err?: any): void { + const id = err?.id ?? null; + + switch (id) { case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { os.alert({ type: 'error', @@ -275,6 +320,14 @@ function loginFailed(err: any): void { }); break; } + case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.incorrectTotp, + }); + break; + } case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { os.alert({ type: 'error', @@ -283,6 +336,14 @@ function loginFailed(err: any): void { }); break; } + case '93b86c4b-72f9-40eb-9815-798928603d1e': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.passkeyVerificationFailed, + }); + break; + } case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { os.alert({ type: 'error', @@ -309,113 +370,55 @@ function loginFailed(err: any): void { } } - totpLogin.value = false; - signing.value = false; -} - -function resetPassword(): void { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { - closed: () => dispose(), - }); -} - -function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void { - switch (options.type) { - case 'web': - case 'lookup': { - let _path: string; - - if (options.type === 'lookup') { - // TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼ - // _path = `/lookup?uri=${encodeURIComponent(_path)}`; - _path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`; - } else { - _path = options.path; - } - - if (targetHost) { - window.open(`https://${targetHost}${_path}`, '_blank', 'noopener'); - } else { - window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener'); - } - break; - } - case 'share': { - const params = query(options.params); - if (targetHost) { - window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener'); - } else { - window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener'); - } - break; - } + if (doingPasskeyFromInputPage.value === true) { + doingPasskeyFromInputPage.value = false; + page.value = 'input'; + password.value = ''; } -} - -async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> { - const { canceled, result: hostTemp } = await os.inputText({ - title: i18n.ts.inputHostName, - placeholder: 'misskey.example.com', + passwordPageEl.value?.resetCaptcha(); + nextTick(() => { + waiting.value = false; }); - - if (canceled) return; - - let targetHost: string | null = hostTemp; - - // ドメイン部分だけを取り出す - targetHost = extractDomain(targetHost); - if (targetHost == null) { - os.alert({ - type: 'error', - title: i18n.ts.invalidValue, - text: i18n.ts.tryAgain, - }); - return; - } - openRemote(options, targetHost); } + +onBeforeUnmount(() => { + password.value = ''; + needCaptcha.value = false; + userInfo.value = null; +}); </script> <style lang="scss" module> -.avatar { - margin: 0 auto 0 auto; - width: 64px; - height: 64px; - background: #ddd; - background-position: center; - background-size: cover; - border-radius: var(--radius-full); +.transition_enterActive, +.transition_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); } - -.instanceManualSelectButton { - display: block; - text-align: center; - opacity: .7; - font-size: .8em; - - &:hover { - text-decoration: underline; - } +.transition_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_leaveTo { + opacity: 0; + transform: translateX(-50px); } -.orHr { +.signinRoot { + overflow-x: hidden; + overflow-x: clip; + position: relative; - margin: .4em auto; - width: 100%; - height: 1px; - background: var(--divider); } -.orMsg { +.waitingRoot { position: absolute; - top: -.6em; - display: inline-block; - padding: 0 1em; - background: var(--panel); - font-size: 0.8em; - color: var(--fgOnPanel); - margin: 0; - left: 50%; - transform: translateX(-50%); + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%); + display: flex; + justify-content: center; + align-items: center; + z-index: 1; } </style> diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index d48780e9de..676a336ec7 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -4,26 +4,30 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModalWindow - ref="dialog" - :width="400" - :height="450" - @close="onClose" +<MkModal + ref="modal" + :preferType="'dialog'" + @click="onClose" @closed="emit('closed')" > - <template #header>{{ i18n.ts.login }}</template> - - <MkSpacer :marginMin="20" :marginMax="28"> - <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/> - </MkSpacer> -</MkModalWindow> + <div :class="$style.root"> + <div :class="$style.header"> + <div :class="$style.headerText"><i class="ti ti-login-2"></i> {{ i18n.ts.login }}</div> + <button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button> + </div> + <div :class="$style.content"> + <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/> + </div> + </div> +</MkModal> </template> <script lang="ts" setup> +import * as Misskey from 'misskey-js'; import { shallowRef } from 'vue'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import MkSignin from '@/components/MkSignin.vue'; -import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkModal from '@/components/MkModal.vue'; import { i18n } from '@/i18n.js'; withDefaults(defineProps<{ @@ -37,20 +41,67 @@ withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done', v: any): void; + (ev: 'done', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; (ev: 'closed'): void; (ev: 'cancelled'): void; }>(); -const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +const modal = shallowRef<InstanceType<typeof MkModal>>(); function onClose() { emit('cancelled'); - if (dialog.value) dialog.value.close(); + if (modal.value) modal.value.close(); } -function onLogin(res) { +function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) { emit('done', res); - if (dialog.value) dialog.value.close(); + if (modal.value) modal.value.close(); } </script> + +<style lang="scss" module> +.root { + overflow: auto; + margin: auto; + position: relative; + width: 100%; + max-width: 400px; + height: 100%; + max-height: 450px; + box-sizing: border-box; + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); +} + +.header { + position: sticky; + top: 0; + left: 0; + width: 100%; + height: 50px; + box-sizing: border-box; + display: flex; + align-items: center; + font-weight: bold; + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + z-index: 1; +} + +.headerText { + padding: 0 20px; + box-sizing: border-box; +} + +.closeButton { + margin-left: auto; + padding: 16px; + font-size: 16px; + line-height: 16px; +} + +.content { + padding: 32px; + box-sizing: border-box; +} +</style> diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 4c55831a3a..e636712389 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption> <div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> <span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> - <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> - <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> - <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> + <span v-else-if="usernameState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="usernameState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="usernameState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="usernameState === 'invalid-format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> + <span v-else-if="usernameState === 'min-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> + <span v-else-if="usernameState === 'max-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> </template> </MkInput> <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> @@ -34,32 +34,32 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-mail"></i></template> <template #caption> <span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> - <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> - <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> - <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> - <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> - <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> - <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="emailState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="emailState === 'unavailable:used'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> + <span v-else-if="emailState === 'unavailable:format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> + <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> + <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> + <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> + <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> + <span v-else-if="emailState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="emailState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> </template> </MkInput> <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword"> <template #label>{{ i18n.ts.password }}</template> <template #prefix><i class="ti ti-lock"></i></template> <template #caption> - <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> - <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> - <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> + <span v-if="passwordStrength == 'low'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> + <span v-if="passwordStrength == 'medium'" style="color: var(--MI_THEME-warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> + <span v-if="passwordStrength == 'high'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> </template> </MkInput> <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> <template #prefix><i class="ti ti-lock"></i></template> <template #caption> - <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> - <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> + <span v-if="passwordRetypeState == 'match'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> + <span v-if="passwordRetypeState == 'not-match'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> </template> </MkInput> <MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason> @@ -71,6 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> <MkCaptcha v-if="instance.enableFC" ref="fc" v-model="fcResponse" :class="$style.captcha" provider="fc" :sitekey="instance.fcSiteKey"/> + <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/> <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;"> <template v-if="submitting"> <MkLoading :em="true" :colored="false"/> @@ -86,10 +87,10 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import { toUnicode } from 'punycode/'; 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, { type Captcha } from '@/components/MkCaptcha.vue'; -import * as config from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; @@ -103,7 +104,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'signup', user: Misskey.entities.SigninResponse): void; + (ev: 'signup', user: Misskey.entities.SignupResponse): void; (ev: 'signupEmailPending'): void; (ev: 'approvalPending'): void; }>(); @@ -111,9 +112,11 @@ const emit = defineEmits<{ const host = toUnicode(config.host); const hcaptcha = ref<Captcha | undefined>(); +const mcaptcha = ref<Captcha | undefined>(); const recaptcha = ref<Captcha | undefined>(); const turnstile = ref<Captcha | undefined>(); const fc = ref<Captcha | undefined>(); +const testcaptcha = ref<Captcha | undefined>(); const username = ref<string>(''); const password = ref<string>(''); @@ -131,6 +134,7 @@ const mCaptchaResponse = ref<string | null>(null); const reCaptchaResponse = ref<string | null>(null); const turnstileResponse = ref<string | null>(null); const fcResponse = ref<string | null>(null); +const testcaptchaResponse = ref<string | null>(null); const usernameAbortController = ref<null | AbortController>(null); const emailAbortController = ref<null | AbortController>(null); @@ -141,6 +145,7 @@ const shouldDisableSubmitting = computed((): boolean => { instance.enableRecaptcha && !reCaptchaResponse.value || instance.enableTurnstile && !turnstileResponse.value || instance.enableFC && !fcResponse.value || + instance.enableTestcaptcha && !testcaptchaResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || usernameState.value !== 'ok' || passwordRetypeState.value !== 'match'; @@ -259,20 +264,33 @@ async function onSubmit(): Promise<void> { if (submitting.value) return; submitting.value = true; - try { - await misskeyApi('signup', { - username: username.value, - password: password.value, - emailAddress: email.value, - invitationCode: invitationCode.value, - reason: reason.value, - 'hcaptcha-response': hCaptchaResponse.value, - 'm-captcha-response': mCaptchaResponse.value, - 'g-recaptcha-response': reCaptchaResponse.value, - 'turnstile-response': turnstileResponse.value, - 'frc-captcha-solution': fcResponse.value, - }); - if (instance.emailRequiredForSignup) { + const signupPayload: Misskey.entities.SignupRequest = { + username: username.value, + password: password.value, + emailAddress: email.value, + invitationCode: invitationCode.value, + reason: reason.value, + 'hcaptcha-response': hCaptchaResponse.value, + 'm-captcha-response': mCaptchaResponse.value, + 'g-recaptcha-response': reCaptchaResponse.value, + 'turnstile-response': turnstileResponse.value, + 'frc-captcha-solution': fcResponse.value, + 'testcaptcha-response': testcaptchaResponse.value, + }; + + const res = await fetch(`${config.apiUrl}/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(signupPayload), + }).catch(() => { + onSignupApiError(); + return null; + }); + + if (res && res.ok) { + if (res.status === 204 || instance.emailRequiredForSignup) { os.alert({ type: 'success', title: i18n.ts._signup.almostThere, @@ -287,28 +305,35 @@ async function onSubmit(): Promise<void> { }); emit('approvalPending'); } else { - const res = await misskeyApi('signin', { - username: username.value, - password: password.value, - }); - emit('signup', res); + const resJson = (await res.json()) as Misskey.entities.SignupResponse; + if (_DEV_) console.log(resJson); + + emit('signup', resJson); if (props.autoSet) { - return login(res.i); + await login(resJson.token); } } - } catch { - submitting.value = false; - hcaptcha.value?.reset?.(); - recaptcha.value?.reset?.(); - turnstile.value?.reset?.(); - fc.value?.reset?.(); - - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); + } else { + onSignupApiError(); } + + submitting.value = false; +} + +function onSignupApiError() { + submitting.value = false; + hcaptcha.value?.reset?.(); + mcaptcha.value?.reset?.(); + recaptcha.value?.reset?.(); + turnstile.value?.reset?.(); + fc.value?.reset?.(); + testcaptcha.value?.reset?.(); + + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); } </script> @@ -317,8 +342,8 @@ async function onSubmit(): Promise<void> { padding: 16px; text-align: center; font-size: 26px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); } .captcha { diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 251c805401..06481b808c 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="availableServerRules" :defaultOpen="true"> <template #label>{{ i18n.ts.serverRules }}</template> - <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template> <ol class="_gaps_s" :class="$style.rules"> <li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="sanitizeHtml(item)"></div></li> @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true"> <template #label>{{ tosPrivacyPolicyLabel }}</template> - <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template> <div class="_gaps_s"> <div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div> <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div> @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template> - <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template> <a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> @@ -151,8 +151,8 @@ async function updateAgreeNote(v: boolean) { padding: 16px; text-align: center; font-size: 26px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); } .rules { @@ -171,19 +171,19 @@ async function updateAgreeNote(v: boolean) { flex-shrink: 0; display: flex; position: sticky; - top: calc(var(--stickyTop, 0px) + 8px); + top: calc(var(--MI-stickyTop, 0px) + 8px); counter-increment: item; content: counter(item); width: 32px; height: 32px; line-height: 32px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); font-size: 13px; font-weight: bold; align-items: center; justify-content: center; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } } diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 91e7d5dd53..291c3ecc2f 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only ref="dialog" :width="500" :height="600" - @close="dialog?.close()" - @closed="$emit('closed')" + @close="onClose" + @closed="emit('closed')" > <template #header>{{ i18n.ts.signup }}</template> @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="$style.transition_x_leaveTo" > <template v-if="!isAcceptedServerRule"> - <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/> + <XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/> </template> <template v-else> <XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending" @approvalPending="onApprovalPending"/> @@ -47,7 +47,8 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done', res: Misskey.entities.SigninResponse): void; + (ev: 'done', res: Misskey.entities.SignupResponse): void; + (ev: 'cancelled'): void; (ev: 'closed'): void; }>(); @@ -55,7 +56,12 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const isAcceptedServerRule = ref(false); -function onSignup(res: Misskey.entities.SigninResponse) { +function onClose() { + emit('cancelled'); + dialog.value?.close(); +} + +function onSignup(res: Misskey.entities.SignupResponse) { emit('done', res); dialog.value?.close(); } diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue index 7743a89242..84dc244b23 100644 --- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue +++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue @@ -63,22 +63,22 @@ function close() { .root { position: fixed; z-index: v-bind(zIndex); - bottom: var(--margin); + bottom: var(--MI-margin); left: 0; right: 0; margin: auto; box-sizing: border-box; - width: calc(100% - (var(--margin) * 2)); + width: calc(100% - (var(--MI-margin) * 2)); max-width: 500px; display: flex; - backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .icon { text-align: center; padding-top: 25px; width: 100px; - color: var(--accent); + color: var(--MI_THEME-accent); } @media (max-width: 500px) { .icon { diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 6bd00fcc2a..a32fd53c51 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -110,20 +110,20 @@ watch(() => props.expandAllCws, (expandAllCws) => { left: 0; width: 100%; height: 64px; - // background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + // background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } &:hover { > .fadeLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } } @@ -132,18 +132,18 @@ watch(() => props.expandAllCws, (expandAllCws) => { .reply { margin-right: 6px; - color: var(--accent); + color: var(--MI_THEME-accent); } .rp { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -152,7 +152,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { width: 100%; margin-top: 14px; position: sticky; - bottom: calc(var(--stickyBottom, 0px) - 100px); + bottom: calc(var(--MI-stickyBottom, 0px) - 100px); } .playMFMButton { @@ -161,10 +161,10 @@ watch(() => props.expandAllCws, (expandAllCws) => { .showLessLabel { display: inline-block; - background: var(--popup); + background: var(--MI_THEME-popup); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 430e3c7958..c9c173aa35 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> -import { } from 'vue'; +<script lang="ts"> +export type SuperMenuDef = { + title?: string; + items: ({ + type: 'a'; + href: string; + target?: string; + icon?: string; + text: string; + danger?: boolean; + active?: boolean; + } | { + type: 'button'; + icon?: string; + text: string; + danger?: boolean; + active?: boolean; + action: (ev: MouseEvent) => void; + } | { + type: 'link'; + to: string; + icon?: string; + text: string; + danger?: boolean; + active?: boolean; + })[]; +}; +</script> +<script lang="ts" setup> defineProps<{ - def: any[]; + def: SuperMenuDef[]; grid?: boolean; }>(); </script> @@ -43,7 +70,7 @@ defineProps<{ & + .group { margin-top: 16px; padding-top: 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } > .title { @@ -59,12 +86,12 @@ defineProps<{ width: 100%; box-sizing: border-box; padding: 9px 16px 9px 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); font-size: 0.9em; &:hover { text-decoration: none; - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } &:focus-visible { @@ -72,12 +99,12 @@ defineProps<{ } &.active { - color: var(--accent); - background: var(--accentedBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-accentedBg); } &.danger { - color: var(--error); + color: var(--MI_THEME-error); } > .icon { @@ -128,10 +155,10 @@ defineProps<{ &:hover { text-decoration: none; background: none; - color: var(--accent); + color: var(--MI_THEME-accent); > .icon { - background: var(--accentedBg); + background: var(--MI_THEME-accentedBg); } } @@ -144,8 +171,8 @@ defineProps<{ width: 60px; height: 60px; aspect-ratio: 1; - background: var(--panel); - border-radius: var(--radius-full); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-full); } > .text { diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue index f7c413e1d3..581aa4e644 100644 --- a/packages/frontend/src/components/MkSwitch.button.vue +++ b/packages/frontend/src/components/MkSwitch.button.vue @@ -51,18 +51,18 @@ const toggle = () => { width: calc(var(--height) * 1.6); height: calc(var(--height) + 2px); // 枠線 outline: none; - background: var(--switchOffBg); + background: var(--MI_THEME-switchOffBg); background-clip: content-box; - border: solid 1px var(--switchOffBg); - border-radius: var(--radius-ellipse); + border: solid 1px var(--MI_THEME-switchOffBg); + border-radius: var(--MI-radius-ellipse); cursor: pointer; transition: inherit; user-select: none; } .buttonChecked { - background-color: var(--switchOnBg) !important; - border-color: var(--switchOnBg) !important; + background-color: var(--MI_THEME-switchOnBg) !important; + border-color: var(--MI_THEME-switchOnBg) !important; } .buttonDisabled { @@ -75,17 +75,17 @@ const toggle = () => { top: 3px; width: calc(var(--height) - 6px); height: calc(var(--height) - 6px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); transition: all 0.2s ease; &:not(.knobChecked) { left: 3px; - background: var(--switchOffFg); + background: var(--MI_THEME-switchOffFg); } } .knobChecked { left: calc(calc(100% - var(--height)) + 3px); - background: var(--switchOnFg); + background: var(--MI_THEME-switchOnFg); } </style> diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index a0994d9cc9..5e6029ee40 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -59,7 +59,7 @@ const toggle = () => { &:hover { > .button { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } @@ -77,7 +77,7 @@ const toggle = () => { margin: 0; &:focus-visible ~ .toggle { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: 2px; } } @@ -87,7 +87,7 @@ const toggle = () => { margin-top: 2px; display: block; transition: inherit; - color: var(--fg); + color: var(--MI_THEME-fg); } .label { @@ -99,7 +99,7 @@ const toggle = () => { .caption { margin: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: 0.85em; &:empty { diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue index ec3b1c90ca..485d003f93 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.vue +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -55,6 +55,18 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton> </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.inactiveModeratorsWarning" :disabled="disabledEvents.inactiveModeratorsWarning"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsWarning }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsWarning)" @click="test('inactiveModeratorsWarning')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.inactiveModeratorsInvitationOnlyChanged" :disabled="disabledEvents.inactiveModeratorsInvitationOnlyChanged"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsInvitationOnlyChanged }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsInvitationOnlyChanged)" @click="test('inactiveModeratorsInvitationOnlyChanged')"><i class="ti ti-send"></i></MkButton> + </div> </div> <div v-show="mode === 'edit'" :class="$style.description"> @@ -100,6 +112,8 @@ type EventType = { abuseReport: boolean; abuseReportResolved: boolean; userCreated: boolean; + inactiveModeratorsWarning: boolean; + inactiveModeratorsInvitationOnlyChanged: boolean; } const emit = defineEmits<{ @@ -123,6 +137,8 @@ const events = ref<EventType>({ abuseReport: true, abuseReportResolved: true, userCreated: true, + inactiveModeratorsWarning: true, + inactiveModeratorsInvitationOnlyChanged: true, }); const isActive = ref<boolean>(true); @@ -130,6 +146,8 @@ const disabledEvents = ref<EventType>({ abuseReport: false, abuseReportResolved: false, userCreated: false, + inactiveModeratorsWarning: false, + inactiveModeratorsInvitationOnlyChanged: false, }); const disableSubmitButton = computed(() => { @@ -261,10 +279,10 @@ onMounted(async () => { bottom: 0; left: 0; padding: 12px; - border-top: solid 0.5px var(--divider); - background: var(--acrylicBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .switchBox { @@ -289,6 +307,6 @@ onMounted(async () => { .description { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } </style> diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue index 54ab8fc663..52e4d304bc 100644 --- a/packages/frontend/src/components/MkTab.vue +++ b/packages/frontend/src/components/MkTab.vue @@ -39,7 +39,7 @@ export default defineComponent({ > button { flex: 1; padding: 10px 8px; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); &:disabled { opacity: 1 !important; @@ -47,13 +47,13 @@ export default defineComponent({ } &.active { - color: var(--accent); - background: var(--accentedBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-accentedBg); } &:not(.active):hover { - color: var(--fgHighlighted); - background: var(--panelHighlight); + color: var(--MI_THEME-fgHighlighted); + background: var(--MI_THEME-panelHighlight); } &:not(:first-child) { diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue index 6b9c181597..87aa046963 100644 --- a/packages/frontend/src/components/MkTagCloud.vue +++ b/packages/frontend/src/components/MkTagCloud.vue @@ -33,7 +33,7 @@ watch(available, () => { try { window.TagCanvas.Start(idForCanvas, idForTags, { textColour: '#ffffff', - outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(), + outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(), outlineRadius: 10, initial: [-0.030, -0.010], frontSelect: true, diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue index 72d6e12656..9deb6528d1 100644 --- a/packages/frontend/src/components/MkTextarea.vue +++ b/packages/frontend/src/components/MkTextarea.vue @@ -159,7 +159,7 @@ onUnmounted(() => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; @@ -179,23 +179,23 @@ onUnmounted(() => { font: inherit; font-weight: normal; font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); outline: none; box-shadow: none; box-sizing: border-box; transition: border-color 0.1s ease-out; &:hover { - border-color: var(--inputBorderHover) !important; + border-color: var(--MI_THEME-inputBorderHover) !important; } } .focused { > .textarea { - border-color: var(--accent) !important; + border-color: var(--MI_THEME-accent) !important; } } @@ -226,7 +226,7 @@ onUnmounted(() => { .mfmPreview { padding: 12px; - border-radius: var(--radius); + border-radius: var(--MI-radius); box-sizing: border-box; min-height: 130px; pointer-events: none; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index b69c19eb9e..7a9abab62e 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -39,10 +39,12 @@ const props = withDefaults(defineProps<{ withRenotes?: boolean; withReplies?: boolean; withBots?: boolean; + withSensitive?: boolean; onlyFiles?: boolean; }>(), { withRenotes: true, withReplies: false, + withSensitive: true, onlyFiles: false, withBots: true, }); @@ -53,6 +55,7 @@ const emit = defineEmits<{ }>(); provide('inTimeline', true); +provide('tl_withSensitive', computed(() => props.withSensitive)); provide('inChannel', computed(() => props.src === 'channel')); type TimelineQueryType = { @@ -275,6 +278,9 @@ function refreshEndpointAndChannel() { // IDが切り替わったら切り替え先のTLを表示させたい watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); +// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK +watch(() => props.withSensitive, reloadTimeline); + // 初回表示用 refreshEndpointAndChannel(); diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue index f731b3264f..38b537cbc9 100644 --- a/packages/frontend/src/components/MkToast.vue +++ b/packages/frontend/src/components/MkToast.vue @@ -70,7 +70,7 @@ onMounted(() => { max-width: calc(100% - 32px); width: min-content; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; text-align: center; pointer-events: none; diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index b32066c950..73aef68964 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only :okButtonDisabled="false" :canClose="false" @close="dialog?.close()" - @closed="$emit('closed')" + @closed="emit('closed')" @ok="ok()" > <template #header>{{ title || i18n.ts.generateAccessToken }}</template> @@ -136,15 +136,15 @@ function enableAll(): void { .adminPermissions { margin: 8px -6px 0; padding: 24px 6px 6px; - border: 2px solid var(--error); - border-radius: calc(var(--radius) / 2); + border: 2px solid var(--MI_THEME-error); + border-radius: calc(var(--MI-radius) / 2); } .adminPermissionsHeader { margin: -34px 0 6px 12px; padding: 0 4px; width: fit-content; - color: var(--error); - background: var(--panel); + color: var(--MI_THEME-error); + background: var(--MI_THEME-panel); } </style> diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index aac07008a4..22e74aa6d1 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -109,8 +109,8 @@ onUnmounted(() => { padding: 8px 12px; box-sizing: border-box; text-align: center; - border-radius: var(--radius-xs); - border: solid 0.5px var(--divider); + border-radius: var(--MI-radius-xs); + border: solid 0.5px var(--MI_THEME-divider); pointer-events: none; transform-origin: center center; } diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index cec7d69943..53b8db38b2 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/> <div v-if="onceReacted"> - <b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br> + <b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br> <I18n :src="i18n.ts._initialTutorial._reaction.reactDone"> <template #undo> <i class="ph-minus ph-bold ph-lg"></i> @@ -116,13 +116,13 @@ function removeReaction(emoji) { <style lang="scss" module> .exampleNoteRoot { - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue index a9014d4202..367c573617 100644 --- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue +++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue @@ -81,14 +81,14 @@ const exampleCWNote = reactive<Misskey.entities.Note>({ <style lang="scss" module> .exampleRoot { max-width: none!important; - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .image { @@ -101,7 +101,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({ display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -116,8 +116,8 @@ const exampleCWNote = reactive<Misskey.entities.Note>({ left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } } diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue index 322082f5a0..e1fc3e4f26 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only :initialNote="exampleNote" @fileChangeSensitive="doSucceeded" ></MkPostForm> - <div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> + <div v-if="onceSucceeded"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> <MkFolder> <template #label>{{ i18n.ts.previewNoteText }}</template> <MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote> @@ -91,14 +91,14 @@ const exampleNote = reactive<Misskey.entities.Note>({ <style lang="scss" module> .exampleRoot { - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .image { @@ -111,7 +111,7 @@ const exampleNote = reactive<Misskey.entities.Note>({ display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -126,8 +126,8 @@ const exampleNote = reactive<Misskey.entities.Note>({ left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } } diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue index b900a30c85..931b7343c5 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -31,14 +31,14 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; <style lang="scss" module> .exampleNoteRoot { - border-radius: var(--radius); - border: var(--panelBorder); - background: var(--panel); + border-radius: var(--MI-radius); + border: var(--MI_THEME-panelBorder); + background: var(--MI_THEME-panel); } .divider { height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .image { @@ -51,7 +51,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -66,8 +66,8 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } } diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 1f5a2b9381..11d7c8dc4d 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div> <div>{{ i18n.ts._initialTutorial._landing.description }}</div> <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton> @@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div> <I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;"> <template #link> @@ -223,7 +223,7 @@ async function close(skip: boolean) { .progressBarValue { height: 100%; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); transition: all 0.5s cubic-bezier(0,.5,.5,1); } @@ -253,7 +253,7 @@ async function close(skip: boolean) { left: 0; flex-shrink: 0; padding: 12px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); -webkit-backdrop-filter: blur(15px); backdrop-filter: blur(15px); } diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 91f5b86c2d..7cafb1b0af 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -46,8 +46,8 @@ onMounted(() => { max-width: 480px; box-sizing: border-box; text-align: center; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } .title { diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 04f5314463..0808a052cd 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -84,13 +84,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue'; -import type { summaly } from '@misskey-dev/summaly'; import { url as local } from '@@/js/config.js'; +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 MkButton from '@/components/MkButton.vue'; -import { versatileLang } from '@@/js/intl-const.js'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; @@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa sensitive.value = info.sensitive ?? false; }); -function adjustTweetHeight(message: any) { +function adjustTweetHeight(message: MessageEvent) { if (message.origin !== 'https://platform.twitter.com') return; const embed = message.data?.['twttr.embed']; if (embed?.method !== 'twttr.private.resize') return; @@ -193,14 +193,16 @@ function openPlayer(): void { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), { url: requestUrl.href, }, { - // TODO + closed: () => { + dispose(); + }, }); } -(window as any).addEventListener('message', adjustTweetHeight); +window.addEventListener('message', adjustTweetHeight); onUnmounted(() => { - (window as any).removeEventListener('message', adjustTweetHeight); + window.removeEventListener('message', adjustTweetHeight); }); </script> @@ -219,7 +221,7 @@ onUnmounted(() => { height: 1.5em; padding: 0; margin: 0; - color: var(--fg); + color: var(--MI_THEME-fg); background: rgba(128, 128, 128, 0.2); opacity: 0.7; @@ -240,8 +242,8 @@ onUnmounted(() => { position: relative; display: block; font-size: 14px; - box-shadow: 0 0 0 1px var(--divider); - border-radius: var(--radius-sm); + box-shadow: 0 0 0 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); overflow: clip; &:hover { @@ -270,7 +272,7 @@ onUnmounted(() => { height: 100%; background-position: center; background-size: cover; - background-color: var(--bg); + background-color: var(--MI_THEME-bg); display: flex; justify-content: center; align-items: center; @@ -317,7 +319,6 @@ onUnmounted(() => { .siteName { display: inline-block; margin: 0; - color: var(--urlPreviewInfo); font-size: 0.8em; line-height: 16px; vertical-align: top; diff --git a/packages/frontend/src/components/MkUrlWarningDialog.vue b/packages/frontend/src/components/MkUrlWarningDialog.vue index f2e9b11fd9..3bec6eecdd 100644 --- a/packages/frontend/src/components/MkUrlWarningDialog.vue +++ b/packages/frontend/src/components/MkUrlWarningDialog.vue @@ -94,7 +94,7 @@ onBeforeUnmount(() => { min-width: 320px; max-width: 480px; box-sizing: border-box; - background: var(--panel); + background: var(--MI_THEME-panel); border-radius: 16px; } @@ -106,7 +106,7 @@ onBeforeUnmount(() => { .icon { font-size: 18px; - color: var(--warn); + color: var(--MI_THEME-warn); } .title { @@ -117,7 +117,7 @@ onBeforeUnmount(() => { .urlAddress { padding: 10px 14px; border-radius: 8px; - border: 1px solid var(--divider); + border: 1px solid var(--MI_THEME-divider); overflow-x: auto; white-space: nowrap; } diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 3c5f563aa0..fe499fabbf 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="dialog" :width="400" @close="dialog?.close()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template v-if="announcement" #header>:{{ announcement.title }}:</template> <template v-else #header>New announcement</template> @@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="icon"> <template #label>{{ i18n.ts.icon }}</template> <option value="info"><i class="ti ti-info-circle"></i></option> - <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option> - <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option> - <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option> + <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option> + <option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option> + <option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option> </MkRadios> <MkRadios v-model="display"> <template #label>{{ i18n.ts.display }}</template> @@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; +type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; } + const props = defineProps<{ user: Misskey.entities.User, - announcement?: Misskey.entities.Announcement, + announcement?: Required<AdminAnnouncementType>, +}>(); + +const emit = defineEmits<{ + (ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void, + (ev: 'closed'): void }>(); const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null); @@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info'); const display = ref(props.announcement ? props.announcement.display : 'dialog'); const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false); -const emit = defineEmits<{ - (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, - (ev: 'closed'): void -}>(); - async function done() { const params = { title: title.value, @@ -88,7 +90,7 @@ async function done() { display: display.value, needConfirmationToRead: needConfirmationToRead.value, userId: props.user.id, - }; + } satisfies Misskey.entities.AdminAnnouncementsCreateRequest; if (props.announcement) { await os.apiWithDialog('admin/announcements/update', { @@ -141,8 +143,8 @@ async function del() { bottom: 0; left: 0; padding: 12px; - border-top: solid 0.5px var(--divider); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index 603f9f2435..ce28f6ec5e 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -23,7 +23,7 @@ import { acct } from '@/filters/user.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.User; - withChart: boolean; + withChart?: boolean; }>(), { withChart: true, }); @@ -49,8 +49,8 @@ $bodyInfoHieght: 16px; display: flex; align-items: center; padding: 16px; - background: var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); } .avatar { @@ -64,7 +64,7 @@ $bodyInfoHieght: 16px; flex: 1; overflow: hidden; font-size: 0.9em; - color: var(--fg); + color: var(--MI_THEME-fg); padding-right: 8px; } diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index 73cdd9ce00..a6bbacacee 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -75,9 +75,9 @@ defineProps<{ top: 62px; left: 13px; z-index: 2; - width: var(--avatar); - height: var(--avatar); - border: solid 4px var(--panel); + width: var(--MI-avatar); + height: var(--MI-avatar); + border: solid 4px var(--MI_THEME-panel); } .title { @@ -98,7 +98,7 @@ defineProps<{ margin: 0; line-height: 16px; font-size: 0.8em; - color: var(--fg); + color: var(--MI_THEME-fg); opacity: 0.7; } @@ -110,13 +110,13 @@ defineProps<{ color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .description { padding: 16px; font-size: 0.8em; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .mfm { @@ -128,7 +128,7 @@ defineProps<{ .status { padding: 10px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .statusItem { @@ -139,12 +139,12 @@ defineProps<{ .statusItemLabel { margin: 0; font-size: 0.7em; - color: var(--fg); + color: var(--MI_THEME-fg); } .statusItemValue { font-size: 1em; - color: var(--accent); + color: var(--MI_THEME-accent); } .follow { @@ -169,7 +169,7 @@ defineProps<{ color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); list-style-type: none; margin-left: 0; } diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index ac82ecc3d6..8dc01a08ab 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -39,6 +39,6 @@ const props = withDefaults(defineProps<{ .root { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: var(--margin); + grid-gap: var(--MI-margin); } </style> diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue index 9f04353f62..7a6ded110a 100644 --- a/packages/frontend/src/components/MkUserOnlineIndicator.vue +++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue @@ -36,11 +36,11 @@ const text = computed(() => { <style lang="scss" module> .root { - box-shadow: 0 0 0 3px var(--panel); + box-shadow: 0 0 0 3px var(--MI_THEME-panel); // sharkey: the comment mentions something about 100% radius not behaving correctly on blink. // couldn't reproduce, assuming the 120% here was just an old workaround - border-radius: var(--radius-full); // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる + border-radius: var(--MI-radius-full); // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる &.status_online { background: #58d4c9; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 40199c12ff..73e38bef09 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <svg viewBox="0 0 128 128" :class="$style.avatarBack"> <g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)"> - <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/> + <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--MI_THEME-popup);"/> </g> </svg> <MkAvatar :class="$style.avatar" :user="user" indicator/> @@ -162,7 +162,7 @@ onMounted(() => { color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .locked:first-child { @@ -173,7 +173,7 @@ onMounted(() => { color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } .locked:not(:first-child) { @@ -184,7 +184,7 @@ onMounted(() => { color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } .avatarBack { @@ -204,8 +204,8 @@ onMounted(() => { right: 0; margin: 0 auto; z-index: 2; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .title { @@ -233,15 +233,15 @@ onMounted(() => { padding: 16px 26px; font-size: 0.8em; text-align: center; - border-top: solid 1px var(--divider); - border-bottom: solid 1px var(--divider); + border-top: solid 1px var(--MI_THEME-divider); + border-bottom: solid 1px var(--MI_THEME-divider); } .fields { font-size: 0.8em; padding: 16px; - border-top: solid 1px var(--divider); - border-bottom: solid 1px var(--divider); + border-top: solid 1px var(--MI_THEME-divider); + border-bottom: solid 1px var(--MI_THEME-divider); } .field { @@ -298,7 +298,7 @@ onMounted(() => { .statusItemLabel { font-size: 0.7em; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } .menu { @@ -306,8 +306,8 @@ onMounted(() => { top: 8px; right: 44px; padding: 6px; - background: var(--panel); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-ellipse); } .follow { diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index a5b48c8ce2..85d4666172 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click="cancel()" @close="cancel()" @ok="ok()" - @closed="$emit('closed')" + @closed="emit('closed')" > <template #header>{{ i18n.ts.selectUser }}</template> <div> @@ -196,11 +196,11 @@ onMounted(() => { font-size: 14px; &:hover { - background: var(--X7); + background: var(--MI_THEME-X7); } &.selected { - background: var(--accent); + background: var(--MI_THEME-accent); color: #fff; } } diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue index 1524ea0ec9..5153c06139 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue @@ -62,7 +62,7 @@ const popularUsers: Paging = { .users { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); - grid-gap: var(--margin); + grid-gap: var(--MI-margin); justify-content: center; } </style> diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 3194641cdb..7cb48f6afb 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -51,6 +51,11 @@ watch(name, () => { // 空文字列をnullにしたいので??は使うな // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing name: name.value || null, + }, undefined, { + '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': { + title: i18n.ts.yourNameContainsProhibitedWords, + text: i18n.ts.yourNameContainsProhibitedWordsDescription, + }, }); }); diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index c80349d034..4c4f4989c5 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -59,9 +59,9 @@ async function follow() { top: 30px; left: 13px; z-index: 2; - width: var(--avatar); - height: var(--avatar); - border: solid 4px var(--panel); + width: var(--MI-avatar); + height: var(--MI-avatar); + border: solid 4px var(--MI_THEME-panel); } .title { @@ -82,7 +82,7 @@ async function follow() { margin: 0; line-height: 16px; font-size: 0.8em; - color: var(--fg); + color: var(--MI_THEME-fg); opacity: 0.7; } @@ -99,7 +99,7 @@ async function follow() { } .footer { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); padding: 16px; } </style> diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 1fb1eda039..b7261129ef 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div> <div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div> <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton> @@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.centerPage"> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div> <div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div> <MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/> @@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div> <div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div> <div class="_buttonsCenter" style="margin-top: 16px;"> @@ -223,7 +223,7 @@ async function later(later: boolean) { .progressBarValue { height: 100%; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); transition: all 0.5s cubic-bezier(0,.5,.5,1); } @@ -252,7 +252,7 @@ async function later(later: boolean) { left: 0; flex-shrink: 0; padding: 12px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); -webkit-backdrop-filter: blur(15px); backdrop-filter: blur(15px); } diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue index 054a503257..0cb7f22e93 100644 --- a/packages/frontend/src/components/MkUsersTooltip.vue +++ b/packages/frontend/src/components/MkUsersTooltip.vue @@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import * as Misskey from 'misskey-js'; import MkTooltip from './MkTooltip.vue'; defineProps<{ showing: boolean; - users: any[]; // TODO + users: Misskey.entities.UserLite[]; count: number; targetElement: HTMLElement; }>(); diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index 3c3f9e94b6..5624abbd33 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -82,7 +82,7 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void { &.asDrawer { padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; width: 100%; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); border-bottom-right-radius: 0; border-bottom-left-radius: 0; @@ -124,7 +124,7 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void { } &.active { - color: var(--accent); + color: var(--MI_THEME-accent); } } diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue index cab42cd59d..d098dad9a1 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue @@ -62,7 +62,7 @@ async function renderChart() { const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const computedStyle = getComputedStyle(document.documentElement); - const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(); + const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); const colorRead = accent; const colorWrite = '#2ecc71'; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 874eff6c79..54f2ee655c 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -110,8 +110,8 @@ function showMenu(ev: MouseEvent) { .panel { position: relative; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); box-shadow: 0 12px 32px rgb(0 0 0 / 25%); } @@ -140,7 +140,7 @@ function showMenu(ev: MouseEvent) { right: 16px; width: 32px; height: 32px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); font-size: 18px; z-index: 50; } @@ -191,14 +191,14 @@ function showMenu(ev: MouseEvent) { } .statsItemLabel { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: 0.9em; } .statsItemCount { font-weight: bold; font-size: 1.2em; - color: var(--accent); + color: var(--MI_THEME-accent); } .tl { @@ -207,7 +207,7 @@ function showMenu(ev: MouseEvent) { .tlHeader { padding: 12px 16px; - border-bottom: solid 1px var(--divider); + border-bottom: solid 1px var(--MI_THEME-divider); } .tlBody { diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue index 60b75b6d30..34fa6b0723 100644 --- a/packages/frontend/src/components/MkWaitingDialog.vue +++ b/packages/frontend/src/components/MkWaitingDialog.vue @@ -47,8 +47,8 @@ watch(() => props.showing, () => { padding: 32px; box-sizing: border-box; text-align: center; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); width: 250px; &.iconOnly { @@ -65,7 +65,7 @@ watch(() => props.showing, () => { font-size: 32px; &.success { - color: var(--accent); + color: var(--MI_THEME-accent); } &.waiting { diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 99840bf8d7..b987283a65 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -7,12 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <template v-if="edit"> <header :class="$style.editHeader"> - <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select> + <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select> <template #label>{{ i18n.ts.selectWidget }}</template> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option> </MkSelect> <MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> + <MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton> </header> <Sortable :modelValue="props.widgets" @@ -123,7 +123,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { .widget { contain: content; - margin: var(--margin) 0; + margin: var(--MI-margin) 0; &:first-of-type { margin-top: 0; @@ -158,7 +158,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { height: 32px; color: #fff; background: rgba(#000, 0.7); - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } &Config { diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 08906a1205..2953f656d4 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only :enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''" appear - @afterLeave="$emit('closed')" + @afterLeave="emit('closed')" > <div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]"> <div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown"> @@ -54,12 +54,19 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; +import type { MenuItem } from '@/types/menu.js'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; -import type { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; +type WindowButton = { + title: string; + icon: string; + onClick: () => void; + highlighted?: boolean; +}; + const minHeight = 50; const minWidth = 250; @@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{ mini?: boolean; front?: boolean; contextmenu?: MenuItem[] | null; - buttonsLeft?: any[]; - buttonsRight?: any[]; + buttonsLeft?: WindowButton[]; + buttonsRight?: WindowButton[]; }>(), { initialWidth: 400, initialHeight: null, @@ -484,6 +491,10 @@ defineExpose({ } .root { + // universal.vueとかで直接--MI-stickyBottomが定義されていたりするのでリセット + --MI-stickyTop: 0; + --MI-stickyBottom: 0; + position: fixed; top: 0; left: 0; @@ -502,7 +513,7 @@ defineExpose({ contain: content; width: 100%; height: 100%; - border-radius: var(--radius); + border-radius: var(--MI-radius); } .header { @@ -514,10 +525,10 @@ defineExpose({ flex-shrink: 0; user-select: none; height: var(--height); - background: var(--windowHeader); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - //border-bottom: solid 1px var(--divider); + background: var(--MI_THEME-windowHeader); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + //border-bottom: solid 1px var(--MI_THEME-divider); font-size: 90%; font-weight: bold; @@ -531,11 +542,11 @@ defineExpose({ width: var(--height); &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } &.highlighted { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -560,7 +571,7 @@ defineExpose({ .content { flex: 1; overflow: auto; - background: var(--panel); + background: var(--MI_THEME-panel); container-type: size; } diff --git a/packages/frontend/src/components/SkFlashPlayer.vue b/packages/frontend/src/components/SkFlashPlayer.vue index 739a7bc74c..2b61974ef7 100644 --- a/packages/frontend/src/components/SkFlashPlayer.vue +++ b/packages/frontend/src/components/SkFlashPlayer.vue @@ -244,9 +244,9 @@ onDeactivated(() => { } .hide { - border-radius: var(--radius-sm) !important; + border-radius: var(--MI-radius-sm) !important; background-color: black !important; - color: var(--accentLighten) !important; + color: var(--MI_THEME-accentLighten) !important; font-size: 12px !important; } @@ -260,9 +260,9 @@ onDeactivated(() => { > i { display: block; position: absolute; - border-radius: var(--radius-sm); - background-color: var(--fg); - color: var(--accentLighten); + border-radius: var(--MI-radius-sm); + background-color: var(--MI_THEME-fg); + color: var(--MI_THEME-accentLighten); font-size: 14px; opacity: .5; padding: 3px 6px; @@ -276,9 +276,9 @@ onDeactivated(() => { > .alt { display: block; position: absolute; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); background-color: black; - color: var(--accentLighten); + color: var(--MI_THEME-accentLighten); font-size: 0.8em; font-weight: bold; opacity: .5; @@ -311,10 +311,10 @@ onDeactivated(() => { justify-content: center; align-items: center; background: rgba(64, 64, 64, 0.3); - backdrop-filter: var(--modalBgFilter); + backdrop-filter: var(--MI-modalBgFilter); color: #fff; font-size: 12px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); position: absolute; z-index: 4; @@ -334,7 +334,7 @@ onDeactivated(() => { > .controls { display: flex; width: 100%; - background-color: var(--bg); + background-color: var(--MI_THEME-bg); z-index: 5; > * { @@ -344,12 +344,12 @@ onDeactivated(() => { > button, a { border: none; background-color: transparent; - color: var(--accent); + color: var(--MI_THEME-accent); text-decoration: none; cursor: pointer; &:hover { - background-color: var(--fg); + background-color: var(--MI_THEME-fg); } &:disabled { @@ -385,11 +385,11 @@ onDeactivated(() => { outline: none; &::-webkit-slider-runnable-track { - background: var(--bg); + background: var(--MI_THEME-bg); } &::-ms-fill-lower, &::-ms-fill-upper { - background: var(--bg); + background: var(--MI_THEME-bg); } } @@ -398,8 +398,8 @@ onDeactivated(() => { height: 100%; border-radius: 0; animate: 0.2s; - background: var(--bg); - border: 1px solid var(--fg); + background: var(--MI_THEME-bg); + border: 1px solid var(--MI_THEME-fg); overflow-x: hidden; } @@ -408,9 +408,9 @@ onDeactivated(() => { height: 100%; width: 14px; border-radius: 0; - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); -webkit-appearance: none; - box-shadow: calc(-100vw - 14px) 0 0 100vw var(--accent); + box-shadow: calc(-100vw - 14px) 0 0 100vw var(--MI_THEME-accent); clip-path: polygon(1px 0, 100% 0, 100% 100%, 1px 100%, 1px calc(50% + 10.5px), -100vw calc(50% + 10.5px), -100vw calc(50% - 10.5px), 0 calc(50% - 10.5px)); z-index: 1; } @@ -420,13 +420,13 @@ onDeactivated(() => { height: 100%; border-radius: 0; animate: 0.2s; - background: var(--bg); - border: 1px solid var(--fg); + background: var(--MI_THEME-bg); + border: 1px solid var(--MI_THEME-fg); } &::-moz-range-progress { height: 100%; - background: var(--accent); + background: var(--MI_THEME-accent); } &::-moz-range-thumb { @@ -434,7 +434,7 @@ onDeactivated(() => { height: 100%; border-radius: 0; width: 14px; - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } &::-ms-track { @@ -448,14 +448,14 @@ onDeactivated(() => { } &::-ms-fill-lower { - background: var(--accent); - border: 1px solid var(--fg); + background: var(--MI_THEME-accent); + border: 1px solid var(--MI_THEME-fg); border-radius: 0; } &::-ms-fill-upper { - background: var(--bg); - border: 1px solid var(--fg); + background: var(--MI_THEME-bg); + border: 1px solid var(--MI_THEME-fg); border-radius: 0; } @@ -465,7 +465,7 @@ onDeactivated(() => { height: 100%; width: 14px; border-radius: 0; - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } } } diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue index 6daa8feba5..7e71065082 100644 --- a/packages/frontend/src/components/SkFollowingRecentNotes.vue +++ b/packages/frontend/src/components/SkFollowingRecentNotes.vue @@ -118,15 +118,15 @@ function checkMute(note: Misskey.entities.Note | undefined | null, mutes: Mutes) <style module lang="scss"> .panel { - background: var(--panel); + background: var(--MI_THEME-panel); } @keyframes border { from { - border-left: 0 solid var(--accent); + border-left: 0 solid var(--MI_THEME-accent); } to { - border-left: 6px solid var(--accent); + border-left: 6px solid var(--MI_THEME-accent); } } diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue index eb987d9c77..2bfe5cc157 100644 --- a/packages/frontend/src/components/SkInstanceTicker.vue +++ b/packages/frontend/src/components/SkInstanceTicker.vue @@ -45,7 +45,7 @@ const bg = { display: flex; align-items: center; height: 1.5ex; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); padding: 4px; overflow: clip; color: #fff; diff --git a/packages/frontend/src/components/SkMfmWindow.vue b/packages/frontend/src/components/SkMfmWindow.vue index c599531ec5..58ca3d38ab 100644 --- a/packages/frontend/src/components/SkMfmWindow.vue +++ b/packages/frontend/src/components/SkMfmWindow.vue @@ -491,12 +491,12 @@ const preview_fade = ref(`$[fade 🍮] $[fade.out 🍮] $[fade.speed=3s 🍮] $[ > .title { position: sticky; z-index: 1; - top: var(--stickyTop, 0px); + top: var(--MI-stickyTop, 0px); padding: 16px; font-weight: bold; - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); - background-color: var(--X16); + -webkit-backdrop-filter: var(--MI-blur, blur(10px)); + backdrop-filter: var(--MI-blur, blur(10px)); + background-color: var(--MI_THEME-X16); } > .content { @@ -507,7 +507,7 @@ const preview_fade = ref(`$[fade 🍮] $[fade.out 🍮] $[fade.speed=3s 🍮] $[ } > .preview { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); padding: 16px; } } diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index dc8c32a7d9..528e86646b 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" - :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" + :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]" :tabindex="isDeleted ? '-1' : '0'" > <SkNoteSub v-if="appearNote.reply" v-show="!renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> @@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" :class="$style.footerButton" class="_button" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @click.stop @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()" > @@ -157,8 +157,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <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="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -202,6 +202,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro import * as mfm from '@transfem-org/sfm-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 { MenuItem } from '@/types/menu.js'; import SkNoteSub from '@/components/SkNoteSub.vue'; import SkNoteHeader from '@/components/SkNoteHeader.vue'; import SkNoteSimple from '@/components/SkNoteSimple.vue'; @@ -215,6 +218,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkButton from '@/components/MkButton.vue'; import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/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'; @@ -233,13 +237,10 @@ 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 type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { useRouter } from '@/router/supplier.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; -import { shouldCollapsed } from '@@/js/collapsed.js'; -import { host } from '@@/js/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; @@ -264,6 +265,7 @@ const emit = defineEmits<{ const router = useRouter(); const inTimeline = inject<boolean>('inTimeline', false); +const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true)); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); @@ -343,15 +345,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; */ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { - if (mutedWords == null) return false; - - if (checkWordMute(noteToCheck, $i, mutedWords)) return true; - if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; - if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + if (mutedWords != null) { + if (checkWordMute(noteToCheck, $i, mutedWords)) return true; + if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; + if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + } if (checkOnly) return false; - if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; + if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) { + return 'sensitiveMute'; + } + return false; } @@ -514,7 +519,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -564,7 +569,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (props.mock) { return; @@ -625,7 +630,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (props.mock) { return; } @@ -638,7 +643,7 @@ function reply(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.mock) { @@ -660,7 +665,7 @@ function like(): void { } function react(viaKeyboard = false): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -808,15 +813,24 @@ function showRenoteMenu(): void { }; } + const renoteDetailsMenu: MenuItem = { + type: 'link', + text: i18n.ts.renoteDetails, + icon: 'ti ti-info-circle', + to: notePage(note.value), + }; + if (isMyRenote) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), ], renoteTime.value); } else { os.popupMenu([ + renoteDetailsMenu, getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), @@ -901,8 +915,8 @@ function emitUpdReaction(emoji: string, delta: number) { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: solid 2px var(--focus); - border-radius: var(--radius); + border: solid 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -929,9 +943,9 @@ function emitUpdReaction(emoji: string, delta: number) { right: 12px; padding: 0 4px; margin-bottom: 0 !important; - background: var(--popup); - border-radius: var(--radius-sm); - box-shadow: 0px 4px 32px var(--shadow); + background: var(--MI_THEME-popup); + border-radius: var(--MI-radius-sm); + box-shadow: 0px 4px 32px var(--MI_THEME-shadow); } .footerButton { @@ -950,6 +964,11 @@ function emitUpdReaction(emoji: string, delta: number) { } } +.skipRender { + content-visibility: auto; + contain-intrinsic-size: 0 150px; +} + .tip { display: flex; align-items: center; @@ -972,18 +991,18 @@ function emitUpdReaction(emoji: string, delta: number) { position: relative; display: flex; align-items: center; - padding: 24px 32px 0 calc(32px + var(--avatar) + 14px); + padding: 24px 32px 0 calc(32px + var(--MI-avatar) + 14px); line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); &::before { content: ''; position: absolute; top: 0; - left: calc(32px + .5 * var(--avatar)); + left: calc(32px + .5 * var(--MI-avatar)); bottom: -8px; - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); } &:first-child { @@ -1072,9 +1091,9 @@ function emitUpdReaction(emoji: string, delta: number) { .collapsedInReplyToLine { position: absolute; - left: calc(32px + .5 * var(--avatar)); + left: calc(32px + .5 * var(--MI-avatar)); // using solid instead of dotted, stylelistic choice - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); top: calc(28px + 28px); // 28px of .root padding, plus 28px of avatar height (see SkNote) height: 28px; } @@ -1090,7 +1109,7 @@ function emitUpdReaction(emoji: string, delta: number) { left: 8px; width: 5px; height: calc(100% - 16px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -1099,10 +1118,10 @@ function emitUpdReaction(emoji: string, delta: number) { display: block !important; position: sticky !important; margin: 0 14px 0 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); position: sticky !important; - top: calc(22px + var(--stickyTop, 0px)); + top: calc(22px + var(--MI-stickyTop, 0px)); left: 0; transition: top 0.5s; @@ -1128,15 +1147,15 @@ function emitUpdReaction(emoji: string, delta: number) { width: 100%; margin-top: 14px; position: sticky; - bottom: calc(var(--stickyBottom, 0px) - 100px); + bottom: calc(var(--MI-stickyBottom, 0px) - 100px); } .showLessLabel { display: inline-block; - background: var(--popup); + background: var(--MI_THEME-popup); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1154,19 +1173,19 @@ function emitUpdReaction(emoji: string, delta: number) { z-index: 2; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); &:hover > .collapsedLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } .collapsedLabel { display: inline-block; - background: var(--panel); + background: var(--MI_THEME-panel); padding: 6px 10px; font-size: 0.8em; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } @@ -1175,13 +1194,13 @@ function emitUpdReaction(emoji: string, delta: number) { } .replyIcon { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1205,8 +1224,8 @@ function emitUpdReaction(emoji: string, delta: number) { .quoteNote { padding: 16px; // Made border solid, stylistic choice - border: solid 1px var(--renote); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1229,7 +1248,7 @@ function emitUpdReaction(emoji: string, delta: number) { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1242,14 +1261,14 @@ function emitUpdReaction(emoji: string, delta: number) { @container (max-width: 580px) { .root { font-size: 0.95em; - --avatar: 46px; + --MI-avatar: 46px; } .renote { - padding: 24px 26px 0 calc(26px + var(--avatar) + 14px); + padding: 24px 26px 0 calc(26px + var(--MI-avatar) + 14px); &::before { - left: calc(26px + .5 * var(--avatar)); + left: calc(26px + .5 * var(--MI-avatar)); } } @@ -1262,7 +1281,7 @@ function emitUpdReaction(emoji: string, delta: number) { } .collapsedInReplyToLine { - left: calc(26px + .5 * var(--avatar)); + left: calc(26px + .5 * var(--MI-avatar)); } .article { @@ -1284,26 +1303,26 @@ function emitUpdReaction(emoji: string, delta: number) { } .collapsedInReplyToLine { - left: calc(25px + .5 * var(--avatar)); + left: calc(25px + .5 * var(--MI-avatar)); } } @container (max-width: 500px) { .renote { - padding: 23px 25px 0 calc(25px + var(--avatar) + 14px); + padding: 23px 25px 0 calc(25px + var(--MI-avatar) + 14px); &::before { - left: calc(25px + .5 * var(--avatar)); + left: calc(25px + .5 * var(--MI-avatar)); } } } @container (max-width: 480px) { .renote { - padding: 22px 24px 0 calc(24px + var(--avatar) + 14px); + padding: 22px 24px 0 calc(24px + var(--MI-avatar) + 14px); &::before { - left: calc(24px + .5 * var(--avatar)); + left: calc(24px + .5 * var(--MI-avatar)); } } @@ -1321,7 +1340,7 @@ function emitUpdReaction(emoji: string, delta: number) { } .collapsedInReplyToLine { - left: calc(24px + .5 * var(--avatar)); + left: calc(24px + .5 * var(--MI-avatar)); top: calc(22px + 28px); // 22px of .root padding, plus 28px of avatar height } @@ -1332,12 +1351,12 @@ function emitUpdReaction(emoji: string, delta: number) { @container (max-width: 450px) { .root { - --avatar: 44px; + --MI-avatar: 44px; } .avatar { margin: 0 10px 0 0; - top: calc(14px + var(--stickyTop, 0px)); + top: calc(14px + var(--MI-stickyTop, 0px)); } } diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 7907da6c94..6c14c6627c 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -58,7 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only <img v-for="role in appearNote.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/> </span> </div> - <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> + <div :class="$style.noteHeaderUsernameAndBadgeRoles"> + <div :class="$style.noteHeaderUsername"> + <MkAcct :user="appearNote.user"/> + </div> + <div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles"> + <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/> + </div> + </div> </div> </div> <div style="display: flex; align-items: flex-end; margin-left: auto;"> @@ -143,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown.prevent="renoted ? undoRenote() : boostVisibility()" > <i class="ti ti-repeat"></i> @@ -165,8 +172,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> + <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="ph-smiley ph-bold ph-lg"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -244,6 +251,7 @@ import { computed, inject, onMounted, onUnmounted, onUpdated, provide, ref, shal import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; +import { host } from '@@/js/config.js'; import SkNoteSub from '@/components/SkNoteSub.vue'; import SkNoteSimple from '@/components/SkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -267,7 +275,6 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { host } from '@@/js/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; @@ -515,7 +522,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); renoting = true; @@ -561,7 +568,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -619,7 +626,7 @@ function quote() { } function reply(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: appearNote.value, @@ -630,7 +637,7 @@ function reply(): void { } function react(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -667,7 +674,7 @@ function react(): void { } function like(): void { - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -749,7 +756,7 @@ async function clip(): Promise<void> { function showRenoteMenu(): void { if (!isMyRenote) return; - pleaseLogin(undefined, pleaseLoginContext.value); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([{ text: i18n.ts.unrenote, icon: 'ti ti-trash', @@ -877,8 +884,8 @@ onUnmounted(() => { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 2px var(--focus); - border-radius: var(--radius); + border: dashed 2px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -904,7 +911,7 @@ onUnmounted(() => { padding: 16px 32px 8px 32px; line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); } .renoteAvatar { @@ -913,7 +920,7 @@ onUnmounted(() => { width: 28px; height: 28px; margin: 0 8px 0 0; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .renoteText { @@ -967,8 +974,8 @@ onUnmounted(() => { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: solid 1px var(--focus); - border-radius: var(--radius); + border: solid 1px var(--MI_THEME-focus); + border-radius: var(--MI-radius); box-sizing: border-box; } } @@ -985,8 +992,8 @@ onUnmounted(() => { .noteHeaderAvatar { display: block; flex-shrink: 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .noteHeaderBody { @@ -1009,8 +1016,8 @@ onUnmounted(() => { padding: 4px 6px; font-size: 80%; line-height: 1; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .noteHeaderInfo { @@ -1018,8 +1025,13 @@ onUnmounted(() => { text-align: right; } +.noteHeaderUsernameAndBadgeRoles { + display: flex; +} + .noteHeaderUsername { margin-bottom: 2px; + margin-right: 0.5em; line-height: 1.3; word-wrap: anywhere; text-overflow: ellipsis; @@ -1034,6 +1046,19 @@ onUnmounted(() => { margin-top: 5px; } +.noteHeaderBadgeRoles { + margin: 0 .5em 0 0; +} + +.noteHeaderBadgeRole { + height: 1.3em; + vertical-align: -20%; + + & + .noteHeaderBadgeRole { + margin-left: 0.2em; + } +} + .noteContent { container-type: inline-size; overflow-wrap: break-word; @@ -1049,19 +1074,19 @@ onUnmounted(() => { } .noteReplyTarget { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .rn { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -1076,8 +1101,8 @@ onUnmounted(() => { .quoteNote { padding: 16px; - border: solid 1px var(--renote); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-sm); overflow: clip; } @@ -1102,7 +1127,7 @@ onUnmounted(() => { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -1112,17 +1137,17 @@ onUnmounted(() => { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } .reply:not(:first-child) { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .tabs { - border-top: solid 0.5px var(--divider); - border-bottom: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); display: flex; } @@ -1141,7 +1166,7 @@ onUnmounted(() => { } .tabActive { - border-bottom: solid 2px var(--accent); + border-bottom: solid 2px var(--MI_THEME-accent); } .tab_renotes { @@ -1161,12 +1186,12 @@ onUnmounted(() => { .reactionTab { padding: 4px 6px; - border: solid 1px var(--divider); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); } .reactionTabActive { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } @container (max-width: 500px) { @@ -1221,7 +1246,7 @@ onUnmounted(() => { margin: 0 10px 0 0 !important; width: 40px !important; height: 40px !important; - border-radius: var(--radius-sm) !important; + border-radius: var(--MI-radius-sm) !important; } .muted { diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue index 45218fafb6..6bcc30f6cb 100644 --- a/packages/frontend/src/components/SkNoteHeader.vue +++ b/packages/frontend/src/components/SkNoteHeader.vue @@ -161,7 +161,7 @@ const mock = inject<boolean>('mock', false); } &:hover { - color: var(--nameHover); + color: var(--MI_THEME-nameHover); text-decoration: none; } } @@ -188,8 +188,8 @@ const mock = inject<boolean>('mock', false); margin: 0 .5em 0 0; padding: 1px 6px; font-size: 80%; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .username { @@ -255,7 +255,7 @@ const mock = inject<boolean>('mock', false); } .danger { - color: var(--accent); + color: var(--MI_THEME-accent); } @container (max-width: 500px) { diff --git a/packages/frontend/src/components/SkNoteSimple.vue b/packages/frontend/src/components/SkNoteSimple.vue index b31e337a99..b9895305f2 100644 --- a/packages/frontend/src/components/SkNoteSimple.vue +++ b/packages/frontend/src/components/SkNoteSimple.vue @@ -50,7 +50,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { font-size: 0.95em; &:hover, &:focus-within { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); transition: background .2s; } } @@ -61,9 +61,9 @@ watch(() => props.expandAllCws, (expandAllCws) => { margin: 0 10px 0 0; width: 34px; height: 34px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); position: sticky !important; - top: calc(16px + var(--stickyTop, 0px)); + top: calc(16px + var(--MI-stickyTop, 0px)); left: 0; } diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index fac35191b9..bd25e1e3ad 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="renoteButton" class="_button" :class="$style.noteFooterButton" - :style="renoted ? 'color: var(--accent) !important;' : ''" + :style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''" @mousedown="renoted ? undoRenote() : boostVisibility()" > <i class="ph-rocket-launch ph-bold ph-lg"></i> @@ -106,7 +106,8 @@ import { $i } from '@/account.js'; import { userPage } from '@/filters/user.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { defaultStore } from '@/store.js'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import { host } from '@@/js/config.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -159,6 +160,11 @@ const isRenote = ( props.note.poll == null ); +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + async function addReplyTo(replyNote: Misskey.entities.Note) { replies.value.unshift(replyNote); appearNote.value.repliesCount += 1; @@ -196,7 +202,7 @@ function focus() { } function reply(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); os.post({ reply: props.note, @@ -208,7 +214,7 @@ function reply(viaKeyboard = false): void { } function react(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.note.reactionAcceptance === 'likeOnly') { @@ -242,7 +248,7 @@ function react(viaKeyboard = false): void { } function like(): void { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -302,7 +308,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -346,7 +352,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); if (appearNote.value.channel) { @@ -426,7 +432,7 @@ if (props.detail) { padding: 28px 32px; position: relative; - --reply-indent: calc(.5 * var(--avatar)); + --reply-indent: calc(.5 * var(--MI-avatar)); &.children { padding: 10px 0 0 8px; @@ -434,16 +440,16 @@ if (props.detail) { &.isReply { /* @link https://utopia.fyi/clamp/calculator?a=450,580,26—36 */ - --avatar: clamp(26px, -8.6154px + 7.6923cqi, 36px); + --MI-avatar: clamp(26px, -8.6154px + 7.6923cqi, 36px); } } .line { position: absolute; - left: calc(32px + .5 * var(--avatar)); + left: calc(32px + .5 * var(--MI-avatar)); // using solid instead of dotted, stylelistic choice - border-left: var(--thread-width) solid var(--thread); - top: calc(28px + var(--avatar)); // 28px of .root padding, plus 58px of avatar height (see SkNote) + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); + top: calc(28px + var(--MI-avatar)); // 28px of .root padding, plus 58px of avatar height (see SkNote) bottom: -28px; } @@ -468,8 +474,8 @@ if (props.detail) { right: -12px; left: -12px; bottom: -12px; - background: var(--panelHighlight); - border-radius: var(--radius); + background: var(--MI_THEME-panelHighlight); + border-radius: var(--MI-radius); opacity: 0; transition: opacity .2s, background .2s; z-index: -1; @@ -487,7 +493,7 @@ if (props.detail) { left: 8px; width: 5px; height: calc(100% - 8px); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); pointer-events: none; } @@ -495,9 +501,9 @@ if (props.detail) { flex-shrink: 0; display: block; margin: 0 14px 0 0; - width: var(--avatar); - height: var(--avatar); - border-radius: var(--radius-sm); + width: var(--MI-avatar); + height: var(--MI-avatar); + border-radius: var(--MI-radius-sm); } .body { @@ -525,12 +531,12 @@ if (props.detail) { opacity: 0.7; &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } // Responsible for Reply borders 448 and 508 .reply, .more { - //border-left: solid 0.5px var(--divider); + //border-left: solid 0.5px var(--MI_THEME-divider); margin-top: 10px; } @@ -541,11 +547,11 @@ if (props.detail) { @container (max-width: 580px) { .root { padding: 28px 26px 0; - --avatar: 46px; + --MI-avatar: 46px; } .line { - left: calc(26px + .5 * var(--avatar)); + left: calc(26px + .5 * var(--MI-avatar)); } } @@ -555,8 +561,8 @@ if (props.detail) { } .line { - top: calc(23px + var(--avatar)); - left: calc(25px + .5 * var(--avatar)); + top: calc(23px + var(--MI-avatar)); + left: calc(25px + .5 * var(--MI-avatar)); } } @@ -574,7 +580,7 @@ if (props.detail) { opacity: 0.7; &.reacted { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -591,7 +597,7 @@ if (props.detail) { } .reply, .more { - //border-left: solid 0.5px var(--divider); + //border-left: solid 0.5px var(--MI_THEME-divider); margin-top: 10px; } @@ -605,23 +611,23 @@ if (props.detail) { } .line { - top: calc(22px + var(--avatar)); - left: calc(24px + .5 * var(--avatar)); + top: calc(22px + var(--MI-avatar)); + left: calc(24px + .5 * var(--MI-avatar)); } } @container (max-width: 450px) { .root { - --avatar: 44px; + --MI-avatar: 44px; } } .muted { text-align: center; padding: 8px !important; - border: 1px solid var(--divider); + border: 1px solid var(--MI_THEME-divider); margin: 8px 8px 0 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } // avatar container with line @@ -633,7 +639,7 @@ if (props.detail) { .threadLine { width: 0; flex-grow: 1; - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); margin-left: var(--reply-indent); } @@ -642,10 +648,10 @@ if (props.detail) { } .reply:not(:last-child) { - border-left: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); &::before { - left: calc(-1 * var(--thread-width)); + left: calc(-1 * var(--MI-thread-width)); } } @@ -654,10 +660,10 @@ if (props.detail) { content: ''; left: 0px; top: -10px; - height: calc(10px + 10px + .5 * var(--avatar)); + height: calc(10px + 10px + .5 * var(--MI-avatar)); width: 15px; - border-left: var(--thread-width) solid var(--thread); - border-bottom: var(--thread-width) solid var(--thread); + border-left: var(--MI-thread-width) solid var(--MI_THEME-thread); + border-bottom: var(--MI-thread-width) solid var(--MI_THEME-thread); border-bottom-left-radius: 15px; } diff --git a/packages/frontend/src/components/SkOldNoteWindow.vue b/packages/frontend/src/components/SkOldNoteWindow.vue index 3810b62366..48b9020402 100644 --- a/packages/frontend/src/components/SkOldNoteWindow.vue +++ b/packages/frontend/src/components/SkOldNoteWindow.vue @@ -182,8 +182,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS .noteHeaderAvatar { display: block; flex-shrink: 0; - width: var(--avatar); - height: var(--avatar); + width: var(--MI-avatar); + height: var(--MI-avatar); } .noteHeaderBody { @@ -206,8 +206,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS padding: 4px 6px; font-size: 80%; line-height: 1; - border: solid 0.5px var(--divider); - border-radius: var(--radius-xs); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius-xs); } .noteHeaderInfo { @@ -240,19 +240,19 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS } .noteReplyTarget { - color: var(--accent); + color: var(--MI_THEME-accent); margin-right: 0.5em; } .rn { margin-left: 4px; font-style: oblique; - color: var(--renote); + color: var(--MI_THEME-renote); } .translation { - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; margin-top: 8px; } @@ -267,8 +267,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS .quoteNote { padding: 16px; - border: dashed 1px var(--renote); - border-radius: var(--radius-xs); + border: dashed 1px var(--MI_THEME-renote); + border-radius: var(--MI-radius-xs); overflow: clip; } @@ -287,7 +287,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } diff --git a/packages/frontend/src/components/SkOneko.vue b/packages/frontend/src/components/SkOneko.vue index 24bb392335..ef7bdd74f0 100644 --- a/packages/frontend/src/components/SkOneko.vue +++ b/packages/frontend/src/components/SkOneko.vue @@ -240,6 +240,6 @@ onMounted(init); pointer-events: none; image-rendering: pixelated; z-index: 2147483647; - background-image: var(--oneko-image, url(/client-assets/oneko.gif)); + background-image: var(--MI_THEME-oneko-image, url(/client-assets/oneko.gif)); } </style> diff --git a/packages/frontend/src/components/SkUserRecentNotes.vue b/packages/frontend/src/components/SkUserRecentNotes.vue index 7cfa0fb3f6..908affcdaf 100644 --- a/packages/frontend/src/components/SkUserRecentNotes.vue +++ b/packages/frontend/src/components/SkUserRecentNotes.vue @@ -94,7 +94,7 @@ onMounted(async () => { <style lang="scss" module> .panel { - background: var(--panel); + background: var(--MI_THEME-panel); } .userInfo { diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index f5546edf1e..b8837d7133 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -60,18 +60,18 @@ const props = defineProps<{ width: 100%; box-sizing: border-box; padding: 10px 14px; - background: var(--folderHeaderBg); - border-radius: var(--radius-sm); + background: var(--MI_THEME-folderHeaderBg); + border-radius: var(--MI-radius-sm); font-size: 0.9em; &:hover { text-decoration: none; - background: var(--folderHeaderHoverBg); + background: var(--MI_THEME-folderHeaderHoverBg); } &.active { - color: var(--accent); - background: var(--folderHeaderHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-folderHeaderHoverBg); } } @@ -79,7 +79,7 @@ const props = defineProps<{ margin-right: 0.75em; flex-shrink: 0; text-align: center; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue index ad37daa265..5fca3acc31 100644 --- a/packages/frontend/src/components/form/section.vue +++ b/packages/frontend/src/components/form/section.vue @@ -21,8 +21,8 @@ defineProps<{ <style lang="scss" module> .root { - border-top: solid 0.5px var(--divider); - //border-bottom: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); + //border-bottom: solid 0.5px var(--MI_THEME-divider); } .rootFirst { @@ -49,7 +49,7 @@ defineProps<{ .description { font-size: 0.85em; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); 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 f54db0ca82..da94b7abbb 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(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); &:empty { display: none; diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue index 5226c61d68..821f07510b 100644 --- a/packages/frontend/src/components/form/suspense.vue +++ b/packages/frontend/src/components/form/suspense.vue @@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> +<script lang="ts" setup generic="T extends unknown"> import { ref, watch } from 'vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - p: () => Promise<any>; + p: () => Promise<T>; }>(); const pending = ref(true); const resolved = ref(false); const rejected = ref(false); -const result = ref<any>(null); +const result = ref<T | null>(null); const process = () => { if (props.p == null) { diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 1238e6ef23..fc6c64d2aa 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div v-if="chosen && !shouldHide" :class="$style.root"> +<div v-if="chosen && !shouldHide"> <div v-if="!showMenu" :class="[$style.main, { @@ -30,12 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only </component> </div> <div v-else :class="$style.menu"> - <div :class="$style.menuContainer"> - <div>Ads by {{ host }}</div> - <!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>--> - <MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton> - <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button> - </div> + <div>Ads by {{ host }}</div> + <!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>--> + <MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton> + <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button> </div> </div> <div v-else></div> @@ -43,9 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; +import { url as local, host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { url as local, host } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; @@ -122,11 +120,6 @@ function reduceFrequency(): void { </script> <style lang="scss" module> -.root { - background-size: auto auto; - background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px ); -} - .main { text-align: center; @@ -139,8 +132,6 @@ function reduceFrequency(): void { } &.form_horizontal { - padding: 8px; - > .link, > .link > .img { max-width: min(600px, 100%); @@ -149,8 +140,6 @@ function reduceFrequency(): void { } &.form_horizontalBig { - padding: 8px; - > .link, > .link > .img { max-width: min(600px, 100%); @@ -182,7 +171,7 @@ function reduceFrequency(): void { display: block; object-fit: contain; margin: auto; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } .i { @@ -191,8 +180,8 @@ function reduceFrequency(): void { right: 1px; display: grid; place-content: center; - background: var(--panel); - border-radius: var(--radius-full); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-full); padding: 2px; } @@ -202,15 +191,12 @@ function reduceFrequency(): void { } .menu { - padding: 8px; text-align: center; -} - -.menuContainer { padding: 8px; margin: 0 auto; max-width: 400px; - border: solid 1px var(--divider); + background: var(--MI_THEME-panel); + border: solid 1px var(--MI_THEME-divider); } .menuButton { diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index fbc716016c..90fa522f3d 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -25,17 +25,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, ref } from 'vue'; +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 { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; -import { misskeyApiGet } from '@/scripts/misskey-api.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 { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; -import type { MenuItem } from '@/types/menu.js'; +import { $i } from '@/account.js'; const props = defineProps<{ name: string; @@ -127,9 +128,31 @@ function onClick(ev: MouseEvent) { }, }); + if ($i?.isModerator ?? $i?.isAdmin) { + menuItems.push({ + text: i18n.ts.edit, + icon: 'ti ti-pencil', + action: async () => { + await edit(props.name); + }, + }); + } + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } + +async function edit(name: string) { + const emoji = await misskeyApi('emoji', { + name: name, + }); + const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), { + emoji: emoji, + }, { + closed: () => dispose(), + }); +} + </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue index 64c828ba4a..77dddaff89 100644 --- a/packages/frontend/src/components/global/MkError.vue +++ b/packages/frontend/src/components/global/MkError.vue @@ -44,6 +44,6 @@ const emit = defineEmits<{ width: 128px; height: 128px; margin-bottom: 16px; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } </style> diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue index 49d8ace37b..47d797606b 100644 --- a/packages/frontend/src/components/global/MkLoading.vue +++ b/packages/frontend/src/components/global/MkLoading.vue @@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{ --size: 38px; &.colored { - color: var(--accent); + color: var(--MI_THEME-accent); } &.inline { diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts index 9bf9f4a872..1039572a06 100644 --- a/packages/frontend/src/components/global/MkMfm.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -7,6 +7,7 @@ import { VNode, h, defineAsyncComponent, SetupContext, provide } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import CkFollowMouse from '../CkFollowMouse.vue'; +import { host } from '@@/js/config.js'; import MkUrl from '@/components/global/MkUrl.vue'; import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; @@ -18,7 +19,6 @@ import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; import MkA, { MkABehavior } from '@/components/global/MkA.vue'; -import { host } from '@@/js/config.js'; import { defaultStore } from '@/store.js'; function safeParseFloat(str: unknown): number | null { @@ -32,8 +32,8 @@ const QUOTE_STYLE = ` display: block; margin: 8px; padding: 6px 0 6px 12px; -color: var(--fg); -border-left: solid 3px var(--fg); +color: var(--MI_THEME-fg); +border-left: solid 3px var(--MI_THEME-fg); opacity: 0.7; `.split('\n').join(' '); @@ -60,7 +60,8 @@ type MfmEvents = { // eslint-disable-next-line import/no-default-export export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) { - provide('linkNavigationBehavior', props.linkNavigationBehavior); + // こうしたいところだけど functional component 内では provide は使えない + //provide('linkNavigationBehavior', props.linkNavigationBehavior); const isNote = props.isNote ?? true; const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat && !defaultStore.state.disableCatSpeak; @@ -328,7 +329,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'border': { let color = validColor(token.props.args.color); - color = color ? `#${color}` : 'var(--accent)'; + color = color ? `#${color}` : 'var(--MI_THEME-accent)'; let b_style = token.props.args.style; if ( typeof b_style !== 'string' || @@ -361,7 +362,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const child = token.children[0]; const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); return h('span', { - style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: var(--radius-ellipse); padding: 4px 10px 4px 6px;', + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: var(--MI-radius-ellipse); padding: 4px 10px 4px 6px;', }, [ h('i', { class: 'ti ti-clock', @@ -409,6 +410,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven key: Math.random(), url: token.props.url, rel: 'nofollow noopener', + navigationBehavior: props.linkNavigationBehavior, }))]; } @@ -417,6 +419,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven key: Math.random(), url: token.props.url, rel: 'nofollow noopener', + navigationBehavior: props.linkNavigationBehavior, }, genEl(token.children, scale, true)))]; } @@ -425,6 +428,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven key: Math.random(), host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, username: token.props.username, + navigationBehavior: props.linkNavigationBehavior, }))]; } @@ -432,7 +436,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven return [h('bdi', h(MkA, { key: Math.random(), to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, - style: 'color:var(--hashtag);', + style: 'color:var(--MI_THEME-hashtag);', + behavior: props.linkNavigationBehavior, }, `#${token.props.hashtag}`))]; } @@ -527,8 +532,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } default: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - console.error('unrecognized ast type:', (token as any).type); + // @ts-expect-error 存在しないASTタイプ + console.error('unrecognized ast type:', token.type); return []; } diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 7d13fb9279..ffa6f13ff6 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -53,7 +53,7 @@ export type Tab = { </script> <script lang="ts" setup> -import { onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue'; +import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ @@ -120,14 +120,14 @@ function onTabWheel(ev: WheelEvent) { let entering = false; -async function enter(element: Element) { +async function enter(el: Element) { + if (!(el instanceof HTMLElement)) return; entering = true; - const el = element as HTMLElement; const elementWidth = el.getBoundingClientRect().width; el.style.width = '0'; el.style.paddingLeft = '0'; - el.offsetWidth; // force reflow - el.style.width = elementWidth + 'px'; + el.offsetWidth; // reflow + el.style.width = `${elementWidth}px`; el.style.paddingLeft = ''; nextTick(() => { entering = false; @@ -136,22 +136,23 @@ async function enter(element: Element) { setTimeout(renderTab, 170); } -function afterEnter(element: Element) { - //el.style.width = ''; +function afterEnter(el: Element) { + if (!(el instanceof HTMLElement)) return; + // element.style.width = ''; } -async function leave(element: Element) { - const el = element as HTMLElement; +async function leave(el: Element) { + if (!(el instanceof HTMLElement)) return; const elementWidth = el.getBoundingClientRect().width; - el.style.width = elementWidth + 'px'; + el.style.width = `${elementWidth}px`; el.style.paddingLeft = ''; - el.offsetWidth; // force reflow + el.offsetWidth; // reflow el.style.width = '0'; el.style.paddingLeft = '0'; } -function afterLeave(element: Element) { - const el = element as HTMLElement; +function afterLeave(el: Element) { + if (!(el instanceof HTMLElement)) return; el.style.width = ''; } @@ -219,7 +220,7 @@ onUnmounted(() => { &.active { opacity: 1; - color: var(--accent); + color: var(--MI_THEME-accent); } &.animate { @@ -248,8 +249,8 @@ onUnmounted(() => { position: absolute; bottom: 0; height: 3px; - background: var(--accent); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-accent); + border-radius: var(--MI-radius-ellipse); transition: none; pointer-events: none; diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index bb20f54fa4..18c97b1bdb 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -115,7 +115,7 @@ function goBack(): void { } const calcBg = () => { - const rawBg = 'var(--bg)'; + 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(); @@ -146,9 +146,9 @@ onUnmounted(() => { <style lang="scss" module> .root { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - border-bottom: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-bottom: solid 0.5px var(--MI_THEME-divider); width: 100%; } @@ -161,7 +161,7 @@ onUnmounted(() => { .upper { --height: 50px; display: flex; - gap: var(--margin); + gap: var(--MI-margin); height: var(--height); .tabs:first-child { @@ -239,14 +239,14 @@ onUnmounted(() => { width: calc(var(--height) - (var(--margin))); box-sizing: border-box; position: relative; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); &:hover { background: rgba(0, 0, 0, 0.05); } &.highlighted { - color: var(--accent); + color: var(--MI_THEME-accent); } } diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index 72993991ce..1aebf487bb 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -5,32 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="rootEl"> - <div ref="headerEl"> + <div ref="headerEl" :class="$style.header"> <slot name="header"></slot> </div> <div - ref="bodyEl" + :class="$style.body" :data-sticky-container-header-height="headerHeight" :data-sticky-container-footer-height="footerHeight" - style="position: relative; z-index: 0;" > <slot></slot> </div> - <div ref="footerEl"> + <div ref="footerEl" :class="$style.footer"> <slot name="footer"></slot> </div> </div> </template> <script lang="ts" setup> -import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue'; +import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, useTemplateRef } from 'vue'; import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js'; -const rootEl = shallowRef<HTMLElement>(); -const headerEl = shallowRef<HTMLElement>(); -const footerEl = shallowRef<HTMLElement>(); -const bodyEl = shallowRef<HTMLElement>(); +const rootEl = useTemplateRef('rootEl'); +const headerEl = useTemplateRef('headerEl'); +const footerEl = useTemplateRef('footerEl'); const headerHeight = ref<string | undefined>(); const childStickyTop = ref(0); @@ -67,31 +65,11 @@ onMounted(() => { watch([parentStickyTop, parentStickyBottom], calc); - watch(childStickyTop, () => { - if (bodyEl.value == null) return; - bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`); - }, { - immediate: true, - }); - - watch(childStickyBottom, () => { - if (bodyEl.value == null) return; - bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`); - }, { - immediate: true, - }); - if (headerEl.value != null) { - headerEl.value.style.position = 'sticky'; - headerEl.value.style.top = 'var(--stickyTop, 0)'; - headerEl.value.style.zIndex = '1'; observer.observe(headerEl.value); } if (footerEl.value != null) { - footerEl.value.style.position = 'sticky'; - footerEl.value.style.bottom = 'var(--stickyBottom, 0)'; - footerEl.value.style.zIndex = '1'; observer.observe(footerEl.value); } }); @@ -101,6 +79,27 @@ onUnmounted(() => { }); defineExpose({ - rootEl: rootEl, + rootEl, }); </script> + +<style lang='scss' module> +.body { + position: relative; + z-index: 0; + --MI-stickyTop: v-bind("childStickyTop + 'px'"); + --MI-stickyBottom: v-bind("childStickyBottom + 'px'"); +} + +.header { + position: sticky; + top: var(--MI-stickyTop, 0); + z-index: 1; +} + +.footer { + position: sticky; + bottom: var(--MI-stickyBottom, 0); + z-index: 1; +} +</style> diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 50bec990a1..f600f7eed2 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -99,10 +99,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod <style lang="scss" module> .old1 { - color: var(--warn); + color: var(--MI_THEME-warn); } .old1.old2 { - color: var(--error); + color: var(--MI_THEME-error); } </style> diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 19bd794a5d..38bdfc52d4 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -27,6 +27,7 @@ import MkLoadingPage from '@/pages/_loading_.vue'; const props = defineProps<{ router?: IRouter; + nested?: boolean; }>(); const router = props.router ?? inject('router'); @@ -39,6 +40,8 @@ const currentDepth = inject('routerCurrentDepth', 0); provide('routerCurrentDepth', currentDepth + 1); function resolveNested(current: Resolved, d = 0): Resolved | null { + if (!props.nested) return current; + if (d === currentDepth) { return current; } else { diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue index 8c511a690d..c2449931c1 100644 --- a/packages/frontend/src/components/page/page.dynamic.vue +++ b/packages/frontend/src/components/page/page.dynamic.vue @@ -27,9 +27,9 @@ const props = defineProps<{ <style lang="scss" module> .root { - border: 1px solid var(--divider); - border-radius: var(--radius); - padding: var(--margin); + border: 1px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); + padding: var(--MI-margin); text-align: center; } diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue index fc1ce9fc7b..69443ce7dd 100644 --- a/packages/frontend/src/components/page/page.image.vue +++ b/packages/frontend/src/components/page/page.image.vue @@ -28,8 +28,8 @@ onMounted(() => { <style lang="scss" module> .root { - border: 1px solid var(--divider); - border-radius: var(--radius); + border: 1px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); overflow: hidden; } .mediaList { diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue index b5ba407806..84436e7adb 100644 --- a/packages/frontend/src/components/page/page.note.vue +++ b/packages/frontend/src/components/page/page.note.vue @@ -35,7 +35,7 @@ onMounted(() => { <style lang="scss" module> .root { - border: 1px solid var(--divider); - border-radius: var(--radius); + border: 1px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius); } </style> diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts index 23fd1bddf4..f88996019f 100644 --- a/packages/frontend/src/directives/adaptive-bg.ts +++ b/packages/frontend/src/directives/adaptive-bg.ts @@ -4,24 +4,16 @@ */ import { Directive } from 'vue'; +import { getBgColor } from '@/scripts/get-bg-color.js'; export default { mounted(src, binding, vn) { - const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } - }; - - const parentBg = getBgColor(src.parentElement); + const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; if (parentBg === myBg) { - src.style.backgroundColor = 'var(--bg)'; + src.style.backgroundColor = 'var(--MI_THEME-bg)'; } else { src.style.backgroundColor = myBg; } diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts index b436075fcd..1305f312bd 100644 --- a/packages/frontend/src/directives/adaptive-border.ts +++ b/packages/frontend/src/directives/adaptive-border.ts @@ -4,24 +4,16 @@ */ import { Directive } from 'vue'; +import { getBgColor } from '@/scripts/get-bg-color.js'; export default { mounted(src, binding, vn) { - const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } - }; - - const parentBg = getBgColor(src.parentElement); + const parentBg = getBgColor(src.parentElement) ?? 'transparent'; const myBg = window.getComputedStyle(src).backgroundColor; if (parentBg === myBg) { - src.style.borderColor = 'var(--divider)'; + src.style.borderColor = 'var(--MI_THEME-divider)'; } else { src.style.borderColor = myBg; } diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts index bbcc220e09..aa26b94d0b 100644 --- a/packages/frontend/src/directives/panel.ts +++ b/packages/frontend/src/directives/panel.ts @@ -4,26 +4,18 @@ */ import { Directive } from 'vue'; +import { getBgColor } from '@/scripts/get-bg-color.js'; export default { mounted(src, binding, vn) { - const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } - }; + const parentBg = getBgColor(src.parentElement) ?? 'transparent'; - const parentBg = getBgColor(src.parentElement); - - const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel'); + const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'); if (parentBg === myBg) { - src.style.backgroundColor = 'var(--bg)'; + src.style.backgroundColor = 'var(--MI_THEME-bg)'; } else { - src.style.backgroundColor = 'var(--panel)'; + src.style.backgroundColor = 'var(--MI_THEME-panel)'; } }, } as Directive; diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts index 25f853453a..965bd6f0bc 100644 --- a/packages/frontend/src/nirax.ts +++ b/packages/frontend/src/nirax.ts @@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase { export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect; +export type RouterFlag = 'forcePage'; + type ParsedPath = (string | { name: string; startsWith?: string; @@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> { current: Resolved; currentRef: ShallowRef<Resolved>; currentRoute: ShallowRef<RouteDef>; - navHook: ((path: string, flag?: any) => boolean) | null; + navHook: ((path: string, flag?: RouterFlag) => boolean) | null; /** * ルートの初期化(eventListenerの定義後に必ず呼び出すこと) @@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> { resolve(path: string): Resolved | null; - getCurrentPath(): any; + getCurrentPath(): string; getCurrentKey(): string; - push(path: string, flag?: any): void; + push(path: string, flag?: RouterFlag): void; replace(path: string, key?: string | null): void; @@ -197,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter { private currentKey = Date.now().toString(); private redirectCount = 0; - public navHook: ((path: string, flag?: any) => boolean) | null = null; + public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null; constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) { super(); @@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter { return this.currentKey; } - public push(path: string, flag?: any) { + public push(path: string, flag?: RouterFlag) { const beforePath = this.currentPath; if (path === beforePath) { this.emit('same'); diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index deb629d534..a81f67aef3 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -10,6 +10,7 @@ import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/scripts/form.js'; +import type { MenuItem } from '@/types/menu.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -22,19 +23,20 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue'; import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue'; -import type { MenuItem } from '@/types/menu.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; import { focusParent } from '@/scripts/focus.js'; +import type { PostFormProps } from '@/types/post-form.js'; export const openingWindowsCount = ref(0); -export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>( +export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>( endpoint: E, - data: P = {} as any, + data: P, token?: string | null | undefined, + customErrors?: Record<string, { title?: string; text: string; }>, ) => { const promise = misskeyApi(endpoint, data, token); promiseDialog(promise, null, async (err) => { @@ -77,6 +79,9 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey } else if (err.message.startsWith('Unexpected token')) { title = i18n.ts.gotInvalidResponseError; text = i18n.ts.gotInvalidResponseErrorDescription; + } else if (customErrors && customErrors[err.id] != null) { + title = customErrors[err.id].title; + text = customErrors[err.id].text; } alert({ type: 'error', @@ -86,11 +91,11 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey }); return promise; -}) as typeof misskeyApi; +}); export function promiseDialog<T extends Promise<any>>( promise: T, - onSuccess?: ((res: any) => void) | null, + onSuccess?: ((res: Awaited<T>) => void) | null, onFailure?: ((err: Misskey.api.APIError) => void) | null, text?: string, ): T { @@ -139,12 +144,12 @@ export function promiseDialog<T extends Promise<any>>( } let popupIdCount = 0; -export const popups = ref([]) as Ref<{ +export const popups = ref<{ id: number; component: Component; props: Record<string, any>; events: Record<string, any>; -}[]>; +}[]>([]); const zIndexes = { veryLow: 500000, @@ -463,7 +468,7 @@ type SelectItem<C> = { }; // default が指定されていたら result は null になり得ないことを保証する overload function -export function select<C = any>(props: { +export function select<C = unknown>(props: { title?: string; text?: string; default: string; @@ -476,7 +481,7 @@ export function select<C = any>(props: { } | { canceled: false; result: C; }>; -export function select<C = any>(props: { +export function select<C = unknown>(props: { title?: string; text?: string; default?: string | null; @@ -489,7 +494,7 @@ export function select<C = any>(props: { } | { canceled: false; result: C | null; }>; -export function select<C = any>(props: { +export function select<C = unknown>(props: { title?: string; text?: string; default?: string | null; @@ -692,15 +697,17 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> { })); } -export function post(props: Record<string, any> = {}): Promise<void | boolean> { - pleaseLogin(undefined, (props.initialText || props.initialNote ? { - type: 'share', - params: { - text: props.initialText ?? props.initialNote.text, - visibility: props.initialVisibility ?? props.initialNote?.visibility, - localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0', - }, - } : undefined)); +export function post(props: PostFormProps = {}): Promise<void | boolean> { + pleaseLogin({ + openOnRemote: (props.initialText || props.initialNote ? { + type: 'share', + params: { + text: props.initialText ?? props.initialNote?.text ?? '', + visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public', + localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0', + }, + } : undefined), + }); showMovedDialog(); return new Promise(resolve => { diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index 83f596b870..0bafc385a6 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -83,6 +83,6 @@ definePageMetadata(() => ({ vertical-align: bottom; height: 128px; margin-bottom: 24px; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } </style> diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue index f689f9b98e..ac2bb9b14e 100644 --- a/packages/frontend/src/pages/about-sharkey.vue +++ b/packages/frontend/src/pages/about-sharkey.vue @@ -352,7 +352,7 @@ definePageMetadata(() => ({ .znqjceqz { > .about { position: relative; - border-radius: var(--radius); + border-radius: var(--MI-radius); > .treasure { position: absolute; @@ -391,7 +391,7 @@ definePageMetadata(() => ({ display: block; width: 80px; margin: 0 auto; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); position: relative; z-index: 1; transform: translateX(-10%); @@ -441,23 +441,23 @@ definePageMetadata(() => ({ display: flex; align-items: center; padding: 12px; - background: var(--buttonBg); - border-radius: var(--radius-sm); + background: var(--MI_THEME-buttonBg); + border-radius: var(--MI-radius-sm); &:hover { text-decoration: none; - background: var(--buttonHoverBg); + background: var(--MI_THEME-buttonHoverBg); } &.active { - color: var(--accent); - background: var(--buttonHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-buttonHoverBg); } } .contributorAvatar { width: 30px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .contributorUsername { @@ -474,13 +474,13 @@ definePageMetadata(() => ({ display: flex; align-items: center; padding: 12px; - background: var(--buttonBg); - border-radius: var(--radius-sm); + background: var(--MI_THEME-buttonBg); + border-radius: var(--MI-radius-sm); } .patronIcon { width: 24px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .patronName { diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 71353c7dfa..2190be8bec 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.host }}</template> </MkInput> - <FormSplit style="margin-top: var(--margin);"> + <FormSplit style="margin-top: var(--MI-margin);"> <MkSelect v-model="state"> <template #label>{{ i18n.ts.state }}</template> <option value="all">{{ i18n.ts.all }}</option> diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue index ca070c65a4..347214c8fa 100644 --- a/packages/frontend/src/pages/about.overview.vue +++ b/packages/frontend/src/pages/about.overview.vue @@ -170,9 +170,9 @@ await misskeyApi('sponsors', { instance: true }).then((res) => sponsors.value.pu <style lang="scss" module> .banner { text-align: center; - border-radius: var(--radius); + border-radius: var(--MI-radius); overflow: clip; - background-color: var(--panel); + background-color: var(--MI_THEME-panel); background-size: cover; background-position: center center; } @@ -181,7 +181,7 @@ await misskeyApi('sponsors', { instance: true }).then((res) => sponsors.value.pu display: block; margin: 16px auto 0 auto; max-height: 96px; - border-radius: var(--radius-sm);; + border-radius: var(--MI-radius-sm);; } .bannerName { @@ -208,19 +208,19 @@ await misskeyApi('sponsors', { instance: true }).then((res) => sponsors.value.pu flex-shrink: 0; display: flex; position: sticky; - top: calc(var(--stickyTop, 0px) + 8px); + top: calc(var(--MI-stickyTop, 0px) + 8px); counter-increment: item; content: counter(item); width: 32px; height: 32px; line-height: 32px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); font-size: 13px; font-weight: bold; align-items: center; justify-content: center; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } } @@ -238,23 +238,23 @@ await misskeyApi('sponsors', { instance: true }).then((res) => sponsors.value.pu display: flex; align-items: center; padding: 12px; - background: var(--buttonBg); - border-radius: var(--radius-sm); + background: var(--MI_THEME-buttonBg); + border-radius: var(--MI-radius-sm); &:hover { text-decoration: none; - background: var(--buttonHoverBg); + background: var(--MI_THEME-buttonHoverBg); } &.active { - color: var(--accent); - background: var(--buttonHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-buttonHoverBg); } } .contributorAvatar { width: 30px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .contributorUsername { diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 22e16effe0..11a34d34ef 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTextarea v-model="moderationNote" manualSave> <template #label>{{ i18n.ts.moderationNote }}</template> + <template #caption>{{ i18n.ts.moderationNoteDescription }}</template> </MkTextarea> <FormSection v-if="user.host"> @@ -140,15 +141,21 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'announcements'" class="_gaps"> <MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts._announcement.new }}</MkButton> + <MkSelect v-model="announcementsStatus"> + <template #label>{{ i18n.ts.filter }}</template> + <option value="active">{{ i18n.ts.active }}</option> + <option value="archived">{{ i18n.ts.archived }}</option> + </MkSelect> + <MkPagination :pagination="announcementsPagination"> <template #default="{ items }"> <div class="_gaps_s"> <div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)"> <span style="margin-right: 0.5em;"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i> </span> <span>{{ announcement.title }}</span> <span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span> @@ -193,6 +200,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, defineAsyncComponent, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; import MkChart from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -208,7 +216,6 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@@/js/config.js'; import { acct } from '@/filters/user.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -244,11 +251,15 @@ const filesPagination = { userId: props.userId, })), }; + +const announcementsStatus = ref<'active' | 'archived'>('active'); + const announcementsPagination = { endpoint: 'admin/announcements/list' as const, limit: 10, params: computed(() => ({ userId: props.userId, + status: announcementsStatus.value, })), }; const expandedRoles = ref([]); @@ -594,24 +605,24 @@ definePageMetadata(() => ({ > .suspended, > .silenced, > .moderator { display: inline-block; border: solid 1px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); padding: 2px 6px; font-size: 85%; } > .suspended { - color: var(--error); - border-color: var(--error); + color: var(--MI_THEME-error); + border-color: var(--MI_THEME-error); } > .silenced { - color: var(--warn); - border-color: var(--warn); + color: var(--MI_THEME-warn); + border-color: var(--MI_THEME-warn); } > .moderator { - color: var(--success); - border-color: var(--success); + color: var(--MI_THEME-success); + border-color: var(--MI_THEME-success); } } } @@ -633,13 +644,13 @@ definePageMetadata(() => ({ .casdwq { .silenced { - color: var(--warn); - border-color: var(--warn); + color: var(--MI_THEME-warn); + border-color: var(--MI_THEME-warn); } .moderator { - color: var(--success); - border-color: var(--success); + color: var(--MI_THEME-success); + border-color: var(--MI_THEME-success); } } </style> @@ -647,6 +658,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .ip { display: flex; + word-break: break-all; > :global(.date) { opacity: 0.7; @@ -670,7 +682,7 @@ definePageMetadata(() => ({ .roleItemSub { padding: 6px 12px; font-size: 85%; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } .roleUnassign { @@ -683,7 +695,7 @@ definePageMetadata(() => ({ .announcementItem { display: flex; padding: 8px 12px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); cursor: pointer; } </style> diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index f001a4ac20..4762ef3f97 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -155,12 +155,12 @@ function removeSelf() { } .item { - border: solid 2px var(--divider); - border-radius: var(--radius); + border: solid 2px var(--MI_THEME-divider); + border-radius: var(--MI-radius); padding: 12px; &:hover { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } } </style> diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue index 2a71e3efab..b0651150a6 100644 --- a/packages/frontend/src/pages/admin/_header_.vue +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -119,7 +119,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void { } const calcBg = () => { - const rawBg = pageMetadata.value?.bg ?? 'var(--bg)'; + const rawBg = pageMetadata.value?.bg ?? '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(); @@ -156,8 +156,8 @@ onUnmounted(() => { --height: 60px; display: flex; width: 100%; - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); > .buttons { --margin: 8px; @@ -182,14 +182,14 @@ onUnmounted(() => { width: calc(var(--height) - (var(--margin) * 2)); box-sizing: border-box; position: relative; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); &:hover { background: rgba(0, 0, 0, 0.05); } &.highlighted { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -286,8 +286,8 @@ onUnmounted(() => { position: absolute; bottom: 0; height: 3px; - background: var(--accent); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-accent); + border-radius: var(--MI-radius-ellipse); transition: all 0.2s ease; pointer-events: none; } diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue index 827e22e8ae..eef24afd32 100644 --- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue @@ -294,10 +294,10 @@ onMounted(async () => { bottom: 0; left: 0; padding: 12px; - border-top: solid 0.5px var(--divider); - background: var(--acrylicBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } .systemWebhook { diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue index 0b86808faf..36d586bd23 100644 --- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue @@ -87,7 +87,7 @@ function onDeleteButtonClicked() { } .rightDivider { - border-right: 0.5px solid var(--divider); + border-right: 0.5px solid var(--MI_THEME-divider); } .recipientButtons { @@ -108,7 +108,7 @@ function onDeleteButtonClicked() { padding: 8px; &:hover { - background-color: var(--buttonBg); + background-color: var(--MI_THEME-buttonBg); } } </style> diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index c8a9ca7112..a164ecb1fe 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton> </div> + <MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()"> + {{ i18n.ts._abuseUserReport.resolveTutorial }} + </MkInfo> + <div :class="$style.inputs" class="_gaps"> <MkSelect v-model="state" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.state }}</template> @@ -44,8 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> --> - <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);"> - <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> + <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50"> + <div class="_gaps"> + <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> + </div> </MkPagination> </div> </MkSpacer> @@ -54,7 +60,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, shallowRef, ref } from 'vue'; - import XHeader from './_header_.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -62,6 +67,8 @@ import XAbuseReport from '@/components/MkAbuseReport.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import { defaultStore } from '@/store.js'; const reports = shallowRef<InstanceType<typeof MkPagination>>(); @@ -85,6 +92,10 @@ function resolved(reportId) { reports.value?.removeItem(reportId); } +function closeTutorial() { + defaultStore.set('abusesTutorial', false); +} + const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index 6c8901b10b..0d67359e47 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -266,7 +266,7 @@ definePageMetadata(() => ({ padding: 32px; &:not(:last-child) { - margin-bottom: var(--margin); + margin-bottom: var(--MI-margin); } } .input { diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index fd37311b21..e420586017 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -24,9 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ announcement.title }}</template> <template #icon> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i> </template> <template #caption>{{ announcement.text }}</template> <template #footer> @@ -51,9 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="announcement.icon"> <template #label>{{ i18n.ts.icon }}</template> <option value="info"><i class="ti ti-info-circle"></i></option> - <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option> - <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option> - <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option> + <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option> + <option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option> + <option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option> </MkRadios> <MkRadios v-model="announcement.display"> <template #label>{{ i18n.ts.display }}</template> diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 644436cde6..2f6dac8097 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template> <template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template> <template v-else-if="botProtectionForm.savedState.provider === 'fc'" #suffix>FriendlyCaptcha</template> + <template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template> <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> <template v-if="botProtectionForm.modified.value" #footer> <MkFormFooter :form="botProtectionForm"/> @@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="recaptcha">reCAPTCHA</option> <option value="turnstile">Turnstile</option> <option value="fc">FriendlyCaptcha</option> + <option value="testcaptcha">testCaptcha</option> </MkRadios> <template v-if="botProtectionForm.state.provider === 'hcaptcha'"> @@ -101,6 +103,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCaptcha provider="fc" :sitekey="botProtectionForm.state.fcSiteKey"/> </FormSlot> </template> + <template v-else-if="botProtectionForm.state.provider === 'testcaptcha'"> + <MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo> + <FormSlot> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="testcaptcha"/> + </FormSlot> + </template> </div> </MkFolder> </template> @@ -117,6 +126,7 @@ import { i18n } from '@/i18n.js'; import { useForm } from '@/scripts/use-form.js'; import MkFormFooter from '@/components/MkFormFooter.vue'; import MkFolder from '@/components/MkFolder.vue'; +import MkInfo from '@/components/MkInfo.vue'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); @@ -133,6 +143,8 @@ const botProtectionForm = useForm({ ? 'mcaptcha' : meta.enableFC ? 'fc' + : meta.enableTestcaptcha + ? 'testcaptcha' : null, hcaptchaSiteKey: meta.hcaptchaSiteKey, hcaptchaSecretKey: meta.hcaptchaSecretKey, @@ -163,6 +175,7 @@ const botProtectionForm = useForm({ enableFC: state.provider === 'fc', fcSiteKey: state.fcSiteKey, fcSecretKey: state.fcSecretKey, + enableTestcaptcha: state.provider === 'testcaptcha', }); fetchInstance(true); }); diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index d3d52002fe..cc05466832 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -218,7 +218,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index ddfe5ae81f..2f1d12ebb5 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -138,7 +138,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index 1902c97724..ef6bbb865b 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.host }}</template> </MkInput> - <FormSplit style="margin-top: var(--margin);"> + <FormSplit style="margin-top: var(--MI-margin);"> <MkSelect v-model="state"> <template #label>{{ i18n.ts.state }}</template> <option value="all">{{ i18n.ts.all }}</option> diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index 5132b85c64..4cc859227f 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><XHeader :actions="headerActions"/></template> <MkSpacer :contentMax="900"> <div class="_gaps"> - <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;"> <MkSelect v-model="origin" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.instance }}</template> <option value="combined">{{ i18n.ts.all }}</option> @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.host }}</template> </MkInput> </div> - <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;"> <MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;"> <template #label>User ID</template> </MkInput> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index d6cd1f0fb1..6cdf0eda7a 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s"> <MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo> <MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> - <MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> + <MkInfo v-if="noInquiryUrl" warn class="info">{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="pendingUserApprovals" warn class="info">{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/approvals" class="_link">{{ i18n.ts.check }}</MkA></MkInfo> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> </div> <div v-if="!(narrow && currentPage?.route.name == null)" class="main"> - <RouterView/> + <RouterView nested/> </div> </div> </template> @@ -346,7 +346,7 @@ defineExpose({ width: 32%; max-width: 280px; box-sizing: border-box; - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--MI_THEME-divider); overflow: auto; height: 100%; } @@ -366,7 +366,7 @@ defineExpose({ display: block; margin: auto; height: 42px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } } } diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index bbcf2a6f77..27cbfda078 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -10,8 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> - <MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration"> - <template #label>{{ i18n.ts.enableRegistration }}</template> + <MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration"> + <template #label>{{ i18n.ts._serverSettings.openRegistration }}</template> + <template #caption> + <div>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</div> + <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.openRegistrationWarning }}</div> + </template> </MkSwitch> <MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup"> @@ -85,6 +89,18 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> + <template #icon><i class="ti ti-user-x"></i></template> + <template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template> + + <div class="_gaps"> + <MkTextarea v-model="prohibitedWordsForNameOfUser"> + <template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template> + </MkTextarea> + <MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> <template #icon><i class="ti ti-eye-off"></i></template> <template #label>{{ i18n.ts.hiddenTags }}</template> @@ -160,6 +176,7 @@ const approvalRequiredForSignup = ref<boolean>(false); const bubbleTimelineEnabled = ref<boolean>(false); const sensitiveWords = ref<string>(''); const prohibitedWords = ref<string>(''); +const prohibitedWordsForNameOfUser = ref<string>(''); const hiddenTags = ref<string>(''); const preservedUsernames = ref<string>(''); const bubbleTimeline = ref<string>(''); @@ -175,17 +192,28 @@ async function init() { approvalRequiredForSignup.value = meta.approvalRequiredForSignup; sensitiveWords.value = meta.sensitiveWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n'); + prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n'); bubbleTimeline.value = meta.bubbleInstances.join('\n'); bubbleTimelineEnabled.value = meta.policies.btlAvailable; trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n'); blockedHosts.value = meta.blockedHosts.join('\n'); - silencedHosts.value = meta.silencedHosts.join('\n'); + silencedHosts.value = meta.silencedHosts?.join('\n') ?? ''; mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); } -function onChange_enableRegistration(value: boolean) { +async function onChange_enableRegistration(value: boolean) { + if (value) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.acknowledgeNotesAndEnable, + }); + if (canceled) return; + } + + enableRegistration.value = value; + os.apiWithDialog('admin/update-meta', { disableRegistration: !value, }).then(() => { @@ -249,6 +277,14 @@ function save_prohibitedWords() { }); } +function save_prohibitedWordsForNameOfUser() { + os.apiWithDialog('admin/update-meta', { + prohibitedWordsForNameOfUser: prohibitedWordsForNameOfUser.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + function save_hiddenTags() { os.apiWithDialog('admin/update-meta', { hiddenTags: hiddenTags.value.split('\n'), diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 6c81155c51..37a9cc83e7 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div> - <div style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;"> <div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div> <div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div> </div> @@ -180,6 +180,11 @@ SPDX-License-Identifier: AGPL-3.0-only <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> </div> </template> + <template v-else-if="log.type === 'updateAbuseReportNote'"> + <div :class="$style.diff"> + <CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/> + </div> + </template> <details> <summary>raw</summary> @@ -210,19 +215,19 @@ const props = defineProps<{ .diff { background: #fff; color: #000; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; } .logYellow { - color: var(--warn); + color: var(--MI_THEME-warn); } .logRed { - color: var(--error); + color: var(--MI_THEME-error); } .logGreen { - color: var(--success); + color: var(--MI_THEME-success); } </style> diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue index 4e0d0f941e..35f939f1be 100644 --- a/packages/frontend/src/pages/admin/modlog.vue +++ b/packages/frontend/src/pages/admin/modlog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="900"> <div> - <div style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;"> <MkSelect v-model="type" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.type }}</template> <option :value="null">{{ i18n.ts.all }}</option> @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </div> - <MkPagination v-slot="{items}" ref="logs" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);"> - <div class="_gaps_s"> - <XModLog v-for="item in items" :key="item.id" :log="item"/> - </div> + <MkPagination v-slot="{items}" ref="logs" :pagination="pagination" :displayLimit="50" style="margin-top: var(--MI-margin);"> + <MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--MI-margin: 8px;"> + <XModLog :key="item.id" :log="item"/> + </MkDateSeparatedList> </MkPagination> </div> </MkSpacer> @@ -39,6 +39,7 @@ import MkInput from '@/components/MkInput.vue'; import MkPagination from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; const logs = shallowRef<InstanceType<typeof MkPagination>>(); diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue index 5fddb715cd..d5a664934c 100644 --- a/packages/frontend/src/pages/admin/object-storage.vue +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -157,7 +157,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index 4bbb9210af..570fcddc07 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -278,7 +278,7 @@ onMounted(async () => { padding: 16px; &:first-child { - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); } } } diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue index ea01d073ea..9062c73ff6 100644 --- a/packages/frontend/src/pages/admin/overview.federation.vue +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -151,9 +151,9 @@ onMounted(async () => { height: 100%; aspect-ratio: 1; margin-right: 12px; - background: var(--accentedBg); - color: var(--accent); - border-radius: var(--radius); + background: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + border-radius: var(--MI-radius); } &.sub { diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue index c7a9f2a702..a21ec6c464 100644 --- a/packages/frontend/src/pages/admin/overview.pie.vue +++ b/packages/frontend/src/pages/admin/overview.pie.vue @@ -41,7 +41,7 @@ onMounted(() => { labels: props.data.map(x => x.name), datasets: [{ backgroundColor: props.data.map(x => x.color), - borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'), + borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'), borderWidth: 2, hoverOffset: 0, data: props.data.map(x => x.value), diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index fb190f5325..de6b254412 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -119,8 +119,8 @@ onUnmounted(() => { > .chart { min-width: 0; padding: 16px; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); > .title { font-size: 0.85em; diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index 37399a5724..a967b305f7 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -114,9 +114,9 @@ onMounted(async () => { height: 100%; aspect-ratio: 1; margin-right: 12px; - background: var(--accentedBg); - color: var(--accent); - border-radius: var(--radius); + background: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); + border-radius: var(--MI-radius); } &.users { diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 7e0a932f82..12338f0bf9 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -30,6 +30,13 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances"> + <template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> <MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances"> <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> @@ -120,6 +127,7 @@ const meta = await misskeyApi('admin/meta'); const enableServerMachineStats = ref(meta.enableServerMachineStats); const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration); const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser); +const enableStatsForFederatedInstances = ref(meta.enableStatsForFederatedInstances); const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances); function onChange_enableServerMachineStats(value: boolean) { @@ -146,6 +154,14 @@ function onChange_enableChartsForRemoteUser(value: boolean) { }); } +function onChange_enableStatsForFederatedInstances(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableStatsForFederatedInstances: value, + }).then(() => { + fetchInstance(true); + }); +} + function onChange_enableChartsForFederatedInstances(value: boolean) { os.apiWithDialog('admin/update-meta', { enableChartsForFederatedInstances: value, diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 960a263a86..7c171ba0e1 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -135,8 +135,8 @@ onUnmounted(() => { .chart { min-width: 0; padding: 16px; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } .chartTitle { diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 04982eea1f..17e99e6593 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -11,8 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;"> <div>{{ relay.inbox }}</div> <div style="margin: 8px 0;"> - <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i> - <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i> + <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i> + <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i> <i v-else class="ti ti-clock" :class="$style.icon"></i> <span>{{ i18n.ts._relayStatus[relay.status] }}</span> </div> diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index 60f06d50ba..2b4006c3f7 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -95,7 +95,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 6bce60cfb9..d1c7be39d6 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -184,7 +184,7 @@ definePageMetadata(() => ({ .userItemSub { padding: 6px 12px; font-size: 85%; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } .userItemMainBody { diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue index 4788c16830..6c1227b3cf 100644 --- a/packages/frontend/src/pages/admin/server-rules.vue +++ b/packages/frontend/src/pages/admin/server-rules.vue @@ -76,7 +76,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .item { display: block; - color: var(--navFg); + color: var(--MI_THEME-navFg); } .itemHeader { @@ -96,15 +96,15 @@ definePageMetadata(() => ({ .itemNumber { display: flex; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); font-size: 14px; font-weight: bold; width: 28px; height: 28px; align-items: center; justify-content: center; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); margin-right: 8px; } @@ -117,12 +117,12 @@ definePageMetadata(() => ({ .itemRemove { width: 40px; height: 40px; - color: var(--error); + color: var(--MI_THEME-error); margin-left: auto; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); &:hover { - background: var(--X5); + background: var(--MI_THEME-X5); } } diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 04975dcbf3..68f211de5c 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -438,6 +438,6 @@ definePageMetadata(() => ({ <style lang="scss" module> .subCaption { font-size: 0.85em; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } </style> diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue index 4e767fba16..45f0fff107 100644 --- a/packages/frontend/src/pages/admin/system-webhook.item.vue +++ b/packages/frontend/src/pages/admin/system-webhook.item.vue @@ -6,15 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkFolder> <template #label>{{ entity.name || entity.url }}</template> + <template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template> <template #icon> <i v-if="!entity.isActive" class="ti ti-player-pause"/> <i v-else-if="entity.latestStatus === null" class="ti ti-circle"/> <i v-else-if="[200, 201, 204].includes(entity.latestStatus)" class="ti ti-check" - :style="{ color: 'var(--success)' }" + :style="{ color: 'var(--MI_THEME-success)' }" /> - <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/> + <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"/> </template> <template #suffix> <MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/> @@ -74,6 +75,6 @@ function onDeleteClick() { margin-right: 0.75em; flex-shrink: 0; text-align: center; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } </style> diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index e99dcfa489..e26a2c827e 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -100,19 +100,19 @@ async function addUser() { const { canceled: canceled1, result: username } = await os.inputText({ title: i18n.ts.username, }); - if (canceled1) return; + if (canceled1 || username == null) return; const { canceled: canceled2, result: password } = await os.inputText({ title: i18n.ts.password, type: 'password', }); - if (canceled2) return; + if (canceled2 || password == null) return; os.apiWithDialog('admin/accounts/create', { username: username, password: password, }).then(res => { - paginationComponent.value.reload(); + paginationComponent.value?.reload(); }); } diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue index 802a6bf399..56c10fb292 100644 --- a/packages/frontend/src/pages/announcement.vue +++ b/packages/frontend/src/pages/announcement.vue @@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> <span style="margin-right: 0.5em;"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i> </span> <Mfm :text="announcement.title"/> </div> @@ -55,7 +55,7 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { $i, updateAccount } from '@/account.js'; +import { $i, updateAccountPartial } from '@/account.js'; import { defaultStore } from '@/store.js'; const props = defineProps<{ @@ -90,7 +90,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> { target.isRead = true; await misskeyApi('i/read-announcement', { announcementId: target.id }); if ($i) { - updateAccount({ + updateAccountPartial({ unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id), }); } @@ -103,7 +103,7 @@ const headerActions = computed(() => []); const headerTabs = computed(() => []); definePageMetadata(() => ({ - title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements, + title: announcement.value ? announcement.value.title : i18n.ts.announcements, icon: 'ti ti-speakerphone', })); </script> diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index a9bcfee803..79b9e7607d 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -17,9 +17,9 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> <span style="margin-right: 0.5em;"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i> </span> <MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA> </div> @@ -56,7 +56,7 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { $i, updateAccount } from '@/account.js'; +import { $i, updateAccountPartial } from '@/account.js'; const paginationCurrent = { endpoint: 'announcements' as const, @@ -94,7 +94,7 @@ async function read(target) { return a; }); misskeyApi('i/read-announcement', { announcementId: target.id }); - updateAccount({ + updateAccountPartial({ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id), }); } diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index e57e212b60..350f772d65 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -97,26 +97,26 @@ definePageMetadata(() => ({ <style lang="scss" module> .new { position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); + top: calc(var(--MI-stickyTop, 0px) + 16px); z-index: 1000; width: 100%; margin: calc(-0.675em - 8px) 0; &:first-child { - margin-top: calc(-0.675em - 8px - var(--margin)); + margin-top: calc(-0.675em - 8px - var(--MI-margin)); } } .newButton { display: block; - margin: var(--margin) auto 0 auto; + margin: var(--MI-margin) auto 0 auto; padding: 8px 16px; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); } .tl { - background: var(--bg); - border-radius: var(--radius); + background: var(--MI_THEME-bg); + border-radius: var(--MI-radius); overflow: clip; } </style> diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index 75a44802f8..30e0e99326 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -83,7 +83,7 @@ function accepted() { location.href = callbackUrl.toString(); } else if (session.value && session.value.app.callbackUrl) { const url = new URL(session.value.app.callbackUrl); - if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url'); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url'); location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`; } } diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue new file mode 100644 index 0000000000..a834f1c5fd --- /dev/null +++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue @@ -0,0 +1,220 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkWindow + ref="windowEl" + :initialWidth="400" + :initialHeight="500" + :canResize="true" + @close="windowEl?.close()" + @closed="emit('closed')" +> + <template v-if="avatarDecoration" #header>{{ avatarDecoration.name }}</template> + <template v-else #header>New decoration</template> + + <div style="display: flex; flex-direction: column; min-height: 100%;"> + <MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;"> + <div class="_gaps_m"> + <div :class="$style.preview"> + <div :class="[$style.previewItem, $style.light]"> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/> + </div> + <div :class="[$style.previewItem, $style.dark]"> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/> + </div> + </div> + <MkInput v-model="name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkInput v-model="url"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <MkTextarea v-model="description"> + <template #label>{{ i18n.ts.description }}</template> + </MkTextarea> + <MkFolder> + <template #label>{{ i18n.ts.availableRoles }}</template> + <template #suffix>{{ rolesThatCanBeUsedThisDecoration.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisDecoration.length }}</template> + + <div class="_gaps"> + <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + + <div v-for="role in rolesThatCanBeUsedThisDecoration" :key="role.id" :class="$style.roleItem"> + <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/> + <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button> + <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> + </div> + </div> + </MkFolder> + <MkButton v-if="avatarDecoration" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </MkSpacer> + <div :class="$style.footer"> + <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton> + </div> + </div> +</MkWindow> +</template> + +<script lang="ts" setup> +import { computed, watch, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkWindow from '@/components/MkWindow.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkRolePreview from '@/components/MkRolePreview.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); + +const props = defineProps<{ + avatarDecoration?: any, +}>(); + +const emit = defineEmits<{ + (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, + (ev: 'closed'): void +}>(); + +const windowEl = ref<InstanceType<typeof MkWindow> | null>(null); +const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : ''); +const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : ''); +const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : ''); +const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []); +const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]); + +watch(roleIdsThatCanBeUsedThisDecoration, async () => { + rolesThatCanBeUsedThisDecoration.value = (await Promise.all(roleIdsThatCanBeUsedThisDecoration.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); +}, { immediate: true }); + +async function addRole() { + const roles = await misskeyApi('admin/roles/list'); + const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id); + + const { canceled, result: role } = await os.select({ + items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })), + }); + if (canceled || role == null) return; + + rolesThatCanBeUsedThisDecoration.value.push(role); +} + +async function removeRole(role, ev) { + rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id); +} + +async function done() { + const params = { + url: url.value, + name: name.value, + description: description.value, + roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id), + }; + + if (props.avatarDecoration) { + await os.apiWithDialog('admin/avatar-decorations/update', { + id: props.avatarDecoration.id, + ...params, + }); + + emit('done', { + updated: { + id: props.avatarDecoration.id, + ...params, + }, + }); + + windowEl.value?.close(); + } else { + const created = await os.apiWithDialog('admin/avatar-decorations/create', params); + + emit('done', { + created: created, + }); + + windowEl.value?.close(); + } +} + +async function del() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.tsx.removeAreYouSure({ x: name.value }), + }); + if (canceled) return; + + misskeyApi('admin/avatar-decorations/delete', { + id: props.avatarDecoration.id, + }).then(() => { + emit('done', { + deleted: true, + }); + windowEl.value?.close(); + }); +} +</script> + +<style lang="scss" module> +.preview { + display: grid; + place-items: center; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + gap: var(--MI-margin); +} + +.previewItem { + width: 100%; + height: 100%; + min-height: 160px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--MI-radius); + + &.light { + background: #eee; + } + + &.dark { + background: #222; + } +} + +.roleItem { + display: flex; +} + +.role { + flex: 1; +} + +.roleUnassign { + width: 32px; + height: 32px; + margin-left: 8px; + align-self: center; +} + +.footer { + position: sticky; + z-index: 10000; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); +} +</style> diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index b377314856..a5cafb1678 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -5,92 +5,38 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="900"> <div class="_gaps"> - <MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null"> - <template #label>{{ avatarDecoration.name }}</template> - <template #caption>{{ avatarDecoration.description }}</template> - - <div :class="$style.editorRoot"> - <div :class="$style.editorWrapper"> - <div :class="$style.preview"> - <div :class="[$style.previewItem, $style.light]"> - <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/> - </div> - <div :class="[$style.previewItem, $style.dark]"> - <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/> - </div> - </div> - <div class="_gaps_m"> - <MkInput v-model="avatarDecoration.name"> - <template #label>{{ i18n.ts.name }}</template> - </MkInput> - <MkTextarea v-model="avatarDecoration.description"> - <template #label>{{ i18n.ts.description }}</template> - </MkTextarea> - <MkInput v-model="avatarDecoration.url"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <div class="_buttons"> - <MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> - </div> - </div> + <div :class="$style.decorations"> + <div + v-for="avatarDecoration in avatarDecorations" + :key="avatarDecoration.id" + v-panel + :class="$style.decoration" + @click="edit(avatarDecoration)" + > + <div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/> </div> - </MkFolder> + </div> </div> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; -import MkButton from '@/components/MkButton.vue'; -import MkInput from '@/components/MkInput.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; import { signinRequired } from '@/account.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import MkFolder from '@/components/MkFolder.vue'; - -const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]); const $i = signinRequired(); -function add() { - avatarDecorations.value.unshift({ - _id: Math.random().toString(36), - id: null, - name: '', - description: '', - url: '', - }); -} - -function del(avatarDecoration) { - os.confirm({ - type: 'warning', - text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }), - }).then(({ canceled }) => { - if (canceled) return; - avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration); - misskeyApi('admin/avatar-decorations/delete', avatarDecoration); - }); -} - -async function save(avatarDecoration) { - if (avatarDecoration.id == null) { - await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration); - load(); - } else { - os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration); - } -} +const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]); function load() { misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => { @@ -100,6 +46,37 @@ function load() { load(); +async function add(ev: MouseEvent) { + const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), { + }, { + done: result => { + if (result.created) { + avatarDecorations.value.unshift(result.created); + } + }, + closed: () => dispose(), + }); +} + +function edit(avatarDecoration) { + const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), { + avatarDecoration: avatarDecoration, + }, { + done: result => { + if (result.updated) { + const index = avatarDecorations.value.findIndex(x => x.id === avatarDecoration.id); + avatarDecorations.value[index] = { + ...avatarDecorations.value[index], + ...result.updated, + }; + } else if (result.deleted) { + avatarDecorations.value = avatarDecorations.value.filter(x => x.id !== avatarDecoration.id); + } + }, + closed: () => dispose(), + }); +} + const headerActions = computed(() => [{ asFullButton: true, icon: 'ti ti-plus', @@ -116,53 +93,26 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> -.editorRoot { - container: editor / inline-size; -} - -.editorWrapper { +.decorations { display: grid; - grid-template-columns: 1fr; - grid-template-rows: auto auto; - gap: var(--margin); + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + grid-gap: 12px; } -.preview { - display: grid; - place-items: center; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr; - gap: var(--margin); -} - -.previewItem { - width: 100%; - height: 100%; - min-height: 160px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius); - - &.light { - background: #eee; - } - - &.dark { - background: #222; - } +.decoration { + cursor: pointer; + padding: 16px 16px 28px 16px; + border-radius: 8px; + text-align: center; + font-size: 90%; + overflow: clip; + contain: content; } -@container editor (min-width: 600px) { - .editorWrapper { - grid-template-columns: 200px 1fr; - grid-template-rows: 1fr; - gap: calc(var(--margin) * 2); - } - - .preview { - grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; - } +.decorationName { + position: relative; + z-index: 10; + font-weight: bold; + margin-bottom: 20px; } </style> diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index d3f4a65b89..6d8274a55c 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -216,7 +216,7 @@ definePageMetadata(() => ({ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - color: var(--navFg); + color: var(--MI_THEME-navFg); } .pinnedNoteRemove { diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 790e16e471..9cd2546312 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -300,14 +300,14 @@ definePageMetadata(() => ({ <style lang="scss" module> .main { - min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px))); + min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px))); } .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - background: var(--acrylicBg); - border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + border-top: solid 0.5px var(--MI_THEME-divider); } .bannerContainer { @@ -341,7 +341,7 @@ definePageMetadata(() => ({ left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); } .bannerStatus { @@ -352,7 +352,7 @@ definePageMetadata(() => ({ padding: 8px 12px; font-size: 80%; background: rgba(0, 0, 0, 0.7); - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); color: #fff; } @@ -366,8 +366,8 @@ definePageMetadata(() => ({ bottom: 16px; left: 16px; background: rgba(0, 0, 0, 0.7); - color: var(--warn); - border-radius: var(--radius-sm); + color: var(--MI_THEME-warn); + border-radius: var(--MI-radius-sm); font-weight: bold; font-size: 1em; padding: 4px 7px; diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 52b852ad17..716cd9a73f 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -33,25 +33,28 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, provide, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; +import type { MenuItem } from '@/types/menu.js'; import MkNotes from '@/components/MkNotes.vue'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { url } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import { clipsCache } from '@/cache.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { genEmbedCode } from '@/scripts/get-embed-code.js'; -import type { MenuItem } from '@/types/menu.js'; +import { getServerContext } from '@/server-context.js'; + +const CTX_CLIP = getServerContext('clip'); const props = defineProps<{ clipId: string, }>(); -const clip = ref<Misskey.entities.Clip | null>(null); +const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP); const favorited = ref(false); const pagination = { endpoint: 'clips/notes' as const, @@ -64,6 +67,11 @@ const pagination = { const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId)); watch(() => props.clipId, async () => { + if (CTX_CLIP && CTX_CLIP.id === props.clipId) { + clip.value = CTX_CLIP; + return; + } + clip.value = await misskeyApi('clips/show', { clipId: props.clipId, }); @@ -198,7 +206,7 @@ definePageMetadata(() => ({ .user { --height: 32px; padding: 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); line-height: var(--height); } diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 8904096875..850c1c5eb0 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -119,7 +119,7 @@ const selectAll = () => { if (selectedEmojis.value.length > 0) { selectedEmojis.value = []; } else { - selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id); + selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id); } }; @@ -136,7 +136,7 @@ const add = async (ev: MouseEvent) => { }, { done: result => { if (result.created) { - emojisPaginationComponent.value.prepend(result.created); + emojisPaginationComponent.value?.prepend(result.created); } }, closed: () => dispose(), @@ -149,12 +149,12 @@ const edit = (emoji) => { }, { done: result => { if (result.updated) { - emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ + emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({ ...oldEmoji, ...result.updated, })); } else if (result.deleted) { - emojisPaginationComponent.value.removeItem(emoji.id); + emojisPaginationComponent.value?.removeItem(emoji.id); } }, closed: () => dispose(), @@ -239,7 +239,7 @@ const setCategoryBulk = async () => { ids: selectedEmojis.value, category: result, }); - emojisPaginationComponent.value.reload(); + emojisPaginationComponent.value?.reload(); }; const setLicenseBulk = async () => { @@ -251,43 +251,43 @@ const setLicenseBulk = async () => { ids: selectedEmojis.value, license: result, }); - emojisPaginationComponent.value.reload(); + emojisPaginationComponent.value?.reload(); }; const addTagBulk = async () => { const { canceled, result } = await os.inputText({ title: 'Tag', }); - if (canceled) return; + if (canceled || result == null) return; await os.apiWithDialog('admin/emoji/add-aliases-bulk', { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value.reload(); + emojisPaginationComponent.value?.reload(); }; const removeTagBulk = async () => { const { canceled, result } = await os.inputText({ title: 'Tag', }); - if (canceled) return; + if (canceled || result == null) return; await os.apiWithDialog('admin/emoji/remove-aliases-bulk', { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value.reload(); + emojisPaginationComponent.value?.reload(); }; const setTagBulk = async () => { const { canceled, result } = await os.inputText({ title: 'Tag', }); - if (canceled) return; + if (canceled || result == null) return; await os.apiWithDialog('admin/emoji/set-aliases-bulk', { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value.reload(); + emojisPaginationComponent.value?.reload(); }; const delBulk = async () => { @@ -299,7 +299,7 @@ const delBulk = async () => { await os.apiWithDialog('admin/emoji/delete-bulk', { ids: selectedEmojis.value, }); - emojisPaginationComponent.value.reload(); + emojisPaginationComponent.value?.reload(); }; const headerActions = computed(() => [{ @@ -330,28 +330,28 @@ definePageMetadata(() => ({ .ogwlenmc { > .local { .empty { - margin: var(--margin); + margin: var(--MI-margin); } .ldhfsamy { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); grid-gap: 12px; - margin: var(--margin) 0; + margin: var(--MI-margin) 0; > .emoji { display: flex; align-items: center; padding: 11px; text-align: left; - border: solid 1px var(--panel); + border: solid 1px var(--MI_THEME-panel); &:hover { - border-color: var(--inputBorderHover); + border-color: var(--MI_THEME-inputBorderHover); } &.selected { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } > .img { @@ -382,14 +382,14 @@ definePageMetadata(() => ({ > .remote { .empty { - margin: var(--margin); + margin: var(--MI-margin); } .ldhfsamy { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); grid-gap: 12px; - margin: var(--margin) 0; + margin: var(--MI-margin) 0; > .emoji { display: flex; @@ -398,7 +398,7 @@ definePageMetadata(() => ({ text-align: left; &:hover { - color: var(--accent); + color: var(--MI_THEME-accent); } > .img { diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 36e95cec2d..9d74a47ec6 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -231,8 +231,8 @@ onMounted(async () => { <style lang="scss" module> .filePreviewRoot { - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); // MkMediaList 内の上部マージン 4px padding: calc(1rem - 4px) 1rem 1rem; } @@ -258,12 +258,12 @@ onMounted(async () => { .fileQuickActionsOthersButton { padding: .5rem; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); &:hover, &:focus-visible { - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); text-decoration: none; outline: none; } @@ -285,7 +285,7 @@ onMounted(async () => { align-items: center; min-width: 0; font-weight: 700; - border-radius: var(--radius); + border-radius: var(--MI-radius); font-size: .8rem; >.fileNameEditIcon { @@ -299,12 +299,12 @@ onMounted(async () => { } &:hover { - background-color: var(--accentedBg); + background-color: var(--MI_THEME-accentedBg); >.fileName, >.fileNameEditIcon { visibility: visible; - color: var(--accent); + color: var(--MI_THEME-accent); } } } @@ -322,7 +322,7 @@ onMounted(async () => { display: block; width: 100%; padding: .5rem 1rem; - border-radius: var(--radius); + border-radius: var(--MI-radius); .kvEditIcon { display: inline-block; @@ -332,11 +332,11 @@ onMounted(async () => { } &:hover { - color: var(--accent); - background-color: var(--accentedBg); + color: var(--MI_THEME-accent); + background-color: var(--MI_THEME-accentedBg); .kvEditIcon { - color: var(--accent); + color: var(--MI_THEME-accent); visibility: visible; } } diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index 4db952eac2..fb4d599c28 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="replaying" class="_woodenFrame"> <div class="_woodenFrameInner"> <div style="background: #0004;"> - <div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div> + <div style="height: 10px; background: var(--MI_THEME-accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div> </div> </div> <div class="_woodenFrameInner"> diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index c5f0dde878..d3e9ca0dcf 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only ref="windowEl" :initialWidth="400" :initialHeight="500" - :canResize="false" - @close="windowEl.close()" - @closed="$emit('closed')" + :canResize="true" + @close="windowEl?.close()" + @closed="emit('closed')" > <template v-if="emoji" #header>:{{ emoji.name }}:</template> <template v-else #header>New emoji</template> @@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; const props = defineProps<{ - emoji?: any, + emoji?: Misskey.entities.EmojiDetailed, +}>(); + +const emit = defineEmits<{ + (ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void, + (ev: 'closed'): void }>(); const windowEl = ref<InstanceType<typeof MkWindow> | null>(null); const name = ref<string>(props.emoji ? props.emoji.name : ''); -const category = ref<string>(props.emoji ? props.emoji.category : ''); +const category = ref<string>(props.emoji?.category ? props.emoji.category : ''); const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : ''); -const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : ''); +const license = ref<string>(props.emoji?.license ? props.emoji.license : ''); const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false); const localOnly = ref(props.emoji ? props.emoji.localOnly : false); const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); @@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); -const emit = defineEmits<{ - (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, - (ev: 'closed'): void -}>(); - -async function changeImage(ev) { +async function changeImage(ev: Event) { file.value = await selectFile(ev.currentTarget ?? ev.target, null); const candidate = file.value.name.replace(/\.(.+)$/, ''); if (candidate.match(/^[a-z0-9_]+$/)) { @@ -140,7 +140,7 @@ async function addRole() { rolesThatCanBeUsedThisEmojiAsReaction.value.push(role); } -async function removeRole(role, ev) { +async function removeRole(role: Misskey.entities.RoleLite, ev: Event) { rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id); } @@ -172,7 +172,7 @@ async function done() { }, }); - windowEl.value.close(); + windowEl.value?.close(); } else { const created = await os.apiWithDialog('admin/emoji/add', params); @@ -180,11 +180,12 @@ async function done() { created: created, }); - windowEl.value.close(); + windowEl.value?.close(); } } async function del() { + if (!props.emoji) return; const { canceled } = await os.confirm({ type: 'warning', text: i18n.tsx.removeAreYouSure({ x: name.value }), @@ -197,7 +198,7 @@ async function del() { emit('done', { deleted: true, }); - windowEl.value.close(); + windowEl.value?.close(); }); } </script> @@ -212,7 +213,7 @@ async function del() { .imgContainer { padding: 8px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .img { @@ -243,9 +244,9 @@ async function del() { bottom: 0; left: 0; padding: 12px; - border-top: solid 0.5px var(--divider); - background: var(--acrylicBg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index 03a3b8f1c0..594a8eda0e 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -15,18 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; +import { defineAsyncComponent } from 'vue'; +import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; +import { $i } from '@/account.js'; const props = defineProps<{ emoji: Misskey.entities.EmojiSimple; }>(); function menu(ev) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + menuItems.push({ type: 'label', text: ':' + props.emoji.name + ':', }, { @@ -48,8 +52,28 @@ function menu(ev) { closed: () => dispose(), }); }, - }], ev.currentTarget ?? ev.target); + }); + + if ($i?.isModerator ?? $i?.isAdmin) { + menuItems.push({ + text: i18n.ts.edit, + icon: 'ti ti-pencil', + action: () => { + edit(props.emoji); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } + +const edit = async (emoji) => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), { + emoji: emoji, + }, { + closed: () => dispose(), + }); +}; </script> <style lang="scss" module> @@ -58,11 +82,11 @@ function menu(ev) { align-items: center; padding: 12px; text-align: left; - background: var(--panel); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-sm); &:hover { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } } diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue index cfdb235d3a..8b16a88ff3 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkSpacer :contentMax="800"> - <MkTab v-model="tab" style="margin-bottom: var(--margin);"> + <MkTab v-model="tab" style="margin-bottom: var(--MI-margin);"> <option value="notes">{{ i18n.ts.notes }}</option> <option value="polls">{{ i18n.ts.poll }}</option> </MkTab> diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index 805d826946..2550100a42 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkSpacer :contentMax="1200"> - <MkTab v-model="origin" style="margin-bottom: var(--margin);"> + <MkTab v-model="origin" style="margin-bottom: var(--MI-margin);"> <option value="local">{{ i18n.ts.local }}</option> <option value="remote">{{ i18n.ts.remote }}</option> </MkTab> diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue index cb1feef016..ab26573b19 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -53,7 +53,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .note { - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } </style> diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index fd6fadd0b3..d84ec4873b 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -467,8 +467,8 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> .footer { - backdrop-filter: var(--blur, blur(15px)); - background: var(--acrylicBg); - border-top: solid .5px var(--divider); + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + border-top: solid .5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index f63a799365..2b85489706 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -55,7 +55,8 @@ const tab = ref('featured'); const featuredFlashsPagination = { endpoint: 'flash/featured' as const, - noPaging: true, + limit: 5, + offsetMode: true, }; const myFlashsPagination = { endpoint: 'flash/my' as const, diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 1229fcfd4e..7a7d52691c 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div> </div> </div> - <MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA> + <MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--MI_THEME-accent);">{{ i18n.ts._play.editThisPage }}</MkA> <MkAd :prefer="['horizontal', 'horizontal-big']"/> </div> <MkError v-else-if="error" @retry="fetchFlash()"/> @@ -367,7 +367,7 @@ definePageMetadata(() => ({ justify-content: center; gap: 12px; padding: 16px; - border-bottom: 1px solid var(--divider); + border-bottom: 1px solid var(--MI_THEME-divider); &:last-child { border-bottom: none; diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 400dfdbe6d..2f9cacbde9 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> <template #default="{items}"> - <div class="mk-follow-requests"> + <div class="mk-follow-requests _gaps"> <div v-for="req in items" :key="req.id" class="user _panel"> <MkAvatar class="avatar" :user="displayUser(req)" indicator link preview/> <div class="body"> @@ -29,6 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton> <MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton> </div> + <div v-else class="commands"> + <MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.cancel }}</MkButton> + </div> </div> </div> </div> @@ -41,38 +44,42 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import * as Misskey from 'misskey-js'; import { shallowRef, computed, ref } from 'vue'; -import MkPagination from '@/components/MkPagination.vue'; +import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { userPage, acct } from '@/filters/user.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { infoImageUrl } from '@/instance.js'; +import { $i } from '@/account.js'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import { $i } from '@/account'; const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); -const pagination = computed(() => tab.value === 'list' - ? { - endpoint: 'following/requests/list' as const, - limit: 10, - } - : { - endpoint: 'following/requests/sent' as const, - limit: 10, - }, -); +const pagination = computed<Paging>(() => tab.value === 'list' ? { + endpoint: 'following/requests/list', + limit: 10, +} : { + endpoint: 'following/requests/sent', + limit: 10, +}); + +function accept(user: Misskey.entities.UserLite) { + os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => { + paginationComponent.value?.reload(); + }); +} -function accept(user) { - misskeyApi('following/requests/accept', { userId: user.id }).then(() => { +function reject(user: Misskey.entities.UserLite) { + os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => { paginationComponent.value?.reload(); }); } -function reject(user) { - misskeyApi('following/requests/reject', { userId: user.id }).then(() => { +function cancel(user: Misskey.entities.UserLite) { + os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => { paginationComponent.value?.reload(); }); } @@ -86,11 +93,11 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [ { key: 'list', - title: i18n.ts.followRequests, + title: i18n.ts._followRequest.recieved, icon: 'ph-envelope ph-bold ph-lg', }, { key: 'sent', - title: i18n.ts.pendingFollowRequests, + title: i18n.ts._followRequest.sent, icon: 'ph-paper-plane-tilt ph-bold ph-lg', }, ]); @@ -115,7 +122,7 @@ definePageMetadata(() => ({ margin: 0 12px 0 0; width: 42px; height: 42px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } > .body { diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 91f74b2cf9..7fb13c8fcb 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -127,7 +127,7 @@ definePageMetadata(() => ({ // The universal layout inserts a "spacer" thing that causes a stray scroll bar. // We have to create fake "space" for it to "roll up" and back into the viewport, which removes the scrollbar. - margin-bottom: calc(-1 * var(--minBottomSpacing)); + margin-bottom: calc(-1 * var(--MI-minBottomSpacing)); // Some "just in case" backup properties. // These should not be needed, but help to maintain the layout if the above trick ever stops working. diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index a68a7e5c41..70f8b2c31d 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -141,7 +141,7 @@ definePageMetadata(() => ({ top: 8px; left: 9px; padding: 8px; - background: var(--panel); + background: var(--MI_THEME-panel); } > .remove { @@ -149,7 +149,7 @@ definePageMetadata(() => ({ top: 8px; right: 9px; padding: 8px; - background: var(--panel); + background: var(--MI_THEME-panel); } } </style> diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index e0b137ed28..2162e269bf 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -130,6 +130,6 @@ definePageMetadata(() => ({ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-gap: 12px; - margin: 0 var(--margin); + margin: 0 var(--MI-margin); } </style> diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 6ed119c0c4..539a6a9a7b 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -262,14 +262,14 @@ definePageMetadata(() => ({ align-items: center; margin-top: 16px; padding: 16px 0 0 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); > .like { > .button { - --accent: rgb(241 97 132); - --X8: rgb(241 92 128); - --buttonBg: rgb(216 71 106 / 5%); - --buttonHoverBg: rgb(216 71 106 / 10%); + --MI_THEME-accent: rgb(241 97 132); + --MI_THEME-X8: rgb(241 92 128); + --MI_THEME-buttonBg: rgb(216 71 106 / 5%); + --MI_THEME-buttonHoverBg: rgb(216 71 106 / 10%); color: #ff002f; ::v-deep(.count) { @@ -286,7 +286,7 @@ definePageMetadata(() => ({ margin: 0 8px; &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } } @@ -295,7 +295,7 @@ definePageMetadata(() => ({ > .user { margin-top: 16px; padding: 16px 0 0 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); display: flex; align-items: center; flex-wrap: wrap; @@ -321,7 +321,7 @@ definePageMetadata(() => ({ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-gap: 12px; - margin: var(--margin); + margin: var(--MI-margin); > .post { diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue index b52f4decaa..998b8be0f3 100644 --- a/packages/frontend/src/pages/games.vue +++ b/packages/frontend/src/pages/games.vue @@ -35,7 +35,7 @@ definePageMetadata(() => ({ <style module> .link:focus-within { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: -2px; } </style> diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue index 4bee437f65..6d68ed83b4 100644 --- a/packages/frontend/src/pages/install-extensions.vue +++ b/packages/frontend/src/pages/install-extensions.vue @@ -8,76 +8,26 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="500"> <MkLoading v-if="uiPhase === 'fetching'"/> - <div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot"> - <div :class="$style.extInstallerIconWrapper"> - <i v-if="data.type === 'plugin'" class="ti ti-plug"></i> - <i v-else-if="data.type === 'theme'" class="ti ti-palette"></i> - <i v-else class="ti ti-download"></i> - </div> - <h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2> - <div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div> - <MkInfo v-if="data.type === 'plugin'" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo> - <FormSection> - <template #label>{{ i18n.ts._externalResourceInstaller[`_${data.type}`].metaTitle }}</template> - <div class="_gaps_s"> - <FormSplit> + <MkExtensionInstaller v-else-if="uiPhase === 'confirm' && data" :extension="data" @confirm="install()"> + <template #additionalInfo> + <FormSection> + <template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template> + <div class="_gaps_s"> <MkKeyValue> - <template #key>{{ i18n.ts.name }}</template> - <template #value>{{ data.meta?.name }}</template> + <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template> + <template #value><MkUrl :url="url" :showUrlPreview="false"></MkUrl></template> </MkKeyValue> <MkKeyValue> - <template #key>{{ i18n.ts.author }}</template> - <template #value>{{ data.meta?.author }}</template> + <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template> + <template #value> + <!-- この画面が出ている時点でハッシュの検証には成功している --> + <i class="ti ti-check" style="color: var(--MI_THEME-accent)"></i> + </template> </MkKeyValue> - </FormSplit> - <MkKeyValue v-if="data.type === 'plugin'"> - <template #key>{{ i18n.ts.description }}</template> - <template #value>{{ data.meta?.description }}</template> - </MkKeyValue> - <MkKeyValue v-if="data.type === 'plugin'"> - <template #key>{{ i18n.ts.version }}</template> - <template #value>{{ data.meta?.version }}</template> - </MkKeyValue> - <MkKeyValue v-if="data.type === 'plugin'"> - <template #key>{{ i18n.ts.permission }}</template> - <template #value> - <ul :class="$style.extInstallerKVList"> - <li v-for="permission in data.meta?.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li> - </ul> - </template> - </MkKeyValue> - <MkKeyValue v-if="data.type === 'theme' && data.meta?.base"> - <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template> - <template #value>{{ i18n.ts[data.meta.base] }}</template> - </MkKeyValue> - <MkFolder> - <template #icon><i class="ti ti-code"></i></template> - <template #label>{{ i18n.ts._plugin.viewSource }}</template> - - <MkCode :code="data.raw ?? ''"/> - </MkFolder> - </div> - </FormSection> - <FormSection> - <template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template> - <div class="_gaps_s"> - <MkKeyValue> - <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template> - <template #value><MkUrl :url="url ?? ''" :showUrlPreview="false"></MkUrl></template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template> - <template #value> - <!--この画面が出ている時点でハッシュの検証には成功している--> - <i class="ti ti-check" style="color: var(--accent)"></i> - </template> - </MkKeyValue> - </div> - </FormSection> - <div class="_buttonsCenter"> - <MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> - </div> - </div> + </div> + </FormSection> + </template> + </MkExtensionInstaller> <div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]"> <div :class="$style.extInstallerIconWrapper"> <i class="ti ti-circle-x"></i> @@ -96,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue'; import MkLoading from '@/components/global/MkLoading.vue'; +import MkExtensionInstaller, { type Extension } from '@/components/MkExtensionInstaller.vue'; import MkButton from '@/components/MkButton.vue'; -import FormSection from '@/components/form/section.vue'; -import FormSplit from '@/components/form/split.vue'; -import MkCode from '@/components/MkCode.vue'; -import MkUrl from '@/components/global/MkUrl.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; +import MkUrl from '@/components/global/MkUrl.vue'; +import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js'; @@ -124,24 +71,7 @@ const errorKV = ref<{ const url = ref<string | null>(null); const hash = ref<string | null>(null); -const data = ref<{ - type: 'plugin' | 'theme'; - raw: string; - meta?: { - // Plugin & Theme Common - name: string; - author: string; - - // Plugin - description?: string; - version?: string; - permissions?: string[]; - config?: Record<string, any>; - - // Theme - base?: 'light' | 'dark'; - }; -} | null>(null); +const data = ref<Extension | null>(null); function goBack(): void { history.back(); @@ -227,7 +157,7 @@ async function fetch() { data.value = { type: 'theme', meta: { - description, + // description, // 使用されていない ...meta, }, raw: res.data, @@ -320,8 +250,8 @@ definePageMetadata(() => ({ <style lang="scss" module> .extInstallerRoot { - border-radius: var(--radius); - background: var(--panel); + border-radius: var(--MI-radius); + background: var(--MI_THEME-panel); padding: 1.5rem; } @@ -335,8 +265,8 @@ definePageMetadata(() => ({ margin-left: auto; margin-right: auto; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); } .error .extInstallerIconWrapper { @@ -353,9 +283,4 @@ definePageMetadata(() => ({ .extInstallerNormDesc { text-align: center; } - -.extInstallerKVList { - margin-top: 0; - margin-bottom: 0; -} </style> diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index a5e6e5ac33..96aebb14d2 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -59,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkTextarea v-model="moderationNote" manualSave> <template #label>{{ i18n.ts.moderationNote }}</template> + <template #caption>{{ i18n.ts.moderationNoteDescription }}</template> </MkTextarea> </div> </FormSection> @@ -460,7 +461,7 @@ definePageMetadata(() => ({ display: block; margin: 0 16px 0 0; height: 64px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } > .name { diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index ef485a9446..3416ee6cfc 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -5,10 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header> - <MkPageHeader/> - </template> - <MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200"> + <template #header><MkPageHeader/></template> + <MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200"> <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <div :class="$style.text"> @@ -16,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.nothing }} </div> </div> - </MKSpacer> + </MkSpacer> <MkSpacer v-else :contentMax="800"> <div class="_gaps_m" style="text-align: center;"> <div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div> @@ -115,6 +113,6 @@ definePageMetadata(() => ({ width: 128px; height: 128px; margin-bottom: 16px; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } </style> diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index 03ab804d05..e1ba424afc 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> + <MkSpacer v-if="error != null" :contentMax="1200"> <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"> @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.nothing }} </p> </div> - </MKSpacer> + </MkSpacer> <MkSpacer v-else-if="list" :contentMax="700" :class="$style.main"> <div v-if="list" class="members _margin"> <div :class="$style.member_text">{{ i18n.ts.members }}</div> @@ -50,7 +50,7 @@ const props = defineProps<{ }>(); const list = ref<Misskey.entities.UserList | null>(null); -const error = ref(); +const error = ref<unknown | null>(null); const users = ref<Misskey.entities.UserDetailed[]>([]); function fetchList(): void { @@ -108,7 +108,7 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> .main { - min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px))); + min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px))); } .userItem { @@ -143,7 +143,7 @@ definePageMetadata(() => ({ width: 128px; height: 128px; margin-bottom: 16px; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } .button { diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue index 3233953942..6f10c69640 100644 --- a/packages/frontend/src/pages/lookup.vue +++ b/packages/frontend/src/pages/lookup.vue @@ -40,7 +40,7 @@ function fetch() { return; } - let promise: Promise<any>; + let promise: Promise<unknown>; if (uri.startsWith('https://')) { promise = misskeyApi('ap/show', { diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 7e70f91710..e85d2c29c1 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -4,95 +4,79 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="800"> - <div v-if="$i"> - <div v-if="state == 'waiting'"> - <MkLoading/> - </div> - <div v-if="state == 'denied'"> - <p>{{ i18n.ts._auth.denied }}</p> - </div> - <div v-else-if="state == 'accepted'" class="accepted"> - <p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p> - <p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p> - </div> - <div v-else> - <div v-if="_permissions.length > 0"> - <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p> - <p v-else>{{ i18n.ts._auth.permissionAsk }}</p> - <ul> - <li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li> - </ul> - </div> - <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div> - <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div> - <div :class="$style.buttons"> - <MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton> - <MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton> - </div> - </div> +<div> + <MkAnimBg style="position: fixed; top: 0;"/> + <div :class="$style.formContainer"> + <div :class="$style.form"> + <MkAuthConfirm + ref="authRoot" + :name="name" + :icon="icon || undefined" + :permissions="_permissions" + @accept="onAccept" + @deny="onDeny" + > + <template #consentAdditionalInfo> + <div v-if="callback != null" class="_gaps_s" :class="$style.redirectRoot"> + <div>{{ i18n.ts._auth.byClickingYouWillBeRedirectedToThisUrl }}</div> + <div class="_monospace" :class="$style.redirectUrl">{{ callback }}</div> + </div> + </template> + </MkAuthConfirm> </div> - <div v-else> - <p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p> - <MkSignin @login="onLogin"/> - </div> - </MkSpacer> -</MkStickyContainer> + </div> +</div> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; -import MkSignin from '@/components/MkSignin.vue'; -import MkButton from '@/components/MkButton.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { $i, login } from '@/account.js'; +import { computed, useTemplateRef } from 'vue'; +import * as Misskey from 'misskey-js'; + +import MkAnimBg from '@/components/MkAnimBg.vue'; +import MkAuthConfirm from '@/components/MkAuthConfirm.vue'; + import { i18n } from '@/i18n.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const props = defineProps<{ session: string; callback?: string; - name: string; - icon: string; - permission: string; // コンマ区切り + name?: string; + icon?: string; + permission?: string; // コンマ区切り }>(); -const _permissions = computed(() => props.permission ? props.permission.split(',') : []); +const _permissions = computed(() => { + return (props.permission ? props.permission.split(',').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) : []); +}); -const state = ref<string | null>(null); +const authRoot = useTemplateRef('authRoot'); -async function accept(): Promise<void> { - state.value = 'waiting'; +async function onAccept(token: string) { await misskeyApi('miauth/gen-token', { session: props.session, name: props.name, iconUrl: props.icon, permission: _permissions.value, + }, token).catch(() => { + authRoot.value?.showUI('failed'); }); - state.value = 'accepted'; - if (props.callback) { + if (props.callback && props.callback !== '') { const cbUrl = new URL(props.callback); - if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url'); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url'); cbUrl.searchParams.set('session', props.session); - location.href = cbUrl.href; + location.href = cbUrl.toString(); + } else { + authRoot.value?.showUI('success'); } } -function deny(): void { - state.value = 'denied'; -} - -function onLogin(res): void { - login(res.i); +function onDeny() { + authRoot.value?.showUI('denied'); } -const headerActions = computed(() => []); - -const headerTabs = computed(() => []); - definePageMetadata(() => ({ title: 'MiAuth', icon: 'ti ti-apps', @@ -100,15 +84,38 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> -.buttons { - margin-top: 16px; - display: flex; - gap: 8px; - flex-wrap: wrap; +.formContainer { + min-height: 100svh; + padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px; + box-sizing: border-box; + display: grid; + place-content: center; +} + +.form { + position: relative; + z-index: 10; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-panel); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + overflow: clip; + max-width: 500px; + width: calc(100vw - 64px); + height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px))); + overflow-y: scroll; +} + +.redirectRoot { + padding: 16px; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-bg); } -.loginMessage { - text-align: center; - margin: 8px 0 24px; +.redirectUrl { + font-size: 90%; + padding: 12px; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-panel); + overflow-x: scroll; } </style> diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue index 9233695900..540cb34904 100644 --- a/packages/frontend/src/pages/my-antennas/index.vue +++ b/packages/frontend/src/pages/my-antennas/index.vue @@ -73,11 +73,11 @@ onActivated(() => { .antenna { display: block; padding: 16px; - border: solid 1px var(--divider); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); &:hover { - border: solid 1px var(--accent); + border: solid 1px var(--MI_THEME-accent); text-decoration: none; } } diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index ece998a7a5..acf37a9a2f 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -77,15 +77,15 @@ async function create() { clipsCache.delete(); - pagingComponent.value.reload(); + pagingComponent.value?.reload(); } function onClipCreated() { - pagingComponent.value.reload(); + pagingComponent.value?.reload(); } function onClipDeleted() { - pagingComponent.value.reload(); + pagingComponent.value?.reload(); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index cd68be4ac3..d08f6fec5a 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -85,12 +85,12 @@ onActivated(() => { .list { display: block; padding: 16px; - border: solid 1px var(--divider); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); margin-bottom: 8px; &:hover { - border: solid 1px var(--accent); + border: solid 1px var(--MI_THEME-accent); text-decoration: none; } } diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 5f195693cc..69e404bd85 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -110,7 +110,7 @@ function addUser() { listId: list.value.id, userId: user.id, }).then(() => { - paginationEl.value.reload(); + paginationEl.value?.reload(); }); }); } @@ -126,7 +126,7 @@ async function removeUser(item, ev) { listId: list.value.id, userId: item.userId, }).then(() => { - paginationEl.value.removeItem(item.id); + paginationEl.value?.removeItem(item.id); }); }, }], ev.currentTarget ?? ev.target); @@ -134,7 +134,7 @@ async function removeUser(item, ev) { async function showMembershipMenu(item, ev) { const withRepliesRef = ref(item.withReplies); - + os.popupMenu([{ type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, @@ -199,7 +199,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .main { - min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px))); + min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px))); } .userItem { @@ -234,8 +234,8 @@ definePageMetadata(() => ({ } .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue index 93a792c42f..6a2d01b6fa 100644 --- a/packages/frontend/src/pages/not-found.vue +++ b/packages/frontend/src/pages/not-found.vue @@ -24,7 +24,7 @@ const props = defineProps<{ }>(); if (props.showLoginPopup) { - pleaseLogin('/'); + pleaseLogin({ path: '/' }); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index d0bbaeddd9..737b0eea4c 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -60,6 +60,10 @@ import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; import MkClipPreview from '@/components/MkClipPreview.vue'; import { defaultStore } from '@/store.js'; +import { pleaseLogin } from '@/scripts/please-login.js'; +import { getServerContext } from '@/server-context.js'; + +const CTX_NOTE = getServerContext('note'); const MkNoteDetailed = defineAsyncComponent(() => (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') : @@ -72,7 +76,7 @@ const props = defineProps<{ initialTab?: string; }>(); -const note = ref<null | Misskey.entities.Note>(); +const note = ref<null | Misskey.entities.Note>(CTX_NOTE); const clips = ref<Misskey.entities.Clip[]>(); const showPrev = ref<'user' | 'channel' | false>(false); const showNext = ref<'user' | 'channel' | false>(false); @@ -121,6 +125,12 @@ function fetchNote() { showPrev.value = false; showNext.value = false; note.value = null; + + if (CTX_NOTE && CTX_NOTE.id === props.noteId) { + note.value = CTX_NOTE; + return; + } + misskeyApi('notes/show', { noteId: props.noteId, }).then(res => { @@ -134,6 +144,11 @@ function fetchNote() { }); } }).catch(err => { + if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') { + pleaseLogin({ + message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor, + }); + } error.value = err; }); } @@ -182,11 +197,11 @@ definePageMetadata(() => ({ } .loadNext { - margin-bottom: var(--margin); + margin-bottom: var(--MI-margin); } .loadPrev { - margin-top: var(--margin); + margin-top: var(--MI-margin); } .loadButton { @@ -194,7 +209,7 @@ definePageMetadata(() => ({ } .note { - border-radius: var(--radius); - background: var(--panel); + border-radius: var(--MI-radius); + background: var(--MI_THEME-panel); } </style> diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index bd93fc8369..46ee501c76 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -102,7 +102,7 @@ definePageMetadata(() => ({ <style module lang="scss"> .notifications { - border-radius: var(--radius); + border-radius: var(--MI-radius); overflow: clip; } </style> diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue index 733e34eb2c..8719a769e5 100644 --- a/packages/frontend/src/pages/oauth.vue +++ b/packages/frontend/src/pages/oauth.vue @@ -4,40 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkStickyContainer> - <template #header><MkPageHeader/></template> - <MkSpacer :contentMax="800"> - <div v-if="$i"> - <div v-if="permissions.length > 0"> - <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p> - <p v-else>{{ i18n.ts._auth.permissionAsk }}</p> - <ul> - <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li> - </ul> - </div> - <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div> - <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div> - <form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post"> - <input name="login_token" type="hidden" :value="$i.token"/> - <input name="transaction_id" type="hidden" :value="transactionIdMeta?.content"/> - <MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton> - <MkButton inline primary>{{ i18n.ts.accept }}</MkButton> - </form> +<div> + <MkAnimBg style="position: fixed; top: 0;"/> + <div :class="$style.formContainer"> + <div :class="$style.form"> + <MkAuthConfirm + ref="authRoot" + :name="name" + :permissions="permissions" + :waitOnDeny="true" + @accept="onAccept" + @deny="onDeny" + /> </div> - <div v-else> - <p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p> - <MkSignin @login="onLogin"/> - </div> - </MkSpacer> -</MkStickyContainer> + </div> +</div> </template> <script lang="ts" setup> -import MkSignin from '@/components/MkSignin.vue'; -import MkButton from '@/components/MkButton.vue'; -import { $i, login } from '@/account.js'; -import { i18n } from '@/i18n.js'; +import * as Misskey from 'misskey-js'; +import MkAnimBg from '@/components/MkAnimBg.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkAuthConfirm from '@/components/MkAuthConfirm.vue'; const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]'); if (transactionIdMeta) { @@ -45,10 +33,44 @@ if (transactionIdMeta) { } const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content; -const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ') ?? []; +const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? []; + +function doPost(token: string, decision: 'accept' | 'deny') { + const form = document.createElement('form'); + form.action = '/oauth/decision'; + form.method = 'post'; + form.acceptCharset = 'utf-8'; + + const loginToken = document.createElement('input'); + loginToken.type = 'hidden'; + loginToken.name = 'login_token'; + loginToken.value = token; + form.appendChild(loginToken); + + const transactionId = document.createElement('input'); + transactionId.type = 'hidden'; + transactionId.name = 'transaction_id'; + transactionId.value = transactionIdMeta?.content ?? ''; + form.appendChild(transactionId); + + if (decision === 'deny') { + const cancel = document.createElement('input'); + cancel.type = 'hidden'; + cancel.name = 'cancel'; + cancel.value = 'cancel'; + form.appendChild(cancel); + } + + document.body.appendChild(form); + form.submit(); +} + +function onAccept(token: string) { + doPost(token, 'accept'); +} -function onLogin(res): void { - login(res.i); +function onDeny(token: string) { + doPost(token, 'deny'); } definePageMetadata(() => ({ @@ -58,15 +80,24 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> -.buttons { - margin-top: 16px; - display: flex; - gap: 8px; - flex-wrap: wrap; +.formContainer { + min-height: 100svh; + padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px; + box-sizing: border-box; + display: grid; + place-content: center; } -.loginMessage { - text-align: center; - margin: 8px 0 24px; +.form { + position: relative; + z-index: 10; + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-panel); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + overflow: clip; + max-width: 500px; + width: calc(100vw - 64px); + height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px))); + overflow-y: scroll; } </style> diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue index 1cfe7a6d2d..c3ad6657b0 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => $emit('remove')"> +<XContainer :draggable="true" @remove="() => emit('remove')"> <template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template> <template #func> <button @click="choose()"> @@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - modelValue: any + modelValue: Misskey.entities.PageBlock & { type: 'image' }; }>(); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any): void; + (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void; + (ev: 'remove'): void; }>(); const file = ref<Misskey.entities.DriveFile | null>(null); diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue index 0a28386986..36e03b4790 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => $emit('remove')"> +<XContainer :draggable="true" @remove="() => emit('remove')"> <template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template> <section style="padding: 16px;" class="_gaps_s"> @@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - modelValue: any + modelValue: Misskey.entities.PageBlock & { type: 'note' }; }>(); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any): void; + (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void; }>(); -const id = ref<any>(props.modelValue.note); +const id = ref(props.modelValue.note); const note = ref<Misskey.entities.Note | null>(null); watch(id, async () => { if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) { - id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop(); + id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null; + } + + if (!id.value) { + note.value = null; + return; } emit('update:modelValue', { diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue index 0f8dc33143..3fed07f7e8 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => $emit('remove')"> +<XContainer :draggable="true" @remove="() => emit('remove')"> <template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template> <template #func> <button class="_button" @click="rename()"> @@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -/* eslint-disable vue/no-mutating-props */ + import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue'; +import * as Misskey from 'misskey-js'; import { v4 as uuid } from 'uuid'; import XContainer from '../page-editor.container.vue'; import * as os from '@/os.js'; @@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js'; const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue')); -const props = withDefaults(defineProps<{ - modelValue: any, -}>(), { - modelValue: {}, -}); +const props = defineProps<{ + modelValue: Misskey.entities.PageBlock & { type: 'section'; }, +}>(); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any): void; + (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void; + (ev: 'remove'): void; }>(); const children = ref(deepClone(props.modelValue.children ?? [])); diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue index 14c3e6845e..5795b46c00 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => $emit('remove')"> +<XContainer :draggable="true" @remove="() => emit('remove')"> <template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template> <section> @@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -/* eslint-disable vue/no-mutating-props */ + import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue'; +import * as Misskey from 'misskey-js'; import XContainer from '../page-editor.container.vue'; import { i18n } from '@/i18n.js'; import { Autocomplete } from '@/scripts/autocomplete.js'; const props = defineProps<{ - modelValue: any + modelValue: Misskey.entities.PageBlock & { type: 'text' } }>(); const emit = defineEmits<{ - (ev: 'update:modelValue', value: any): void; + (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void; }>(); let autocomplete: Autocomplete; @@ -63,7 +64,7 @@ onUnmounted(() => { box-shadow: none; padding: 16px; background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); font-size: 14px; box-sizing: border-box; } diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue index 4967e73000..f191320180 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)"> +<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)"> <template #item="{element}"> <div :class="$style.item"> <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 --> diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue index 2531de0e62..1739c2fc00 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.container.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue @@ -60,12 +60,12 @@ function remove() { .cpjygsrt { position: relative; overflow: hidden; - background: var(--panel); - border: solid 2px var(--X12); - border-radius: var(--radius-sm); + background: var(--MI_THEME-panel); + border: solid 2px var(--MI_THEME-X12); + border-radius: var(--MI-radius-sm); &:hover { - border: solid 2px var(--X13); + border: solid 2px var(--MI_THEME-X13); } &.warn { diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 12b70fa64f..544e112111 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -357,24 +357,24 @@ definePageMetadata(() => ({ &:hover, &:focus-visible { - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); text-decoration: none; outline: none; } } .pageMain { - border-radius: var(--radius); + border-radius: var(--MI-radius); padding: 2rem; - background: var(--panel); + background: var(--MI_THEME-panel); box-sizing: border-box; } .pageBanner { width: calc(100% + 4rem); margin: -2rem -2rem 1.5rem; - border-radius: var(--radius) var(--radius) 0 0; + border-radius: var(--MI-radius) var(--MI-radius) 0 0; overflow: hidden; position: relative; @@ -399,7 +399,7 @@ definePageMetadata(() => ({ } .pageBannerBgFallback2 { - background-color: var(--accentedBg); + background-color: var(--MI_THEME-accentedBg); } &::after { @@ -409,7 +409,7 @@ definePageMetadata(() => ({ bottom: 0; width: 100%; height: 100px; - background: linear-gradient(0deg, var(--panel), transparent); + background: linear-gradient(0deg, var(--MI_THEME-panel), transparent); } } @@ -433,7 +433,7 @@ definePageMetadata(() => ({ h1 { font-size: 2rem; font-weight: 700; - color: var(--fg); + color: var(--MI_THEME-fg); margin: 0; } @@ -458,7 +458,7 @@ definePageMetadata(() => ({ flex-shrink: 0; display: flex; align-items: center; - gap: var(--marginHalf); + gap: var(--MI-marginHalf); margin-left: auto; } } @@ -472,14 +472,14 @@ definePageMetadata(() => ({ display: flex; align-items: center; - border-top: 1px solid var(--divider); + border-top: 1px solid var(--MI_THEME-divider); padding-top: 1.5rem; margin-bottom: 1.5rem; > .other { margin-left: auto; display: flex; - gap: var(--marginHalf); + gap: var(--MI-marginHalf); } } @@ -487,7 +487,7 @@ definePageMetadata(() => ({ display: flex; align-items: center; - border-top: 1px solid var(--divider); + border-top: 1px solid var(--MI_THEME-divider); padding-top: 1.5rem; margin-bottom: 1.5rem; @@ -526,14 +526,14 @@ definePageMetadata(() => ({ display: flex; align-items: center; flex-wrap: wrap; - gap: var(--marginHalf); + gap: var(--MI-marginHalf); } .relatedPagesRoot { - padding: var(--margin); + padding: var(--MI-margin); } .relatedPagesItem > article { - background-color: var(--panelHighlight) !important; + background-color: var(--MI_THEME-panelHighlight) !important; } </style> diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index 8dcb8fa477..a13a8f1f4b 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -52,7 +52,7 @@ const props = defineProps<{ const scope = computed(() => props.path ? props.path.split('/') : []); -const keys = ref<any>(null); +const keys = ref<[string, string][]>([]); function fetchKeys() { misskeyApi('i/registry/keys-with-type', { diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 54e66f6e16..429f502133 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -504,7 +504,7 @@ $gap: 4px; .boardInner { padding: 32px; - background: var(--panel); + background: var(--MI_THEME-panel); box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; border-radius: 8px; } @@ -574,34 +574,34 @@ $gap: 4px; transition: border 0.25s ease, opacity 0.25s ease; &.boardCell_empty { - border: solid 2px var(--divider); + border: solid 2px var(--MI_THEME-divider); } &.boardCell_empty.boardCell_can { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); opacity: 0.5; } &.boardCell_empty.boardCell_myTurn { - border-color: var(--divider); + border-color: var(--MI_THEME-divider); opacity: 1; &.boardCell_can { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); cursor: pointer; &:hover { - background: var(--accent); + background: var(--MI_THEME-accent); } } } &.boardCell_prev { - box-shadow: 0 0 0 4px var(--accent); + box-shadow: 0 0 0 4px var(--MI_THEME-accent); } &.boardCell_isEnded { - border-color: var(--divider); + border-color: var(--MI_THEME-divider); } &.boardCell_none { diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index 08bb3cb76c..437a1a2294 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template v-else> <div class="_panel"> - <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);"> + <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--MI_THEME-divider);"> <div>{{ mapName }}</div> <MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton> </div> @@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <div style="text-align: center;" class="_gaps_s"> - <div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div> + <div v-if="opponentHasSettingsChanged" style="color: var(--MI_THEME-warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div> <div> <template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template> <template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template> @@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x. const props = defineProps<{ game: Misskey.entities.ReversiGameDetailed; - connection: Misskey.ChannelConnection; + connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>; }>(); const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false }); @@ -217,14 +217,14 @@ function onChangeReadyStates(states) { game.value.user2Ready = states.user2; } -function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) { +function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) { props.connection.send('updateSettings', { key: key, value: game.value[key], }); } -function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) { +function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) { if (userId === $i.id) return; if (game.value[key] === value) return; game.value[key] = value; @@ -273,14 +273,14 @@ onUnmounted(() => { width: 300px; height: 300px; margin: 0 auto; - color: var(--fg); + color: var(--MI_THEME-fg); } .boardCell { display: grid; place-items: center; background: transparent; - border: solid 2px var(--divider); + border: solid 2px var(--MI_THEME-divider); border-radius: 6px; overflow: clip; cursor: pointer; @@ -290,9 +290,9 @@ onUnmounted(() => { } .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - background: var(--acrylicBg); - border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + border-top: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index d823861b4a..d608a2411c 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> <div :class="$style.gamePreviewPlayers"> - <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> + <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/> <span style="margin: 0 1em;">vs</span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> - <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> + <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> </div> <div :class="$style.gamePreviewFooter"> <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span> @@ -63,13 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> <div :class="$style.gamePreviewPlayers"> - <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> + <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/> <span style="margin: 0 1em;">vs</span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> - <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> + <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> </div> <div :class="$style.gamePreviewFooter"> <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span> @@ -285,7 +285,7 @@ definePageMetadata(() => ({ .gamePreviews { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: var(--margin); + grid-gap: var(--MI-margin); } .gamePreview { @@ -295,11 +295,11 @@ definePageMetadata(() => ({ } .gamePreviewActive { - box-shadow: inset 0 0 8px 0px var(--accent); + box-shadow: inset 0 0 8px 0px var(--MI_THEME-accent); } .gamePreviewWaiting { - box-shadow: inset 0 0 8px 0px var(--warn); + box-shadow: inset 0 0 8px 0px var(--MI_THEME-warn); } .gamePreviewPlayers { @@ -324,19 +324,19 @@ definePageMetadata(() => ({ .gamePreviewFooter { display: flex; align-items: baseline; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); padding: 6px 10px; font-size: 0.9em; } .gamePreviewStatusActive { - color: var(--accent); + color: var(--MI_THEME-accent); font-weight: bold; animation: blink 2s infinite; } .gamePreviewStatusWaiting { - color: var(--warn); + color: var(--MI_THEME-warn); font-weight: bold; animation: blink 2s infinite; } diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 9ee3fd7f01..d985349bc5 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :displayBackButton="true" :tabs="headerTabs"/></template> - <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> + <MkSpacer v-if="error != null" :contentMax="1200"> <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"> @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ error }} </p> </div> - </MKSpacer> + </MkSpacer> <MkSpacer v-else-if="tab === 'users'" :contentMax="1200"> <div class="_gaps_s"> <div v-if="role">{{ role.description }}</div> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkSpacer> <MkSpacer v-else-if="tab === 'timeline'" :contentMax="700"> - <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/> + <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/> <div v-else-if="!visible" class="_fullinfo"> <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.nothing }}</div> @@ -47,23 +47,24 @@ import { instanceName } from '@@/js/config.js'; import { serverErrorImageUrl, infoImageUrl } from '@/instance.js'; const props = withDefaults(defineProps<{ - role: string; + roleId: string; initialTab?: string; }>(), { initialTab: 'users', }); +// eslint-disable-next-line vue/no-setup-props-reactivity-loss const tab = ref(props.initialTab); -const role = ref<Misskey.entities.Role>(); -const error = ref(); +const role = ref<Misskey.entities.Role | null>(null); +const error = ref<string | null>(null); const visible = ref(false); -watch(() => props.role, () => { +watch(() => props.roleId, () => { misskeyApi('roles/show', { - roleId: props.role, + roleId: props.roleId, }).then(res => { role.value = res; - document.title = `${role.value.name} | ${instanceName}`; + error.value = null; visible.value = res.isExplorable && res.isPublic; }).catch((err) => { if (err.code === 'NO_SUCH_ROLE') { @@ -71,7 +72,6 @@ watch(() => props.role, () => { } else { error.value = i18n.ts.somethingHappened; } - document.title = `${error.value} | ${instanceName}`; }); }, { immediate: true }); @@ -79,7 +79,7 @@ const users = computed(() => ({ endpoint: 'roles/users' as const, limit: 30, params: { - roleId: props.role, + roleId: props.roleId, }, })); @@ -94,7 +94,7 @@ const headerTabs = computed(() => [{ }]); definePageMetadata(() => ({ - title: role.value ? role.value.name : i18n.ts.role, + title: role.value ? role.value.name : (error.value ?? i18n.ts.role), icon: 'ti ti-badge', })); </script> @@ -115,6 +115,6 @@ definePageMetadata(() => ({ width: 128px; height: 128px; margin-bottom: 16px; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } </style> diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index 155d8b82d7..88171f7d70 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js'; const parser = new Parser(); let aiscript: Interpreter; const code = ref(''); -const logs = ref<any[]>([]); +const logs = ref<{ + id: number; + text: string; + print: boolean; +}[]>([]); const root = ref<AsUiRoot>(); const components = ref<Ref<AsUiComponent>[]>([]); const uiKey = ref(0); @@ -204,7 +208,7 @@ definePageMetadata(() => ({ .root { display: flex; flex-direction: column; - gap: var(--margin); + gap: var(--MI-margin); } .editor { @@ -242,14 +246,14 @@ definePageMetadata(() => ({ } .uiInspectorUnShown { - color: var(--fgTransparent); + color: var(--MI_THEME-fgTransparent); } .uiInspectorType { display: inline-block; border: hidden; border-radius: 10px; - background-color: var(--panelHighlight); + background-color: var(--MI_THEME-panelHighlight); padding: 2px 8px; font-size: 12px; } diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 66c5c92480..d64537d289 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -225,12 +225,12 @@ async function search() { justify-content: center; } .addMeButton { - border: 2px dashed var(--fgTransparent); + border: 2px dashed var(--MI_THEME-fgTransparent); padding: 12px; margin-right: 16px; } .addUserButton { - border: 2px dashed var(--fgTransparent); + border: 2px dashed var(--MI_THEME-fgTransparent); padding: 12px; flex-grow: 1; } diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue index 2244047b31..18c82ffdf6 100644 --- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -138,12 +138,13 @@ const token = ref<string | number | null>(null); const backupCodes = ref<string[]>(); function cancel() { - dialog.value.close(); + dialog.value?.close(); } async function tokenDone() { + if (token.value == null) return; const res = await os.apiWithDialog('i/2fa/done', { - token: token.value.toString(), + token: typeof token.value === 'string' ? token.value : token.value.toString(), }); backupCodes.value = res.backupCodes; @@ -166,7 +167,7 @@ function downloadBackupCodes() { } function allDone() { - dialog.value.close(); + dialog.value?.close(); } </script> diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 6a9a1e16e2..776f59dda3 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-shield-lock"></i></template> <template #label>{{ i18n.ts.totp }}</template> <template #caption>{{ i18n.ts.totpDescription }}</template> - <template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--success)"></i></template> + <template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template> <div v-if="$i.twoFactorEnabled" class="_gaps_s"> <div v-text="i18n.ts._2fa.alreadyRegistered"/> @@ -84,7 +84,7 @@ import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkLink from '@/components/MkLink.vue'; import * as os from '@/os.js'; -import { signinRequired, updateAccount } from '@/account.js'; +import { signinRequired, updateAccountPartial } from '@/account.js'; import { i18n } from '@/i18n.js'; const $i = signinRequired(); @@ -123,7 +123,7 @@ async function unregisterTOTP(): Promise<void> { password: auth.result.password, token: auth.result.token, }).then(res => { - updateAccount({ + updateAccountPartial({ twoFactorEnabled: false, }); }).catch(error => { diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index 08c9261dcf..97e960675f 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -19,13 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, ref, computed } from 'vue'; +import { ref, computed } from 'vue'; import type * as Misskey from 'misskey-js'; import FormSuspense from '@/components/form/suspense.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account.js'; +import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; @@ -45,7 +45,7 @@ const init = async () => { }); }; -function menu(account, ev) { +function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) { os.popupMenu([{ text: i18n.ts.switch, icon: 'ti ti-switch-horizontal', @@ -58,7 +58,7 @@ function menu(account, ev) { }], ev.currentTarget ?? ev.target); } -function addAccount(ev) { +function addAccount(ev: MouseEvent) { os.popupMenu([{ text: i18n.ts.existingAccount, action: () => { addExistingAccount(); }, @@ -68,35 +68,31 @@ function addAccount(ev) { }], ev.currentTarget ?? ev.target); } -async function removeAccount(account) { +async function removeAccount(account: Misskey.entities.UserDetailed) { await _removeAccount(account.id); accounts.value = accounts.value.filter(x => x.id !== account.id); } function addExistingAccount() { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { - done: async res => { - await addAccounts(res.id, res.i); + getAccountWithSigninDialog().then((res) => { + if (res != null) { os.success(); init(); - }, - closed: () => dispose(), + } }); } function createAccount() { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { - done: async res => { - await addAccounts(res.id, res.i); - switchAccountWithToken(res.i); - }, - closed: () => dispose(), + getAccountWithSignupDialog().then((res) => { + if (res != null) { + switchAccountWithToken(res.token); + } }); } -async function switchAccount(account: any) { - const fetchedAccounts: any[] = await getAccounts(); - const token = fetchedAccounts.find(x => x.id === account.id).token; +async function switchAccount(account: Misskey.entities.UserDetailed) { + const fetchedAccounts = await getAccounts(); + const token = fetchedAccounts.find(x => x.id === account.id)!.token; switchAccountWithToken(token); } diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index c58ea5d378..6515503505 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -14,30 +14,39 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{items}"> <div class="_gaps"> - <div v-for="token in items" :key="token.id" class="_panel" :class="$style.app"> - <img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/> - <div :class="$style.appBody"> - <div :class="$style.appName">{{ token.name }}</div> - <div>{{ token.description }}</div> - <MkKeyValue oneline> - <template #key>{{ i18n.ts.installedDate }}</template> - <template #value><MkTime :time="token.createdAt"/></template> - </MkKeyValue> - <MkKeyValue oneline> - <template #key>{{ i18n.ts.lastUsedDate }}</template> - <template #value><MkTime :time="token.lastUsedAt"/></template> - </MkKeyValue> - <details> - <summary>{{ i18n.ts.details }}</summary> + <MkFolder v-for="token in items" :key="token.id" :defaultOpen="true"> + <template #icon> + <img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/> + <i v-else class="ti ti-plug"/> + </template> + <template #label>{{ token.name }}</template> + <template #caption>{{ token.description }}</template> + <template #suffix><MkTime :time="token.lastUsedAt"/></template> + <template #footer> + <MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </template> + + <div class="_gaps_s"> + <div v-if="token.description">{{ token.description }}</div> + <div> + <MkKeyValue oneline> + <template #key>{{ i18n.ts.installedDate }}</template> + <template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template> + </MkKeyValue> + <MkKeyValue oneline> + <template #key>{{ i18n.ts.lastUsedDate }}</template> + <template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template> + </MkKeyValue> + </div> + <MkFolder> + <template #label>{{ i18n.ts.permission }}</template> + <template #suffix>{{ Object.keys(token.permission).length === 0 ? i18n.ts.none : Object.keys(token.permission).length }}</template> <ul> <li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li> </ul> - </details> - <div> - <MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton> - </div> + </MkFolder> </div> - </div> + </MkFolder> </div> </template> </FormPagination> @@ -46,12 +55,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; +import * as Misskey from 'misskey-js'; import FormPagination from '@/components/MkPagination.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; import { infoImageUrl } from '@/instance.js'; const list = ref<InstanceType<typeof FormPagination>>(); @@ -67,7 +78,7 @@ const pagination = { function revoke(token) { misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => { - list.value.reload(); + list.value?.reload(); }); } @@ -82,26 +93,9 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> -.app { - display: flex; - padding: 16px; -} - .appIcon { - display: block; - flex-shrink: 0; - margin: 0 12px 0 0; - width: 50px; - height: 50px; - border-radius: var(--radius-sm); -} - -.appBody { - width: calc(100% - 62px); - position: relative; -} - -.appName { - font-weight: bold; + width: 20px; + height: 20px; + border-radius: 4px; } </style> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue index 9f7852a71d..c71595380e 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue @@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only > <div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div> <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY, showBelow }]" forceShowDecoration/> - <i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i> + <i v-if="locked" :class="$style.lock" class="ti ti-lock"></i> </div> </template> <script lang="ts" setup> -import { } from 'vue'; +import { computed } from 'vue'; import { signinRequired } from '@/account.js'; const $i = signinRequired(); @@ -38,14 +38,16 @@ const props = defineProps<{ const emit = defineEmits<{ (ev: 'click'): void; }>(); + +const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))); </script> <style lang="scss" module> .root { cursor: pointer; padding: 16px 16px 28px 16px; - border: solid 2px var(--divider); - border-radius: var(--radius-sm); + border: solid 2px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); text-align: center; font-size: 90%; overflow: clip; @@ -53,8 +55,8 @@ const emit = defineEmits<{ } .active { - background-color: var(--accentedBg); - border-color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + border-color: var(--MI_THEME-accent); } .name { @@ -68,5 +70,6 @@ const emit = defineEmits<{ position: absolute; bottom: 12px; right: 12px; + color: var(--MI_THEME-warn); } </style> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index 4ec4610279..77f9d4af20 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.footer" class="_buttonsCenter"> <MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton> <MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton> - <MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton> + <MkButton v-else :disabled="exceeded || locked" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton> </div> </div> </MkModalWindow> @@ -64,6 +64,7 @@ const props = defineProps<{ id: string; url: string; name: string; + roleIdsThatCanBeUsedThisDecoration: string[]; }; }>(); @@ -88,6 +89,7 @@ const emit = defineEmits<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0); +const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))); const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0); const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0); @@ -115,7 +117,7 @@ const decorationsForPreview = computed(() => { }); function cancel() { - dialog.value.close(); + dialog.value?.close(); } async function update() { @@ -126,7 +128,7 @@ async function update() { offsetY: offsetY.value, showBelow: showBelow.value, }); - dialog.value.close(); + dialog.value?.close(); } async function attach() { @@ -137,12 +139,12 @@ async function attach() { offsetY: offsetY.value, showBelow: showBelow.value, }); - dialog.value.close(); + dialog.value?.close(); } async function detach() { emit('detach'); - dialog.value.close(); + dialog.value?.close(); } </script> @@ -159,8 +161,8 @@ async function detach() { bottom: 0; left: 0; padding: 12px; - border-top: solid 0.5px var(--divider); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + border-top: solid 0.5px var(--MI_THEME-divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } </style> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index 5324a6b7f7..efcd2a5f3f 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -148,7 +148,7 @@ definePageMetadata(() => ({ .current { padding: 16px; - border-radius: var(--radius); + border-radius: var(--MI-radius); } .decorations { diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 432295aa7f..1ad3613e4b 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -177,7 +177,7 @@ definePageMetadata(() => ({ align-items: center; &:hover { - color: var(--accent); + color: var(--MI_THEME-accent); } } @@ -197,7 +197,7 @@ definePageMetadata(() => ({ height: 12px; background: rgba(0, 0, 0, 0.1); overflow: clip; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } .meterValue { diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index c1e3258631..eef2e5b505 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -152,12 +152,12 @@ definePageMetadata(() => ({ .meter { height: 10px; background: rgba(0, 0, 0, 0.1); - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); overflow: clip; } .meterValue { height: 100%; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } </style> diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index f226647569..d452f249b6 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="emailAddress" type="email" manualSave> <template #prefix><i class="ti ti-mail"></i></template> <template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template> - <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template> + <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i> {{ i18n.ts.emailVerified }}</template> </MkInput> </FormSection> diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue index bb919c6c53..7ddc1eab71 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <div> - <div v-panel style="border-radius: var(--radius-sm);"> + <div v-panel style="border-radius: var(--MI-radius-sm);"> <Sortable v-model="pinnedEmojisForReaction" :class="$style.emojis" @@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <div> - <div v-panel style="border-radius: var(--radius-sm);"> + <div v-panel style="border-radius: var(--MI-radius-sm);"> <Sortable v-model="pinnedEmojis" :class="$style.emojis" @@ -287,9 +287,9 @@ definePageMetadata(() => ({ <style lang="scss" module> .tab { - margin: calc(var(--margin) / 2) 0; - padding: calc(var(--margin) / 2) 0; - background: var(--bg); + margin: calc(var(--MI-margin) / 2) 0; + padding: calc(var(--MI-margin) / 2) 0; + background: var(--MI_THEME-bg); } .emojis { @@ -311,6 +311,6 @@ definePageMetadata(() => ({ .editorCaption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } </style> diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index f6f2ffc553..552b4ee028 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="!(narrow && currentPage?.route.name == null)" class="main"> <div class="bkzroven" style="container-type: inline-size;"> - <RouterView/> + <RouterView nested/> </div> </div> </div> diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index a5c381200f..82aeb6063f 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -243,7 +243,7 @@ definePageMetadata(() => ({ .userItemSub { padding: 6px 12px; font-size: 85%; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } .userItemMainBody { diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index a0e6cad9c8..c38cdc4fc2 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -100,10 +100,6 @@ function reset() { })); } -watch(menuDisplay, async () => { - await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); -}); - const headerActions = computed(() => []); const headerTabs = computed(() => []); @@ -122,7 +118,7 @@ definePageMetadata(() => ({ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - color: var(--navFg); + color: var(--MI_THEME-navFg); } .itemIcon { diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue index 1a945aa3ad..0ea415f673 100644 --- a/packages/frontend/src/pages/settings/notifications.notification-config.vue +++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue @@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> <MkSelect v-model="type"> - <option value="all">{{ i18n.ts.all }}</option> - <option v-if="hasSender" value="following">{{ i18n.ts.following }}</option> - <option v-if="hasSender" value="follower">{{ i18n.ts.followers }}</option> - <option v-if="hasSender" value="mutualFollow">{{ i18n.ts.mutualFollow }}</option> - <option v-if="hasSender" value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option> - <option v-if="hasSender" value="list">{{ i18n.ts.userList }}</option> - <option value="never">{{ i18n.ts.none }}</option> + <option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option> </MkSelect> <MkSelect v-if="type === 'list'" v-model="userListId"> @@ -21,34 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSelect> <div class="_buttons"> - <MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton inline primary :disabled="type === 'list' && userListId === null" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> </template> +<script lang="ts"> +const notificationConfigTypes = [ + 'all', + 'following', + 'follower', + 'mutualFollow', + 'followingOrFollower', + 'list', + 'never' +] as const; + +export type NotificationConfig = { + type: Exclude<typeof notificationConfigTypes[number], 'list'>; +} | { + type: 'list'; + userListId: string; +}; +</script> + <script lang="ts" setup> -import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { ref } from 'vue'; import MkSelect from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -const props = withDefaults(defineProps<{ - value: any; +const props = defineProps<{ + value: NotificationConfig; userLists: Misskey.entities.UserList[]; - hasSender: boolean; -}>(), { - hasSender: true, -}); + configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable +}>(); const emit = defineEmits<{ - (ev: 'update', result: any): void; + (ev: 'update', result: NotificationConfig): void; }>(); +const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[number], string> = { + all: i18n.ts.all, + following: i18n.ts.following, + follower: i18n.ts.followers, + mutualFollow: i18n.ts.mutualFollow, + followingOrFollower: i18n.ts.followingOrFollower, + list: i18n.ts.userList, + never: i18n.ts.none, +}; + const type = ref(props.value.type); -const userListId = ref(props.value.userListId); +const userListId = ref(props.value.type === 'list' ? props.value.userListId : null); function save() { - emit('update', { type: type.value, userListId: userListId.value }); + emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value }); } </script> diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 491102fece..8ffe0d6a7a 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only }} </template> - <XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" :hasSender="!(notificationTypesWithoutSender.includes(type))" @update="(res) => updateReceiveConfig(type, res)"/> + <XNotificationConfig + :userLists="userLists" + :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" + :configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined" + @update="(res) => updateReceiveConfig(type, res)" + /> </MkFolder> </div> </FormSection> @@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { shallowRef, computed } from 'vue'; -import XNotificationConfig from './notifications.notification-config.vue'; +import XNotificationConfig, { type NotificationConfig } from './notifications.notification-config.vue'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -73,8 +78,9 @@ import { notificationTypes } from '@@/js/const.js'; const $i = signinRequired(); -const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[]; -const notificationTypesWithoutSender = ['achievementEarned'] as const satisfies (typeof notificationTypes[number])[]; +const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[]; + +const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[]; const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); @@ -89,7 +95,7 @@ async function readAllNotifications() { await os.apiWithDialog('notifications/mark-all-as-read'); } -async function updateReceiveConfig(type, value) { +async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) { await os.apiWithDialog('i/update', { notificationRecieveConfig: { ...$i.notificationRecieveConfig, diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index eb9d158c1e..abe4524126 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -65,6 +65,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="enableCondensedLine"> <template #label>Enable condensed line</template> </MkSwitch> + <MkSwitch v-model="skipNoteRender"> + <template #label>Enable note render skipping</template> + </MkSwitch> </div> </MkFolder> @@ -116,9 +119,14 @@ const $i = signinRequired(); const reportError = computed(defaultStore.makeGetterSetter('reportError')); const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine')); +const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender')); const devMode = computed(defaultStore.makeGetterSetter('devMode')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); +watch(skipNoteRender, async () => { + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); +}); + async function deleteAccount() { { const { canceled } = await os.confirm({ diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 8036ef5555..f7dcc7b139 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -469,7 +469,7 @@ definePageMetadata(() => ({ <style lang="scss" module> .buttons { display: flex; - gap: var(--margin); + gap: var(--MI-margin); flex-wrap: wrap; } diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index dccfb584ae..790f9e44e2 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -49,6 +49,93 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> <FormSection> + <template #label>{{ i18n.ts.lockdown }}<span class="_beta">{{ i18n.ts.beta }}</span></template> + + <div class="_gaps_m"> + <MkSwitch :modelValue="requireSigninToViewContents" @update:modelValue="update_requireSigninToViewContents"> + {{ i18n.ts._accountSettings.requireSigninToViewContents }} + <template #caption> + <div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div> + <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div> + <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div> + </template> + </MkSwitch> + + <FormSlot> + <template #label>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</template> + + <div class="_gaps_s"> + <MkSelect :modelValue="makeNotesFollowersOnlyBefore_type" @update:modelValue="makeNotesFollowersOnlyBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null"> + <option :value="null">{{ i18n.ts.none }}</option> + <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option> + <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option> + </MkSelect> + + <MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore"> + <option :value="-3600">{{ i18n.ts.oneHour }}</option> + <option :value="-86400">{{ i18n.ts.oneDay }}</option> + <option :value="-259200">{{ i18n.ts.threeDays }}</option> + <option :value="-604800">{{ i18n.ts.oneWeek }}</option> + <option :value="-2592000">{{ i18n.ts.oneMonth }}</option> + <option :value="-7776000">{{ i18n.ts.threeMonths }}</option> + <option :value="-31104000">{{ i18n.ts.oneYear }}</option> + </MkSelect> + + <MkInput + v-if="makeNotesFollowersOnlyBefore_type === 'absolute'" + :modelValue="formatDateTimeString(new Date(makeNotesFollowersOnlyBefore * 1000), 'yyyy-MM-dd')" + type="date" + :manualSave="true" + @update:modelValue="makeNotesFollowersOnlyBefore = Math.floor(new Date($event).getTime() / 1000)" + > + </MkInput> + </div> + + <template #caption> + <div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div> + <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div> + </template> + </FormSlot> + + <FormSlot> + <template #label>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</template> + + <div class="_gaps_s"> + <MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null"> + <option :value="null">{{ i18n.ts.none }}</option> + <option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option> + <option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option> + </MkSelect> + + <MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore"> + <option :value="-3600">{{ i18n.ts.oneHour }}</option> + <option :value="-86400">{{ i18n.ts.oneDay }}</option> + <option :value="-259200">{{ i18n.ts.threeDays }}</option> + <option :value="-604800">{{ i18n.ts.oneWeek }}</option> + <option :value="-2592000">{{ i18n.ts.oneMonth }}</option> + <option :value="-7776000">{{ i18n.ts.threeMonths }}</option> + <option :value="-31104000">{{ i18n.ts.oneYear }}</option> + </MkSelect> + + <MkInput + v-if="makeNotesHiddenBefore_type === 'absolute'" + :modelValue="formatDateTimeString(new Date(makeNotesHiddenBefore * 1000), 'yyyy-MM-dd')" + type="date" + :manualSave="true" + @update:modelValue="makeNotesHiddenBefore = Math.floor(new Date($event).getTime() / 1000)" + > + </MkInput> + </div> + + <template #caption> + <div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div> + <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div> + </template> + </FormSlot> + </div> + </FormSection> + + <FormSection> <div class="_gaps_m"> <MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch> <MkFolder v-if="!rememberNoteVisibility"> @@ -76,7 +163,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSection from '@/components/form/section.vue'; @@ -86,6 +173,10 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { signinRequired } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import FormSlot from '@/components/form/slot.vue'; +import { formatDateTimeString } from '@/scripts/format-time-string.js'; +import MkInput from '@/components/MkInput.vue'; +import * as os from '@/os.js'; const $i = signinRequired(); @@ -95,6 +186,9 @@ const noCrawle = ref($i.noCrawle); const noindex = ref($i.noindex); const enableRss = ref($i.enableRss); const isExplorable = ref($i.isExplorable); +const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false); +const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null); +const makeNotesHiddenBefore = ref($i.makeNotesHiddenBefore ?? null); const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); const followingVisibility = ref($i.followingVisibility); @@ -105,6 +199,43 @@ const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNote const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility')); const keepCw = computed(defaultStore.makeGetterSetter('keepCw')); +const makeNotesFollowersOnlyBefore_type = computed(() => { + if (makeNotesFollowersOnlyBefore.value == null) { + return null; + } else if (makeNotesFollowersOnlyBefore.value >= 0) { + return 'absolute'; + } else { + return 'relative'; + } +}); + +const makeNotesHiddenBefore_type = computed(() => { + if (makeNotesHiddenBefore.value == null) { + return null; + } else if (makeNotesHiddenBefore.value >= 0) { + return 'absolute'; + } else { + return 'relative'; + } +}); + +watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => { + save(); +}); + +async function update_requireSigninToViewContents(value: boolean) { + if (value) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.acknowledgeNotesAndEnable, + }); + if (canceled) return; + } + + requireSigninToViewContents.value = value; + save(); +} + function save() { misskeyApi('i/update', { isLocked: !!isLocked.value, @@ -113,6 +244,9 @@ function save() { noindex: !!noindex.value, enableRss: !!enableRss.value, isExplorable: !!isExplorable.value, + requireSigninToViewContents: !!requireSigninToViewContents.value, + makeNotesFollowersOnlyBefore: makeNotesFollowersOnlyBefore.value, + makeNotesHiddenBefore: makeNotesHiddenBefore.value, hideOnlineStatus: !!hideOnlineStatus.value, publicReactions: !!publicReactions.value, followingVisibility: followingVisibility.value, diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index c94cd512f3..9ff4a63e09 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -52,14 +52,17 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder> <template #icon><i class="ti ti-list"></i></template> <template #label>{{ i18n.ts._profile.metadataEdit }}</template> - - <div :class="$style.metadataRoot"> - <div :class="$style.metadataMargin"> - <MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - <MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton> - <MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <template #footer> + <div class="_buttons"> + <MkButton primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton :disabled="fields.length >= 16" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" danger @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-else @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton> </div> + </template> + + <div :class="$style.metadataRoot" class="_gaps_s"> + <MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo> <Sortable v-model="fields" @@ -71,24 +74,20 @@ SPDX-License-Identifier: AGPL-3.0-only @end="e => e.item.classList.remove('active')" > <template #item="{element, index}"> - <div :class="$style.fieldDragItem"> + <div v-panel :class="$style.fieldDragItem"> <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button> <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button> <div :class="$style.dragItemForm"> <FormSplit :minWidth="200"> - <MkInput v-model="element.name" small> - <template #label>{{ i18n.ts._profile.metadataLabel }}</template> + <MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel"> </MkInput> - <MkInput v-model="element.value" small> - <template #label>{{ i18n.ts._profile.metadataContent }}</template> + <MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent"> </MkInput> </FormSplit> </div> </div> </template> </Sortable> - - <MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo> </div> </MkFolder> <template #caption>{{ i18n.ts._profile.metadataDescription }}</template> @@ -158,6 +157,10 @@ const setMaxBirthDate = () => { return `${y}-12-31`; }; +function assertVaildLang(lang: string | null): lang is keyof typeof langmap { + return lang != null && lang in langmap; +} + const profile = reactive({ name: $i.name, description: $i.description, @@ -165,7 +168,7 @@ const profile = reactive({ location: $i.location, birthday: $i.birthday, listenbrainz: $i.listenbrainz, - lang: $i.lang, + lang: assertVaildLang($i.lang) ? $i.lang : null, isBot: $i.isBot ?? false, isCat: $i.isCat ?? false, speakAsCat: $i.speakAsCat ?? false, @@ -229,6 +232,11 @@ function save() { isBot: !!profile.isBot, isCat: !!profile.isCat, speakAsCat: !!profile.speakAsCat, + }, undefined, { + '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': { + title: i18n.ts.yourNameContainsProhibitedWords, + text: i18n.ts.yourNameContainsProhibitedWordsDescription, + }, }); globalEvents.emit('requestClearPageCache'); claimAchievement('profileFilled'); @@ -417,7 +425,7 @@ definePageMetadata(() => ({ height: 130px; background-size: cover; background-position: center; - border-bottom: solid 1px var(--divider); + border-bottom: solid 1px var(--MI_THEME-divider); overflow: clip; } @@ -449,19 +457,11 @@ definePageMetadata(() => ({ container-type: inline-size; } -.metadataMargin { - margin-bottom: 1.5em; -} - .fieldDragItem { display: flex; - padding-bottom: .75em; + padding: 10px; align-items: flex-end; - border-bottom: solid 0.5px var(--divider); - - &:last-child { - border-bottom: 0; - } + border-radius: 6px; /* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */ @container (max-width: 452px) { diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index de0f63630e..8f9d4f858b 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -124,7 +124,7 @@ definePageMetadata(() => ({ } &:not(:last-child) { - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); } > header { @@ -136,11 +136,11 @@ definePageMetadata(() => ({ margin-right: 0.75em; &.succ { - color: var(--success); + color: var(--MI_THEME-success); } &.fail { - color: var(--error); + color: var(--MI_THEME-error); } } diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index 81478fede5..56f65e2309 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -194,7 +194,7 @@ function save() { flex-grow: 1; min-width: 0; font-weight: 700; - color: var(--error); + color: var(--MI_THEME-error); } .fileSelectorButton { @@ -203,6 +203,6 @@ function save() { .fileNotSelected { font-weight: 700; - color: var(--infoWarnFg); + color: var(--MI_THEME-infoWarnFg); } </style> diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index e7aef55a53..9f7842ecdc 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -175,7 +175,7 @@ definePageMetadata(() => ({ <style lang="scss" scoped> .rfqxtzch { - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); > .toggle { position: relative; @@ -204,7 +204,7 @@ definePageMetadata(() => ({ } .dn:focus-visible ~ .toggle { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: 2px; } @@ -227,12 +227,12 @@ definePageMetadata(() => ({ > .before { left: -70px; - color: var(--accent); + color: var(--MI_THEME-accent); } > .after { right: -68px; - color: var(--fg); + color: var(--MI_THEME-fg); } } @@ -255,7 +255,7 @@ definePageMetadata(() => ({ background-color: #E8CDA5; opacity: 0; transition: opacity 200ms ease-in-out !important; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); } .crater--1 { @@ -350,11 +350,11 @@ definePageMetadata(() => ({ background-color: #749DD6; > .before { - color: var(--fg); + color: var(--MI_THEME-fg); } > .after { - color: var(--accent); + color: var(--MI_THEME-accent); } .toggle__handler { @@ -405,14 +405,14 @@ definePageMetadata(() => ({ > .sync { padding: 14px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } } .rsljpzjq { > .selects { display: flex; - gap: 1.5em var(--margin); + gap: 1.5em var(--MI-margin); flex-wrap: wrap; > .select { diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index adeaf8550c..22b008fb61 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton> </div> <div :class="$style.switchBox"> - <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> + <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton> </div> <div :class="$style.switchBox"> @@ -184,6 +184,6 @@ definePageMetadata(() => ({ .description { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } </style> diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index d62357caaf..727c4df2d6 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> - <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> + <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> </div> </FormSection> diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue index 0d11b00c97..af8b7ca945 100644 --- a/packages/frontend/src/pages/settings/webhook.vue +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon> <i v-if="webhook.active === false" class="ti ti-player-pause"></i> <i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i> - <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i> - <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i> + <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--MI_THEME-success)' }"></i> + <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"></i> </template> {{ webhook.name || webhook.url }} <template #suffix> diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 226f08bf8e..503e6e0f73 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -78,7 +78,7 @@ place-content: center; .form { position: relative; z-index: 10; - border-radius: var(--radius); + border-radius: var(--MI-radius); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); overflow: clip; max-width: 500px; @@ -88,7 +88,7 @@ place-content: center; padding: 16px; text-align: center; font-size: 26px; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); } </style> diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 0d261b1af3..a45b61fb10 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -76,10 +76,10 @@ definePageMetadata(() => ({ <style lang="scss" module> .footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - background: var(--acrylicBg); - border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + border-top: solid 0.5px var(--MI_THEME-divider); display: flex; } diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 2fa6eb81ba..a530f4b5d6 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -236,7 +236,7 @@ definePageMetadata(() => ({ position: relative; width: 64px; height: 64px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); > .preview { position: absolute; @@ -247,7 +247,7 @@ definePageMetadata(() => ({ margin: auto; width: 42px; height: 42px; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); box-shadow: 0 2px 4px rgb(0 0 0 / 30%); transition: transform 0.15s ease; } @@ -259,14 +259,14 @@ definePageMetadata(() => ({ } &.active { - box-shadow: 0 0 0 2px var(--divider) inset; + box-shadow: 0 0 0 2px var(--MI_THEME-divider) inset; } &.rounded { - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); > .preview { - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } } diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 60974da971..032a19a6eb 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -9,19 +9,20 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="800"> <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> <div :key="src" ref="rootEl"> - <MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> + <MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> {{ i18n.ts._timelineDescription[src] }} </MkInfo> - <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> + <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div :class="$style.tl"> <MkTimeline ref="tlComponent" - :key="src + withRenotes + withBots + withReplies + onlyFiles" + :key="src + withRenotes + withBots + withReplies + onlyFiles + withSensitive" :src="src.split(':')[0]" :list="src.split(':')[1]" :withRenotes="withRenotes" :withReplies="withReplies" + :withSensitive="withSensitive" :onlyFiles="onlyFiles" :withBots="withBots" :sound="true" @@ -130,11 +131,6 @@ watch(src, () => { queue.value = 0; }); -watch(withSensitive, () => { - // これだけはクライアント側で完結する処理なので手動でリロード - tlComponent.value?.reloadTimeline(); -}); - function queueUpdated(q: number): void { queue.value = q; } @@ -363,30 +359,30 @@ definePageMetadata(() => ({ <style lang="scss" module> .new { position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); + top: calc(var(--MI-stickyTop, 0px) + 16px); z-index: 1000; width: 100%; margin: calc(-0.675em - 8px) 0; &:first-child { - margin-top: calc(-0.675em - 8px - var(--margin)); + margin-top: calc(-0.675em - 8px - var(--MI-margin)); } } .newButton { display: block; - margin: var(--margin) auto 0 auto; + margin: var(--MI-margin) auto 0 auto; padding: 8px 16px; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); } .postForm { - border-radius: var(--radius); + border-radius: var(--MI-radius); } .tl { - background: var(--bg); - border-radius: var(--radius); + background: var(--MI_THEME-bg); + border-radius: var(--MI-radius); overflow: clip; } </style> diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 396e6eb14a..85e44b2503 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -114,26 +114,26 @@ definePageMetadata(() => ({ <style lang="scss" module> .new { position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); + top: calc(var(--MI-stickyTop, 0px) + 16px); z-index: 1000; width: 100%; margin: calc(-0.675em - 8px) 0; &:first-child { - margin-top: calc(-0.675em - 8px - var(--margin)); + margin-top: calc(-0.675em - 8px - var(--MI-margin)); } } .newButton { display: block; - margin: var(--margin) auto 0 auto; + margin: var(--MI-margin) auto 0 auto; padding: 8px 16px; - border-radius: var(--radius-xl); + border-radius: var(--MI-radius-xl); } .tl { - background: var(--bg); - border-radius: var(--radius); + background: var(--MI_THEME-bg); + border-radius: var(--MI-radius); overflow: clip; } </style> diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue index ac01cff8cd..38ce78e8d5 100644 --- a/packages/frontend/src/pages/user/clips.vue +++ b/packages/frontend/src/pages/user/clips.vue @@ -43,6 +43,6 @@ const pagination = { .description { margin-top: 8px; padding-top: 8px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue index e60dccec17..868767e8f4 100644 --- a/packages/frontend/src/pages/user/follow-list.vue +++ b/packages/frontend/src/pages/user/follow-list.vue @@ -45,6 +45,6 @@ const followersPagination = { .users { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - grid-gap: var(--margin); + grid-gap: var(--MI-margin); } </style> diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue index 9ba81322ba..0bc5628528 100644 --- a/packages/frontend/src/pages/user/gallery.vue +++ b/packages/frontend/src/pages/user/gallery.vue @@ -38,6 +38,6 @@ const pagination = { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-gap: 12px; - margin: var(--margin); + margin: var(--MI-margin); } </style> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 7bb8c3fd3d..5565555ca4 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserName class="name" :user="user" :nowrap="true"/> <div class="bottom"> <span class="username"><MkAcct :user="user" :detail="true"/></span> - <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span> + <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> @@ -49,15 +49,16 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserName :user="user" :nowrap="false" class="name"/> <div class="bottom"> <span class="username"><MkAcct :user="user" :detail="true"/></span> - <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span> + <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> </div> </div> <div v-if="user.followedMessage != null" class="followedMessage"> - <div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;"> - <Mfm :text="user.followedMessage" :author="user"/> - </div> + <MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow> + <div class="messageHeader">{{ i18n.ts.messageToFollower }}</div> + <div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div> + </MkFukidashi> </div> <div v-if="user.roles.length > 0" class="roles"> <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> @@ -70,6 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="iAmModerator" class="moderationNote"> <MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave> <template #label>{{ i18n.ts.moderationNote }}</template> + <template #caption>{{ i18n.ts.moderationNoteDescription }}</template> </MkTextarea> <div v-else> <MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton> @@ -190,15 +192,16 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { getScrollPosition } from '@@/js/scroll.js'; import MkTab from '@/components/MkTab.vue'; import MkNotes from '@/components/MkNotes.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import MkAccountMoved from '@/components/MkAccountMoved.vue'; +import MkFukidashi from '@/components/MkFukidashi.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkButton from '@/components/MkButton.vue'; -import { getScrollPosition } from '@@/js/scroll.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; @@ -213,11 +216,12 @@ import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFf import { useRouter } from '@/router/supplier.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { infoImageUrl } from '@/instance.js'; +import MkSparkle from '@/components/MkSparkle.vue'; const MkNote = defineAsyncComponent(() => defaultStore.state.noteDesign === 'sharkey' - ? import('@/components/SkNote.vue') - : import('@/components/MkNote.vue'), + ? import('@/components/SkNote.vue') + : import('@/components/MkNote.vue'), ); function calcAge(birthdate: string): number { @@ -347,7 +351,7 @@ function parallaxLoop() { } function parallax() { - const banner = bannerEl.value as any; + const banner = bannerEl.value; if (banner == null) return; const top = getScrollPosition(rootEl.value); @@ -419,7 +423,7 @@ onUnmounted(() => { background-size: cover; background-position: center; pointer-events: none; - filter: var(--blur, blur(10px)) opacity(0.6); + filter: var(--MI-blur, blur(10px)) opacity(0.6); // Funny CSS schenanigans to make background escape container left: -100%; top: -5%; @@ -442,7 +446,7 @@ onUnmounted(() => { > .main { position: relative; overflow: clip; - background: color-mix(in srgb, var(--panel) 65%, transparent); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); > .banner-container { position: relative; @@ -473,11 +477,11 @@ onUnmounted(() => { position: absolute; top: 12px; right: 12px; - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); background: rgba(0, 0, 0, 0.2); padding: 8px; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); > .menu { vertical-align: bottom; @@ -528,9 +532,9 @@ onUnmounted(() => { > .add-note-button { background: rgba(0, 0, 0, 0.2); color: #fff; - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - border-radius: var(--radius-lg); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); + border-radius: var(--MI-radius-lg); padding: 4px 8px; font-size: 80%; } @@ -543,7 +547,7 @@ onUnmounted(() => { text-align: center; padding: 50px 8px 16px 8px; font-weight: bold; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); > .bottom { > * { @@ -567,7 +571,18 @@ onUnmounted(() => { > .followedMessage { padding: 24px 24px 0 154px; - font-size: 0.9em; + + > .fukidashi { + display: block; + --fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%); + --fukidashi-radius: 16px; + font-size: 0.9em; + + .messageHeader { + opacity: 0.7; + font-size: 0.85em; + } + } } > .roles { @@ -578,8 +593,8 @@ onUnmounted(() => { gap: 8px; > .role { - border: solid 1px var(--color, var(--divider)); - border-radius: var(--radius-ellipse); + border: solid 1px var(--color, var(--MI_THEME-divider)); + border-radius: var(--MI-radius-ellipse); margin-right: 4px; padding: 3px 8px; } @@ -592,15 +607,15 @@ onUnmounted(() => { > .memo { margin: 12px 24px 0 154px; background: transparent; - color: var(--fg); - border: 1px solid var(--divider); - border-radius: var(--radius-sm); + color: var(--MI_THEME-fg); + border: 1px solid var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); padding: 8px; line-height: 0; > .heading { text-align: left; - color: var(--fgTransparent); + color: var(--MI_THEME-fgTransparent); line-height: 1.5; font-size: 85%; } @@ -615,7 +630,7 @@ onUnmounted(() => { height: auto; min-height: 0; line-height: 1.5; - color: var(--fg); + color: var(--MI_THEME-fg); overflow: hidden; background: transparent; font-family: inherit; @@ -635,7 +650,7 @@ onUnmounted(() => { > .fields { padding: 24px; font-size: 0.9em; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); > .field { display: flex; @@ -672,14 +687,14 @@ onUnmounted(() => { > .status { display: flex; padding: 24px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); > a { flex: 1; text-align: center; &.active { - color: var(--accent); + color: var(--MI_THEME-accent); } &:hover { @@ -701,7 +716,7 @@ onUnmounted(() => { > .contents { > .content { - margin-bottom: var(--margin); + margin-bottom: var(--MI-margin); } } } @@ -718,7 +733,7 @@ onUnmounted(() => { > .sub { max-width: 350px; min-width: 350px; - margin-left: var(--margin); + margin-left: var(--MI-margin); } } } @@ -796,7 +811,7 @@ onUnmounted(() => { <style lang="scss" module> .tl { background-color: rgba(0, 0, 0, 0); - border-radius: var(--radius); + border-radius: var(--MI-radius); overflow: clip; z-index: 0; } @@ -804,12 +819,12 @@ onUnmounted(() => { .tab { margin-bottom: calc(var(--margin) / 2); padding: calc(var(--margin) / 2) 0; - background: color-mix(in srgb, var(--bg) 65%, transparent); - backdrop-filter: var(--blur, blur(15px)); - border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--MI_THEME-bg) 65%, transparent); + backdrop-filter: var(--MI-blur, blur(15px)); + border-radius: var(--MI-radius-sm); > button { - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); margin-left: 0.4rem; margin-right: 0.4rem; } @@ -817,7 +832,7 @@ onUnmounted(() => { .verifiedLink { margin-left: 4px; - color: var(--success); + color: var(--MI_THEME-success); } .infoBadges { @@ -836,7 +851,7 @@ onUnmounted(() => { color: #fff; background: rgba(0, 0, 0, 0.7); font-size: 0.7em; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); list-style-type: none; margin-left: 0; } diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index 23fd4ca23e..7fe90da865 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -96,7 +96,7 @@ onMounted(() => { .img { position: relative; height: 128px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; } diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 28a5091059..3aa9b10214 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -69,13 +69,13 @@ const pagination = computed(() => tab.value === 'featured' ? { <style lang="scss" module> .tab { - padding: calc(var(--margin) / 2) 0; - background: var(--bg); + padding: calc(var(--MI-margin) / 2) 0; + background: var(--MI_THEME-bg); } .tl { - background: var(--bg); - border-radius: var(--radius); + background: var(--MI_THEME-bg); + border-radius: var(--MI-radius); overflow: clip; } </style> diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 87c8bb2866..a35250bf5f 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -39,6 +39,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import { getServerContext } from '@/server-context.js'; const XHome = defineAsyncComponent(() => import('./home.vue')); const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); @@ -52,6 +53,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue')); const XRaw = defineAsyncComponent(() => import('./raw.vue')); +const CTX_USER = getServerContext('user'); + const props = withDefaults(defineProps<{ acct: string; page?: string; @@ -61,13 +64,24 @@ const props = withDefaults(defineProps<{ const tab = ref(props.page); -const user = ref<null | Misskey.entities.UserDetailed>(null); +const user = ref<null | Misskey.entities.UserDetailed>(CTX_USER); const error = ref<any>(null); function fetchUser(): void { if (props.acct == null) return; + + const { username, host } = Misskey.acct.parse(props.acct); + + if (CTX_USER && CTX_USER.username === username && CTX_USER.host === host) { + user.value = CTX_USER; + return; + } + user.value = null; - misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => { + misskeyApi('users/show', { + username, + host, + }).then(u => { user.value = u; }).catch(err => { error.value = err; diff --git a/packages/frontend/src/pages/user/lists.vue b/packages/frontend/src/pages/user/lists.vue index 8f95ce2dc9..a3d1974ced 100644 --- a/packages/frontend/src/pages/user/lists.vue +++ b/packages/frontend/src/pages/user/lists.vue @@ -44,12 +44,12 @@ const pagination = { .list { display: block; padding: 16px; - border: solid 1px var(--divider); - border-radius: var(--radius-sm); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius-sm); margin-bottom: 8px; &:hover { - border: solid 1px var(--accent); + border: solid 1px var(--MI_THEME-accent); text-decoration: none; } } diff --git a/packages/frontend/src/pages/user/raw.vue b/packages/frontend/src/pages/user/raw.vue index ac18ad9392..f24a215afc 100644 --- a/packages/frontend/src/pages/user/raw.vue +++ b/packages/frontend/src/pages/user/raw.vue @@ -107,24 +107,24 @@ const suspended = computed(() => props.user.isSuspended ?? false); > .moderator { display: inline-block; border: solid 1px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); padding: 2px 6px; font-size: 85%; } > .suspended { - color: var(--error); - border-color: var(--error); + color: var(--MI_THEME-error); + border-color: var(--MI_THEME-error); } > .silenced { - color: var(--warn); - border-color: var(--warn); + color: var(--MI_THEME-warn); + border-color: var(--MI_THEME-warn); } > .moderator { - color: var(--success); - border-color: var(--success); + color: var(--MI_THEME-success); + border-color: var(--MI_THEME-success); } } </style> diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue index 3671decc18..7168778e12 100644 --- a/packages/frontend/src/pages/user/reactions.vue +++ b/packages/frontend/src/pages/user/reactions.vue @@ -44,7 +44,7 @@ const pagination = { align-items: center; padding: 8px 16px; margin-bottom: 8px; - border-bottom: solid 2px var(--divider); + border-bottom: solid 2px var(--MI_THEME-divider); } .avatar { diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 0d132e6a86..f1842255e0 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -98,7 +98,7 @@ misskeyApiGet('federation/instances', { left: 0; width: 100vw; height: 100vh; - background: var(--accent); + background: var(--MI_THEME-accent); clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%); } > .shape2 { @@ -107,7 +107,7 @@ misskeyApiGet('federation/instances', { left: 0; width: 100vw; height: 100vh; - background: var(--accent); + background: var(--MI_THEME-accent); clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%); opacity: 0.5; } @@ -164,10 +164,10 @@ misskeyApiGet('federation/instances', { left: 0; right: 0; margin: auto; - background: var(--acrylicPanel); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-acrylicPanel); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-radius: var(--MI-radius-ellipse); overflow: clip; width: 800px; padding: 8px 0; @@ -186,15 +186,15 @@ misskeyApiGet('federation/instances', { vertical-align: bottom; padding: 6px 12px 6px 6px; margin: 0 10px 0 0; - background: var(--panel); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius-ellipse); > :global(.icon) { display: inline-block; width: 20px; height: 20px; margin-right: 5px; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); } } </style> diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 5a41100bf1..58e815d89c 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -14,6 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_gaps_m" style="padding: 32px;"> <div>{{ i18n.ts.intro }}</div> + <MkInput v-model="setupPassword" type="password" data-cy-admin-initial-password> + <template #label>{{ i18n.ts.initialPasswordForSetup }} <div v-tooltip:dialog="i18n.ts.initialPasswordForSetupDescription" class="_button _help"><i class="ti ti-help-circle"></i></div></template> + <template #prefix><i class="ti ti-lock"></i></template> + </MkInput> <MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username> <template #label>{{ i18n.ts.username }}</template> <template #prefix>@</template> @@ -36,9 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import { host, version } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; -import { host, version } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; @@ -47,6 +51,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue'; const username = ref(''); const password = ref(''); +const setupPassword = ref(''); const submitting = ref(false); function submit() { @@ -56,14 +61,27 @@ function submit() { misskeyApi('admin/accounts/create', { username: username.value, password: password.value, + setupPassword: setupPassword.value === '' ? null : setupPassword.value, }).then(res => { return login(res.token); - }).catch(() => { + }).catch((err) => { submitting.value = false; + let title = i18n.ts.somethingHappened; + let text = err.message + '\n' + err.id; + + if (err.code === 'ACCESS_DENIED') { + title = i18n.ts.permissionDeniedError; + text = i18n.ts.operationForbidden; + } else if (err.code === 'INCORRECT_INITIAL_PASSWORD') { + title = i18n.ts.permissionDeniedError; + text = i18n.ts.incorrectPassword; + } + os.alert({ type: 'error', - text: i18n.ts.somethingHappened, + title, + text, }); }); } @@ -74,14 +92,14 @@ function submit() { min-height: 100svh; padding: 32px 32px 64px 32px; box-sizing: border-box; -display: grid; -place-content: center; + display: grid; + place-content: center; } .form { position: relative; z-index: 10; - border-radius: var(--radius); + border-radius: var(--MI-radius); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); overflow: clip; max-width: 500px; @@ -92,8 +110,8 @@ place-content: center; font-size: 1.5em; text-align: center; padding: 32px; - background: var(--accentedBg); - color: var(--accent); + background: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); font-weight: bold; } diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue index ee8d4e1d62..460a225f23 100644 --- a/packages/frontend/src/pages/welcome.timeline.note.vue +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -84,7 +84,7 @@ onUpdated(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0)); } } @@ -92,7 +92,7 @@ onUpdated(() => { padding: 16px; margin: 0 0 0 auto; max-width: max-content; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } .reactions { @@ -100,7 +100,7 @@ onUpdated(() => { margin: 8px -16px -8px; padding: 8px 16px 0; width: calc(100% + 32px); - border-top: 1px solid var(--divider); + border-top: 1px solid var(--MI_THEME-divider); } .richcontent { diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 16d558cc91..2964b15164 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -60,7 +60,7 @@ onUpdated(() => { transform: translate3d(0, 0, 0); } 100% { - transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0); + transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0); } } @@ -69,7 +69,7 @@ onUpdated(() => { transform: translate3d(0, -128px, 0); } 100% { - transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0); + transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0); } } diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index ac325e923f..7740fe0d39 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -241,9 +241,13 @@ export class Storage<T extends StateDef> { * 特定のキーの、簡易的なgetter/setterを作ります * 主にvue上で設定コントロールのmodelとして使う用 */ - public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): { - get: () => T[K]['default']; - set: (value: T[K]['default']) => void; + public makeGetterSetter<K extends keyof T, R = T[K]['default']>( + key: K, + getter?: (v: T[K]['default']) => R, + setter?: (v: R) => T[K]['default'], + ): { + get: () => R; + set: (value: R) => void; } { const valueRef = ref(this.state[key]); @@ -265,7 +269,7 @@ export class Storage<T extends StateDef> { return valueRef.value; } }, - set: (value: unknown) => { + set: (value) => { const val = setter ? setter(value) : value; this.set(key, val); valueRef.value = val; diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 686ac6920a..c7637a1db9 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -10,7 +10,7 @@ import { $i, iAmModerator } from '@/account.js'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; -export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ +export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ loader: loader, loadingComponent: MkLoading, errorComponent: MkError, @@ -217,7 +217,7 @@ const routes: RouteDef[] = [{ component: page(() => import('@/pages/theme-editor.vue')), loginRequired: true, }, { - path: '/roles/:role', + path: '/roles/:roleId', component: page(() => import('@/pages/role.vue')), }, { path: '/user-tags/:tag', diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts index 709c508741..8307df1150 100644 --- a/packages/frontend/src/router/main.ts +++ b/packages/frontend/src/router/main.ts @@ -4,7 +4,7 @@ */ import { EventEmitter } from 'eventemitter3'; -import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; +import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js'; import type { App, ShallowRef } from 'vue'; @@ -83,7 +83,7 @@ class MainRouterProxy implements IRouter { return this.supplier().currentRoute; } - get navHook(): ((path: string, flag?: any) => boolean) | null { + get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null { return this.supplier().navHook; } @@ -95,11 +95,11 @@ class MainRouterProxy implements IRouter { return this.supplier().getCurrentKey(); } - getCurrentPath(): any { + getCurrentPath(): string { return this.supplier().getCurrentPath(); } - push(path: string, flag?: any): void { + push(path: string, flag?: RouterFlag): void { this.supplier().push(path, flag); } diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts index e65c327ffe..6525c207f7 100644 --- a/packages/frontend/src/scripts/check-word-mute.ts +++ b/packages/frontend/src/scripts/check-word-mute.ts @@ -2,10 +2,9 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import * as Misskey from 'misskey-js'; -import type { Note, MeDetailed } from "misskey-js/entities.js"; - -export function checkWordMute(note: Note, me: MeDetailed | null | undefined, mutedWords: Array<string | string[]>): boolean { +export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean { // 自分自身 if (me && (note.userId === me.id)) return false; diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts index 7c33f8ccee..7aadb617ca 100644 --- a/packages/frontend/src/scripts/device-kind.ts +++ b/packages/frontend/src/scripts/device-kind.ts @@ -3,22 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { defaultStore } from '@/store.js'; - -await defaultStore.ready; +export type DeviceKind = 'smartphone' | 'tablet' | 'desktop'; const ua = navigator.userAgent.toLowerCase(); const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); -const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; -// navigator.platform may be deprecated but this check is still required -const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; -const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; +export const DEFAULT_DEVICE_KIND: DeviceKind = ( + isSmartphone + ? 'smartphone' + : isTablet + ? 'tablet' + : 'desktop' +); -export const isFullscreenNotSupported = isIPhone || isIos; +export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND; -export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind - : isSmartphone ? 'smartphone' - : isTablet ? 'tablet' - : 'desktop'; +export function updateDeviceKind(kind: DeviceKind | null) { + deviceKind = kind ?? DEFAULT_DEVICE_KIND; +} diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index a5972276e2..1f62c9ca70 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -78,7 +78,7 @@ class FavIconDot { this.ctx.beginPath(); this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI); const computedStyle = getComputedStyle(document.documentElement); - this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--navIndicator')).toHexString(); + this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--MI_THEME-navIndicator')).toHexString(); this.ctx.strokeStyle = 'white'; this.ctx.fill(); this.ctx.stroke(); @@ -104,7 +104,7 @@ class FavIconDot { this.drawDot(); this.canvas.toDataURL('image/png'); } catch (error) { - return false; + return false; } return true; } @@ -140,6 +140,6 @@ export async function worksOnInstance() { icon = new FavIconDot(); await icon.setup(); } - + return await icon.worksOnInstance(); } diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index 242a504c3b..1032e97ac9 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean); export type FormItem = { label?: string; type: 'string'; - default: string | null; + default?: string | null; description?: string; required?: boolean; hidden?: Hidden; @@ -24,7 +24,7 @@ export type FormItem = { } | { label?: string; type: 'number'; - default: number | null; + default?: number | null; description?: string; required?: boolean; hidden?: Hidden; @@ -32,20 +32,20 @@ export type FormItem = { } | { label?: string; type: 'boolean'; - default: boolean | null; + default?: boolean | null; description?: string; hidden?: Hidden; } | { label?: string; type: 'enum'; - default: string | null; + default?: string | null; required?: boolean; hidden?: Hidden; enum: EnumItem[]; } | { label?: string; type: 'radio'; - default: unknown | null; + default?: unknown | null; required?: boolean; hidden?: Hidden; options: { @@ -55,7 +55,7 @@ export type FormItem = { } | { label?: string; type: 'range'; - default: number | null; + default?: number | null; description?: string; required?: boolean; step?: number; @@ -66,12 +66,12 @@ export type FormItem = { } | { label?: string; type: 'object'; - default: Record<string, unknown> | null; + default?: Record<string, unknown> | null; hidden: Hidden; } | { label?: string; type: 'array'; - default: unknown[] | null; + default?: unknown[] | null; hidden: Hidden; } | { type: 'button'; diff --git a/packages/frontend/src/scripts/fullscreen.ts b/packages/frontend/src/scripts/fullscreen.ts new file mode 100644 index 0000000000..7a0a018ef3 --- /dev/null +++ b/packages/frontend/src/scripts/fullscreen.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; + +type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & { + webkitEnterFullscreen?(): void; + webkitExitFullscreen?(): void; +}; + +type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>; + +type RequestFullscreenProps = { + readonly videoEl: VideoEl; + readonly playerEl: PlayerEl; + readonly options?: FullscreenOptions | null; +}; + +type ExitFullscreenProps = { + readonly videoEl: VideoEl; +}; + +export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => { + if (playerEl.requestFullscreen != null) { + playerEl.requestFullscreen(options ?? undefined); + return; + } + if (videoEl.webkitEnterFullscreen != null) { + videoEl.webkitEnterFullscreen(); + return; + } +}; + +export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (document.exitFullscreen != null) { + document.exitFullscreen(); + return; + } + if (videoEl.webkitExitFullscreen != null) { + videoEl.webkitExitFullscreen(); + return; + } +}; diff --git a/packages/frontend/src/scripts/get-bg-color.ts b/packages/frontend/src/scripts/get-bg-color.ts new file mode 100644 index 0000000000..ccf60b454f --- /dev/null +++ b/packages/frontend/src/scripts/get-bg-color.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import tinycolor from 'tinycolor2'; + +export const getBgColor = (elem?: Element | null | undefined): string | null => { + if (elem == null) return null; + + const { backgroundColor: bg } = window.getComputedStyle(elem); + + if (bg && tinycolor(bg).getAlpha() !== 0) { + return bg; + } + + return getBgColor(elem.parentElement); +}; diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 4f3fb65665..c56fd185b6 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -267,13 +267,10 @@ export function getNoteMenu(props: { function togglePin(pin: boolean): void { os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { noteId: appearNote.id, - }, undefined, null, res => { - if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { - os.alert({ - type: 'error', - text: i18n.ts.pinLimitExceeded, - }); - } + }, undefined, { + '72dab508-c64d-498f-8740-a8eec1ba385a': { + text: i18n.ts.pinLimitExceeded, + }, }); } diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts index 2465a14703..41e1636aa7 100644 --- a/packages/frontend/src/scripts/init-chart.ts +++ b/packages/frontend/src/scripts/init-chart.ts @@ -50,7 +50,7 @@ export function initChart() { ); // フォントカラー - Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg'); Chart.defaults.borderColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index 1b1159fd01..e7a92e2d5c 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -17,7 +17,7 @@ export function misskeyApi< _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, >( endpoint: E, - data: P = {} as any, + data: P & { i?: string | null; } = {} as any, token?: string | null | undefined, signal?: AbortSignal, ): Promise<_ResT> { @@ -30,8 +30,8 @@ export function misskeyApi< const promise = new Promise<_ResT>((resolve, reject) => { // Append a credential - if ($i) (data as any).i = $i.token; - if (token !== undefined) (data as any).i = token; + if ($i) data.i = $i.token; + if (token !== undefined) data.i = token; // Send request window.fetch(`${apiUrl}/${endpoint}`, { diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts index 18f05bc7f4..43dcf11936 100644 --- a/packages/frontend/src/scripts/please-login.ts +++ b/packages/frontend/src/scripts/please-login.ts @@ -44,17 +44,21 @@ export type OpenOnRemoteOptions = { params: Record<string, string>; }; -export function pleaseLogin(path?: string, openOnRemote?: OpenOnRemoteOptions) { +export function pleaseLogin(opts: { + path?: string; + message?: string; + openOnRemote?: OpenOnRemoteOptions; +} = {}) { if ($i) return; const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { autoSet: true, - message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired, - openOnRemote, + message: opts.message ?? (opts.openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired), + openOnRemote: opts.openOnRemote, }, { cancelled: () => { - if (path) { - window.location.href = path; + if (opts.path) { + window.location.href = opts.path; } }, closed: () => dispose(), diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index 9aa38178b2..b037aa8acc 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> { }); } -function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> { +function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> { return new Promise((res, rej) => { const keepOriginal = ref(defaultStore.state.keepOriginalUploading); @@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss }); } -export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> { +export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> { return select(src, label, false).then(files => files[0]); } -export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> { +export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> { return select(src, label, true); } diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts index fed16bc71c..1f6ef1928c 100644 --- a/packages/frontend/src/scripts/shuffle.ts +++ b/packages/frontend/src/scripts/shuffle.ts @@ -6,8 +6,9 @@ /** * 配列をシャッフル (破壊的) */ -export function shuffle<T extends any[]>(array: T): T { - let currentIndex = array.length, randomIndex; +export function shuffle<T extends unknown[]>(array: T): T { + let currentIndex = array.length; + let randomIndex: number; // While there remain elements to shuffle. while (currentIndex !== 0) { diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index bd3cddde67..8242e7d2e4 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -123,7 +123,7 @@ export function applyTheme(theme: Theme, persist = true) { for (const [k, v] of Object.entries(props)) { if (k.startsWith('font')) continue; - document.documentElement.style.setProperty(`--${k}`, v.toString()); + document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); } document.documentElement.style.setProperty('color-scheme', colorScheme); diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 22dce609c6..713573a377 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -32,13 +32,13 @@ const mimeTypeMap = { export function uploadFile( file: File, - folder?: any, + folder?: string | Misskey.entities.DriveFolder, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading, ): Promise<Misskey.entities.DriveFile> { if ($i == null) throw new Error('Not logged in'); - if (folder && typeof folder === 'object') folder = folder.id; + const _folder = typeof folder === 'string' ? folder : folder?.id; if (file.size > instance.maxFileSize) { alert({ @@ -89,11 +89,11 @@ export function uploadFile( } const formData = new FormData(); - formData.append('i', $i.token); + formData.append('i', $i!.token); formData.append('force', 'true'); formData.append('file', resizedImage ?? file); formData.append('name', ctx.name); - if (folder) formData.append('folderId', folder); + if (_folder) formData.append('folderId', _folder); const xhr = new XMLHttpRequest(); xhr.open('POST', apiUrl + '/drive/files/create', true); diff --git a/packages/frontend/src/server-context.ts b/packages/frontend/src/server-context.ts new file mode 100644 index 0000000000..aa44a10290 --- /dev/null +++ b/packages/frontend/src/server-context.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import * as Misskey from 'misskey-js'; +import { $i } from '@/account.js'; + +const providedContextEl = document.getElementById('misskey_clientCtx'); + +export type ServerContext = { + clip?: Misskey.entities.Clip; + note?: Misskey.entities.Note; + user?: Misskey.entities.UserLite; +} | null; + +export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null; + +export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null { + // contextは非ログイン状態の情報しかないためログイン時は利用できない + if ($i) return null; + + return serverContext ? (serverContext[entity] ?? null) : null; +} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index bbd9873ad8..c34e0bbf48 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -8,11 +8,13 @@ import * as Misskey from 'misskey-js'; import { hemisphere } from '@@/js/intl-const.js'; import lightTheme from '@@/themes/l-cherry.json5'; import darkTheme from '@@/themes/d-ice.json5'; -import { miLocalStorage } from './local-storage.js'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; +import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js'; +import { miLocalStorage } from '@/local-storage.js'; import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; +import type { Ast } from '@syuilo/aiscript'; interface PostFormAction { title: string, @@ -80,6 +82,10 @@ export const defaultStore = markRaw(new Storage('base', { global: false, }, }, + abusesTutorial: { + where: 'account', + default: false, + }, keepCw: { where: 'account', default: true, @@ -249,7 +255,7 @@ export const defaultStore = markRaw(new Storage('base', { overridedDeviceKind: { where: 'device', - default: null as null | 'smartphone' | 'tablet' | 'desktop', + default: null as DeviceKind | null, }, serverDisconnectedBehavior: { where: 'device', @@ -269,7 +275,7 @@ export const defaultStore = markRaw(new Storage('base', { }, animatedMfm: { where: 'device', - default: false, + default: !window.matchMedia('(prefers-reduced-motion)').matches, }, advancedMfm: { where: 'device', @@ -317,11 +323,11 @@ export const defaultStore = markRaw(new Storage('base', { }, useBlurEffectForModal: { where: 'device', - default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない + default: DEFAULT_DEVICE_KIND === 'desktop', }, useBlurEffect: { where: 'device', - default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない + default: DEFAULT_DEVICE_KIND === 'desktop', }, showFixedPostForm: { where: 'device', @@ -551,6 +557,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'app' as 'app' | 'appWithShift' | 'native', }, + skipNoteRender: { + where: 'device', + default: true, + }, sound_masterVolume: { where: 'device', @@ -595,7 +605,7 @@ export type Plugin = { token: string; src: string | null; version: string; - ast: any[]; + ast: Ast.Node[]; author?: string; description?: string; permissions?: string[]; @@ -633,13 +643,13 @@ export class ColdDeviceStorage { } public static getAll(): Partial<typeof this.default> { - return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => { + return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<Partial<typeof this.default>>((acc, key) => { const value = localStorage.getItem(PREFIX + key); if (value != null) { acc[key] = JSON.parse(value); } return acc; - }, {} as any); + }, {}); } public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void { @@ -684,7 +694,7 @@ export class ColdDeviceStorage { get: () => { return valueRef.value; }, - set: (value: unknown) => { + set: (value: typeof ColdDeviceStorage.default[K]) => { const val = value; ColdDeviceStorage.set(key, val); }, diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index d990a706b3..2e8060fda7 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -15,54 +15,52 @@ */ :root { - --radius-xs: 5px; - --radius-sm: 5px; - --radius: 5px; - --radius-md: 5px; - --radius-lg: 5px; - --radius-xl: 5px; - --radius-ellipse: 5px; - --radius-full: 5px; + --MI-radius-xs: 5px; + --MI-radius-sm: 5px; + --MI-radius: 5px; + --MI-radius-md: 5px; + --MI-radius-lg: 5px; + --MI-radius-xl: 5px; + --MI-radius-ellipse: 5px; + --MI-radius-full: 5px; - --marginFull: 16px; - --marginHalf: 10px; + --MI-marginFull: 16px; + --MI-marginHalf: 10px; - --margin: var(--marginFull); + --MI-margin: var(--MI-marginFull); // switch dynamically - --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); - --minBottomSpacing: var(--minBottomSpacingMobile); + --MI-minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); + --MI-minBottomSpacing: var(--MI-minBottomSpacingMobile); - //--ad: rgb(255 169 0 / 10%); + --MI-avatar: 48px; + --MI-thread-width: 2px; @media (max-width: 500px) { - --margin: var(--marginHalf); + --MI-margin: var(--MI-marginHalf); } - - --avatar: 48px; - --thread-width: 2px; } html.radius-misskey { - --radius-xs: 4px; - --radius-sm: 8px; - --radius: 12px; - --radius-md: 16px; - --radius-lg: 24px; - --radius-xl: 32px; - --radius-ellipse: 999px; - --radius-full: 100%; + --MI-radius-xs: 4px; + --MI-radius-sm: 8px; + --MI-radius: 12px; + --MI-radius-md: 16px; + --MI-radius-lg: 24px; + --MI-radius-xl: 32px; + --MI-radius-ellipse: 999px; + --MI-radius-full: 100%; } ::selection { - color: var(--fgOnAccent); - background-color: var(--accent); + color: var(--MI_THEME-fgOnAccent); + background-color: var(--MI_THEME-accent); } html { - background-color: var(--bg); - color: var(--fg); - accent-color: var(--accent); + background-color: var(--MI_THEME-bg); + color: var(--MI_THEME-fg); + accent-color: var(--MI_THEME-accent); overflow: auto; overflow-wrap: break-word; font-family: 'sharkey-theme-font-face', 'Lexend', 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; @@ -73,7 +71,7 @@ html { -webkit-text-size-adjust: 100%; &, * { - scrollbar-color: var(--scrollbarHandle) transparent; + scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; scrollbar-width: thin; &::-webkit-scrollbar { @@ -86,14 +84,14 @@ html { } &::-webkit-scrollbar-thumb { - background: var(--scrollbarHandle); + background: var(--MI_THEME-scrollbarHandle); &:hover { - background: var(--scrollbarHandleHover); + background: var(--MI_THEME-scrollbarHandleHover); } &:active { - background: var(--accent); + background: var(--MI_THEME-accent); } } } @@ -155,15 +153,15 @@ textarea, input { } optgroup, option { - background: var(--panel); - color: var(--fg); + background: var(--MI_THEME-panel); + color: var(--MI_THEME-fg); } hr { - margin: var(--margin) 0 var(--margin) 0; + margin: var(--MI-margin) 0 var(--MI-margin) 0; border: none; height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } rt { @@ -171,7 +169,7 @@ rt { } :focus-visible { - outline: var(--focus) solid 2px; + outline: var(--MI_THEME-focus) solid 2px; outline-offset: -2px; &:hover { @@ -198,20 +196,20 @@ rt { display: inline-block; width: 1em; height: 1em; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); background: currentColor; } ._indicateCounter { display: inline-flex; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: 700; - background: var(--indicator); + background: var(--MI_THEME-indicator); height: 1.5em; min-width: 1em; align-items: center; justify-content: center; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); padding: 0.3em 0.5em; } @@ -239,13 +237,13 @@ rt { left: 0; width: 100%; height: 100%; - background: var(--modalBg); - -webkit-backdrop-filter: var(--modalBgFilter); - backdrop-filter: var(--modalBgFilter); + background: var(--MI_THEME-modalBg); + -webkit-backdrop-filter: var(--MI-modalBgFilter); + backdrop-filter: var(--MI-modalBgFilter); } ._shadow { - box-shadow: 0px 4px 32px var(--shadow) !important; + box-shadow: 0px 4px 32px var(--MI_THEME-shadow) !important; } ._button { @@ -278,40 +276,40 @@ rt { ._buttonPrimary { @extend ._button; - color: var(--fgOnAccent); - background: var(--accent); + color: var(--MI_THEME-fgOnAccent); + background: var(--MI_THEME-accent); &:not(:disabled):hover { - background: hsl(from var(--accent) h s calc(l + 5)); + background: hsl(from var(--MI_THEME-accent) h s calc(l + 5)); } &:not(:disabled):active { - background: hsl(from var(--accent) h s calc(l - 5)); + background: hsl(from var(--MI_THEME-accent) h s calc(l - 5)); } } ._buttonGradate { @extend ._buttonPrimary; - color: var(--fgOnAccent); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + color: var(--MI_THEME-fgOnAccent); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + 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))); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + 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))); } } ._help { - color: var(--accent); + color: var(--MI_THEME-accent); cursor: help; } ._textButton { @extend ._button; - color: var(--accent); + color: var(--MI_THEME-accent); &:focus-visible { outline-offset: 2px; @@ -323,13 +321,13 @@ rt { } ._panel { - background: color-mix(in srgb, var(--panel) 65%, transparent); - border-radius: var(--radius); + background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent); + border-radius: var(--MI-radius); overflow: clip; } ._margin { - margin: var(--margin) 0; + margin: var(--MI-margin) 0; } ._gaps_m { @@ -347,7 +345,7 @@ rt { ._gaps { display: flex; flex-direction: column; - gap: var(--margin); + gap: var(--MI-margin); } ._buttons { @@ -369,24 +367,24 @@ rt { padding: 10px; box-sizing: border-box; text-align: center; - border: solid 0.5px var(--divider); - border-radius: var(--radius); + border: solid 0.5px var(--MI_THEME-divider); + border-radius: var(--MI-radius); &:active { - border-color: var(--accent); + border-color: var(--MI_THEME-accent); } } ._popup { - background: var(--popup); - border-radius: var(--radius); + background: var(--MI_THEME-popup); + border-radius: var(--MI-radius); contain: content; } ._acrylic { - background: var(--acrylicPanel); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); + background: var(--MI_THEME-acrylicPanel); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); } ._formLinksGrid { @@ -399,9 +397,9 @@ rt { margin-left: 0.7em; font-size: 65%; padding: 2px 3px; - color: var(--accent); - border: solid 1px var(--accent); - border-radius: var(--radius-xs); + color: var(--MI_THEME-accent); + border: solid 1px var(--MI_THEME-accent); + border-radius: var(--MI-radius-xs); vertical-align: top; } @@ -409,8 +407,8 @@ rt { margin-left: 0.7em; font-size: 65%; padding: 2px 3px; - color: var(--warn); - border: solid 1px var(--warn); + color: var(--MI_THEME-warn); + border: solid 1px var(--MI_THEME-warn); border-radius: 4px; vertical-align: top; } @@ -451,12 +449,12 @@ rt { vertical-align: bottom; height: 128px; margin-bottom: 16px; - border-radius: var(--radius-md); + border-radius: var(--MI-radius-md); } } ._link { - color: var(--link); + color: var(--MI_THEME-link); } ._caption { @@ -480,14 +478,14 @@ rt { box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; border-radius: 10px; - --bg: #F1E8DC; - --fg: #693410; + --MI_THEME-bg: #F1E8DC; + --MI_THEME-fg: #693410; } html[data-color-scheme=dark] ._woodenFrame { - --bg: #1d0c02; - --fg: #F1E8DC; - --panel: #192320; + --MI_THEME-bg: #1d0c02; + --MI_THEME-fg: #F1E8DC; + --MI_THEME-panel: #192320; } ._woodenFrameH { @@ -498,10 +496,10 @@ html[data-color-scheme=dark] ._woodenFrame { ._woodenFrameInner { padding: 8px; margin-top: 8px; - background: var(--bg); + background: var(--MI_THEME-bg); box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; border-radius: 6px; - color: var(--fg); + color: var(--MI_THEME-fg); &:first-child { margin-top: 0; @@ -516,7 +514,11 @@ html[data-color-scheme=dark] ._woodenFrame { transform: scale(0.9); } -@keyframes global-blink { +._blink { + animation: blink 1s infinite; +} + +@keyframes blink { 0% { opacity: 1; transform: scale(1); } 30% { opacity: 1; transform: scale(1); } 90% { opacity: 0; transform: scale(0.5); } @@ -529,20 +531,17 @@ html[data-color-scheme=dark] ._woodenFrame { 10%, 20% { - transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.91, 0.91, 0.91) rotate3d(0, 0, 1, -2deg); } 30%, - 50%, - 70%, - 90% { - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + 70% { + transform: scale3d(1.09, 1.09, 1.09) rotate3d(0, 0, 1, 2deg); } - 40%, - 60%, - 80% { - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + 50%, + 90% { + transform: scale3d(1.09, 1.09, 1.09) rotate3d(0, 0, 1, -2deg); } to { diff --git a/packages/frontend/src/types/post-form.ts b/packages/frontend/src/types/post-form.ts new file mode 100644 index 0000000000..94958b6623 --- /dev/null +++ b/packages/frontend/src/types/post-form.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; + +export interface PostFormProps { + reply?: Misskey.entities.Note; + renote?: Misskey.entities.Note; + channel?: Misskey.entities.Channel; // TODO + mention?: Misskey.entities.User; + specified?: Misskey.entities.UserDetailed; + initialText?: string; + initialCw?: string; + initialVisibility?: (typeof Misskey.noteVisibilities)[number]; + initialFiles?: Misskey.entities.DriveFile[]; + initialLocalOnly?: boolean; + initialVisibleUsers?: Misskey.entities.UserDetailed[]; + initialNote?: Misskey.entities.Note & { + isSchedule?: boolean, + }; + instant?: boolean; +} diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue index 374bc20b54..d153dc8726 100644 --- a/packages/frontend/src/ui/_common_/announcements.vue +++ b/packages/frontend/src/ui/_common_/announcements.vue @@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only > <span :class="$style.icon"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i> </span> <span :class="$style.title">{{ announcement.title }}</span> <span :class="$style.body">{{ announcement.text }}</span> @@ -30,7 +30,7 @@ import { $i } from '@/account.js'; <style lang="scss" module> .root { font-size: 15px; - background: var(--panel); + background: var(--MI_THEME-panel); } .item { @@ -44,8 +44,8 @@ import { $i } from '@/account.js'; height: var(--height); overflow: clip; contain: strict; - background: var(--accent); - color: var(--fgOnAccent); + background: var(--MI_THEME-accent); + color: var(--MI_THEME-fgOnAccent); @container (max-width: 1000px) { display: block; diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index a8ff2a4c8d..7e29a5eeff 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -129,26 +129,26 @@ function getPointerEvents() { .notifications { position: fixed; z-index: 3900000; - padding: 0 var(--margin); + padding: 0 var(--MI-margin); display: flex; &.notificationsPosition_leftTop { - top: var(--margin); + top: var(--MI-margin); left: 0; } &.notificationsPosition_rightTop { - top: var(--margin); + top: var(--MI-margin); right: 0; } &.notificationsPosition_leftBottom { - bottom: calc(var(--minBottomSpacing) + var(--margin)); + bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin)); left: 0; } &.notificationsPosition_rightBottom { - bottom: calc(var(--minBottomSpacing) + var(--margin)); + bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin)); right: 0; } @@ -246,8 +246,8 @@ function getPointerEvents() { height: 18px; box-sizing: border-box; border: solid 2px transparent; - border-top-color: var(--accent); - border-left-color: var(--accent); + border-top-color: var(--MI_THEME-accent); + border-left-color: var(--MI_THEME-accent); border-radius: 50%; animation: progress-spinner 400ms linear infinite; } diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index f3244b5697..aa72de6089 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="item === '-'" :class="$style.divider"></div> <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"> + <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink"> <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> <i v-else class="_indicatorCircle"></i> </span> @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> <button :class="$style.item" class="_button" @click="more"> <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> - <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> + <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span> </button> <MkA :class="$style.item" :activeClass="$style.active" to="/settings"> <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> @@ -82,7 +82,7 @@ function more() { <style lang="scss" module> .root { - --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5); + --nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5); display: flex; flex-direction: column; @@ -94,8 +94,8 @@ function more() { z-index: 1; padding: 20px 0; background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); } .banner { @@ -135,8 +135,8 @@ function more() { bottom: 0; padding: 20px 0; background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); } .post { @@ -144,7 +144,7 @@ function more() { display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -159,13 +159,13 @@ function more() { left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } &:hover, &.active { &::before { - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } } } @@ -209,7 +209,7 @@ function more() { .divider { margin: 16px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .item { @@ -223,15 +223,15 @@ function more() { width: 100%; text-align: left; box-sizing: border-box; - color: var(--navFg); + color: var(--MI_THEME-navFg); &:hover { text-decoration: none; - color: var(--navHoverFg); + color: var(--MI_THEME-navHoverFg); } &.active { - color: var(--navActive); + color: var(--MI_THEME-navActive); } &:hover, &.active { @@ -246,8 +246,8 @@ function more() { left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: var(--accentedBg); + border-radius: var(--MI-radius-ellipse); + background: var(--MI_THEME-accentedBg); } } } @@ -262,9 +262,8 @@ function more() { position: absolute; top: 0; left: 20px; - color: var(--navIndicator); + color: var(--MI_THEME-navIndicator); font-size: 8px; - animation: global-blink 1s infinite; &:has(.itemIndicateValueIcon) { animation: none; diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 17690df412..5cc0e52f77 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}" > <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"> + <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink"> <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> <i v-else class="_indicatorCircle"></i> </span> @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> <button class="_button" :class="$style.item" @click="more"> <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> - <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> + <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span> </button> <MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings"> <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> @@ -56,6 +56,21 @@ SPDX-License-Identifier: AGPL-3.0-only </button> </div> </div> + <button v-if="!forceIconOnly" class="_button" :class="$style.toggleButton" @click="toggleIconOnly"> + <!-- + <svg viewBox="0 0 16 48" :class="$style.toggleButtonShape"> + <g transform="matrix(0.333333,0,0,0.222222,0.000895785,13.3333)"> + <path d="M23.935,-24C37.223,-24 47.995,-7.842 47.995,12.09C47.995,34.077 47.995,62.07 47.995,84.034C47.995,93.573 45.469,102.721 40.972,109.466C36.475,116.211 30.377,120 24.018,120L23.997,120C10.743,120 -0.003,136.118 -0.003,156C-0.003,156 -0.003,156 -0.003,156L-0.003,-60L-0.003,-59.901C-0.003,-50.379 2.519,-41.248 7.007,-34.515C11.496,-27.782 17.584,-24 23.931,-24C23.932,-24 23.934,-24 23.935,-24Z" style="fill:var(--MI_THEME-navBg);"/> + </g> + </svg> + --> + <svg viewBox="0 0 16 64" :class="$style.toggleButtonShape"> + <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)"> + <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/> + </g> + </svg> + <i :class="'ti ' + `ti-chevron-${ iconOnly ? 'right' : 'left' }`" style="font-size: 12px; margin-left: -8px;"></i> + </button> </div> </template> @@ -80,9 +95,11 @@ const otherMenuItemIndicated = computed(() => { return false; }); -const calcViewState = () => { - iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon'); -}; +const forceIconOnly = window.innerWidth <= 1279; + +function calcViewState() { + iconOnly.value = forceIconOnly || (defaultStore.state.menuDisplay === 'sideIcon'); +} calcViewState(); @@ -92,6 +109,10 @@ watch(defaultStore.reactiveState.menuDisplay, () => { calcViewState(); }); +function toggleIconOnly() { + defaultStore.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon'); +} + function openAccountMenu(ev: MouseEvent) { openAccountMenu_({ withExtraOperation: true, @@ -111,7 +132,7 @@ function more(ev: MouseEvent) { .root { --nav-width: 250px; --nav-icon-only-width: 80px; - --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5); + --nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5); flex: 0 0 var(--nav-width); width: var(--nav-width); @@ -129,10 +150,42 @@ function more(ev: MouseEvent) { overflow: auto; overflow-x: clip; overscroll-behavior: contain; - background: var(--navBg); + background: var(--MI_THEME-navBg); contain: strict; display: flex; flex-direction: column; + direction: rtl; // スクロールバーを左に表示したいため +} + +.top { + direction: ltr; +} + +.middle { + direction: ltr; +} + +.bottom { + direction: ltr; +} + +.toggleButton { + position: fixed; + bottom: 20px; + left: var(--nav-width); + z-index: 1001; + width: 16px; + height: 64px; + box-sizing: border-box; +} + +.toggleButtonShape { + position: absolute; + z-index: -1; + top: 0; + left: 0; + width: 16px; + height: 64px; } .root:not(.iconOnly) { @@ -146,8 +199,8 @@ function more(ev: MouseEvent) { z-index: 1; padding: 20px 0; background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); } .banner { @@ -172,7 +225,7 @@ function more(ev: MouseEvent) { outline: none; > .instanceIcon { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: 2px; } } @@ -196,8 +249,8 @@ function more(ev: MouseEvent) { bottom: 0; padding-top: 20px; background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); } .post { @@ -205,7 +258,7 @@ function more(ev: MouseEvent) { display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -220,22 +273,22 @@ function more(ev: MouseEvent) { left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-ellipse); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } &:focus-visible { outline: none; &::before { - outline: 2px solid var(--fgOnAccent); + outline: 2px solid var(--MI_THEME-fgOnAccent); outline-offset: -4px; } } &:hover, &.active { &::before { - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } } } @@ -265,7 +318,7 @@ function more(ev: MouseEvent) { outline: none; > .avatar { - box-shadow: 0 0 0 4px var(--focus); + box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } } @@ -291,7 +344,7 @@ function more(ev: MouseEvent) { .divider { margin: 16px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .item { @@ -305,28 +358,28 @@ function more(ev: MouseEvent) { width: 100%; text-align: left; box-sizing: border-box; - color: var(--navFg); + color: var(--MI_THEME-navFg); &:hover { text-decoration: none; - color: var(--navHoverFg); + color: var(--MI_THEME-navHoverFg); } &.active { - color: var(--navActive); + color: var(--MI_THEME-navActive); } &:focus-visible { outline: none; &::before { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: -2px; } } &:hover, &.active, &:focus { - color: var(--accent); + color: var(--MI_THEME-accent); &::before { content: ""; @@ -339,8 +392,8 @@ function more(ev: MouseEvent) { left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: var(--accentedBg); + border-radius: var(--MI-radius-ellipse); + background: var(--MI_THEME-accentedBg); } } } @@ -355,9 +408,8 @@ function more(ev: MouseEvent) { position: absolute; top: 0; left: 20px; - color: var(--navIndicator); + color: var(--MI_THEME-navIndicator); font-size: 8px; - animation: global-blink 1s infinite; &:has(.itemIndicateValueIcon) { animation: none; @@ -371,6 +423,10 @@ function more(ev: MouseEvent) { position: relative; font-size: 0.9em; } + + .toggleButton { + left: var(--nav-width); + } } .root.iconOnly { @@ -387,8 +443,8 @@ function more(ev: MouseEvent) { z-index: 1; padding: 20px 0; background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); } .instance { @@ -400,7 +456,7 @@ function more(ev: MouseEvent) { outline: none; > .instanceIcon { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: 2px; } } @@ -417,8 +473,8 @@ function more(ev: MouseEvent) { bottom: 0; padding-top: 20px; background: var(--nav-bg-transparent); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); + -webkit-backdrop-filter: var(--MI-blur, blur(8px)); + backdrop-filter: var(--MI-blur, blur(8px)); } .post { @@ -439,29 +495,29 @@ function more(ev: MouseEvent) { margin: auto; width: 52px; aspect-ratio: 1/1; - border-radius: var(--radius-full); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + border-radius: var(--MI-radius-full); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); } &:focus-visible { outline: none; &::before { - outline: 2px solid var(--fgOnAccent); + outline: 2px solid var(--MI_THEME-fgOnAccent); outline-offset: -4px; } } &:hover, &.active { &::before { - background: var(--accentLighten); + background: var(--MI_THEME-accentLighten); } } } .postIcon { position: relative; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); } .postText { @@ -479,7 +535,7 @@ function more(ev: MouseEvent) { outline: none; > .avatar { - box-shadow: 0 0 0 4px var(--focus); + box-shadow: 0 0 0 4px var(--MI_THEME-focus); } } } @@ -501,7 +557,7 @@ function more(ev: MouseEvent) { .divider { margin: 8px auto; width: calc(100% - 32px); - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .item { @@ -515,14 +571,14 @@ function more(ev: MouseEvent) { outline: none; &::before { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: -2px; } } &:hover, &.active, &:focus { text-decoration: none; - color: var(--accent); + color: var(--MI_THEME-accent); &::before { content: ""; @@ -535,8 +591,8 @@ function more(ev: MouseEvent) { left: 0; right: 0; bottom: 0; - border-radius: var(--radius-ellipse); - background: var(--accentedBg); + border-radius: var(--MI-radius-ellipse); + background: var(--MI_THEME-accentedBg); } > .icon, @@ -560,9 +616,8 @@ function more(ev: MouseEvent) { position: absolute; top: 6px; left: 24px; - color: var(--navIndicator); + color: var(--MI_THEME-navIndicator); font-size: 8px; - animation: global-blink 1s infinite; &:has(.itemIndicateValueIcon) { animation: none; @@ -572,5 +627,9 @@ function more(ev: MouseEvent) { font-size: 10px; } } + + .toggleButton { + left: var(--nav-icon-only-width); + } } </style> diff --git a/packages/frontend/src/ui/_common_/notification.vue b/packages/frontend/src/ui/_common_/notification.vue index 29ae04387a..2d527c1a65 100644 --- a/packages/frontend/src/ui/_common_/notification.vue +++ b/packages/frontend/src/ui/_common_/notification.vue @@ -22,7 +22,7 @@ defineProps<{ <style lang="scss" module> .root { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); overflow: clip; contain: content; } diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index 550fc39b00..da8fa8bb21 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -48,7 +48,7 @@ const fetching = ref(true); const key = ref(0); const tick = () => { - window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => { + window.fetch(`/api/fetch-rss?url=${encodeURIComponent(props.url)}`, {}).then(res => { res.json().then((feed: Misskey.entities.FetchRssResponse) => { if (props.shuffle) { shuffle(feed.items); diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index 690366307b..5f9a938017 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -32,7 +32,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') <style lang="scss" module> .root { font-size: 15px; - background: var(--panel); + background: var(--MI_THEME-panel); } .item { @@ -81,7 +81,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') .name { padding: 0 var(--nameMargin); font-weight: bold; - color: var(--accent); + color: var(--MI_THEME-accent); &:empty { display: none; diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index ad93b7e61c..cc62a28b14 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -48,8 +48,8 @@ onUnmounted(() => { .root { position: fixed; z-index: v-bind(zIndex); - bottom: calc(var(--minBottomSpacing) + var(--margin)); - right: var(--margin); + bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin)); + right: var(--MI-margin); margin: 0; padding: 12px; font-size: 0.9em; diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue index 96030c7897..12de579d90 100644 --- a/packages/frontend/src/ui/_common_/upload.vue +++ b/packages/frontend/src/ui/_common_/upload.vue @@ -40,7 +40,7 @@ const zIndex = os.claimZIndex('high'); padding: 16px 20px; pointer-events: none; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } .mk-uploader:empty { display: none; @@ -116,7 +116,7 @@ const zIndex = os.claimZIndex('high'); display: block; background: transparent; border: none; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); overflow: hidden; grid-column: 2/3; grid-row: 2/3; @@ -125,10 +125,10 @@ const zIndex = os.claimZIndex('high'); height: 8px; } .mk-uploader > ol > li > progress::-webkit-progress-value { - background: var(--accent); + background: var(--MI_THEME-accent); } .mk-uploader > ol > li > progress::-webkit-progress-bar { - //background: var(--accentAlpha01); + //background: var(--MI_THEME-accentAlpha01); background: transparent; } </style> diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue index c03afd6cd6..f4633314ae 100644 --- a/packages/frontend/src/ui/classic.header.vue +++ b/packages/frontend/src/ui/classic.header.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="item === '-'" class="divider"></div> <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> <i class="ti-fw" :class="navbarItemDef[item].icon"></i> - <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span> + <span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> </component> </template> <div class="divider"></div> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> <button v-click-anime class="item _button" @click="more"> <i class="ti ti-dots ti-fw"></i> - <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> + <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> </button> </div> <div class="right"> @@ -104,7 +104,7 @@ onMounted(() => { z-index: 1000; width: 100%; height: $height; - background-color: var(--bg); + background-color: var(--MI_THEME-bg); > .body { max-width: 1380px; @@ -140,18 +140,17 @@ onMounted(() => { position: absolute; top: 0; left: 0; - color: var(--navIndicator); + color: var(--MI_THEME-navIndicator); font-size: 8px; - animation: global-blink 1s infinite; } &:hover { text-decoration: none; - color: var(--navHoverFg); + color: var(--MI_THEME-navHoverFg); } &.active { - color: var(--navActive); + color: var(--MI_THEME-navActive); } } @@ -159,7 +158,7 @@ onMounted(() => { display: inline-block; height: 16px; margin: 0 10px; - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--MI_THEME-divider); } > .instance { diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index 96cc24c9b9..f17027bcde 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="item === '-'" class="divider"></div> <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> <i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span> - <span v-if="navbarItemDef[item].indicated" class="indicator"> + <span v-if="navbarItemDef[item].indicated" class="indicator _blink"> <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> <i v-else class="_indicatorCircle"></i> </span> @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> <button v-click-anime class="item _button" @click="more"> <i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> - <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> + <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> </button> <MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> <i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> @@ -159,7 +159,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => { > .divider { margin: 10px 0; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } > .post { @@ -224,9 +224,8 @@ watch(defaultStore.reactiveState.menuDisplay, () => { position: absolute; top: 0; left: 0; - color: var(--navIndicator); + color: var(--MI_THEME-navIndicator); font-size: 8px; - animation: global-blink 1s infinite; &:has(.itemIndicateValueIcon) { animation: none; @@ -237,11 +236,11 @@ watch(defaultStore.reactiveState.menuDisplay, () => { &:hover { text-decoration: none; - color: var(--navHoverFg); + color: var(--MI_THEME-navHoverFg); } &.active { - color: var(--navActive); + color: var(--MI_THEME-navActive); } } } diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 31bb1ddc14..ded945dda1 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XSidebar/> </div> <div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left"> - <XWidgets place="left" :marginTop="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/> + <XWidgets place="left" :marginTop="'var(--MI-margin)'" @mounted="attachSticky(widgetsLeft)"/> </div> <main class="main" @contextmenu.stop="onContextmenu"> @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only </main> <div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right"> - <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/> + <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--MI-margin)'" @mounted="attachSticky(widgetsRight)"/> </div> </div> @@ -216,8 +216,8 @@ onMounted(() => { box-sizing: border-box; &.wallpaper { - background: var(--wallpaperOverlay); - //backdrop-filter: var(--blur, blur(4px)); + background: var(--MI_THEME-wallpaperOverlay); + //backdrop-filter: var(--MI-blur, blur(4px)); } > .columns { @@ -249,17 +249,16 @@ onMounted(() => { min-width: 0; width: 750px; margin: 0 16px 0 0; - border-left: solid 1px var(--divider); - border-right: solid 1px var(--divider); + border-left: solid 1px var(--MI_THEME-divider); + border-right: solid 1px var(--MI_THEME-divider); border-radius: 0; overflow: clip; - --margin: 12px; + --MI-margin: 12px; } > .widgets { position: sticky; top: 0; - width: 300px; height: 100%; padding-top: 16px; box-sizing: border-box; @@ -281,13 +280,13 @@ onMounted(() => { &.withGlobalHeader { > .main { margin-top: 0; - border: solid 1px var(--divider); - border-radius: var(--radius); - --stickyTop: var(--globalHeaderHeight); + border: solid 1px var(--MI_THEME-divider); + border-radius: var(--MI-radius); + --MI-stickyTop: var(--globalHeaderHeight); } > .widgets { - --stickyTop: var(--globalHeaderHeight); + --MI-stickyTop: var(--globalHeaderHeight); margin-top: 0; } } @@ -296,7 +295,7 @@ onMounted(() => { margin: 0; > .sidebar { - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--MI_THEME-divider); } > .main { @@ -318,10 +317,10 @@ onMounted(() => { right: 0; z-index: 1001; height: 100dvh; - padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)); box-sizing: border-box; overflow: auto; - background: var(--bg); + background: var(--MI_THEME-bg); } > .ivnzpscs { diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index b42a63e090..c0ea833546 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -55,11 +55,11 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="isMobile" :class="$style.nav"> - <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> <i :class="$style.navButtonIcon" class="ti ti-bell"></i> - <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"> + <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink"> <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> </span> </button> @@ -102,6 +102,7 @@ import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; import type { ColumnType } from './deck/deck-store.js'; +import type { MenuItem } from '@/types/menu.js'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; @@ -124,7 +125,6 @@ import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; import XFollowingColumn from '@/ui/deck/following-column.vue'; import { mainRouter } from '@/router/main.js'; -import type { MenuItem } from '@/types/menu.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); @@ -312,7 +312,7 @@ body { .root { $nav-hide-threshold: 650px; // TODO: どこかに集約したい - --margin: var(--marginHalf); + --MI-margin: var(--MI-marginHalf); --columnGap: 6px; @@ -339,7 +339,7 @@ body { overflow-x: auto; overflow-y: clip; overscroll-behavior: contain; - background: var(--deckBg); + background: var(--MI_THEME-deckBg); &.center { > .section:first-of-type { @@ -421,7 +421,7 @@ body { contain: strict; overflow: auto; overscroll-behavior: contain; - background: var(--navBg); + background: var(--MI_THEME-navBg); } .nav { @@ -435,10 +435,10 @@ body { grid-gap: 8px; width: 100%; box-sizing: border-box; - -webkit-backdrop-filter: var(--blur, blur(32px)); - backdrop-filter: var(--blur, blur(32px)); - background-color: var(--header); - border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--MI-blur, blur(32px)); + backdrop-filter: var(--MI-blur, blur(32px)); + background-color: var(--MI_THEME-header); + border-top: solid 0.5px var(--MI_THEME-divider); } .navButton { @@ -448,33 +448,33 @@ body { width: 100%; max-width: 60px; margin: auto; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); &:hover { - color: var(--accent); + color: var(--MI_THEME-accent); } &:active { - color: var(--accent); - background: hsl(from var(--panel) h s calc(l - 2)); + color: var(--MI_THEME-accent); + background: hsl(from var(--MI_THEME-panel) h s calc(l - 2)); } } .postButton { composes: navButton; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); + color: var(--MI_THEME-fgOnAccent); &:hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); - color: var(--fgOnAccent); + 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))); + color: var(--MI_THEME-fgOnAccent); } &:active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); - color: var(--fgOnAccent); + 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))); + color: var(--MI_THEME-fgOnAccent); } } @@ -487,9 +487,8 @@ body { position: absolute; top: 0; left: 0; - color: var(--indicator); + color: var(--MI_THEME-indicator); font-size: 16px; - animation: global-blink 1s infinite; &:has(.itemIndicateValueIcon) { animation: none; diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 5ed3aa754f..2eb232096e 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <svg viewBox="0 0 256 128" :class="$style.tabShape"> <g transform="matrix(6.2431,0,0,6.2431,-677.417,-29.3839)"> - <path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--deckBg);"/> + <path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--MI_THEME-deckBg);"/> </g> </svg> <div :class="$style.color"></div> @@ -287,7 +287,7 @@ function onDrop(ev) { height: 100%; overflow: clip; contain: strict; - border-radius: var(--radius); + border-radius: var(--MI-radius); &.draghover { &::after { @@ -299,7 +299,7 @@ function onDrop(ev) { left: 0; width: 100%; height: 100%; - background: var(--focus); + background: var(--MI_THEME-focus); } } @@ -313,7 +313,7 @@ function onDrop(ev) { left: 0; width: 100%; height: 100%; - background: var(--focus); + background: var(--MI_THEME-focus); opacity: 0.5; } } @@ -331,19 +331,19 @@ function onDrop(ev) { } &.naked { - background: var(--acrylicBg) !important; - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); + background: var(--MI_THEME-acrylicBg) !important; + -webkit-backdrop-filter: var(--MI-blur, blur(10px)); + backdrop-filter: var(--MI-blur, blur(10px)); > .header { background: transparent; box-shadow: none; - color: var(--fg); + color: var(--MI_THEME-fg); } > .body { background: transparent !important; - scrollbar-color: var(--scrollbarHandle) transparent; + scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: transparent; @@ -352,12 +352,12 @@ function onDrop(ev) { } &.paged { - background: var(--bg) !important; + background: var(--MI_THEME-bg) !important; > .body { - background: var(--bg) !important; + background: var(--MI_THEME-bg) !important; overflow-y: scroll !important; - scrollbar-color: var(--scrollbarHandle) transparent; + scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: inherit; @@ -374,9 +374,9 @@ function onDrop(ev) { height: var(--deckColumnHeaderHeight); padding: 0 16px 0 30px; font-size: 0.9em; - color: var(--panelHeaderFg); - background: var(--panelHeaderBg); - box-shadow: 0 1px 0 0 var(--panelHeaderDivider); + color: var(--MI_THEME-panelHeaderFg); + background: var(--MI_THEME-panelHeaderBg); + box-shadow: 0 1px 0 0 var(--MI_THEME-panelHeaderDivider); cursor: pointer; user-select: none; } @@ -387,8 +387,8 @@ function onDrop(ev) { left: 12px; width: 3px; height: calc(100% - 24px); - background: var(--accent); - border-radius: var(--radius-ellipse); + background: var(--MI_THEME-accent); + border-radius: var(--MI-radius-ellipse); } .tabShape { @@ -441,11 +441,11 @@ function onDrop(ev) { overscroll-behavior-y: contain; box-sizing: border-box; container-type: size; - background-color: var(--bg); - scrollbar-color: var(--scrollbarHandle) var(--panel); + background-color: var(--MI_THEME-bg); + scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel); &::-webkit-scrollbar-track { - background: var(--panel); + background: var(--MI_THEME-panel); } } </style> diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index ccc9af8d12..91859b46d7 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -50,6 +50,7 @@ export type Column = { tl?: BasicTimelineType; withRenotes?: boolean; withReplies?: boolean; + withSensitive?: boolean; onlyFiles?: boolean; soundSetting: SoundStore; }; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 8315f7fca5..8f5553ccae 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only :src="column.tl" :withRenotes="withRenotes" :withReplies="withReplies" + :withSensitive="withSensitive" :onlyFiles="onlyFiles" @note="onNote" /> @@ -54,6 +55,7 @@ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); const withRenotes = ref(props.column.withRenotes ?? true); const withReplies = ref(props.column.withReplies ?? false); +const withSensitive = ref(props.column.withSensitive ?? true); const onlyFiles = ref(props.column.onlyFiles ?? false); watch(withRenotes, v => { @@ -68,6 +70,12 @@ watch(withReplies, v => { }); }); +watch(withSensitive, v => { + updateColumn(props.column.id, { + withSensitive: v, + }); +}); + watch(onlyFiles, v => { updateColumn(props.column.id, { onlyFiles: v, @@ -146,6 +154,10 @@ const menu = computed<MenuItem[]>(() => { text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: hasWithReplies(props.column.tl) ? withReplies : false, + }, { + type: 'switch', + text: i18n.ts.withSensitive, + ref: withSensitive, }); return menuItems; diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue index 9995996771..a0e62c8264 100644 --- a/packages/frontend/src/ui/deck/widgets-column.vue +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -57,10 +57,10 @@ const menu = [{ <style lang="scss" module> .root { - --margin: 8px; - --panelBorder: none; + --MI-margin: 8px; + --MI_THEME-panelBorder: none; - padding: 0 var(--margin); + padding: 0 var(--MI-margin); } .intro { diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 2fdaca775b..c552b65318 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> - <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> <button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> <i :class="$style.navButtonIcon" class="ti ti-bell"></i> - <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"> + <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink"> <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> </span> </button> @@ -96,9 +96,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue'; +import { instanceName } from '@@/js/config.js'; +import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; +import { isLink } from '@@/js/is-link.js'; import XCommon from './_common_/common.vue'; import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; -import { instanceName } from '@@/js/config.js'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -108,10 +110,8 @@ import { $i } from '@/account.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; -import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; import { useScrollPositionManager } from '@/nirax.js'; import { mainRouter } from '@/router/main.js'; -import { isLink } from '@@/js/is-link.js'; const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); @@ -225,12 +225,12 @@ provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight); watch(navFooter, () => { if (navFooter.value) { navFooterHeight.value = navFooter.value.offsetHeight; - document.body.style.setProperty('--stickyBottom', `${navFooterHeight.value}px`); - document.body.style.setProperty('--minBottomSpacing', 'var(--minBottomSpacingMobile)'); + document.body.style.setProperty('--MI-stickyBottom', `${navFooterHeight.value}px`); + document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)'); } else { navFooterHeight.value = 0; - document.body.style.setProperty('--stickyBottom', '0px'); - document.body.style.setProperty('--minBottomSpacing', '0px'); + document.body.style.setProperty('--MI-stickyBottom', '0px'); + document.body.style.setProperty('--MI-minBottomSpacing', '0px'); } }, { immediate: true, @@ -318,7 +318,7 @@ $widgets-hide-threshold: 1090px; } .sidebar { - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--MI_THEME-divider); } .contents { @@ -328,7 +328,7 @@ $widgets-hide-threshold: 1090px; overflow: auto; overflow-y: scroll; overscroll-behavior: unset; - background: var(--bg); + background: var(--MI_THEME-bg); } .widgets { @@ -336,9 +336,9 @@ $widgets-hide-threshold: 1090px; height: 100%; box-sizing: border-box; overflow: auto; - padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); - border-left: solid 0.5px var(--divider); - background: var(--bg); + padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)); + border-left: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-bg); @media (max-width: $widgets-hide-threshold) { display: none; @@ -353,10 +353,10 @@ $widgets-hide-threshold: 1090px; right: 32px; width: 64px; height: 64px; - border-radius: var(--radius-full); + border-radius: var(--MI-radius-full); box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); font-size: 22px; - background: var(--panel); + background: var(--MI_THEME-panel); } .widgetsDrawerBg { @@ -370,11 +370,11 @@ $widgets-hide-threshold: 1090px; z-index: 1001; width: 310px; height: 100dvh; - padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important; + padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important; box-sizing: border-box; overflow: auto; overscroll-behavior: contain; - background: var(--bg); + background: var(--MI_THEME-bg); } .widgetsCloseButton { @@ -400,10 +400,10 @@ $widgets-hide-threshold: 1090px; grid-gap: 8px; width: 100%; box-sizing: border-box; - -webkit-backdrop-filter: var(--blur, blur(24px)); - backdrop-filter: var(--blur, blur(24px)); - background-color: var(--header); - border-top: solid 0.5px var(--divider); + -webkit-backdrop-filter: var(--MI-blur, blur(24px)); + backdrop-filter: var(--MI-blur, blur(24px)); + background-color: var(--MI_THEME-header); + border-top: solid 0.5px var(--MI_THEME-divider); } .navButton { @@ -413,34 +413,34 @@ $widgets-hide-threshold: 1090px; width: 100%; max-width: 60px; margin: auto; - border-radius: var(--radius-lg); + border-radius: var(--MI-radius-lg); background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); &:hover { - background: var(--panelHighlight); - color: var(--accent); + background: var(--MI_THEME-panelHighlight); + color: var(--MI_THEME-accent); } &:active { - background: hsl(from var(--panel) h s calc(l - 2)); - color: var(--accent); + background: hsl(from var(--MI_THEME-panel) h s calc(l - 2)); + color: var(--MI_THEME-accent); } } .postButton { composes: navButton; - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); + color: var(--MI_THEME-fgOnAccent); &:hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); - color: var(--fgOnAccent); + 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))); + color: var(--MI_THEME-fgOnAccent); } &:active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); + 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))); } } @@ -453,9 +453,8 @@ $widgets-hide-threshold: 1090px; position: absolute; top: 0; left: 0; - color: var(--indicator); + color: var(--MI_THEME-indicator); font-size: 16px; - animation: global-blink 1s infinite; &:has(.itemIndicateValueIcon) { animation: none; @@ -478,7 +477,7 @@ $widgets-hide-threshold: 1090px; contain: strict; overflow: auto; overscroll-behavior: contain; - background: var(--navBg); + background: var(--MI_THEME-navBg); } .statusbars { @@ -488,6 +487,6 @@ $widgets-hide-threshold: 1090px; } .spacer { - height: calc(var(--minBottomSpacing)); + height: calc(var(--MI-minBottomSpacing)); } </style> diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 510b2a4342..f048ac2124 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -189,7 +189,7 @@ defineExpose({ left: 0; width: 500px; height: 100vh; - background: var(--accent); + background: var(--MI_THEME-accent); z-index: 1; > .banner { @@ -218,7 +218,7 @@ defineExpose({ min-width: 0; > .header { - background: var(--panel); + background: var(--MI_THEME-panel); position: relative; z-index: 1; @@ -255,7 +255,7 @@ defineExpose({ left: 0; width: 240px; height: 100vh; - background: var(--panel); + background: var(--MI_THEME-panel); > .link { display: block; @@ -269,7 +269,7 @@ defineExpose({ > .divider { margin: 8px auto; width: calc(100% - 32px); - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } > .action { @@ -281,10 +281,10 @@ defineExpose({ padding: 10px; box-sizing: border-box; text-align: center; - border-radius: var(--radius-ellipse); + border-radius: var(--MI-radius-ellipse); &._button { - background: var(--panel); + background: var(--MI_THEME-panel); } &:first-child { diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index ac13d7822f..757aa6669d 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -63,12 +63,12 @@ document.documentElement.style.overflowY = 'scroll'; } .rootWithBottom { - min-height: calc(100dvh - (60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px))); + min-height: calc(100dvh - (60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px))); box-sizing: border-box; } .bottom { - height: calc(60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px)); + height: calc(60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px)); width: 100%; margin-top: auto; } @@ -80,10 +80,10 @@ document.documentElement.style.overflowY = 'scroll'; width: 100%; max-width: 60px; margin: auto; - border-radius: var(--radius-full); - background: var(--panel); - color: var(--fg); - right: var(--margin); - bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + border-radius: var(--MI-radius-full); + background: var(--MI_THEME-panel); + color: var(--MI_THEME-fg); + right: var(--MI-margin); + bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)); } </style> diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 8e29254819..946124058c 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -126,10 +126,10 @@ defineExpose<WidgetComponentExpose>({ max-width: 100%; min-width: 100%; padding: 16px; - color: var(--fg); + color: var(--MI_THEME-fg); background: transparent; border: none; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); border-radius: 0; box-sizing: border-box; font: inherit; @@ -145,7 +145,7 @@ defineExpose<WidgetComponentExpose>({ padding: 0 10px; height: 28px; outline: none; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); &:disabled { opacity: 0.7; @@ -154,7 +154,7 @@ defineExpose<WidgetComponentExpose>({ } > .logs { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); text-align: left; padding: 16px; diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index bcfaaf00ab..c2bda85ac7 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -115,7 +115,7 @@ defineExpose<WidgetComponentExpose>({ <style lang="scss" module> .bdayFRoot { overflow: hidden; - min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2)); + min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--MI-margin) * 2)); } .bdayFGrid { display: grid; @@ -123,7 +123,7 @@ defineExpose<WidgetComponentExpose>({ grid-template-rows: repeat(3, 42px); place-content: center; gap: 8px; - margin: var(--margin) auto; + margin: var(--MI-margin) auto; } .bdayFFallback { @@ -139,6 +139,6 @@ defineExpose<WidgetComponentExpose>({ width: auto; max-width: 90%; margin-bottom: 8px; - border-radius: var(--radius); + border-radius: var(--MI-radius); } </style> diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index e4ac1acfc7..a3fe93f025 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -207,8 +207,8 @@ defineExpose<WidgetComponentExpose>({ .meter { width: 100%; overflow: hidden; - background: var(--X11); - border-radius: var(--radius-sm); + background: var(--MI_THEME-X11); + border-radius: var(--MI-radius-sm); } .meterVal { diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index e91a77beab..260dd06c13 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -105,14 +105,14 @@ defineExpose<WidgetComponentExpose>({ display: flex; align-items: center; padding: 14px 16px; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); > img { display: block; width: ($bodyTitleHieght + $bodyInfoHieght); height: ($bodyTitleHieght + $bodyInfoHieght); object-fit: cover; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); margin-right: 8px; } @@ -120,7 +120,7 @@ defineExpose<WidgetComponentExpose>({ flex: 1; overflow: hidden; font-size: 0.9em; - color: var(--fg); + color: var(--MI_THEME-fg); padding-right: 8px; > .a { diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index 014cf01a5d..b99f9bdb2b 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -66,7 +66,7 @@ defineExpose<WidgetComponentExpose>({ display: inline-block; width: 60px; height: 60px; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); box-sizing: border-box; border: solid 3px #fff; } diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index edf6622a13..0ee6b863dc 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -173,14 +173,14 @@ defineExpose<WidgetComponentExpose>({ padding: 16px; &:not(:first-child) { - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } > .label { display: flex; > .icon { - color: var(--warn); + color: var(--MI_THEME-warn); margin-left: auto; animation: warnBlink 1s infinite; } @@ -198,11 +198,11 @@ defineExpose<WidgetComponentExpose>({ > div:last-child { &.inc { - color: var(--warn); + color: var(--MI_THEME-warn); } &.dec { - color: var(--success); + color: var(--MI_THEME-success); } } } diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index ee89beb944..5addf1066d 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -84,10 +84,10 @@ defineExpose<WidgetComponentExpose>({ max-width: 100%; min-width: 100%; padding: 16px; - color: var(--fg); + color: var(--MI_THEME-fg); background: transparent; border: none; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); border-radius: 0; box-sizing: border-box; font: inherit; @@ -107,7 +107,7 @@ defineExpose<WidgetComponentExpose>({ padding: 0 10px; height: 28px; outline: none; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); &:disabled { opacity: 0.7; diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index d56ee96ac1..d8c4e259c8 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -72,6 +72,6 @@ defineExpose<WidgetComponentExpose>({ } .text { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } </style> diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index 86ece91de6..60a4770e40 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -68,10 +68,10 @@ const onDriveFileCreated = (file) => { } }; -const thumbnail = (image: any): string => { +const thumbnail = (image: Misskey.entities.DriveFile): string => { return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) - : image.thumbnailUrl; + : image.thumbnailUrl ?? image.url; }; misskeyApi('drive/stream', { @@ -102,7 +102,7 @@ defineExpose<WidgetComponentExpose>({ .img { border: solid 4px transparent; - border-radius: var(--radius-sm); + border-radius: var(--MI-radius-sm); } } @@ -121,7 +121,7 @@ defineExpose<WidgetComponentExpose>({ background-size: cover; background-clip: content-box; border: solid 2px transparent; - border-radius: var(--radius-xs); + border-radius: var(--MI-radius-xs); } } </style> diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 511777a570..92dc6d148e 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -70,7 +70,7 @@ const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries)); const fetching = ref(true); const fetchEndpoint = computed(() => { const url = new URL('/api/fetch-rss', base); - url.searchParams.set('url', widgetProps.url); + url.searchParams.set('url', encodeURIComponent(widgetProps.url)); return url; }); const intervalClear = ref<(() => void) | undefined>(); @@ -113,7 +113,7 @@ defineExpose<WidgetComponentExpose>({ .item { display: block; padding: 8px 16px; - color: var(--fg); + color: var(--MI_THEME-fg); white-space: nowrap; text-overflow: ellipsis; overflow: hidden; diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index b393ecd74b..6957878572 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -99,7 +99,7 @@ const items = computed(() => { const fetching = ref(true); const fetchEndpoint = computed(() => { const url = new URL('/api/fetch-rss', base); - url.searchParams.set('url', widgetProps.url); + url.searchParams.set('url', encodeURIComponent(widgetProps.url)); return url; }); const intervalClear = ref<(() => void) | undefined>(); @@ -171,7 +171,7 @@ defineExpose<WidgetComponentExpose>({ display: inline-flex; align-items: center; vertical-align: bottom; - color: var(--fg); + color: var(--MI_THEME-fg); } .divider { @@ -179,6 +179,6 @@ defineExpose<WidgetComponentExpose>({ width: 0.5px; height: 16px; margin: 0 1em; - background: var(--divider); + background: var(--MI_THEME-divider); } </style> diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue index 1a328be7ce..974536e880 100644 --- a/packages/frontend/src/widgets/WidgetSearch.vue +++ b/packages/frontend/src/widgets/WidgetSearch.vue @@ -176,6 +176,6 @@ defineExpose<WidgetComponentExpose>({ <style lang="scss" scoped> .skw-search { - border-radius: var(--radius-sm) !important; + border-radius: var(--MI-radius-sm) !important; } </style> diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index a41db513e8..47a4efc106 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -91,13 +91,13 @@ defineExpose<WidgetComponentExpose>({ display: flex; align-items: center; padding: 14px 16px; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); > .tag { flex: 1; overflow: hidden; font-size: 0.9em; - color: var(--fg); + color: var(--MI_THEME-fg); > .a { display: block; |