merge: upstream changes for 2024.11 (!742)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/742

Closes #645 and #646

Approved-by: Hazelnoot <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
dakkar 2024-12-15 17:27:12 +00:00
commit e2352839e4
616 changed files with 16825 additions and 7991 deletions

View file

@ -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>

View file

@ -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 {

View file

@ -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>

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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 {

View file

@ -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>

View file

@ -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(() => []);

View file

@ -266,7 +266,7 @@ definePageMetadata(() => ({
padding: 32px;
&:not(:last-child) {
margin-bottom: var(--margin);
margin-bottom: var(--MI-margin);
}
}
.input {

View file

@ -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>

View file

@ -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);
});

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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">
@ -84,6 +88,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</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'),

View file

@ -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>

View file

@ -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>>();

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -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),

View file

@ -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;

View file

@ -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 {

View file

@ -29,6 +29,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</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>
@ -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,

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -184,7 +184,7 @@ definePageMetadata(() => ({
.userItemSub {
padding: 6px 12px;
font-size: 85%;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
}
.userItemMainBody {

View file

@ -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);
}
}

View file

@ -438,6 +438,6 @@ definePageMetadata(() => ({
<style lang="scss" module>
.subCaption {
font-size: 0.85em;
color: var(--fgTransparentWeak);
color: var(--MI_THEME-fgTransparentWeak);
}
</style>

View file

@ -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>

View file

@ -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();
});
}

View file

@ -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>

View file

@ -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),
});
}

View file

@ -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>

View file

@ -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}`;
}
}

View file

@ -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>

View file

@ -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);
.decoration {
cursor: pointer;
padding: 16px 16px 28px 16px;
border-radius: 8px;
text-align: center;
font-size: 90%;
overflow: clip;
contain: content;
}
.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;
}
}
@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>

View file

@ -216,7 +216,7 @@ definePageMetadata(() => ({
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: var(--navFg);
color: var(--MI_THEME-navFg);
}
.pinnedNoteRemove {

View file

@ -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;

View file

@ -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);
}

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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">

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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;

View file

@ -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) {
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
function accept(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
paginationComponent.value?.reload();
});
}
function reject(user) {
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
function reject(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
paginationComponent.value?.reload();
});
}
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 {

View file

@ -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.

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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>

View file

@ -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 {

View file

@ -40,7 +40,7 @@ function fetch() {
return;
}
let promise: Promise<any>;
let promise: Promise<unknown>;
if (uri.startsWith('https://')) {
promise = misskeyApi('ap/show', {

View file

@ -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 onDeny() {
authRoot.value?.showUI('denied');
}
function onLogin(res): void {
login(res.i);
}
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;
}
.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;
}
.redirectRoot {
padding: 16px;
border-radius: var(--MI-radius);
background-color: var(--MI_THEME-bg);
}
.redirectUrl {
font-size: 90%;
padding: 12px;
border-radius: var(--MI-radius);
background-color: var(--MI_THEME-panel);
overflow-x: scroll;
}
</style>

View file

@ -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;
}
}

View file

@ -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(() => []);

View file

@ -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;
}
}

View file

@ -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>

View file

@ -24,7 +24,7 @@ const props = defineProps<{
}>();
if (props.showLoginPopup) {
pleaseLogin('/');
pleaseLogin({ path: '/' });
}
const headerActions = computed(() => []);

View file

@ -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>

View file

@ -102,7 +102,7 @@ definePageMetadata(() => ({
<style module lang="scss">
.notifications {
border-radius: var(--radius);
border-radius: var(--MI-radius);
overflow: clip;
}
</style>

View file

@ -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 onLogin(res): void {
login(res.i);
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 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>

View file

@ -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);

View file

@ -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', {

View file

@ -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 ?? []));

View file

@ -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;
}

View file

@ -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 -->

View file

@ -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 {

View file

@ -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>

View file

@ -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', {

View file

@ -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 {

View file

@ -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>

View file

@ -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;
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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 => {

View file

@ -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);
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -148,7 +148,7 @@ definePageMetadata(() => ({
.current {
padding: 16px;
border-radius: var(--radius);
border-radius: var(--MI-radius);
}
.decorations {

View file

@ -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 {

View file

@ -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>

View file

@ -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>

Some files were not shown because too many files have changed in this diff Show more